From fde5f00b174fbd38de63d2b48b1db939fe7196b7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 17:01:43 +0000 Subject: [PATCH 1/5] fix(core): overactive GC --- packages/core/src/context/contextManager.ts | 8 +- .../core/src/context/graph/render.test.ts | 111 ++++++++++++++++++ packages/core/src/context/graph/render.ts | 36 +++++- packages/core/src/core/baseLlmClient.ts | 13 ++ 4 files changed, 164 insertions(+), 4 deletions(-) diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts index 48e8dcd88b6..71136e779cc 100644 --- a/packages/core/src/context/contextManager.ts +++ b/packages/core/src/context/contextManager.ts @@ -126,17 +126,21 @@ export class ContextManager { // Walk backwards finding nodes that fall out of the retained budget for (let i = this.buffer.nodes.length - 1; i >= 0; i--) { const node = this.buffer.nodes[i]; + const priorTokens = rollingTokens; rollingTokens += this.env.tokenCalculator.calculateConcreteListTokens([ node, ]); - if (rollingTokens > this.sidecar.config.budget.retainedTokens) { + + // Loose Boundary Policy: If this node is the one that pushes us over the retained limit, + // we KEEP it to prevent aggressive undershooting. We only age out nodes that are + // strictly *older* than the boundary node. + if (priorTokens > this.sidecar.config.budget.retainedTokens) { // Only age out if not protected if (!protectedIds.has(node.id)) { agedOutNodes.add(node.id); } } } - if (agedOutNodes.size > 0) { const targetDeficit = currentTokens - this.sidecar.config.budget.retainedTokens; diff --git a/packages/core/src/context/graph/render.test.ts b/packages/core/src/context/graph/render.test.ts index 22d625695a5..094d2d950ef 100644 --- a/packages/core/src/context/graph/render.test.ts +++ b/packages/core/src/context/graph/render.test.ts @@ -61,4 +61,115 @@ describe('render', () => { expect(result.history).toEqual([{ text: '1' }, { text: '2' }]); }); + + it('simulates the boundary knapsack problem (loose boundary policy)', async () => { + // 10k, 20k, 40k, 5k + const mockNodes: ConcreteNode[] = [ + { id: 'D', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, + { id: 'C', type: NodeType.AGENT_THOUGHT, payload: {} as Part } as unknown as ConcreteNode, + { id: 'B', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, + { id: 'A', type: NodeType.AGENT_THOUGHT, payload: {} as Part } as unknown as ConcreteNode, + ]; + + const tokenMap: Record = { + 'D': 5000, + 'C': 40000, + 'B': 20000, + 'A': 10000 + }; + + const orchestrator = { + executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) => nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id))) + } as unknown as PipelineOrchestrator; + + const sidecar = { + config: { + budget: { maxTokens: 150000, retainedTokens: 65000 } + } + } as unknown as ContextProfile; + + const currentTokens = 160000; + + const env = { + llmClient: { + countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }) + }, + tokenCalculator: { + calculateConcreteListTokens: vi.fn((nodes) => { + if (nodes.length === 1) return tokenMap[nodes[0].id]; + return currentTokens; + }), + calculateTokenBreakdown: vi.fn(() => ({})), + }, + graphMapper: { + fromGraph: vi.fn((nodes: readonly ConcreteNode[]) => nodes.map(n => ({ text: n.id }))) + } + } as unknown as ContextEnvironment; + + const tracer = { + logEvent: vi.fn(), + } as unknown as ContextTracer; + + const result = await render(mockNodes, orchestrator, sidecar, tracer, env, new Map(), 0, new Set()); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const surviving = result.history.map((c: any) => c.text); + // Loose Boundary: A (10k), B (20k), C (40k). Total = 70k. + // Adding C pushes rolling total (70k) above retainedTokens (65k). + // Under loose policy, C survives. D is strictly older and drops. + expect(surviving).toEqual(['C', 'B', 'A']); // D is dropped + }); + + it('drops nodes that are STRICTLY older than the boundary node', async () => { + const mockNodes: ConcreteNode[] = [ + { id: 'A', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, + { id: 'B', type: NodeType.AGENT_THOUGHT, payload: {} as Part } as unknown as ConcreteNode, + { id: 'C', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, + ]; + + const tokenMap: Record = { + 'C': 40000, + 'B': 40000, + 'A': 10000 + }; + + const orchestrator = { + executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) => nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id))) + } as unknown as PipelineOrchestrator; + + const sidecar = { + config: { + budget: { maxTokens: 150000, retainedTokens: 65000 } + } + } as unknown as ContextProfile; + + const currentTokens = 160000; + + const env = { + llmClient: { + countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }) + }, + tokenCalculator: { + calculateConcreteListTokens: vi.fn((nodes) => { + if (nodes.length === 1) return tokenMap[nodes[0].id]; + return currentTokens; + }), + calculateTokenBreakdown: vi.fn(() => ({})), + }, + graphMapper: { + fromGraph: vi.fn((nodes: readonly ConcreteNode[]) => nodes.map(n => ({ text: n.id }))) + } + } as unknown as ContextEnvironment; + + const tracer = { + logEvent: vi.fn(), + } as unknown as ContextTracer; + + const result = await render(mockNodes, orchestrator, sidecar, tracer, env, new Map(), 0, new Set()); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const surviving = result.history.map((c: any) => c.text); + // C(40k), B(40k). Adding B pushes total to 80k. B is the boundary node and survives. A drops. + expect(surviving).toEqual(['B', 'C']); // A is dropped + }); }); diff --git a/packages/core/src/context/graph/render.ts b/packages/core/src/context/graph/render.ts index b4ce596dece..813ae8a767e 100644 --- a/packages/core/src/context/graph/render.ts +++ b/packages/core/src/context/graph/render.ts @@ -10,6 +10,7 @@ import type { ContextTracer } from '../tracer.js'; import type { ContextProfile } from '../config/profiles.js'; import type { PipelineOrchestrator } from '../pipeline/orchestrator.js'; import type { ContextEnvironment } from '../pipeline/environment.js'; +import { debugLogger } from '../../utils/debugLogger.js'; /** * Maps the Episodic Context Graph back into a raw Gemini Content[] array for transmission. @@ -58,6 +59,33 @@ export async function render( reasons: Object.fromEntries(protectionReasons), }); + // Fire-and-forget asynchronous calibration to track estimator drift + const performCalibration = (finalNodes: readonly ConcreteNode[], finalContents: Content[]) => { + void (async () => { + try { + const exactResp = await env.llmClient.countTokens({contents: finalContents}); + const exactTokens = typeof exactResp.totalTokens === 'number' ? exactResp.totalTokens : 0; + const estimatedTokens = env.tokenCalculator.calculateConcreteListTokens(finalNodes); + + const delta = Math.abs(exactTokens - estimatedTokens); + const tolerance = Math.max(exactTokens, estimatedTokens) * 0.20; // 20% tolerance + + tracer.logEvent('Render', 'Token Calibration Measurement', { + exactTokens, + estimatedTokens, + delta, + isWithinTolerance: delta <= tolerance, + }); + + if (delta > tolerance) { + debugLogger.log(`[Token Calibration] Large deviation detected: exact ${exactTokens} vs estimated ${estimatedTokens} (delta: ${delta})`); + } + } catch { + // Ignore API failures during background calibration + } + })(); + }; + if (currentTokens <= maxTokens) { tracer.logEvent( 'Render', @@ -68,6 +96,7 @@ export async function render( tracer.logEvent('Render', 'Render Context for LLM', { renderedContext: contents, }); + performCalibration(visibleNodes, contents); return { history: contents, didApplyManagement: false }; } const targetDelta = currentTokens - sidecar.config.budget.retainedTokens; @@ -83,13 +112,15 @@ export async function render( // Start from newest and count backwards for (let i = nodes.length - 1; i >= 0; i--) { const node = nodes[i]; + const priorTokens = rollingTokens; const nodeTokens = env.tokenCalculator.calculateConcreteListTokens([node]); rollingTokens += nodeTokens; - if (rollingTokens > sidecar.config.budget.retainedTokens) { + + // Loose Boundary Policy: Keep the node that crosses the boundary + if (priorTokens > sidecar.config.budget.retainedTokens) { agedOutNodes.add(node.id); } } - const processedNodes = await orchestrator.executeTriggerSync( 'gc_backstop', nodes, @@ -113,5 +144,6 @@ export async function render( tracer.logEvent('Render', 'Render Sanitized Context for LLM', { renderedContextSanitized: contents, }); + performCalibration(visibleNodes, contents); return { history: contents, didApplyManagement: true }; } diff --git a/packages/core/src/core/baseLlmClient.ts b/packages/core/src/core/baseLlmClient.ts index 6352814f61d..b8219deb941 100644 --- a/packages/core/src/core/baseLlmClient.ts +++ b/packages/core/src/core/baseLlmClient.ts @@ -111,6 +111,11 @@ interface _CommonGenerateOptions { }; } +export interface CountTokenOptions { + modelConfigKey?: ModelConfigKey; + contents: Content[]; +} + /** * A client dedicated to stateless, utility-focused LLM calls. */ @@ -225,6 +230,14 @@ export class BaseLlmClient { return text; } + async countTokens(options: CountTokenOptions): Promise<{ totalTokens: number }> { + const model = options.modelConfigKey ? + this.config.modelConfigService.getResolvedConfig(options.modelConfigKey).model : + this.config.getActiveModel(); + const result = await this.contentGenerator.countTokens({ model, contents: options.contents }); + return { totalTokens: result.totalTokens || 0 }; + } + async generateContent( options: GenerateContentOptions, ): Promise { From c8eb5db8effb57ed9bb78ff2ac7472664ad73c18 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 17:13:45 +0000 Subject: [PATCH 2/5] fix(context): implement loose boundary policy for gc backstop The synchronous GC backstop and background summarization trigger previously used a strict knapsack limit when iterating backwards over history. If the *inclusion* of a node caused the rolling token count to exceed `retainedTokens`, that node (and all older nodes) were aged out. This led to overly aggressive GC when encountering large tool outputs (e.g. accumulating 30k tokens, then dropping a 40k node, leaving the final retained context at only 30k out of a 65k budget). This commit implements a loose boundary policy: we now keep the node that pushes the count over the `retainedTokens` limit, allowing the final context size to safely float slightly above the target without plummeting. Also introduces a fire-and-forget token calibration audit log to `render.ts` to track divergence between the fast heuristic estimator and the exact API token count. --- .../context/contextManager.barrier.test.ts | 4 +- packages/core/src/context/contextManager.ts | 2 +- .../core/src/context/graph/render.test.ts | 126 +++++++++++++----- packages/core/src/context/graph/render.ts | 23 +++- .../lifecycle.golden.test.ts.snap | 14 +- packages/core/src/core/baseLlmClient.ts | 16 ++- 6 files changed, 126 insertions(+), 59 deletions(-) diff --git a/packages/core/src/context/contextManager.barrier.test.ts b/packages/core/src/context/contextManager.barrier.test.ts index c3a7298ddcc..438f9d3230e 100644 --- a/packages/core/src/context/contextManager.barrier.test.ts +++ b/packages/core/src/context/contextManager.barrier.test.ts @@ -60,9 +60,7 @@ describe('ContextManager Sync Pressure Barrier Tests', () => { // Verify Episode 0 (System) was pruned, so we now start with a sentinel due to role alternation expect(projection[0].role).toBe('user'); - expect(projection[0].parts![0].text).toBe( - '[Continuing from previous AI thoughts...]', - ); + expect(projection[0].parts![0].text).toContain('User turn 17'); // Filter out synthetic Yield nodes (they are model responses without actual tool/text bodies) const contentNodes = projection.filter( diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts index 71136e779cc..0ed52fcd2b7 100644 --- a/packages/core/src/context/contextManager.ts +++ b/packages/core/src/context/contextManager.ts @@ -132,7 +132,7 @@ export class ContextManager { ]); // Loose Boundary Policy: If this node is the one that pushes us over the retained limit, - // we KEEP it to prevent aggressive undershooting. We only age out nodes that are + // we KEEP it to prevent aggressive undershooting. We only age out nodes that are // strictly *older* than the boundary node. if (priorTokens > this.sidecar.config.budget.retainedTokens) { // Only age out if not protected diff --git a/packages/core/src/context/graph/render.test.ts b/packages/core/src/context/graph/render.test.ts index 094d2d950ef..e3890ae4371 100644 --- a/packages/core/src/context/graph/render.test.ts +++ b/packages/core/src/context/graph/render.test.ts @@ -65,34 +65,52 @@ describe('render', () => { it('simulates the boundary knapsack problem (loose boundary policy)', async () => { // 10k, 20k, 40k, 5k const mockNodes: ConcreteNode[] = [ - { id: 'D', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, - { id: 'C', type: NodeType.AGENT_THOUGHT, payload: {} as Part } as unknown as ConcreteNode, - { id: 'B', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, - { id: 'A', type: NodeType.AGENT_THOUGHT, payload: {} as Part } as unknown as ConcreteNode, + { + id: 'D', + type: NodeType.USER_PROMPT, + payload: {} as Part, + } as unknown as ConcreteNode, + { + id: 'C', + type: NodeType.AGENT_THOUGHT, + payload: {} as Part, + } as unknown as ConcreteNode, + { + id: 'B', + type: NodeType.USER_PROMPT, + payload: {} as Part, + } as unknown as ConcreteNode, + { + id: 'A', + type: NodeType.AGENT_THOUGHT, + payload: {} as Part, + } as unknown as ConcreteNode, ]; const tokenMap: Record = { - 'D': 5000, - 'C': 40000, - 'B': 20000, - 'A': 10000 + D: 5000, + C: 40000, + B: 20000, + A: 10000, }; const orchestrator = { - executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) => nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id))) + executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) => + nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id)), + ), } as unknown as PipelineOrchestrator; - const sidecar = { + const sidecar = { config: { - budget: { maxTokens: 150000, retainedTokens: 65000 } - } + budget: { maxTokens: 150000, retainedTokens: 65000 }, + }, } as unknown as ContextProfile; - + const currentTokens = 160000; const env = { llmClient: { - countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }) + countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }), }, tokenCalculator: { calculateConcreteListTokens: vi.fn((nodes) => { @@ -102,52 +120,77 @@ describe('render', () => { calculateTokenBreakdown: vi.fn(() => ({})), }, graphMapper: { - fromGraph: vi.fn((nodes: readonly ConcreteNode[]) => nodes.map(n => ({ text: n.id }))) - } + fromGraph: vi.fn((nodes: readonly ConcreteNode[]) => + nodes.map((n) => ({ text: n.id })), + ), + }, } as unknown as ContextEnvironment; - + const tracer = { logEvent: vi.fn(), } as unknown as ContextTracer; - const result = await render(mockNodes, orchestrator, sidecar, tracer, env, new Map(), 0, new Set()); + const result = await render( + mockNodes, + orchestrator, + sidecar, + tracer, + env, + new Map(), + 0, + new Set(), + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const surviving = result.history.map((c: any) => c.text); - // Loose Boundary: A (10k), B (20k), C (40k). Total = 70k. - // Adding C pushes rolling total (70k) above retainedTokens (65k). + // Loose Boundary: A (10k), B (20k), C (40k). Total = 70k. + // Adding C pushes rolling total (70k) above retainedTokens (65k). // Under loose policy, C survives. D is strictly older and drops. expect(surviving).toEqual(['C', 'B', 'A']); // D is dropped }); it('drops nodes that are STRICTLY older than the boundary node', async () => { const mockNodes: ConcreteNode[] = [ - { id: 'A', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, - { id: 'B', type: NodeType.AGENT_THOUGHT, payload: {} as Part } as unknown as ConcreteNode, - { id: 'C', type: NodeType.USER_PROMPT, payload: {} as Part } as unknown as ConcreteNode, + { + id: 'A', + type: NodeType.USER_PROMPT, + payload: {} as Part, + } as unknown as ConcreteNode, + { + id: 'B', + type: NodeType.AGENT_THOUGHT, + payload: {} as Part, + } as unknown as ConcreteNode, + { + id: 'C', + type: NodeType.USER_PROMPT, + payload: {} as Part, + } as unknown as ConcreteNode, ]; const tokenMap: Record = { - 'C': 40000, - 'B': 40000, - 'A': 10000 + C: 40000, + B: 40000, + A: 10000, }; const orchestrator = { - executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) => nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id))) + executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) => + nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id)), + ), } as unknown as PipelineOrchestrator; - const sidecar = { + const sidecar = { config: { - budget: { maxTokens: 150000, retainedTokens: 65000 } - } + budget: { maxTokens: 150000, retainedTokens: 65000 }, + }, } as unknown as ContextProfile; - + const currentTokens = 160000; const env = { llmClient: { - countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }) + countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }), }, tokenCalculator: { calculateConcreteListTokens: vi.fn((nodes) => { @@ -157,15 +200,26 @@ describe('render', () => { calculateTokenBreakdown: vi.fn(() => ({})), }, graphMapper: { - fromGraph: vi.fn((nodes: readonly ConcreteNode[]) => nodes.map(n => ({ text: n.id }))) - } + fromGraph: vi.fn((nodes: readonly ConcreteNode[]) => + nodes.map((n) => ({ text: n.id })), + ), + }, } as unknown as ContextEnvironment; - + const tracer = { logEvent: vi.fn(), } as unknown as ContextTracer; - const result = await render(mockNodes, orchestrator, sidecar, tracer, env, new Map(), 0, new Set()); + const result = await render( + mockNodes, + orchestrator, + sidecar, + tracer, + env, + new Map(), + 0, + new Set(), + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const surviving = result.history.map((c: any) => c.text); diff --git a/packages/core/src/context/graph/render.ts b/packages/core/src/context/graph/render.ts index 813ae8a767e..663fb0266d3 100644 --- a/packages/core/src/context/graph/render.ts +++ b/packages/core/src/context/graph/render.ts @@ -60,15 +60,22 @@ export async function render( }); // Fire-and-forget asynchronous calibration to track estimator drift - const performCalibration = (finalNodes: readonly ConcreteNode[], finalContents: Content[]) => { + const performCalibration = ( + finalNodes: readonly ConcreteNode[], + finalContents: Content[], + ) => { void (async () => { try { - const exactResp = await env.llmClient.countTokens({contents: finalContents}); - const exactTokens = typeof exactResp.totalTokens === 'number' ? exactResp.totalTokens : 0; - const estimatedTokens = env.tokenCalculator.calculateConcreteListTokens(finalNodes); - + const exactResp = await env.llmClient.countTokens({ + contents: finalContents, + }); + const exactTokens = + typeof exactResp.totalTokens === 'number' ? exactResp.totalTokens : 0; + const estimatedTokens = + env.tokenCalculator.calculateConcreteListTokens(finalNodes); + const delta = Math.abs(exactTokens - estimatedTokens); - const tolerance = Math.max(exactTokens, estimatedTokens) * 0.20; // 20% tolerance + const tolerance = Math.max(exactTokens, estimatedTokens) * 0.2; // 20% tolerance tracer.logEvent('Render', 'Token Calibration Measurement', { exactTokens, @@ -78,7 +85,9 @@ export async function render( }); if (delta > tolerance) { - debugLogger.log(`[Token Calibration] Large deviation detected: exact ${exactTokens} vs estimated ${estimatedTokens} (delta: ${delta})`); + debugLogger.error( + `[Token Calibration] Large deviation detected: exact ${exactTokens} vs estimated ${estimatedTokens} (delta: ${delta})`, + ); } } catch { // Ignore API failures during background calibration diff --git a/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap b/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap index 66bf020f8ea..a918738ed20 100644 --- a/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap +++ b/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap @@ -6,7 +6,10 @@ exports[`System Lifecycle Golden Tests > Scenario 1: Organic Growth with Huge To { "parts": [ { - "text": "[Continuing from previous AI thoughts...]", + "inlineData": { + "data": "fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_", + "mimeType": "image/png", + }, }, ], "role": "user", @@ -37,9 +40,6 @@ exports[`System Lifecycle Golden Tests > Scenario 1: Organic Growth with Huge To }, { "parts": [ - { - "text": "[Multi-Modal Blob (image/png, 0.01MB) degraded to text to preserve context window. Saved to: ]", - }, { "text": "", }, @@ -59,17 +59,17 @@ exports[`System Lifecycle Golden Tests > Scenario 1: Organic Growth with Huge To "turnIndex": 1, }, { - "tokensAfterBackground": 327, + "tokensAfterBackground": 20062, "tokensBeforeBackground": 20172, "turnIndex": 2, }, { - "tokensAfterBackground": 393, + "tokensAfterBackground": 3329, "tokensBeforeBackground": 23197, "turnIndex": 3, }, { - "tokensAfterBackground": 411, + "tokensAfterBackground": 3347, "tokensBeforeBackground": 23215, "turnIndex": 4, }, diff --git a/packages/core/src/core/baseLlmClient.ts b/packages/core/src/core/baseLlmClient.ts index b8219deb941..5c1b7f66fa1 100644 --- a/packages/core/src/core/baseLlmClient.ts +++ b/packages/core/src/core/baseLlmClient.ts @@ -230,11 +230,17 @@ export class BaseLlmClient { return text; } - async countTokens(options: CountTokenOptions): Promise<{ totalTokens: number }> { - const model = options.modelConfigKey ? - this.config.modelConfigService.getResolvedConfig(options.modelConfigKey).model : - this.config.getActiveModel(); - const result = await this.contentGenerator.countTokens({ model, contents: options.contents }); + async countTokens( + options: CountTokenOptions, + ): Promise<{ totalTokens: number }> { + const model = options.modelConfigKey + ? this.config.modelConfigService.getResolvedConfig(options.modelConfigKey) + .model + : this.config.getActiveModel(); + const result = await this.contentGenerator.countTokens({ + model, + contents: options.contents, + }); return { totalTokens: result.totalTokens || 0 }; } From 4eac27bf5a1886ccc27877ff7fbeb6d31321252d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 17:32:14 +0000 Subject: [PATCH 3/5] test(context): restore strictness of lifecycle tests and assert new token counts Since we expanded the boundary limit check to be loose, the test data sizes were no longer large enough to trigger the boundaries and test the GC execution. Added more test data so the limits exceed and the tests work properly again. --- .../lifecycle.golden.test.ts.snap | 45 ++++++++++++++----- .../system-tests/lifecycle.golden.test.ts | 28 +++++++++--- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap b/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap index a918738ed20..3e94acf7a19 100644 --- a/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap +++ b/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap @@ -142,7 +142,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "text": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", }, ], "role": "user", @@ -150,7 +150,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "text": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", }, ], "role": "model", @@ -158,7 +158,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "text": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", }, ], "role": "user", @@ -166,7 +166,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "text": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", }, ], "role": "model", @@ -174,7 +174,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", + "text": "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", }, ], "role": "user", @@ -182,7 +182,23 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "text": "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "text": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "text": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", }, ], "role": "model", @@ -198,20 +214,25 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC ], "tokenTrajectory": [ { - "tokensAfterBackground": 42, - "tokensBeforeBackground": 42, + "tokensAfterBackground": 340, + "tokensBeforeBackground": 340, "turnIndex": 0, }, { - "tokensAfterBackground": 84, - "tokensBeforeBackground": 84, + "tokensAfterBackground": 680, + "tokensBeforeBackground": 680, "turnIndex": 1, }, { - "tokensAfterBackground": 126, - "tokensBeforeBackground": 126, + "tokensAfterBackground": 1020, + "tokensBeforeBackground": 1020, "turnIndex": 2, }, + { + "tokensAfterBackground": 1360, + "tokensBeforeBackground": 1360, + "turnIndex": 3, + }, ], } `; diff --git a/packages/core/src/context/system-tests/lifecycle.golden.test.ts b/packages/core/src/context/system-tests/lifecycle.golden.test.ts index 46f082e09c2..aa68272754e 100644 --- a/packages/core/src/context/system-tests/lifecycle.golden.test.ts +++ b/packages/core/src/context/system-tests/lifecycle.golden.test.ts @@ -169,6 +169,9 @@ describe('System Lifecycle Golden Tests', () => { { role: 'model', parts: [{ text: 'Yes we can.' }] }, ]); + // Give the background tasks a moment to inject the snapshot into the graph + await new Promise((resolve) => setTimeout(resolve, 50)); + // Get final state const goldenState = await harness.getGoldenState(); @@ -238,23 +241,34 @@ describe('System Lifecycle Golden Tests', () => { // Turn 0 await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'A'.repeat(50) }] }, - { role: 'model', parts: [{ text: 'B'.repeat(50) }] }, + { role: 'user', parts: [{ text: 'A'.repeat(500) }] }, + { role: 'model', parts: [{ text: 'B'.repeat(500) }] }, ]); // Turn 1 (Should trigger StateSnapshotasync pipeline because we exceed 100 retainedTokens) await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'C'.repeat(50) }] }, - { role: 'model', parts: [{ text: 'D'.repeat(50) }] }, + { role: 'user', parts: [{ text: 'C'.repeat(500) }] }, + { role: 'model', parts: [{ text: 'D'.repeat(500) }] }, + ]); + + // Extra turn to push it over the loose boundary. + // Turn 0 = ~330 tokens. Turn 1 = ~330 tokens. Turn 2 = ~330 tokens. + // Total = ~990 tokens. Backwards loop: + // T2 (330) -> prior 0 + // T1 (330) -> prior 330. 330 > 100, so T1 is kept (loose boundary) + // T0 (330) -> prior 660. 660 > 100, so T0 is aged out! + await harness.simulateTurn([ + { role: 'user', parts: [{ text: 'G'.repeat(500) }] }, + { role: 'model', parts: [{ text: 'H'.repeat(500) }] }, ]); // Give the async background pipeline an extra beat to complete its async execution and emit variants await new Promise((resolve) => setTimeout(resolve, 50)); - // Turn 2 + // Turn 3 await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'E'.repeat(50) }] }, - { role: 'model', parts: [{ text: 'F'.repeat(50) }] }, + { role: 'user', parts: [{ text: 'E'.repeat(500) }] }, + { role: 'model', parts: [{ text: 'F'.repeat(500) }] }, ]); const goldenState = await harness.getGoldenState(); From 2752a58784f279e782d5f7a30f26c13703dc1215 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 18:13:05 +0000 Subject: [PATCH 4/5] fix(context): ensure snapshot summaries are injected at the chronological start of the working buffer Previously, when async snapshot pipelines finished and injected their summaries back into the WorkingBuffer, they relied on finding the index of the raw nodes they had summarized. If those nodes had already been aged-out by a synchronous backstop pass, the index lookup yielded -1, causing the summary to be appended to the END of the conversation history. This confused the LLM by presenting old context as the newest turn. The WorkingBuffer now gracefully unshifts summary nodes to the head of the array if their raw anchors are missing. --- packages/core/src/context/contextManager.ts | 7 +- packages/core/src/context/graph/render.ts | 43 +--- .../core/src/context/pipeline/environment.ts | 3 + .../lifecycle.golden.test.ts.snap | 201 +++++++++++++++--- .../system-tests/lifecycle.golden.test.ts | 198 +++++++++-------- .../src/context/testing/contextTestUtils.ts | 53 ++--- .../src/context/utils/tokenCalibration.ts | 49 +++++ 7 files changed, 373 insertions(+), 181 deletions(-) create mode 100644 packages/core/src/context/utils/tokenCalibration.ts diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts index 0ed52fcd2b7..e949090cc14 100644 --- a/packages/core/src/context/contextManager.ts +++ b/packages/core/src/context/contextManager.ts @@ -72,7 +72,11 @@ export class ContextManager { event.targets, event.returnedNodes, ); - this.evaluateTriggers(new Set()); + // We explicitly DO NOT call evaluateTriggers here. + // The Context Manager is a one-way assembly line. It only evaluates triggers + // when fundamentally new organic context is added via PristineHistoryUpdated. + // Re-evaluating after a processor finishes creates infinite feedback loops if + // the processor fails to reduce the token count below the threshold. }); this.historyObserver.start(); @@ -141,6 +145,7 @@ export class ContextManager { } } } + if (agedOutNodes.size > 0) { const targetDeficit = currentTokens - this.sidecar.config.budget.retainedTokens; diff --git a/packages/core/src/context/graph/render.ts b/packages/core/src/context/graph/render.ts index 663fb0266d3..5c0fa3df0ed 100644 --- a/packages/core/src/context/graph/render.ts +++ b/packages/core/src/context/graph/render.ts @@ -10,7 +10,7 @@ import type { ContextTracer } from '../tracer.js'; import type { ContextProfile } from '../config/profiles.js'; import type { PipelineOrchestrator } from '../pipeline/orchestrator.js'; import type { ContextEnvironment } from '../pipeline/environment.js'; -import { debugLogger } from '../../utils/debugLogger.js'; +import { performCalibration } from '../utils/tokenCalibration.js'; /** * Maps the Episodic Context Graph back into a raw Gemini Content[] array for transmission. @@ -59,42 +59,6 @@ export async function render( reasons: Object.fromEntries(protectionReasons), }); - // Fire-and-forget asynchronous calibration to track estimator drift - const performCalibration = ( - finalNodes: readonly ConcreteNode[], - finalContents: Content[], - ) => { - void (async () => { - try { - const exactResp = await env.llmClient.countTokens({ - contents: finalContents, - }); - const exactTokens = - typeof exactResp.totalTokens === 'number' ? exactResp.totalTokens : 0; - const estimatedTokens = - env.tokenCalculator.calculateConcreteListTokens(finalNodes); - - const delta = Math.abs(exactTokens - estimatedTokens); - const tolerance = Math.max(exactTokens, estimatedTokens) * 0.2; // 20% tolerance - - tracer.logEvent('Render', 'Token Calibration Measurement', { - exactTokens, - estimatedTokens, - delta, - isWithinTolerance: delta <= tolerance, - }); - - if (delta > tolerance) { - debugLogger.error( - `[Token Calibration] Large deviation detected: exact ${exactTokens} vs estimated ${estimatedTokens} (delta: ${delta})`, - ); - } - } catch { - // Ignore API failures during background calibration - } - })(); - }; - if (currentTokens <= maxTokens) { tracer.logEvent( 'Render', @@ -105,7 +69,7 @@ export async function render( tracer.logEvent('Render', 'Render Context for LLM', { renderedContext: contents, }); - performCalibration(visibleNodes, contents); + performCalibration(env, visibleNodes, contents); return { history: contents, didApplyManagement: false }; } const targetDelta = currentTokens - sidecar.config.budget.retainedTokens; @@ -130,6 +94,7 @@ export async function render( agedOutNodes.add(node.id); } } + const processedNodes = await orchestrator.executeTriggerSync( 'gc_backstop', nodes, @@ -153,6 +118,6 @@ export async function render( tracer.logEvent('Render', 'Render Sanitized Context for LLM', { renderedContextSanitized: contents, }); - performCalibration(visibleNodes, contents); + performCalibration(env, visibleNodes, contents); return { history: contents, didApplyManagement: true }; } diff --git a/packages/core/src/context/pipeline/environment.ts b/packages/core/src/context/pipeline/environment.ts index 92c0173e920..0ec9a40fae3 100644 --- a/packages/core/src/context/pipeline/environment.ts +++ b/packages/core/src/context/pipeline/environment.ts @@ -26,4 +26,7 @@ export interface ContextEnvironment { readonly inbox: LiveInbox; readonly behaviorRegistry: NodeBehaviorRegistry; readonly graphMapper: ContextGraphMapper; + readonly renderOptions?: { + calibrateTokenCalculation?: boolean; + }; } diff --git a/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap b/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap index 3e94acf7a19..201fbac191a 100644 --- a/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap +++ b/packages/core/src/context/system-tests/__snapshots__/lifecycle.golden.test.ts.snap @@ -6,14 +6,91 @@ exports[`System Lifecycle Golden Tests > Scenario 1: Organic Growth with Huge To { "parts": [ { - "inlineData": { - "data": "fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_fake_base64_data_", - "mimeType": "image/png", + "text": "System Instructions", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "text": "Ack.", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "text": "Hello!", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "text": "Hi, how can I help?", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "text": "Read the logs.", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "functionCall": { + "args": { + "cmd": "cat server.log", + }, + "name": "run_shell_command", + }, + "thoughtSignature": "skip_thought_signature_validator", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "functionResponse": { + "name": "run_shell_command", + "response": { + "output": " +[Tool observation string (0.02MB, 1 lines) masked to preserve context window. Full string saved to: ] +", + }, }, }, ], "role": "user", }, + { + "parts": [ + { + "text": "The logs are very long.", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "text": "Look at this architecture diagram:", + }, + { + "text": "[Multi-Modal Blob (image/png, 0.01MB) degraded to text to preserve context window. Saved to: ]", + }, + ], + "role": "user", + }, { "parts": [ { @@ -41,7 +118,7 @@ exports[`System Lifecycle Golden Tests > Scenario 1: Organic Growth with Huge To { "parts": [ { - "text": "", + "text": "Please continue.", }, ], "role": "user", @@ -59,18 +136,18 @@ exports[`System Lifecycle Golden Tests > Scenario 1: Organic Growth with Huge To "turnIndex": 1, }, { - "tokensAfterBackground": 20062, + "tokensAfterBackground": 437, "tokensBeforeBackground": 20172, "turnIndex": 2, }, { - "tokensAfterBackground": 3329, - "tokensBeforeBackground": 23197, + "tokensAfterBackground": 526, + "tokensBeforeBackground": 3462, "turnIndex": 3, }, { - "tokensAfterBackground": 3347, - "tokensBeforeBackground": 23215, + "tokensAfterBackground": 544, + "tokensBeforeBackground": 544, "turnIndex": 4, }, ], @@ -136,13 +213,13 @@ exports[`System Lifecycle Golden Tests > Scenario 2: Under Budget (No Modificati } `; -exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC 1`] = ` +exports[`System Lifecycle Golden Tests > Scenario 3: Node Distillation of Large Historical Messages 1`] = ` { "finalProjection": [ { "parts": [ { - "text": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "text": "Mock response from: utility_compressor, for: {"text":"A...AAAAAAAA"}", }, ], "role": "user", @@ -150,7 +227,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "text": "Mock response from: utility_compressor, for: {"text":"B...BBBBBBBB"}", }, ], "role": "model", @@ -158,7 +235,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "text": "Mock response from: utility_compressor, for: {"text":"C...CCCCCCCC"}", }, ], "role": "user", @@ -166,7 +243,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "text": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", }, ], "role": "model", @@ -174,7 +251,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", + "text": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", }, ], "role": "user", @@ -182,7 +259,7 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", + "text": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", }, ], "role": "model", @@ -190,15 +267,82 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC { "parts": [ { - "text": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", + "text": "Please continue.", }, ], "role": "user", }, + ], + "tokenTrajectory": [ + { + "tokensAfterBackground": 3308, + "tokensBeforeBackground": 3308, + "turnIndex": 0, + }, + { + "tokensAfterBackground": 4989, + "tokensBeforeBackground": 6616, + "turnIndex": 1, + }, + { + "tokensAfterBackground": 5043, + "tokensBeforeBackground": 8297, + "turnIndex": 2, + }, + ], +} +`; + +exports[`System Lifecycle Golden Tests > Scenario 4: Async-Driven Background GC via State Snapshots 1`] = ` +{ + "finalProjection": [ { "parts": [ { - "text": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "text": "Mock response from: utility_state_snapshot_processor, for: {"text":"T.........\\n"}", + }, + { + "text": "Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 Msg 4 ..................................................", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "text": "Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 Msg 5 ..................................................", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "text": "Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 Msg 6 ..................................................", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "text": "Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 Msg 7 ..................................................", + }, + ], + "role": "model", + }, + { + "parts": [ + { + "text": "Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 Msg 8 ..................................................", + }, + ], + "role": "user", + }, + { + "parts": [ + { + "text": "Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 Msg 9 ..................................................", }, ], "role": "model", @@ -214,25 +358,30 @@ exports[`System Lifecycle Golden Tests > Scenario 3: Async-Driven Background GC ], "tokenTrajectory": [ { - "tokensAfterBackground": 340, - "tokensBeforeBackground": 340, + "tokensAfterBackground": 140, + "tokensBeforeBackground": 140, "turnIndex": 0, }, { - "tokensAfterBackground": 680, - "tokensBeforeBackground": 680, + "tokensAfterBackground": 280, + "tokensBeforeBackground": 280, "turnIndex": 1, }, { - "tokensAfterBackground": 1020, - "tokensBeforeBackground": 1020, + "tokensAfterBackground": 420, + "tokensBeforeBackground": 420, "turnIndex": 2, }, { - "tokensAfterBackground": 1360, - "tokensBeforeBackground": 1360, + "tokensAfterBackground": 560, + "tokensBeforeBackground": 560, "turnIndex": 3, }, + { + "tokensAfterBackground": 700, + "tokensBeforeBackground": 700, + "turnIndex": 4, + }, ], } `; diff --git a/packages/core/src/context/system-tests/lifecycle.golden.test.ts b/packages/core/src/context/system-tests/lifecycle.golden.test.ts index aa68272754e..82b2cf6898b 100644 --- a/packages/core/src/context/system-tests/lifecycle.golden.test.ts +++ b/packages/core/src/context/system-tests/lifecycle.golden.test.ts @@ -9,11 +9,7 @@ import fs from 'node:fs'; import { SimulationHarness } from './simulationHarness.js'; import { createMockLlmClient } from '../testing/contextTestUtils.js'; import type { ContextProfile } from '../config/profiles.js'; -import { createToolMaskingProcessor } from '../processors/toolMaskingProcessor.js'; -import { createBlobDegradationProcessor } from '../processors/blobDegradationProcessor.js'; -import { createStateSnapshotProcessor } from '../processors/stateSnapshotProcessor.js'; -import { createHistoryTruncationProcessor } from '../processors/historyTruncationProcessor.js'; -import { createStateSnapshotAsyncProcessor } from '../processors/stateSnapshotAsyncProcessor.js'; +import { stressTestProfile } from '../config/profiles.js'; expect.addSnapshotSerializer({ test: (val) => @@ -52,57 +48,22 @@ describe('System Lifecycle Golden Tests', () => { vi.restoreAllMocks(); }); - const getAggressiveConfig = (): ContextProfile => ({ - name: 'Aggressive Test', - config: { - budget: { maxTokens: 1000, retainedTokens: 500 }, // Extremely tight limits - }, - buildPipelines: (env) => [ - { - name: 'Pressure Relief', // Emits from eventBus 'retained_exceeded' - triggers: ['retained_exceeded'], - processors: [ - createBlobDegradationProcessor('BlobDegradationProcessor', env), - createToolMaskingProcessor('ToolMaskingProcessor', env, { - stringLengthThresholdTokens: 50, - }), - createStateSnapshotProcessor('StateSnapshotProcessor', env, {}), - ], - }, - { - name: 'Immediate Sanitization', // The magic string the projector is hardcoded to use - triggers: ['retained_exceeded'], - processors: [ - createHistoryTruncationProcessor( - 'HistoryTruncationProcessor', - env, - {}, - ), - ], - }, - ], - buildAsyncPipelines: (env) => [ - { - name: 'Async', - triggers: ['nodes_aged_out'], - processors: [ - createStateSnapshotAsyncProcessor( - 'StateSnapshotAsyncProcessor', - env, - {}, - ), - ], - }, - ], - }); - - const mockLlmClient = createMockLlmClient([ - '', - ]); + // Uses dynamic role-based mocking to differentiate Snapshot vs Distillation output automatically. + const mockLlmClient = createMockLlmClient(); it('Scenario 1: Organic Growth with Huge Tool Output & Images', async () => { + // Override stressTestProfile limits slightly to ensure immediate overflow + // without having to push 50,000 characters to cross the generalist boundaries. + const customProfile: ContextProfile = { + ...stressTestProfile, + config: { + ...stressTestProfile.config, + budget: { maxTokens: 1000, retainedTokens: 500 }, + }, + }; + const harness = await SimulationHarness.create( - getAggressiveConfig(), + customProfile, mockLlmClient, ); @@ -175,6 +136,11 @@ describe('System Lifecycle Golden Tests', () => { // Get final state const goldenState = await harness.getGoldenState(); + console.log( + 'SCENARIO 1 FINAL PROJECTION TURN 2:', + JSON.stringify(goldenState.finalProjection[2], null, 2), + ); + // In a perfectly functioning opportunistic system, the token trajectory should show // the massive spikes in Turn 2 and 3 being immediately resolved by the background tasks. // The final projection should fit neatly under the Max Tokens limit. @@ -215,65 +181,117 @@ describe('System Lifecycle Golden Tests', () => { expect(goldenState).toMatchSnapshot(); }); - it('Scenario 3: Async-Driven Background GC', async () => { - const gcConfig: ContextProfile = { - name: 'GC Test Config', + it('Scenario 3: Node Distillation of Large Historical Messages', async () => { + // 1 Turn = ~2520 tokens. + // retainedTokens = 4000 ensures Turn 0 is kept intact until Turn 1 pushes the total to ~5040. + const customProfile: ContextProfile = { + ...stressTestProfile, config: { - budget: { maxTokens: 200, retainedTokens: 100 }, - }, - buildPipelines: () => [], - buildAsyncPipelines: (env) => [ - { - name: 'Async', - triggers: ['nodes_aged_out'], - processors: [ - createStateSnapshotAsyncProcessor( - 'StateSnapshotAsyncProcessor', - env, - {}, - ), - ], + ...stressTestProfile.config, + budget: { maxTokens: 10000, retainedTokens: 4000 }, + processorOptions: { + ...stressTestProfile.config?.processorOptions, + NodeDistillation: { + type: 'NodeDistillationProcessor', + options: { + nodeThresholdTokens: 1000, // 1250 > 1000, so older messages will be distilled + }, + }, }, - ], + }, + // Disable async pipelines (StateSnapshots) so they don't compete with the Normalization pipeline + buildAsyncPipelines: () => [], }; - const harness = await SimulationHarness.create(gcConfig, mockLlmClient); + const harness = await SimulationHarness.create( + customProfile, + mockLlmClient, + ); // Turn 0 await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'A'.repeat(500) }] }, - { role: 'model', parts: [{ text: 'B'.repeat(500) }] }, + { role: 'user', parts: [{ text: 'A'.repeat(5000) }] }, + { role: 'model', parts: [{ text: 'B'.repeat(5000) }] }, + ]); + + // Turn 1 + await harness.simulateTurn([ + { role: 'user', parts: [{ text: 'C'.repeat(5000) }] }, + { role: 'model', parts: [{ text: 'D'.repeat(5000) }] }, + ]); + + // Turn 2 + await harness.simulateTurn([ + { role: 'user', parts: [{ text: 'E'.repeat(5000) }] }, + { role: 'model', parts: [{ text: 'F'.repeat(5000) }] }, + ]); + + const goldenState = await harness.getGoldenState(); + + // We should see MOCKED_DISTILLED_NODE replacing older bloated messages, while recent messages are untouched. + expect(goldenState).toMatchSnapshot(); + }); + + it('Scenario 4: Async-Driven Background GC via State Snapshots', async () => { + // Mathematical Token Budgeting: + // 200 chars ≈ 50 tokens. + // 1 Turn (User + Model + Overhead) ≈ 50 + 50 + 20 = 120 Tokens. + const customProfile: ContextProfile = { + ...stressTestProfile, + config: { + ...stressTestProfile.config, + // Retain 3 Turns (~360 tokens). Max 5 Turns (~600 tokens). + budget: { maxTokens: 600, retainedTokens: 360 }, + }, + }; + + const harness = await SimulationHarness.create( + customProfile, + mockLlmClient, + ); + + const createMessage = (index: number) => + `Msg ${index} `.repeat(25).padEnd(200, '.'); + + // Turn 0 (~120 tokens) Total: 120 + await harness.simulateTurn([ + { role: 'user', parts: [{ text: createMessage(0) }] }, + { role: 'model', parts: [{ text: createMessage(1) }] }, + ]); + + // Turn 1 (~120 tokens) Total: 240 + await harness.simulateTurn([ + { role: 'user', parts: [{ text: createMessage(2) }] }, + { role: 'model', parts: [{ text: createMessage(3) }] }, ]); - // Turn 1 (Should trigger StateSnapshotasync pipeline because we exceed 100 retainedTokens) + // Turn 2 (~120 tokens) Total: 360 (At retainedTokens boundary) await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'C'.repeat(500) }] }, - { role: 'model', parts: [{ text: 'D'.repeat(500) }] }, + { role: 'user', parts: [{ text: createMessage(4) }] }, + { role: 'model', parts: [{ text: createMessage(5) }] }, ]); - // Extra turn to push it over the loose boundary. - // Turn 0 = ~330 tokens. Turn 1 = ~330 tokens. Turn 2 = ~330 tokens. - // Total = ~990 tokens. Backwards loop: - // T2 (330) -> prior 0 - // T1 (330) -> prior 330. 330 > 100, so T1 is kept (loose boundary) - // T0 (330) -> prior 660. 660 > 100, so T0 is aged out! + // Turn 3 (~120 tokens) Total: 480 (Exceeds retainedTokens! Triggers GC on Turn 0 & 1) await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'G'.repeat(500) }] }, - { role: 'model', parts: [{ text: 'H'.repeat(500) }] }, + { role: 'user', parts: [{ text: createMessage(6) }] }, + { role: 'model', parts: [{ text: createMessage(7) }] }, ]); - // Give the async background pipeline an extra beat to complete its async execution and emit variants + // Give the async background snapshot pipeline time to complete await new Promise((resolve) => setTimeout(resolve, 50)); - // Turn 3 + // Turn 4 (~120 tokens). + // If GC succeeded, Turn 0 and 1 are now a ~10 token snapshot. + // Total should be: 10 (Snapshot) + 120 (Turn 2) + 120 (Turn 3) + 120 (Turn 4) = ~370 tokens. await harness.simulateTurn([ - { role: 'user', parts: [{ text: 'E'.repeat(500) }] }, - { role: 'model', parts: [{ text: 'F'.repeat(500) }] }, + { role: 'user', parts: [{ text: createMessage(8) }] }, + { role: 'model', parts: [{ text: createMessage(9) }] }, ]); const goldenState = await harness.getGoldenState(); - // We should see ROLLING_SUMMARY nodes injected into the graph, proving the async pipeline ran in the background + // We should see a MOCKED_STATE_SNAPSHOT_SUMMARY rolling up Turns 0 and 1, + // while Turns 2, 3, and 4 remain fully intact. expect(goldenState).toMatchSnapshot(); }); }); diff --git a/packages/core/src/context/testing/contextTestUtils.ts b/packages/core/src/context/testing/contextTestUtils.ts index 898c0988806..9cbdeec9172 100644 --- a/packages/core/src/context/testing/contextTestUtils.ts +++ b/packages/core/src/context/testing/contextTestUtils.ts @@ -19,7 +19,10 @@ import { } from '../graph/types.js'; import type { ContextEnvironment } from '../pipeline/environment.js'; import type { Config } from '../../config/config.js'; -import type { BaseLlmClient } from '../../core/baseLlmClient.js'; +import type { + BaseLlmClient, + GenerateContentOptions, +} from '../../core/baseLlmClient.js'; import type { Content, GenerateContentResponse } from '@google/genai'; import { InboxSnapshotImpl } from '../pipeline/inbox.js'; import type { InboxMessage, ProcessArgs } from '../pipeline.js'; @@ -98,38 +101,38 @@ export function createDummyToolNode( export interface MockLlmClient extends BaseLlmClient { generateContent: Mock; + countTokens: Mock; } export function createMockLlmClient( responses?: Array, ): MockLlmClient { - const generateContentMock = vi.fn(); - - if (responses && responses.length > 0) { - for (const response of responses) { - if (typeof response === 'string') { - generateContentMock.mockResolvedValueOnce( - createMockGenerateContentResponse(response), + const generateContentMock = vi + .fn() + .mockImplementation((options: GenerateContentOptions) => { + // Array-based logic for backwards compatibility, if provided + if (responses && responses.length > 0) { + const callCount = generateContentMock.mock.calls.length - 1; + const idx = + callCount < responses.length ? callCount : responses.length - 1; + const res = responses[idx]; + return Promise.resolve( + typeof res === 'string' + ? createMockGenerateContentResponse(res) + : res, ); - } else { - generateContentMock.mockResolvedValueOnce(response); } - } - // Fallback to the last response for any subsequent calls - const lastResponse = responses[responses.length - 1]; - if (typeof lastResponse === 'string') { - generateContentMock.mockResolvedValue( - createMockGenerateContentResponse(lastResponse), + + const lastContent = options.contents[options.contents.length - 1]; + const lastPart = lastContent?.parts?.[lastContent.parts.length - 1]; + const lastPartString = JSON.stringify(lastPart ?? {}); + const contentSample = `${lastPartString.slice(0, 10)}...${lastPartString.slice(-10)}`; + return Promise.resolve( + createMockGenerateContentResponse( + `Mock response from: ${options.role}, for: ${contentSample}`, + ), ); - } else { - generateContentMock.mockResolvedValue(lastResponse); - } - } else { - // Default fallback - generateContentMock.mockResolvedValue( - createMockGenerateContentResponse('Mock LLM response'), - ); - } + }); // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return { diff --git a/packages/core/src/context/utils/tokenCalibration.ts b/packages/core/src/context/utils/tokenCalibration.ts new file mode 100644 index 00000000000..f153e84db6f --- /dev/null +++ b/packages/core/src/context/utils/tokenCalibration.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import type { Content } from '@google/genai'; +import type { ContextEnvironment } from '../pipeline/environment.js'; +import type { ConcreteNode } from '../graph/types.js'; +import { debugLogger } from '../../utils/debugLogger.js'; + +export function performCalibration( + env: ContextEnvironment, + finalNodes: readonly ConcreteNode[], + finalContents: Content[], +) { + if (!env.renderOptions?.calibrateTokenCalculation) { + return; + } + + void (async () => { + try { + const exactResp = await env.llmClient.countTokens({ + contents: finalContents, + }); + const exactTokens = + typeof exactResp.totalTokens === 'number' ? exactResp.totalTokens : 0; + const estimatedTokens = + env.tokenCalculator.calculateConcreteListTokens(finalNodes); + + const delta = Math.abs(exactTokens - estimatedTokens); + const tolerance = Math.max(exactTokens, estimatedTokens) * 0.2; // 20% tolerance + + env.tracer.logEvent('Render', 'Token Calibration Measurement', { + exactTokens, + estimatedTokens, + delta, + isWithinTolerance: delta <= tolerance, + }); + + if (delta > tolerance) { + debugLogger.error( + `[Token Calibration] Large deviation detected: exact ${exactTokens} vs estimated ${estimatedTokens} (delta: ${delta})`, + ); + } + } catch { + // Ignore API failures during background calibration + } + })(); +} From 6c0612d579742a5992de7eb7e9cecdfdf722ada1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 23:47:33 +0000 Subject: [PATCH 5/5] final details --- packages/core/src/context/initializer.ts | 4 ++++ packages/core/src/context/pipeline/environment.ts | 8 +++++--- packages/core/src/context/pipeline/environmentImpl.ts | 3 ++- .../src/context/system-tests/lifecycle.golden.test.ts | 5 ----- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/src/context/initializer.ts b/packages/core/src/context/initializer.ts index cffaae20b7c..3b37d2bac7a 100644 --- a/packages/core/src/context/initializer.ts +++ b/packages/core/src/context/initializer.ts @@ -94,6 +94,10 @@ export async function initializeContextManager( tracer, 4, eventBus, + { + calibrateTokenCalculation: + !!process.env['GEMINI_CONTEXT_CALIBRATE_TOKEN_CALCULATIONS'], + }, ); const orchestrator = new PipelineOrchestrator( diff --git a/packages/core/src/context/pipeline/environment.ts b/packages/core/src/context/pipeline/environment.ts index 0ec9a40fae3..b57466638a0 100644 --- a/packages/core/src/context/pipeline/environment.ts +++ b/packages/core/src/context/pipeline/environment.ts @@ -13,6 +13,10 @@ import type { ContextGraphMapper } from '../graph/mapper.js'; export type { ContextTracer, ContextEventBus }; +export interface RenderOptions { + calibrateTokenCalculation?: boolean; +} + export interface ContextEnvironment { readonly llmClient: BaseLlmClient; readonly promptId: string; @@ -26,7 +30,5 @@ export interface ContextEnvironment { readonly inbox: LiveInbox; readonly behaviorRegistry: NodeBehaviorRegistry; readonly graphMapper: ContextGraphMapper; - readonly renderOptions?: { - calibrateTokenCalculation?: boolean; - }; + readonly renderOptions?: RenderOptions; } diff --git a/packages/core/src/context/pipeline/environmentImpl.ts b/packages/core/src/context/pipeline/environmentImpl.ts index 67f45aaa7b3..736792d561a 100644 --- a/packages/core/src/context/pipeline/environmentImpl.ts +++ b/packages/core/src/context/pipeline/environmentImpl.ts @@ -6,7 +6,7 @@ import type { BaseLlmClient } from '../../core/baseLlmClient.js'; import type { ContextTracer } from '../tracer.js'; -import type { ContextEnvironment } from './environment.js'; +import type { ContextEnvironment, RenderOptions } from './environment.js'; import type { ContextEventBus } from '../eventBus.js'; import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js'; import { LiveInbox } from './inbox.js'; @@ -29,6 +29,7 @@ export class ContextEnvironmentImpl implements ContextEnvironment { readonly tracer: ContextTracer, readonly charsPerToken: number, readonly eventBus: ContextEventBus, + readonly renderOptions?: RenderOptions, ) { this.behaviorRegistry = new NodeBehaviorRegistry(); registerBuiltInBehaviors(this.behaviorRegistry); diff --git a/packages/core/src/context/system-tests/lifecycle.golden.test.ts b/packages/core/src/context/system-tests/lifecycle.golden.test.ts index 82b2cf6898b..9e6512e6462 100644 --- a/packages/core/src/context/system-tests/lifecycle.golden.test.ts +++ b/packages/core/src/context/system-tests/lifecycle.golden.test.ts @@ -136,11 +136,6 @@ describe('System Lifecycle Golden Tests', () => { // Get final state const goldenState = await harness.getGoldenState(); - console.log( - 'SCENARIO 1 FINAL PROJECTION TURN 2:', - JSON.stringify(goldenState.finalProjection[2], null, 2), - ); - // In a perfectly functioning opportunistic system, the token trajectory should show // the massive spikes in Turn 2 and 3 being immediately resolved by the background tasks. // The final projection should fit neatly under the Max Tokens limit.