diff --git a/packages/core/src/context/contextManager.golden.test.ts b/packages/core/src/context/contextManager.golden.test.ts index d3d3d59354..fccf5206c8 100644 --- a/packages/core/src/context/contextManager.golden.test.ts +++ b/packages/core/src/context/contextManager.golden.test.ts @@ -21,6 +21,9 @@ import { ContextEventBus } from './eventBus.js'; import { ContextTokenCalculator } from './utils/contextTokenCalculator.js'; import type { Content } from '@google/genai'; +import type { BaseLlmClient } from '../core/baseLlmClient.js'; +import type { Episode } from './ir/types.js'; +import type { SidecarConfig } from './sidecar/types.js'; expect.addSnapshotSerializer({ test: (val) => @@ -74,7 +77,7 @@ describe('ContextManager Golden Tests', () => { const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'test-session' }); const eventBus = new ContextEventBus(); const env = new ContextEnvironmentImpl( - {} as any, + { generateContent: async () => ({}), generateJson: async () => ({}) } as unknown as BaseLlmClient, 'test-prompt-id', 'test', '/tmp', @@ -118,7 +121,7 @@ describe('ContextManager Golden Tests', () => { it('should process history and match golden snapshot', async () => { const history = createLargeHistory(); - (contextManager as any).pristineEpisodes = ( + (contextManager as unknown as { pristineEpisodes: Episode[] }).pristineEpisodes = ( await import('./ir/mapper.js') ).IrMapper.toIr(history, new ContextTokenCalculator(4)); const result = await contextManager.projectCompressedHistory(); @@ -127,7 +130,7 @@ describe('ContextManager Golden Tests', () => { it('should not modify history when under budget', async () => { const history = createLargeHistory(); - (contextManager as any).pristineEpisodes = ( + (contextManager as unknown as { pristineEpisodes: Episode[] }).pristineEpisodes = ( await import('./ir/mapper.js') ).IrMapper.toIr(history, new ContextTokenCalculator(4)); // In Golden Tests, we just want to ensure the logic doesn't throw or alter unprotected history in weird ways. @@ -135,7 +138,7 @@ describe('ContextManager Golden Tests', () => { const tracer2 = new ContextTracer({ targetDir: '/tmp', sessionId: 'test2' }); const eventBus2 = new ContextEventBus(); const env2 = new ContextEnvironmentImpl( - {} as any, + { generateContent: async () => ({}), generateJson: async () => ({}) } as unknown as BaseLlmClient, 'test-prompt-id', 'test', '/tmp', @@ -148,12 +151,12 @@ describe('ContextManager Golden Tests', () => { { budget: { retainedTokens: 100000, maxTokens: 150000 }, pipelines: [], - } as any, + } as unknown as SidecarConfig, env2, tracer2, ); - (contextManager as any).pristineEpisodes = ( + (contextManager as unknown as { pristineEpisodes: Episode[] }).pristineEpisodes = ( await import('./ir/mapper.js') ).IrMapper.toIr(history, new ContextTokenCalculator(4)); const result = await contextManager.projectCompressedHistory(); diff --git a/packages/core/src/context/ir/graphUtils.test.ts b/packages/core/src/context/ir/graphUtils.test.ts index b9ef1dadfb..eca87a0d69 100644 --- a/packages/core/src/context/ir/graphUtils.test.ts +++ b/packages/core/src/context/ir/graphUtils.test.ts @@ -8,6 +8,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { generateWorkingBufferView } from './graphUtils.js'; import { createMockEnvironment, createDummyEpisode } from '../testing/contextTestUtils.js'; import type { ContextEnvironment } from '../sidecar/environment.js'; +import type { AgentThought, UserPrompt } from './types.js'; describe('graphUtils (View Generator)', () => { let env: ContextEnvironment; @@ -21,8 +22,8 @@ describe('graphUtils (View Generator)', () => { it('returns pristine episodes untouched if under budget', () => { const episodes = [ - createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]), - createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]), + createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]), + createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]), ]; // We retain 5000 tokens. Total mock tokens = 200. @@ -53,12 +54,12 @@ describe('graphUtils (View Generator)', () => { expect(view[1].id).toBe('ep-2'); // Unchanged (newest) expect(view[0].id).toBe('ep-1'); - expect((view[0].trigger as any).semanticParts[0].presentation.text).toBe(''); + expect((view[0].trigger as UserPrompt).semanticParts[0].presentation?.text).toBe(''); }); it('swaps to Summary variant when over budget', () => { - const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]); - const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]); + const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]); + const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]); ep1.variants = { 'summary': { type: 'summary', status: 'ready', text: '', recoveredTokens: 50 } @@ -71,15 +72,15 @@ describe('graphUtils (View Generator)', () => { // The summary completely replaces the internal steps and clears the yield. expect(view[0].steps).toHaveLength(1); expect(view[0].steps[0].type).toBe('AGENT_THOUGHT'); - expect((view[0].steps[0] as any).text).toBe(''); + expect((view[0].steps[0] as AgentThought).text).toBe(''); expect(view[0].yield).toBeUndefined(); }); it('handles complex N-to-1 Snapshot skipping gracefully', () => { - const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]); - const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]); - const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: '3' }]); - const ep4 = createDummyEpisode('ep-4', 'USER_PROMPT', [{ text: '4' }]); + const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]); + const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]); + const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: '3' }]); + const ep4 = createDummyEpisode('ep-4', 'USER_PROMPT', [{ type: 'text', text: '4' }]); // ep-3 has a snapshot that replaces [ep-1, ep-2, ep-3] const snapshotEp = createDummyEpisode('snap-1', 'SYSTEM_EVENT', []); @@ -103,8 +104,8 @@ describe('graphUtils (View Generator)', () => { }); it('ignores variants that are not yet "ready"', () => { - const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]); - const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]); + const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]); + const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]); ep1.variants = { 'masked': { type: 'masked', status: 'computing', text: '', recoveredTokens: 10 } @@ -114,6 +115,6 @@ describe('graphUtils (View Generator)', () => { // Because the variant was computing, it must fall back to the raw pristine text. expect(view).toHaveLength(2); - expect((view[0].trigger as any).semanticParts[0].presentation).toBeUndefined(); + expect((view[0].trigger as UserPrompt).semanticParts[0].presentation).toBeUndefined(); }); }); diff --git a/packages/core/src/context/processors/emergencyTruncationProcessor.test.ts b/packages/core/src/context/processors/emergencyTruncationProcessor.test.ts index 1aabb515c1..ce0bade3ad 100644 --- a/packages/core/src/context/processors/emergencyTruncationProcessor.test.ts +++ b/packages/core/src/context/processors/emergencyTruncationProcessor.test.ts @@ -28,7 +28,7 @@ describe('EmergencyTruncationProcessor', () => { it('bypasses processing if currentTokens <= maxTokens', async () => { const episodes = [ - createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'short' }]) + createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'short' }]) ]; // State says we are under budget (5000 < 10000) const state = createDummyState(true, 0, new Set(), 5000, 10000); @@ -41,9 +41,9 @@ describe('EmergencyTruncationProcessor', () => { }); it('truncates episodes from the front (oldest) until targetTokens is met', async () => { - const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'oldest' }]); - const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: 'middle' }]); - const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: 'newest' }]); + const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'oldest' }]); + const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: 'middle' }]); + const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: 'newest' }]); // Each is worth 100 tokens according to our mock const episodes = [ep1, ep2, ep3]; @@ -62,9 +62,9 @@ describe('EmergencyTruncationProcessor', () => { }); it('never drops protected episodes (e.g. system instructions)', async () => { - const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'protected system prompt' }]); - const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: 'middle' }]); - const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: 'newest' }]); + const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'protected system prompt' }]); + const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: 'middle' }]); + const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: 'newest' }]); const episodes = [ep1, ep2, ep3]; diff --git a/packages/core/src/context/processors/stateSnapshotProcessor.test.ts b/packages/core/src/context/processors/stateSnapshotProcessor.test.ts index c508801be1..77d6264e59 100644 --- a/packages/core/src/context/processors/stateSnapshotProcessor.test.ts +++ b/packages/core/src/context/processors/stateSnapshotProcessor.test.ts @@ -33,7 +33,7 @@ describe('StateSnapshotProcessor', () => { it('bypasses processing if deficit is <= 0', async () => { const episodes = [ - createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'hello' }]) + createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'hello' }]) ]; // current: 100, max: 1000, retained: 200 (deficit 0) const state = createDummyState(false, 0, new Set(), 100, 1000, 200); @@ -48,7 +48,7 @@ describe('StateSnapshotProcessor', () => { it('bypasses processing if not enough episodes to summarize (needs at least 2 inner episodes)', async () => { const episodes = [ createDummyEpisode('ep-sys', 'SYSTEM_EVENT', []), - createDummyEpisode('ep-active', 'USER_PROMPT', [{ text: 'help' }]), + createDummyEpisode('ep-active', 'USER_PROMPT', [{ type: 'text', text: 'help' }]), ]; // current: 1000, max: 10000, retained: 500. Target deficit = 500 @@ -64,9 +64,9 @@ describe('StateSnapshotProcessor', () => { it('summarizes intermediate episodes into a single snapshot episode', async () => { const episodes = [ createDummyEpisode('ep-0', 'SYSTEM_EVENT', []), - createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'old 1' }]), - createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: 'old 2' }]), - createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: 'current' }]), + createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'old 1' }]), + createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: 'old 2' }]), + createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: 'current' }]), ]; // Target deficit = 200 diff --git a/packages/core/src/context/testing/contextTestUtils.ts b/packages/core/src/context/testing/contextTestUtils.ts index bf3830f682..d2ae694fc3 100644 --- a/packages/core/src/context/testing/contextTestUtils.ts +++ b/packages/core/src/context/testing/contextTestUtils.ts @@ -13,7 +13,7 @@ import { ContextManager } from '../contextManager.js'; import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js'; import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js'; -import type { Episode } from '../ir/types.js'; +import type { Episode, UserPrompt, SystemEvent, SemanticPart } from '../ir/types.js'; import type { ContextAccountingState } from '../pipeline.js'; import { randomUUID } from 'node:crypto'; @@ -38,21 +38,32 @@ export function createDummyState( export function createDummyEpisode( id: string, type: 'USER_PROMPT' | 'SYSTEM_EVENT', - parts: unknown[] = [], + parts: SemanticPart[] = [], toolSteps: Array<{ intent: Record; observation: Record; toolName?: string; tokens?: { intent: number; observation: number } }> = [] ): Episode { + let trigger: UserPrompt | SystemEvent; + + if (type === 'USER_PROMPT') { + trigger = { + id: randomUUID(), + type: 'USER_PROMPT', + semanticParts: parts, + metadata: { originalTokens: 100, currentTokens: 100, transformations: [] }, + }; + } else { + trigger = { + id: randomUUID(), + type: 'SYSTEM_EVENT', + name: 'dummy_event', + payload: {}, + metadata: { originalTokens: 100, currentTokens: 100, transformations: [] }, + }; + } + return { id, timestamp: Date.now(), - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - trigger: { - id: randomUUID(), - type, - name: type === 'SYSTEM_EVENT' ? 'dummy_event' : undefined, - payload: type === 'SYSTEM_EVENT' ? {} : undefined, - semanticParts: type === 'USER_PROMPT' ? parts as any : undefined, - metadata: { originalTokens: 100, currentTokens: 100, transformations: [] }, - } as any, + trigger, steps: toolSteps.map(step => ({ id: randomUUID(), type: 'TOOL_EXECUTION',