Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288// What the context meter says, as the terminal footer's does: how full the// context is, how much of the prompt a request would read from cache, and// how the prepared context splits across system, messages, tool output, and images.
import type { components } from './api.gen';
export type Usage = components['schemas']['Usage'];export type ContextSummary = components['schemas']['ContextSummary'];export type HistoryEntry = components['schemas']['HistoryEntry'];
/** Where compaction starts when no strategy threshold was observed; the share turns to a warning from here. */export const NEAR = 90;
export type EffortTone = 'none' | 'low' | 'medium' | 'high' | 'xhigh';
/** Maps a provider effort identifier to one of the five visual effort tiers. */export function effortTone(level: string | null | undefined): EffortTone | null { switch (level) { case 'none': case 'off': return 'none'; case 'minimal': case 'extra-low': case 'low': return 'low'; case 'medium': return 'medium'; case 'high': return 'high'; case 'xhigh': case 'max': return 'xhigh'; default: return null; }}
export type SegmentKey = 'sys' | 'msg' | 'tool' | 'img';
export interface ContextSegment { key: SegmentKey; label: string; tokens: number; color: string; /** Width share (0..100) inside the used portion, with a gentle perceptual curve (`tokens^0.58`). */ perceptualPct: number;}
interface EntryCost { msgBytes: number; toolBytes: number; imgTokens: number;}
const entryCostCache = new WeakMap<HistoryEntry, EntryCost>();const entryIdCostCache = new Map<string, EntryCost>();
/** What the context holds after a reply: the total, or the prompt and completion when no total was reported. */export function contextTokens(usage: Usage): number | null { if (usage.total_tokens !== null && usage.total_tokens >= 0) return usage.total_tokens; if (usage.prompt_tokens === null || usage.prompt_tokens < 0) return null; return usage.prompt_tokens + Math.max(0, usage.completion_tokens ?? 0);}
/** What a request would read from cache at `now`: the measured count until the cache starts to fade, then each step it has reached. */export function cachedNow(usage: Usage, now: number): number | null { let cached = usage.cached_prompt_tokens; for (const step of usage.cache_fade) { if (Date.parse(step.at) > now) break; cached = step.cached_tokens; } return cached !== null && cached >= 0 ? cached : null;}
/** When the cache next fades, so a view re-renders then and not on a timer; null when it has settled. */export function nextFade(usage: Usage, now: number): number | null { for (const step of usage.cache_fade) { const at = Date.parse(step.at); if (at > now) return at; } return null;}
/** When the prompt cache completely expires (`cached_tokens === 0` or `null`), if a fade schedule was recorded. */export function cacheExpiryAt(usage: Usage): number | null { let expiry: number | null = null; for (const step of usage.cache_fade) { if (step.cached_tokens === null || step.cached_tokens <= 0) { const at = Date.parse(step.at); if (!Number.isNaN(at)) { expiry = at; break; } } } return expiry;}
/** Whether the prompt cache is still warm at `now`. */export function isCacheWarm(usage: Usage, now: number): boolean { const current = cachedNow(usage, now); if (current !== null && current > 0) return true; if (!usage.cache_fade.length) return (usage.cached_prompt_tokens ?? 0) > 0 || (usage.cache_write_tokens ?? 0) > 0; return false;}
/** Compact countdown string (`3m 50s`, `45s`, `18m`) until `targetMs`. */export function formatCountdown(targetMs: number, now: number): string { const sec = Math.max(0, Math.ceil((targetMs - now) / 1000)); if (sec < 60) return `${sec}s`; const min = Math.floor(sec / 60); const rem = sec % 60; if (min >= 60) { const hr = Math.floor(min / 60); const m = min % 60; return m > 0 ? `${hr}h ${m}m` : `${hr}h`; } return rem > 0 ? `${min}m ${rem}s` : `${min}m`;}
/** The percent of the window the context fills. */export function share(tokens: number, window: number): number { return Math.round((100 * tokens) / window);}
/** Usage a different model reported says nothing about the model in use now. */export function stale(usage: Usage, model: string | null): boolean { return usage.model !== null && model !== null && usage.model !== model;}
/** Token counts to about three digits: 812, 4.2k, 392k, 1.2m. */export function shortCount(n: number): string { if (n < 1_000) return String(n); if (n < 10_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}k`; if (n < 999_500) return `${Math.round(n / 1_000)}k`; return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}m`;}
/** Same 512px tile formula as `albedo/harness/compaction.image_tokens`. */export function estimateImageTokens(width: number, height: number): number { const wTiles = Math.max(1, Math.ceil(width / 512)); const hTiles = Math.max(1, Math.ceil(height / 512)); return 85 + 170 * wTiles * hTiles;}
/** Approximate byte length of a decoded JSON value without allocating a serialized string. */function jsonValueBytes(val: unknown): number { if (typeof val === 'string') return val.length; if (val === null || val === undefined) return 4; if (typeof val === 'number' || typeof val === 'boolean') return 6; if (Array.isArray(val)) { let sum = 2; for (const item of val) sum += jsonValueBytes(item) + 1; return sum; } if (typeof val === 'object') { let sum = 2; for (const [k, v] of Object.entries(val as Record<string, unknown>)) { sum += k.length + 4 + jsonValueBytes(v); } return sum; } return 8;}
function measureEntry(entry: HistoryEntry): EntryCost { const byRef = entryCostCache.get(entry); if (byRef) return byRef; if (entry.id) { const byId = entryIdCostCache.get(entry.id); if (byId) { entryCostCache.set(entry, byId); return byId; } }
let msgBytes = 0; let toolBytes = 0; let imgTokens = 0; const isTool = entry.kind === 'tool_call' || entry.kind === 'tool_result'; const isMsg = entry.kind === 'user' || entry.kind === 'assistant' || entry.kind === 'thinking' || entry.kind === 'compaction' || entry.kind === 'continuation';
if (isTool || isMsg) { for (const part of entry.content) { if (part.kind === 'image') { imgTokens += estimateImageTokens(part.image.width, part.image.height) + 20; } else if (part.kind === 'reference') { if (part.reference.field.startsWith('image-')) { imgTokens += 765; } else if (part.reference.field !== 'trace') { if (isTool) toolBytes += part.reference.bytes; else msgBytes += part.reference.bytes; } } else if (part.kind === 'text') { if (isTool) toolBytes += part.text.length; else msgBytes += part.text.length; } else if (part.kind === 'json') { const len = jsonValueBytes(part.value); if (isTool) toolBytes += len; else msgBytes += len; } } }
const cost: EntryCost = { msgBytes, toolBytes, imgTokens }; entryCostCache.set(entry, cost); if (entry.id) { if (entryIdCostCache.size >= 4000) entryIdCostCache.clear(); entryIdCostCache.set(entry.id, cost); } return cost;}
/** * Splits `totalTokens` into `system`, `messages`, `tool output`, and `images` * in prompt order (`system` first), keeping `system` anchored to the prepared * instructions + tool schemas byte count and splitting the remaining history * tokens across `messages`, `tool output`, and `images`. */export function computeSegments( totalTokens: number, summary: ContextSummary | null, entries: HistoryEntry[],): ContextSegment[] { if (totalTokens <= 0) return [];
let sysEst = 0; if (summary && summary.state === 'ready') { for (const sec of summary.sections) { if (sec.kind === 'instructions' || sec.kind === 'tools' || sec.kind === 'extension_context') { sysEst += Math.ceil(sec.utf8_bytes / 4); } } } if (sysEst === 0) { sysEst = Math.min(Math.round(totalTokens * 0.18), 18_000); } const sysTokens = Math.min(sysEst, Math.max(1, Math.round(totalTokens * 0.85))); const historyBudget = Math.max(0, totalTokens - sysTokens);
let msgBytes = 0; let toolBytes = 0; let imgTokens = 0; for (const entry of entries) { const c = measureEntry(entry); msgBytes += c.msgBytes; toolBytes += c.toolBytes; imgTokens += c.imgTokens; }
let msgRaw = Math.ceil(msgBytes / 4); let toolRaw = Math.ceil(toolBytes / 4); const imgRaw = imgTokens; if (msgRaw === 0 && toolRaw === 0 && imgRaw === 0 && historyBudget > 0) { msgRaw = historyBudget; }
const histRawSum = msgRaw + toolRaw + imgRaw || 1; const msgTokens = historyBudget > 0 && msgRaw > 0 ? Math.max(1, Math.round((msgRaw / histRawSum) * historyBudget)) : 0; const toolTokens = historyBudget > 0 && toolRaw > 0 ? Math.max(1, Math.round((toolRaw / histRawSum) * historyBudget)) : 0; const imgScaled = historyBudget > 0 && imgRaw > 0 ? Math.max(1, Math.round((imgRaw / histRawSum) * historyBudget)) : 0;
const rawList: { key: SegmentKey; label: string; tokens: number; color: string }[] = [ { key: 'sys', label: 'system', tokens: sysTokens, color: 'var(--c-sys)' }, { key: 'msg', label: 'messages', tokens: msgTokens, color: 'var(--c-msg)' }, { key: 'tool', label: 'tool output', tokens: toolTokens, color: 'var(--c-tool)' }, { key: 'img', label: 'images', tokens: imgScaled, color: 'var(--c-img)' }, ];
const nonZero = rawList.filter((item) => item.tokens > 0); if (!nonZero.length) return [];
const rawWeights = nonZero.map((item) => Math.pow(item.tokens, 0.58)); const rawWSum = rawWeights.reduce((acc, w) => acc + w, 0) || 1; // Guarantee every non-zero category at least 8% of the used bar so even a 1k // message segment beside 200k of tool output is a clear, hoverable pill. const floored = rawWeights.map((w) => Math.max(8, (w / rawWSum) * 100)); const flooredSum = floored.reduce((acc, w) => acc + w, 0) || 1;
return nonZero.map((item, i) => ({ key: item.key, label: item.label, tokens: item.tokens, color: item.color, perceptualPct: (floored[i] / flooredSum) * 100, }));}