/** * Tri-state classification of the active session's caching behaviour. * - `assume-cached` — cold start; no evidence yet. Treated as cached. * - `confirmed-cached` — cache activity observed; stay conservative. * - `confirmed-uncached`— sustained zero cache activity on eligible turns; * aggressive history-aware curation is safe and useful. */ export type CacheMode = "assume-cached" | "confirmed-cached" | "confirmed-uncached"; /** * Per-turn signal extracted from a `turn_end` assistant message's usage block. * Field names mirror the Forge usage shape already captured in * forge-subagent.ts (`usage.cacheRead`, `usage.cacheWrite`, `usage.totalTokens`). */ export interface CacheSignal { /** Cached prefix tokens read this turn. 0 when the provider has no cache. */ cacheRead: number; /** Tokens written to cache this turn (cold-write). 0 when no cache. */ cacheWrite: number; /** Prefix size this turn (usage.totalTokens). null/undefined if unknown. */ contextTokens: number | null | undefined; } export interface CacheModeConfig { /** * Highest provider minimum-cacheable-prefix we guard against (Opus = 4096). * A turn whose prefix is below `minCacheableTokens * eligibilityMargin` * cannot prove "uncached": a cache-backed provider legitimately reports zero * cache tokens below its floor. Such turns are inconclusive, not evidence. */ minCacheableTokens: number; /** Safety multiplier applied to `minCacheableTokens` for the eligibility gate. */ eligibilityMargin: number; /** * Consecutive ELIGIBLE turns at zero cache activity required before flipping * to `confirmed-uncached`. Hysteresis against a single transient miss. */ uncachedStreakToFlip: number; /** * Span-clamp budget multiplier applied while `confirmed-uncached`. A value * below 1 trims retained tool output harder — with no cache to absorb it, * every retained token is re-paid at full price on every subsequent turn. * Every other mode uses 1.0 (zero behavioural change). */ budgetScaleUncached: number; } export declare const DEFAULT_CACHE_MODE_CONFIG: CacheModeConfig; /** Streak state threaded through `classifyNext` (kept external for testability). */ export interface CacheModeState { mode: CacheMode; zeroStreak: number; } /** * Pure transition function: prior state + one turn's signal → next state. * No side effects, so the classifier can be unit-tested in isolation. * * Rules, in order: * 1. Any cache activity (read or write > 0) → `confirmed-cached`, streak reset. * Caching is live. A cache-backed session does not spontaneously stop * caching mid-run unless the model changes — and a model switch resets the * governor instance anyway — so this state is sticky (see rule 3). * 2. Zero cache activity on an INELIGIBLE turn (prefix below the floor, or * unknown) → inconclusive; state unchanged. Small contexts and the * post-compaction `tokens: null` window must not be read as "uncached". * 3. Zero cache activity on an ELIGIBLE turn: * - if already `confirmed-cached`, treat as a transient miss — unchanged. * - otherwise increment the zero streak; once it reaches * `uncachedStreakToFlip`, flip to `confirmed-uncached`. */ export declare function classifyNext(prev: CacheModeState, signal: CacheSignal, cfg: CacheModeConfig): CacheModeState; export interface CacheModeTracker { /** Feed one turn's usage signal into the classifier. Never throws. */ observe(signal: CacheSignal): void; /** Current classification. */ mode(): CacheMode; /** Span-clamp budget multiplier for the current mode (1.0 unless uncached). */ budgetScale(): number; } /** * Construct a stateful tracker seeded at `assume-cached`. Each `observe` applies * the pure `classifyNext` transition. Used per governor instance (i.e. per * subagent phase), so a phase routed to a non-caching model is detected and * curated independently of the rest of the run. */ export declare function createCacheModeTracker(config?: Partial): CacheModeTracker;