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

122 lines
3.4 KiB
TypeScript

/**
* @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 { ContextProfile } from '../config/profiles.js';
import { ContextEnvironmentImpl } from '../pipeline/environmentImpl.js';
import { ContextTracer } from '../tracer.js';
import { ContextEventBus } from '../eventBus.js';
import { PipelineOrchestrator } from '../pipeline/orchestrator.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
export interface TurnSummary {
turnIndex: number;
tokensBeforeBackground: number;
tokensAfterBackground: number;
}
export class SimulationHarness {
readonly chatHistory: AgentChatHistory;
contextManager!: ContextManager;
env!: ContextEnvironmentImpl;
orchestrator!: PipelineOrchestrator;
readonly eventBus: ContextEventBus;
config!: ContextProfile;
private tracer!: ContextTracer;
private currentTurnIndex = 0;
private tokenTrajectory: TurnSummary[] = [];
static async create(
config: ContextProfile,
mockLlmClient: BaseLlmClient,
mockTempDir = '/tmp/sim',
): Promise<SimulationHarness> {
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: ContextProfile,
mockLlmClient: BaseLlmClient,
mockTempDir: string,
) {
this.config = config;
this.tracer = new ContextTracer({
targetDir: mockTempDir,
sessionId: 'sim-session',
});
this.env = new ContextEnvironmentImpl(
() => mockLlmClient,
'sim-prompt',
'sim-session',
mockTempDir,
mockTempDir,
this.tracer,
1, // 1 char per token average for estimation (but estimator uses 0.33)
this.eventBus,
);
this.orchestrator = new PipelineOrchestrator(
config.buildPipelines(this.env),
config.buildAsyncPipelines(this.env),
this.env,
this.eventBus,
this.tracer,
);
this.contextManager = new ContextManager(
config,
this.env,
this.tracer,
this.orchestrator,
this.chatHistory,
);
}
async simulateTurn(messages: Content[]) {
// 1. Append the new messages
const currentHistory = this.chatHistory.get();
this.chatHistory.set([...currentHistory, ...messages]);
// 2. Measure tokens immediately after append
const tokensBefore = this.env.tokenCalculator.calculateConcreteListTokens(
this.contextManager.getNodes(),
);
// 3. Yield to event loop and wait for async pipelines to finish
await this.contextManager.waitForPipelines();
await new Promise((resolve) => setTimeout(resolve, 100)); // Extra beat for event bus propagation
// 4. Measure tokens after background processors
const tokensAfter = this.env.tokenCalculator.calculateConcreteListTokens(
this.contextManager.getNodes(),
);
this.tokenTrajectory.push({
turnIndex: this.currentTurnIndex++,
tokensBeforeBackground: tokensBefore,
tokensAfterBackground: tokensAfter,
});
}
async getGoldenState() {
const { history: finalProjection } =
await this.contextManager.renderHistory();
return {
tokenTrajectory: this.tokenTrajectory,
finalProjection,
};
}
}