/** * Tests for long-task/context-manager.ts (Phase 3.3 of agent-fix sprint). * * Coverage targets the PM brief acceptance gates: * - Constructor validation (budget/threshold/strategy invariants) * - Token estimation (default + injected estimateTokensFn) * - needsCompression threshold semantics * - compress() — 3 strategies (summarize-only / retrieve-only / hybrid) * - compress() purity (returns new state, doesn't mutate) * - compress() preserves last retainRecentChars verbatim * - evictRetrievalCache() — LRU correctness (insertion order + accessOrder) * - evictRetrievalCache() — archive callback fires * - compressDecisionHistory() — bucket-summarize, last K verbatim, archive * - All compression operations preserve other state fields unchanged * - Replay determinism: deterministic llmCall → identical compressed output * - End-to-end: state shrinks under combined ops within budget */ import { describe, it, expect, vi } from 'vitest'; import { CHECKPOINT_SCHEMA_VERSION, type CheckpointStepState, type Decision, } from '../src/long-task/checkpoint.js'; import { ContextManager, type ContextManagerOptions, type CompressionEvent, type ContextCompressionEvent, } from '../src/long-task/context-manager.js'; import type { LlmCallFn, LlmCallResult } from '../src/retrieval-agent-loop.js'; // ───────────────────────────────────────────────────────────────────────── // Fixtures // ───────────────────────────────────────────────────────────────────────── function buildState(overrides: Partial = {}): CheckpointStepState { return { schema_version: CHECKPOINT_SCHEMA_VERSION, task_id: 'task-cm', run_id: 'run-cm', step_index: 0, timestamp_iso: '2026-04-27T00:00:00.000Z', step_action: 'work', step_input: {}, step_output: {}, accumulated_context: '', retrieval_cache: {}, decision_history: [], ...overrides, }; } function makeFakeLlmCall(opts: { content: string; costUsd?: number; latencyMs?: number } = { content: 'SUMMARY' }): { fn: LlmCallFn; calls: Array<{ model: string; messages: Array<{ role: string; content: string }> }>; } { const calls: Array<{ model: string; messages: Array<{ role: string; content: string }> }> = []; const fn: LlmCallFn = async (input) => { calls.push({ model: input.model, messages: input.messages }); const result: LlmCallResult = { content: opts.content, inTokens: 100, outTokens: 50, costUsd: opts.costUsd ?? 0.001, latencyMs: opts.latencyMs ?? 50, }; return result; }; return { fn, calls }; } function makeMgr(overrides: Partial & { events?: CompressionEvent[] } = {}): { mgr: ContextManager; events: CompressionEvent[]; } { const events: CompressionEvent[] = overrides.events ?? []; const opts: ContextManagerOptions = { contextTokenBudget: 1000, compressionThreshold: 0.7, strategy: 'retrieve-only', // no llmCall needed by default onCompressionEvent: (e) => events.push(e), ...overrides, }; return { mgr: new ContextManager(opts), events }; } // ───────────────────────────────────────────────────────────────────────── // Constructor // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — constructor', () => { it('rejects contextTokenBudget <= 0', () => { expect(() => new ContextManager({ contextTokenBudget: 0, strategy: 'retrieve-only' })).toThrow(/must be > 0/); expect(() => new ContextManager({ contextTokenBudget: -1, strategy: 'retrieve-only' })).toThrow(/must be > 0/); }); it('rejects compressionThreshold outside (0, 1]', () => { expect(() => new ContextManager({ contextTokenBudget: 100, compressionThreshold: 0, strategy: 'retrieve-only' })).toThrow(); expect(() => new ContextManager({ contextTokenBudget: 100, compressionThreshold: 1.1, strategy: 'retrieve-only' })).toThrow(); }); it('strategy=summarize-only requires llmCall + summarizationModel', () => { expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'summarize-only' })) .toThrow(/requires both llmCall and summarizationModel/); const { fn } = makeFakeLlmCall(); expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'summarize-only', llmCall: fn })) .toThrow(/requires both llmCall and summarizationModel/); }); it('strategy=hybrid requires llmCall + summarizationModel', () => { expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'hybrid' })) .toThrow(/requires both llmCall and summarizationModel/); }); it('strategy=retrieve-only does NOT require llmCall', () => { expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'retrieve-only' })).not.toThrow(); }); it('rejects negative retainRecentChars', () => { expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'retrieve-only', retainRecentChars: -1 })).toThrow(); }); it('rejects non-integer retainRecentDecisions', () => { expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'retrieve-only', retainRecentDecisions: 1.5 })).toThrow(); }); it('rejects negative retrievalCacheMaxSize', () => { expect(() => new ContextManager({ contextTokenBudget: 100, strategy: 'retrieve-only', retrievalCacheMaxSize: -5 })).toThrow(); }); }); // ───────────────────────────────────────────────────────────────────────── // Token estimation // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — token estimation', () => { it('uses default content-aware estimator when no override given', () => { const { mgr } = makeMgr({ contextTokenBudget: 1000 }); const tokens = mgr.estimateTokens('a'.repeat(400)); // prose ~ 4 chars/token expect(tokens).toBeGreaterThan(50); expect(tokens).toBeLessThan(150); }); it('uses injected estimateTokensFn when provided', () => { const { mgr } = makeMgr({ contextTokenBudget: 1000, estimateTokensFn: (text) => text.length, }); expect(mgr.estimateTokens('hello')).toBe(5); }); it('returns 0 for empty string with default estimator', () => { const { mgr } = makeMgr(); expect(mgr.estimateTokens('')).toBe(0); }); }); describe('ContextManager — needsCompression', () => { it('false when accumulated_context is empty', () => { const { mgr } = makeMgr({ contextTokenBudget: 1000, compressionThreshold: 0.7 }); expect(mgr.needsCompression(buildState())).toBe(false); }); it('false when below threshold', () => { const { mgr } = makeMgr({ contextTokenBudget: 1000, compressionThreshold: 0.7, estimateTokensFn: () => 500, // below 700 }); expect(mgr.needsCompression(buildState({ accumulated_context: 'x' }))).toBe(false); }); it('true when above threshold', () => { const { mgr } = makeMgr({ contextTokenBudget: 1000, compressionThreshold: 0.7, estimateTokensFn: () => 800, // above 700 }); expect(mgr.needsCompression(buildState({ accumulated_context: 'x' }))).toBe(true); }); it('false when exactly at threshold (strict greater-than)', () => { const { mgr } = makeMgr({ contextTokenBudget: 1000, compressionThreshold: 0.7, estimateTokensFn: () => 700, // equal to 700 }); expect(mgr.needsCompression(buildState({ accumulated_context: 'x' }))).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────────── // compress() — strategy: retrieve-only // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — compress (retrieve-only)', () => { it('no-op when below threshold; emits skipped event', async () => { const { mgr, events } = makeMgr({ contextTokenBudget: 1000, compressionThreshold: 0.7, estimateTokensFn: (t) => t.length, }); const state = buildState({ accumulated_context: 'short' }); const compressed = await mgr.compress(state); expect(compressed.accumulated_context).toBe('short'); expect(events[0]?.type).toBe('context-compression-skipped'); }); it('compresses when above threshold', async () => { const text = 'x'.repeat(10000); const { mgr, events } = makeMgr({ contextTokenBudget: 100, compressionThreshold: 0.7, strategy: 'retrieve-only', retainRecentChars: 200, }); const state = buildState({ accumulated_context: text }); const compressed = await mgr.compress(state); expect(compressed.accumulated_context.length).toBeLessThan(text.length); expect(compressed.accumulated_context).toContain('[archived:'); expect(events.some(e => e.type === 'context-compressed')).toBe(true); }); it('preserves last retainRecentChars verbatim', async () => { const older = 'A'.repeat(5000); const recent = 'RECENT_TAIL_MARKER'; const { mgr } = makeMgr({ contextTokenBudget: 100, strategy: 'retrieve-only', retainRecentChars: recent.length, }); const state = buildState({ accumulated_context: older + recent }); const compressed = await mgr.compress(state); expect(compressed.accumulated_context.endsWith(recent)).toBe(true); }); it('emits archived range with correct offsets', async () => { const older = 'X'.repeat(1000); const recent = 'Y'.repeat(100); const { mgr, events } = makeMgr({ contextTokenBudget: 100, strategy: 'retrieve-only', retainRecentChars: 100, }); await mgr.compress(buildState({ accumulated_context: older + recent })); const evt = events.find((e): e is ContextCompressionEvent => e.type === 'context-compressed'); expect(evt && 'archived' in evt && evt.archived).toMatchObject({ range_start: 0, range_end: 1000, }); expect(evt.archived?.archived_text).toBe(older); }); it('does NOT call llmCall under retrieve-only', async () => { const { fn, calls } = makeFakeLlmCall(); const { mgr } = makeMgr({ contextTokenBudget: 100, strategy: 'retrieve-only', llmCall: fn, summarizationModel: 'budget', retainRecentChars: 50, }); await mgr.compress(buildState({ accumulated_context: 'a'.repeat(2000) })); expect(calls.length).toBe(0); }); it('returns a NEW state (does not mutate input)', async () => { const { mgr } = makeMgr({ contextTokenBudget: 100, strategy: 'retrieve-only', retainRecentChars: 50, }); const state = buildState({ accumulated_context: 'x'.repeat(2000) }); const original = state.accumulated_context; const compressed = await mgr.compress(state); expect(state.accumulated_context).toBe(original); expect(compressed).not.toBe(state); }); it('preserves all other state fields unchanged', async () => { const { mgr } = makeMgr({ contextTokenBudget: 100, strategy: 'retrieve-only', retainRecentChars: 50, }); const state = buildState({ accumulated_context: 'x'.repeat(2000), retrieval_cache: { 'q:1': ['a'] }, decision_history: [{ step_index: 0, decision: 'd' }], step_input: { x: 1 }, step_output: { y: 2 }, }); const compressed = await mgr.compress(state); expect(compressed.retrieval_cache).toEqual({ 'q:1': ['a'] }); expect(compressed.decision_history).toEqual([{ step_index: 0, decision: 'd' }]); expect(compressed.step_input).toEqual({ x: 1 }); expect(compressed.step_output).toEqual({ y: 2 }); expect(compressed.task_id).toBe(state.task_id); expect(compressed.step_index).toBe(state.step_index); }); it('no-op when entire context fits within retainRecentChars', async () => { const { mgr, events } = makeMgr({ contextTokenBudget: 10, compressionThreshold: 0.5, estimateTokensFn: (t) => t.length, // forces above threshold strategy: 'retrieve-only', retainRecentChars: 5000, }); const state = buildState({ accumulated_context: 'short text' }); const compressed = await mgr.compress(state); expect(compressed).toBe(state); // identical reference — no-op expect(events.some(e => e.type === 'context-compression-skipped')).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────── // compress() — strategy: summarize-only // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — compress (summarize-only)', () => { it('calls llmCall when above threshold', async () => { const { fn, calls } = makeFakeLlmCall({ content: 'CONDENSED_FACTS' }); const { mgr } = makeMgr({ contextTokenBudget: 100, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'budget-model', retainRecentChars: 50, }); const state = buildState({ accumulated_context: 'A'.repeat(2000) }); const compressed = await mgr.compress(state); expect(calls.length).toBe(1); expect(calls[0]?.model).toBe('budget-model'); expect(compressed.accumulated_context).toContain('CONDENSED_FACTS'); }); it('event reports cost from llmCall', async () => { const { fn } = makeFakeLlmCall({ content: 's', costUsd: 0.0042 }); const { mgr, events } = makeMgr({ contextTokenBudget: 100, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'budget', retainRecentChars: 50, }); await mgr.compress(buildState({ accumulated_context: 'a'.repeat(2000) })); const evt = events.find((e): e is ContextCompressionEvent => e.type === 'context-compressed'); expect(evt.cost_usd).toBe(0.0042); }); it('does NOT emit archived event for summarize-only', async () => { const { fn } = makeFakeLlmCall(); const { mgr, events } = makeMgr({ contextTokenBudget: 100, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'b', retainRecentChars: 50, }); await mgr.compress(buildState({ accumulated_context: 'a'.repeat(2000) })); const evt = events.find((e): e is ContextCompressionEvent => e.type === 'context-compressed'); expect(evt.archived).toBeUndefined(); }); it('preserves recent verbatim alongside summary', async () => { const { fn } = makeFakeLlmCall({ content: 'GIST' }); const recent = 'TAIL_OF_RECENT_TEXT__'; const { mgr } = makeMgr({ contextTokenBudget: 100, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'b', retainRecentChars: recent.length, }); const state = buildState({ accumulated_context: 'x'.repeat(2000) + recent }); const compressed = await mgr.compress(state); expect(compressed.accumulated_context).toContain('GIST'); expect(compressed.accumulated_context.endsWith(recent)).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────── // compress() — strategy: hybrid // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — compress (hybrid)', () => { it('summarizes AND emits archived event', async () => { const { fn, calls } = makeFakeLlmCall({ content: 'GIST' }); const { mgr, events } = makeMgr({ contextTokenBudget: 100, strategy: 'hybrid', llmCall: fn, summarizationModel: 'b', retainRecentChars: 50, }); await mgr.compress(buildState({ accumulated_context: 'a'.repeat(2000) })); expect(calls.length).toBe(1); // LLM was called const evt = events.find((e): e is ContextCompressionEvent => e.type === 'context-compressed'); expect(evt.archived).toBeDefined(); expect(evt.strategy).toBe('hybrid'); }); }); // ───────────────────────────────────────────────────────────────────────── // evictRetrievalCache // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — evictRetrievalCache', () => { it('no-op when cache size <= maxSize', () => { const { mgr } = makeMgr({ retrievalCacheMaxSize: 5 }); const state = buildState({ retrieval_cache: { a: 1, b: 2, c: 3 } }); const result = mgr.evictRetrievalCache(state); expect(result).toBe(state); }); it('drops oldest entries by insertion order without accessOrder', () => { const { mgr } = makeMgr({ retrievalCacheMaxSize: 2 }); const cache: Record = { a: 1, b: 2, c: 3, d: 4 }; const result = mgr.evictRetrievalCache(buildState({ retrieval_cache: cache })); expect(Object.keys(result.retrieval_cache).sort()).toEqual(['c', 'd']); }); it('keeps most-recently-accessed when accessOrder provided', () => { const { mgr } = makeMgr({ retrievalCacheMaxSize: 2 }); const cache: Record = { a: 1, b: 2, c: 3, d: 4 }; // Access order: a was first accessed, d was most recent. const result = mgr.evictRetrievalCache(buildState({ retrieval_cache: cache }), { accessOrder: ['a', 'b', 'c', 'd'], }); expect(Object.keys(result.retrieval_cache).sort()).toEqual(['c', 'd']); }); it('treats keys NOT in accessOrder as oldest (evict first)', () => { const { mgr } = makeMgr({ retrievalCacheMaxSize: 2 }); const cache: Record = { a: 1, b: 2, c: 3, d: 4 }; // Only 'c' and 'd' have access timestamps; 'a' and 'b' never accessed. const result = mgr.evictRetrievalCache(buildState({ retrieval_cache: cache }), { accessOrder: ['c', 'd'], }); // 'a' and 'b' have no access timestamps → oldest → evicted first. expect(Object.keys(result.retrieval_cache).sort()).toEqual(['c', 'd']); }); it('emits cache-evicted event with before/after/evicted counts', () => { const { mgr, events } = makeMgr({ retrievalCacheMaxSize: 2 }); const cache: Record = { a: 1, b: 2, c: 3, d: 4 }; mgr.evictRetrievalCache(buildState({ retrieval_cache: cache })); const evt = events.find(e => e.type === 'cache-evicted'); expect(evt).toEqual({ type: 'cache-evicted', before_size: 4, after_size: 2, evicted_count: 2, }); }); it('archiveCacheTo callback fires with evicted entries', async () => { const archived: Array = []; const { mgr } = makeMgr({ retrievalCacheMaxSize: 1, archiveCacheTo: async (entries) => { for (const e of entries) archived.push(e); }, }); const cache: Record = { a: 1, b: 2, c: 3 }; mgr.evictRetrievalCache(buildState({ retrieval_cache: cache })); // Wait a tick for the archive promise to settle. await new Promise(resolve => setImmediate(resolve)); expect(archived.length).toBe(2); }); it('preserves all other state fields unchanged', () => { const { mgr } = makeMgr({ retrievalCacheMaxSize: 1 }); const state = buildState({ retrieval_cache: { a: 1, b: 2 }, accumulated_context: 'CTX', decision_history: [{ step_index: 0, decision: 'd' }], step_input: { x: 1 }, }); const result = mgr.evictRetrievalCache(state); expect(result.accumulated_context).toBe('CTX'); expect(result.decision_history).toEqual(state.decision_history); expect(result.step_input).toEqual(state.step_input); }); it('returns a NEW state (does not mutate input)', () => { const { mgr } = makeMgr({ retrievalCacheMaxSize: 1 }); const state = buildState({ retrieval_cache: { a: 1, b: 2 } }); const original = { ...state.retrieval_cache }; mgr.evictRetrievalCache(state); expect(state.retrieval_cache).toEqual(original); }); }); // ───────────────────────────────────────────────────────────────────────── // compressDecisionHistory // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — compressDecisionHistory', () => { const decs = (n: number): readonly Decision[] => Array.from({ length: n }, (_, i) => ({ step_index: i, decision: `d-${i}` })); it('no-op when history.length <= retainRecentDecisions', async () => { const { mgr } = makeMgr({ retainRecentDecisions: 10 }); const state = buildState({ decision_history: decs(5) }); const result = await mgr.compressDecisionHistory(state); expect(result).toBe(state); }); it('keeps last K verbatim and prepends one summary Decision', async () => { const { mgr } = makeMgr({ retainRecentDecisions: 3 }); const state = buildState({ decision_history: decs(10) }); const result = await mgr.compressDecisionHistory(state); expect(result.decision_history.length).toBe(4); // 1 summary + 3 verbatim expect(result.decision_history[1]).toEqual({ step_index: 7, decision: 'd-7' }); expect(result.decision_history[2]).toEqual({ step_index: 8, decision: 'd-8' }); expect(result.decision_history[3]).toEqual({ step_index: 9, decision: 'd-9' }); }); it('summary decision references rolled-up count', async () => { const { mgr } = makeMgr({ retainRecentDecisions: 3 }); const state = buildState({ decision_history: decs(10) }); const result = await mgr.compressDecisionHistory(state); const summary = result.decision_history[0]; expect(summary?.decision).toContain('rolled-up'); expect(summary?.decision).toContain('7'); // 10 - 3 = 7 evicted }); it('uses heuristic summary when llmCall not configured', async () => { const { mgr } = makeMgr({ retainRecentDecisions: 2 }); const state = buildState({ decision_history: decs(5) }); const result = await mgr.compressDecisionHistory(state); const summary = result.decision_history[0]; // Heuristic: concatenated decision strings expect(summary?.rationale).toContain('d-0'); expect(summary?.rationale).toContain('d-1'); expect(summary?.rationale).toContain('d-2'); }); it('uses LLM summary when llmCall configured', async () => { const { fn, calls } = makeFakeLlmCall({ content: 'LLM_DECISION_SUMMARY' }); const { mgr } = makeMgr({ retainRecentDecisions: 2, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'budget', }); const state = buildState({ decision_history: decs(5) }); const result = await mgr.compressDecisionHistory(state); expect(calls.length).toBe(1); expect(result.decision_history[0]?.rationale).toBe('LLM_DECISION_SUMMARY'); }); it('emits decisions-compressed event with counts + cost', async () => { const { fn } = makeFakeLlmCall({ content: 's', costUsd: 0.0007 }); const { mgr, events } = makeMgr({ retainRecentDecisions: 2, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'b', }); await mgr.compressDecisionHistory(buildState({ decision_history: decs(5) })); const evt = events.find(e => e.type === 'decisions-compressed'); expect(evt).toEqual({ type: 'decisions-compressed', before_count: 5, after_count: 3, // 1 summary + 2 verbatim evicted_count: 3, cost_usd: 0.0007, }); }); it('archiveDecisionsTo fires with the older bucket', async () => { const archived: Decision[] = []; const { mgr } = makeMgr({ retainRecentDecisions: 2, archiveDecisionsTo: async (older) => { for (const d of older) archived.push(d); }, }); await mgr.compressDecisionHistory(buildState({ decision_history: decs(5) })); await new Promise(resolve => setImmediate(resolve)); expect(archived.length).toBe(3); // 5 - 2 = 3 archived expect(archived.map(d => d.decision)).toEqual(['d-0', 'd-1', 'd-2']); }); it('returns a NEW state and does not mutate input decision_history', async () => { const { mgr } = makeMgr({ retainRecentDecisions: 2 }); const state = buildState({ decision_history: decs(5) }); const originalLen = state.decision_history.length; const result = await mgr.compressDecisionHistory(state); expect(state.decision_history.length).toBe(originalLen); expect(result).not.toBe(state); }); it('preserves all other state fields unchanged', async () => { const { mgr } = makeMgr({ retainRecentDecisions: 2 }); const state = buildState({ decision_history: decs(5), accumulated_context: 'CTX', retrieval_cache: { x: 1 }, }); const result = await mgr.compressDecisionHistory(state); expect(result.accumulated_context).toBe('CTX'); expect(result.retrieval_cache).toEqual({ x: 1 }); expect(result.task_id).toBe(state.task_id); }); }); // ───────────────────────────────────────────────────────────────────────── // Replay determinism // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — replay determinism', () => { it('deterministic llmCall → identical compressed output across two runs', async () => { const { fn: fnA } = makeFakeLlmCall({ content: 'STABLE_SUMMARY', costUsd: 0.001 }); const { fn: fnB } = makeFakeLlmCall({ content: 'STABLE_SUMMARY', costUsd: 0.001 }); const optsBase = (fn: LlmCallFn): ContextManagerOptions => ({ contextTokenBudget: 100, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'b', retainRecentChars: 50, }); const a = new ContextManager(optsBase(fnA)); const b = new ContextManager(optsBase(fnB)); const state = buildState({ accumulated_context: 'a'.repeat(2000) }); const ra = await a.compress(state); const rb = await b.compress(state); expect(ra.accumulated_context).toBe(rb.accumulated_context); }); it('insertion-order LRU is deterministic', () => { const { mgr: mgrA } = makeMgr({ retrievalCacheMaxSize: 2 }); const { mgr: mgrB } = makeMgr({ retrievalCacheMaxSize: 2 }); const cache: Record = { a: 1, b: 2, c: 3, d: 4 }; const ra = mgrA.evictRetrievalCache(buildState({ retrieval_cache: cache })); const rb = mgrB.evictRetrievalCache(buildState({ retrieval_cache: cache })); expect(Object.keys(ra.retrieval_cache)).toEqual(Object.keys(rb.retrieval_cache)); }); }); // ───────────────────────────────────────────────────────────────────────── // End-to-end // ───────────────────────────────────────────────────────────────────────── describe('ContextManager — end-to-end shrinking', () => { it('compress + evict + compressDecisionHistory yields fully shrunk state', async () => { const { fn } = makeFakeLlmCall({ content: 'CONDENSED' }); const mgr = new ContextManager({ contextTokenBudget: 100, compressionThreshold: 0.7, strategy: 'summarize-only', llmCall: fn, summarizationModel: 'b', retainRecentChars: 50, retainRecentDecisions: 2, retrievalCacheMaxSize: 2, }); const state = buildState({ accumulated_context: 'verbose-text-'.repeat(500), // ~6500 chars decision_history: Array.from({ length: 10 }, (_, i) => ({ step_index: i, decision: `d-${i}` })), retrieval_cache: Object.fromEntries( Array.from({ length: 10 }, (_, i) => [`q-${i}`, [`r-${i}`]]), ), }); const beforeContextTokens = mgr.estimateTokens(state.accumulated_context); expect(beforeContextTokens).toBeGreaterThan(70); // above threshold let s = await mgr.compress(state); s = mgr.evictRetrievalCache(s); s = await mgr.compressDecisionHistory(s); const afterContextTokens = mgr.estimateTokens(s.accumulated_context); expect(afterContextTokens).toBeLessThan(beforeContextTokens); expect(s.decision_history.length).toBe(3); // 1 summary + 2 verbatim expect(Object.keys(s.retrieval_cache).length).toBe(2); }); });