Files
gemini-cli/packages/core/src/context/system-tests/SimulationHarness.ts
T

182 lines
5.8 KiB
TypeScript
Raw Normal View History

2026-04-07 00:47:39 +00:00
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { ContextManager } from '../contextManager.js';
import { AgentChatHistory } from '../../core/agentChatHistory.js';
import type { Content } from '@google/genai';
import type { SidecarConfig } from '../sidecar/types.js';
import { ContextEnvironmentImpl } from '../sidecar/environmentImpl.js';
import { ContextTracer } from '../tracer.js';
import { ContextEventBus } from '../eventBus.js';
2026-04-07 03:13:14 +00:00
import { PipelineOrchestrator } from '../sidecar/orchestrator.js';
import { registerBuiltInProcessors } from '../sidecar/builtins.js';
2026-04-07 04:46:04 +00:00
import { debugLogger } from '../../utils/debugLogger.js';
import { ProcessorRegistry } from '../sidecar/registry.js';
import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js';
import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
2026-04-07 00:47:39 +00:00
export interface TurnSummary {
turnIndex: number;
tokensBeforeBackground: number;
tokensAfterBackground: number;
}
export class SimulationHarness {
2026-04-07 02:16:06 +00:00
readonly chatHistory: AgentChatHistory;
contextManager!: ContextManager;
2026-04-07 03:13:14 +00:00
env!: ContextEnvironmentImpl;
orchestrator!: PipelineOrchestrator;
2026-04-07 02:16:06 +00:00
readonly eventBus: ContextEventBus;
config!: SidecarConfig;
2026-04-07 00:47:39 +00:00
private tracer!: ContextTracer;
private currentTurnIndex = 0;
private tokenTrajectory: TurnSummary[] = [];
2026-04-07 04:46:04 +00:00
static async create(
config: SidecarConfig,
mockLlmClient: BaseLlmClient,
mockTempDir = '/tmp/sim',
): Promise<SimulationHarness> {
2026-04-07 00:47:39 +00:00
const harness = new SimulationHarness();
await harness.init(config, mockLlmClient, mockTempDir);
return harness;
}
private constructor() {
this.chatHistory = new AgentChatHistory();
this.eventBus = new ContextEventBus();
}
private async init(
config: SidecarConfig,
2026-04-07 03:13:14 +00:00
mockLlmClient: BaseLlmClient,
2026-04-07 04:46:04 +00:00
mockTempDir: string,
2026-04-07 00:47:39 +00:00
) {
this.config = config;
2026-04-07 03:58:50 +00:00
const registry = new ProcessorRegistry();
2026-04-07 00:47:39 +00:00
// Register all standard processors
2026-04-07 03:58:50 +00:00
registerBuiltInProcessors(registry);
2026-04-07 00:47:39 +00:00
2026-04-07 04:46:04 +00:00
this.tracer = new ContextTracer({
targetDir: mockTempDir,
sessionId: 'sim-session',
});
2026-04-07 03:13:14 +00:00
this.env = new ContextEnvironmentImpl(
2026-04-07 00:47:39 +00:00
mockLlmClient,
'sim-prompt',
'sim-session',
mockTempDir,
mockTempDir,
this.tracer,
4, // 4 chars per token average
this.eventBus,
2026-04-07 04:46:04 +00:00
new InMemoryFileSystem(),
new DeterministicIdGenerator(),
2026-04-07 00:47:39 +00:00
);
2026-04-07 04:46:04 +00:00
this.orchestrator = new PipelineOrchestrator(
config,
this.env,
this.eventBus,
this.tracer,
registry,
);
this.contextManager = ContextManager.create(
config,
this.env,
this.tracer,
this.orchestrator,
registry,
);
2026-04-07 00:47:39 +00:00
this.contextManager.subscribeToHistory(this.chatHistory);
}
/**
* Simulates a single "Turn" (User input + Model/Tool outputs)
* A turn might consist of multiple Content messages (e.g. user prompt -> model call -> user response -> model answer)
*/
async simulateTurn(messages: Content[]) {
// 1. Append the new messages
const currentHistory = this.chatHistory.get();
2026-04-07 03:13:14 +00:00
this.chatHistory.set([...currentHistory, ...messages]);
2026-04-07 04:46:04 +00:00
2026-04-07 00:47:39 +00:00
// 2. Measure tokens immediately after append (Before background processing)
2026-04-08 02:34:06 +00:00
const tokensBefore = this.env.tokenCalculator.calculateConcreteListTokens(
this.contextManager.getShip(),
2026-04-07 04:46:04 +00:00
);
debugLogger.log(
`[Turn ${this.currentTurnIndex}] Tokens BEFORE: ${tokensBefore}`,
2026-04-07 00:47:39 +00:00
);
2026-04-07 04:46:04 +00:00
2026-04-07 00:47:39 +00:00
// 3. Yield to event loop to allow internal async subscribers and orchestrator to finish
2026-04-07 04:46:04 +00:00
await new Promise((resolve) => setTimeout(resolve, 50));
2026-04-07 00:47:39 +00:00
// 3.1 Simulate what projectCompressedHistory does with the sync handlers
2026-04-08 02:34:06 +00:00
let currentView = this.contextManager.getShip();
2026-04-07 04:46:04 +00:00
const currentTokens =
2026-04-08 02:34:06 +00:00
this.env.tokenCalculator.calculateConcreteListTokens(currentView);
2026-04-07 00:47:39 +00:00
if (this.config.budget && currentTokens > this.config.budget.maxTokens) {
2026-04-07 04:46:04 +00:00
debugLogger.log(
`[Turn ${this.currentTurnIndex}] Sync panic triggered! ${currentTokens} > ${this.config.budget.maxTokens}`,
);
2026-04-07 03:13:14 +00:00
const orchestrator = this.orchestrator;
2026-04-08 02:34:06 +00:00
// In the V2 simulation, we trigger the 'gc_backstop' to simulate emergency pressure.
currentView = await orchestrator.executeTriggerSync(
'gc_backstop',
currentView,
new Set(currentView.map(e => e.id)),
2026-04-08 23:37:46 +00:00
new Set<string>(),
);
2026-04-07 00:47:39 +00:00
// Inject the truncated view back into the graph
2026-04-07 01:57:36 +00:00
for (let i = 0; i < currentView.length; i++) {
2026-04-07 04:46:04 +00:00
const ep = currentView[i];
if (
!this.contextManager
2026-04-08 02:34:06 +00:00
.getShip()
2026-04-07 04:46:04 +00:00
.find((c) => c.id === ep.id)
) {
this.eventBus.emitVariantReady({
targetId: ep.id,
variantId: 'v-emergency',
variant: {
2026-04-08 17:12:12 +00:00
type: 'MASKED_TOOL',
id: 'mock-id',
tokens: { intent: 0, observation: 0 },
intent: {}, observation: {}, toolName: 'tool',
2026-04-07 04:46:04 +00:00
},
});
}
2026-04-07 00:47:39 +00:00
}
// Wait for variant propagation
2026-04-07 04:46:04 +00:00
await new Promise((resolve) => setTimeout(resolve, 50));
2026-04-07 00:47:39 +00:00
}
2026-04-07 04:46:04 +00:00
2026-04-07 00:47:39 +00:00
// 4. Measure tokens after background processors have (hopefully) emitted variants
2026-04-08 02:34:06 +00:00
const tokensAfter = this.env.tokenCalculator.calculateConcreteListTokens(
this.contextManager.getShip(),
2026-04-07 00:47:39 +00:00
);
2026-04-07 04:46:04 +00:00
debugLogger.log(
`[Turn ${this.currentTurnIndex}] Tokens AFTER: ${tokensAfter}`,
);
2026-04-07 00:47:39 +00:00
this.tokenTrajectory.push({
turnIndex: this.currentTurnIndex++,
tokensBeforeBackground: tokensBefore,
tokensAfterBackground: tokensAfter,
});
}
async getGoldenState() {
2026-04-07 04:46:04 +00:00
const finalProjection =
await this.contextManager.projectCompressedHistory();
2026-04-07 00:47:39 +00:00
return {
tokenTrajectory: this.tokenTrajectory,
2026-04-07 04:46:04 +00:00
finalProjection,
2026-04-07 00:47:39 +00:00
};
}
}