refactor environment

This commit is contained in:
Your Name
2026-04-06 19:18:17 +00:00
parent 7c2135574c
commit 2e80fad7a4
11 changed files with 57 additions and 83 deletions
@@ -17,6 +17,7 @@ import { ContextManager } from './contextManager.js';
import { ContextEnvironmentImpl } from './sidecar/environmentImpl.js';
import { SidecarLoader } from './sidecar/SidecarLoader.js';
import { ContextTracer } from './tracer.js';
import { ContextEventBus } from './eventBus.js';
import type { Content } from '@google/genai';
@@ -69,6 +70,7 @@ describe('ContextManager Golden Tests', () => {
const sidecar = SidecarLoader.fromLegacyConfig(mockConfig as any);
const tracer = new ContextTracer('/tmp', 'test-session');
const eventBus = new ContextEventBus();
const env = new ContextEnvironmentImpl(
{} as any,
'test-prompt-id',
@@ -77,6 +79,7 @@ describe('ContextManager Golden Tests', () => {
'/tmp',
tracer,
4,
eventBus
);
contextManager = new ContextManager(sidecar, env, tracer);
});
@@ -128,12 +131,23 @@ describe('ContextManager Golden Tests', () => {
// In Golden Tests, we just want to ensure the logic doesn't throw or alter unprotected history in weird ways.
// Since we're skipping processors due to being under budget, it should equal history.
const tracer2 = new ContextTracer('/tmp', 'test2');
const eventBus2 = new ContextEventBus();
const env2 = new ContextEnvironmentImpl(
{} as any,
'test-prompt-id',
'test',
'/tmp',
'/tmp',
tracer2,
4,
eventBus2
);
contextManager = new ContextManager(
{
budget: { retainedTokens: 100000, maxTokens: 150000 },
pipelines: [],
} as any,
{} as any,
env2,
tracer2,
);
+1 -6
View File
@@ -51,12 +51,7 @@ export class ContextManager {
constructor(private sidecar: SidecarConfig, private env: ContextEnvironment, private readonly tracer: ContextTracer) {
this.eventBus = new ContextEventBus();
if ('setEventBus' in this.env) {
(this.env as any).setEventBus(this.eventBus);
}
this.eventBus = env.eventBus;
// Register built-ins BEFORE creating Orchestrator
ProcessorRegistry.register({ id: 'ToolMaskingProcessor', create: (env, opts) => new ToolMaskingProcessor(env, opts as any) });
+1 -1
View File
@@ -68,7 +68,7 @@ export class IrProjector {
try {
const fs = await import('node:fs/promises');
const path = await import('node:path');
const dumpPath = path.join(env.getTraceDir(), '.gemini', 'projected_context.json');
const dumpPath = path.join(env.traceDir, '.gemini', 'projected_context.json');
await fs.mkdir(path.dirname(dumpPath), { recursive: true });
await fs.writeFile(dumpPath, JSON.stringify(contents, null, 2), 'utf-8');
debugLogger.log(`[Observability] Context successfully dumped to ${dumpPath}`);
@@ -33,10 +33,10 @@ export class BlobDegradationProcessor implements ContextProcessor {
let directoryCreated = false;
let blobOutputsDir = path.join(
this.env.getProjectTempDir(),
this.env.projectTempDir,
'degraded-blobs',
);
const sessionId = this.env.getSessionId();
const sessionId = this.env.sessionId;
if (sessionId) {
blobOutputsDir = path.join(
blobOutputsDir,
@@ -102,7 +102,7 @@ export class BlobDegradationProcessor implements ContextProcessor {
if (newText && tokensSaved > 0) {
const newTokens = estimateTokenCountSync([{ text: newText }], 0, {
charsPerToken: this.env.getCharsPerToken(),
charsPerToken: this.env.charsPerToken,
});
part.presentation = { text: newText, tokens: newTokens };
@@ -15,6 +15,7 @@ import type {
} from '../ir/types.js';
import type { ContextAccountingState } from '../pipeline.js';
import { randomUUID } from 'node:crypto';
import type { BaseLlmClient } from 'src/core/baseLlmClient.js';
describe('SemanticCompressionProcessor', () => {
let processor: SemanticCompressionProcessor;
@@ -26,9 +27,7 @@ describe('SemanticCompressionProcessor', () => {
});
const env = createMockEnvironment();
env.getLlmClient = vi
.fn()
.mockReturnValue({ generateContent: generateContentMock }) as any;
vi.spyOn(env, 'llmClient', 'get').mockReturnValue({ generateContent: generateContentMock } as unknown as BaseLlmClient);
processor = new SemanticCompressionProcessor(env, {
nodeThresholdTokens: 2000,
});
@@ -181,7 +181,7 @@ export class SemanticCompressionProcessor implements ContextProcessor {
): Promise<string> {
const promptMessage = `You are compressing an old episodic context buffer for an AI assistant.\nSummarize this ${contentType} block in 2-3 highly technical sentences. Keep all critical facts, file names, dependencies, and architectural decisions. Discard conversational filler and boilerplate.\n\nContent:\n${content.slice(0, 30000)}`;
const client = this.env.getLlmClient();
const client = this.env.llmClient;
try {
const response = await client.generateContent({
modelConfigKey: { model: this.modelToUse },
@@ -19,7 +19,7 @@ export interface StateSnapshotProcessorOptions {
export class StateSnapshotProcessor implements ContextProcessor {
static create(env: ContextEnvironment, options: StateSnapshotProcessorOptions): StateSnapshotProcessor {
return new StateSnapshotProcessor(env, options, (env as any).getEventBus());
return new StateSnapshotProcessor(env, options, env.eventBus);
}
readonly id = 'StateSnapshotProcessor';
readonly name = 'StateSnapshotProcessor';
@@ -76,7 +76,7 @@ export class StateSnapshotProcessor implements ContextProcessor {
}
private async synthesizeSnapshot(episodes: Episode[]): Promise<Episode> {
const client = this.env.getLlmClient();
const client = this.env.llmClient;
const systemPrompt =
this.options.systemInstruction ??
`You are an expert Context Memory Manager. You will be provided with a raw transcript of older conversation turns between a user and an AI assistant.
@@ -106,7 +106,7 @@ Output ONLY the raw factual snapshot, formatted compactly. Do not include markdo
modelConfigKey: { model: 'state-snapshot-processor' },
contents: [{ role: 'user', parts: [{ text: userPromptText }] }],
systemInstruction: { role: 'system', parts: [{ text: systemPrompt }] },
promptId: this.env.getPromptId(),
promptId: this.env.promptId,
role: LlmRole.UTILITY_STATE_SNAPSHOT_PROCESSOR,
abortSignal: new AbortController().signal,
},
@@ -53,10 +53,10 @@ export class ToolMaskingProcessor implements ContextProcessor {
const limitChars = maskingConfig.stringLengthThresholdTokens * 4;
let toolOutputsDir = path.join(
this.env.getProjectTempDir(),
this.env.projectTempDir,
'tool-outputs',
);
const sessionId = this.env.getSessionId();
const sessionId = this.env.sessionId;
if (sessionId) {
toolOutputsDir = path.join(
toolOutputsDir,
@@ -9,12 +9,13 @@
export type { ContextTracer, ContextEventBus };
export interface ContextEnvironment {
getLlmClient(): BaseLlmClient;
getPromptId(): string;
getSessionId(): string;
getTraceDir(): string;
getProjectTempDir(): string;
getEventBus(): ContextEventBus;
getTracer(): ContextTracer;
getCharsPerToken(): number;
readonly llmClient: BaseLlmClient;
readonly promptId: string;
readonly sessionId: string;
readonly traceDir: string;
readonly projectTempDir: string;
readonly tracer: ContextTracer;
readonly charsPerToken: number;
readonly eventBus: ContextEventBus;
}
@@ -11,52 +11,14 @@ import type { ContextEnvironment } from './environment.js';
import type { ContextEventBus } from '../eventBus.js';
export class ContextEnvironmentImpl implements ContextEnvironment {
private eventBus?: ContextEventBus;
constructor(
private llmClient: BaseLlmClient,
private sessionId: string,
private promptId: string,
private traceDir: string,
private tempDir: string,
private tracer: ContextTracer,
private charsPerToken: number,
public readonly llmClient: BaseLlmClient,
public readonly sessionId: string,
public readonly promptId: string,
public readonly traceDir: string,
public readonly projectTempDir: string,
public readonly tracer: ContextTracer,
public readonly charsPerToken: number,
public readonly eventBus: ContextEventBus,
) {}
setEventBus(bus: ContextEventBus) {
this.eventBus = bus;
}
getEventBus(): ContextEventBus {
if (!this.eventBus) throw new Error('EventBus not bound');
return this.eventBus;
}
getLlmClient(): BaseLlmClient {
return this.llmClient;
}
getSessionId(): string {
return this.sessionId;
}
getTraceDir(): string {
return this.traceDir;
}
getProjectTempDir(): string {
return this.tempDir;
}
getTracer(): ContextTracer {
return this.tracer;
}
getCharsPerToken(): number {
return this.charsPerToken;
}
getPromptId(): string {
return this.promptId;
}
}
@@ -14,18 +14,18 @@ import { ContextManager } from '../contextManager.js';
export function createMockEnvironment(): ContextEnvironment {
return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
getLlmClient: vi.fn().mockReturnValue({
llmClient: vi.fn().mockReturnValue({
generateContent: vi.fn().mockResolvedValue({
text: 'Mock LLM summary response',
}),
} as unknown as BaseLlmClient),
getPromptId: vi.fn().mockReturnValue('mock-prompt-id'),
getSessionId: vi.fn().mockReturnValue('mock-session'),
getTraceDir: vi.fn().mockReturnValue('/tmp/.gemini/trace'),
getProjectTempDir: vi.fn().mockReturnValue('/tmp/.gemini/tool-outputs'),
getEventBus: vi.fn(),
getTracer: vi.fn(),
getCharsPerToken: vi.fn().mockReturnValue(1),
})() as unknown as BaseLlmClient,
promptId: 'mock-prompt-id',
sessionId: 'mock-session',
traceDir: '/tmp/.gemini/trace',
projectTempDir: '/tmp/.gemini/tool-outputs',
eventBus: new ContextEventBus(),
tracer: new ContextTracer('/tmp', 'mock-session'),
charsPerToken: 1,
};
}
@@ -88,12 +88,14 @@ export function createMockContextConfig(
import { ContextTracer } from '../tracer.js';
import { ContextEnvironmentImpl } from '../sidecar/environmentImpl.js';
import { SidecarLoader } from '../sidecar/SidecarLoader.js';
import { ContextEventBus } from '../eventBus.js';
import type { BaseLlmClient } from 'src/core/baseLlmClient.js';
export function setupContextComponentTest(config: Config) {
const chatHistory = new AgentChatHistory();
const sidecar = SidecarLoader.fromLegacyConfig(config);
const tracer = new ContextTracer('/tmp', 'test-session');
const eventBus = new ContextEventBus();
const env = new ContextEnvironmentImpl(
config.getBaseLlmClient(),
'test prompt-id',
@@ -102,6 +104,7 @@ export function setupContextComponentTest(config: Config) {
'/tmp/gemini-test',
tracer,
1,
eventBus
);
const contextManager = new ContextManager(sidecar, env, tracer);