/** * AdaptiveContextBudget — self-tuning context window budget. * * Wraps ContextBudget with a feedback loop that adjusts the context limit * based on measured TTFT (time-to-first-token) and actual token usage. * State persists per-thread so each conversation converges independently. */ import { ContextBudget, type ContextBudgetConfig } from './context-budget.js'; /** Adaptive state persisted per-thread as JSON sidecar */ export interface AdaptiveState { /** Current adapted context limit in tokens */ contextLimit: number; /** Last measured TTFT in milliseconds */ lastTTFT: number | null; /** Tokens actually packed into the last prompt */ lastTokensUsed: number; /** Rolling average of top retrieval relevance scores */ avgRelevanceScore: number; /** Total turns recorded (for rolling average) */ turnCount: number; /** Recent data points for debugging (capped at MAX_HISTORY) */ history: AdaptiveTurnRecord[]; } export interface AdaptiveTurnRecord { timestamp: number; ttft: number; contextLimit: number; tokensUsed: number; relevanceScore: number; } export interface RecordTurnInput { /** Measured TTFT in ms (time from subprocess spawn to first text chunk) */ ttft: number; /** * Tokens actually packed into the prompt this turn. * Compute it with `packedPromptTokens()` — never from the model's response. */ tokensUsed: number; /** Average cosine similarity of top retrieval results (0-1), or 0 if none */ relevanceScore?: number; } /** * The components of a packed prompt, itemized exactly as the debug snapshot and * prompt capture already do (`systemPromptBreakdown` plus the user message). * * Task 169: `tokensUsed` is documented as "tokens packed into the prompt", but each * call site invented its own number — the gateway sent the assistant's *response* * (an output, not prompt at all), the TUI sent the RAG slice alone. Both are one to * two orders of magnitude below the capacity threshold, so the grow branch of * `recordTurn` never fired on any thread in any recorded turn and the budget became a * one-way ratchet to the floor. One helper, every caller, no third definition (Rule #8). */ export interface PackedPromptComponents { /** Static instructions/template — the system prompt minus the packed sections below */ instructionTokens: number; /** Always-include files */ alwaysIncludeTokens: number; /** Recent-conversation window (plus any co-thread ticker riding it) */ recentContextTokens: number; /** Retrieved RAG context — the largest component, and the one the budget exists to size */ ragTokens: number; /** The user message as actually sent to the model */ userQueryTokens: number; } /** * Sum the packed-prompt components into the value `recordTurn` expects. * * Non-finite and negative components are floored at 0: `instructionTokens` is derived * by subtraction at both call sites, so estimator rounding across a section boundary * can push it slightly below zero. */ export declare function packedPromptTokens(components: PackedPromptComponents): number; export interface AdaptiveContextBudgetConfig { /** Target TTFT in ms — budget shrinks when exceeded (default: 8000) */ targetTTFT?: number; /** Floor for context limit — never go below this (default: 100,000) */ floor?: number; /** Ceiling for context limit — never exceed this (default: 1,000,000) */ ceiling?: number; /** Initial context limit for new threads (default: 300,000) */ initialLimit?: number; /** Factor to shrink budget when TTFT too high (default: 0.8) */ shrinkFactor?: number; /** Factor to grow budget when TTFT is comfortable and at capacity (default: 1.2) */ growFactor?: number; /** Fraction of TTFT target below which we consider growing (default: 0.5) */ comfortThreshold?: number; /** Fraction of budget that counts as "at capacity" (default: 0.85) */ capacityThreshold?: number; } /** Smallest accepted per-thread context-limit pin. Below this the budget is unusable. */ export declare const MIN_PINNED_CONTEXT_LIMIT = 10000; /** * Build a config that pins the context limit to a fixed value (task 105). * * Collapsing floor/ceiling/initialLimit onto the same number makes `clamp()` the * identity function, so the pin survives `load()` (an existing sidecar is lifted or * lowered to it) and every `recordTurn()` shrink/grow — no branching needed in the * adaptive loop itself. * * Returns `undefined` for anything that isn't a finite number >= MIN_PINNED_CONTEXT_LIMIT, * leaving the thread fully adaptive. */ export declare function pinnedBudgetConfig(value: unknown): AdaptiveContextBudgetConfig | undefined; export declare class AdaptiveContextBudget { private state; private readonly config; private readonly statePath; constructor(statePath: string, config?: AdaptiveContextBudgetConfig); private defaultState; /** Load persisted state from disk. Returns false if no state file exists. */ load(): Promise; /** Save current state to disk */ save(): Promise; /** Get the current adaptive context limit (for use as CLAUDE_CONTEXT_LIMIT) */ getContextLimit(): number; /** * Get the RAG-side total context budget (analogous to TOTAL_CONTEXT_BUDGET). * Derived proportionally from the context limit. */ getTotalContextBudget(): number; /** Create a ContextBudget using the current adaptive limit */ createBudget(overrides?: Partial): ContextBudget; /** Get the full adaptive state (for debug display) */ getState(): Readonly; /** * Record a completed turn and adapt the budget. * Call this after the Claude response completes. */ recordTurn(input: RecordTurnInput): void; private clamp; } /** Derive the adaptive state file path from a thread path */ export declare function getAdaptiveStatePath(threadPath: string): string; /** Load adaptive state for a thread, creating default if none exists */ export declare function loadAdaptiveState(threadPath: string, config?: AdaptiveContextBudgetConfig): Promise; /** Convenience: load, record a turn, save */ export declare function recordAndSaveAdaptiveTurn(threadPath: string, input: RecordTurnInput, config?: AdaptiveContextBudgetConfig): Promise; //# sourceMappingURL=adaptive-context-budget.d.ts.map