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