2026-04-06 16:43:04 +00:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2026 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
import type { Content } from '@google/genai';
|
|
|
|
|
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
import type { AgentChatHistory } from '../core/agentChatHistory.js';
|
|
|
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
|
|
|
|
import type { Episode } from './ir/types.js';
|
|
|
|
|
|
|
|
|
|
import { ContextEventBus } from './eventBus.js';
|
|
|
|
|
import { ContextTracer } from './tracer.js';
|
|
|
|
|
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
import type { ContextEnvironment } from './sidecar/environment.js';
|
|
|
|
|
|
|
|
|
|
import type { SidecarConfig } from './sidecar/types.js';
|
|
|
|
|
import { ProcessorRegistry } from './sidecar/registry.js';
|
2026-04-06 17:59:01 +00:00
|
|
|
import { PipelineOrchestrator } from './sidecar/orchestrator.js';
|
2026-04-06 18:46:21 +00:00
|
|
|
import { HistoryObserver } from './historyObserver.js';
|
|
|
|
|
import { calculateEpisodeListTokens } from './utils/contextTokenCalculator.js';
|
|
|
|
|
import { generateWorkingBufferView } from './ir/graphUtils.js';
|
2026-04-06 18:01:32 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
import { ToolMaskingProcessor } from './processors/toolMaskingProcessor.js';
|
|
|
|
|
import { BlobDegradationProcessor } from './processors/blobDegradationProcessor.js';
|
|
|
|
|
import { SemanticCompressionProcessor } from './processors/semanticCompressionProcessor.js';
|
|
|
|
|
import { HistorySquashingProcessor } from './processors/historySquashingProcessor.js';
|
2026-04-06 18:01:32 +00:00
|
|
|
import { StateSnapshotProcessor } from './processors/stateSnapshotProcessor.js';
|
2026-04-06 18:46:21 +00:00
|
|
|
import { EmergencyTruncationProcessor } from './processors/emergencyTruncationProcessor.js';
|
|
|
|
|
|
|
|
|
|
import { IrProjector } from './ir/projector.js';
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
export class ContextManager {
|
2026-04-06 17:44:28 +00:00
|
|
|
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
// The stateful, pristine Episodic Intermediate Representation graph.
|
|
|
|
|
// This allows the agent to remember and summarize continuously without losing data across turns.
|
|
|
|
|
private pristineEpisodes: Episode[] = [];
|
|
|
|
|
private readonly eventBus: ContextEventBus;
|
2026-04-06 17:44:28 +00:00
|
|
|
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
// Internal sub-components
|
|
|
|
|
// Synchronous processors are instantiated but effectively used as singletons within this class
|
2026-04-06 17:59:01 +00:00
|
|
|
private orchestrator: PipelineOrchestrator;
|
2026-04-06 18:46:21 +00:00
|
|
|
private historyObserver?: HistoryObserver;
|
2026-04-06 17:44:28 +00:00
|
|
|
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
|
2026-04-06 17:44:28 +00:00
|
|
|
constructor(private sidecar: SidecarConfig, private env: ContextEnvironment, private readonly tracer: ContextTracer) {
|
|
|
|
|
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
this.eventBus = new ContextEventBus();
|
2026-04-06 17:59:01 +00:00
|
|
|
if ('setEventBus' in this.env) {
|
|
|
|
|
(this.env as any).setEventBus(this.eventBus);
|
|
|
|
|
}
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 18:01:32 +00:00
|
|
|
// Register built-ins BEFORE creating Orchestrator
|
2026-04-06 17:44:28 +00:00
|
|
|
ProcessorRegistry.register({ id: 'ToolMaskingProcessor', create: (env, opts) => new ToolMaskingProcessor(env, opts as any) });
|
|
|
|
|
ProcessorRegistry.register({ id: 'BlobDegradationProcessor', create: (env, opts) => new BlobDegradationProcessor(env) });
|
|
|
|
|
ProcessorRegistry.register({ id: 'SemanticCompressionProcessor', create: (env, opts) => new SemanticCompressionProcessor(env, opts as any) });
|
|
|
|
|
ProcessorRegistry.register({ id: 'HistorySquashingProcessor', create: (env, opts) => new HistorySquashingProcessor(env, opts as any) });
|
2026-04-06 18:01:32 +00:00
|
|
|
ProcessorRegistry.register({ id: 'StateSnapshotProcessor', create: (env, opts) => StateSnapshotProcessor.create(env, opts as any) });
|
2026-04-06 18:46:21 +00:00
|
|
|
ProcessorRegistry.register({ id: 'EmergencyTruncationProcessor', create: (env, opts) => EmergencyTruncationProcessor.create(env, opts as any) });
|
2026-04-06 18:01:32 +00:00
|
|
|
|
|
|
|
|
this.orchestrator = new PipelineOrchestrator(this.sidecar, this.env, this.eventBus, this.tracer);
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
this.eventBus.onVariantReady((event) => {
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
// Find the target episode in the pristine graph
|
|
|
|
|
const targetEp = this.pristineEpisodes.find(
|
|
|
|
|
(ep) => ep.id === event.targetId,
|
|
|
|
|
);
|
|
|
|
|
if (targetEp) {
|
|
|
|
|
if (!targetEp.variants) {
|
|
|
|
|
targetEp.variants = {};
|
|
|
|
|
}
|
|
|
|
|
targetEp.variants[event.variantId] = event.variant;
|
2026-04-06 17:44:28 +00:00
|
|
|
this.tracer.logEvent('ContextManager', `Received async variant [${event.variantId}] for Episode ${event.targetId}`);
|
2026-04-06 16:43:04 +00:00
|
|
|
debugLogger.log(
|
|
|
|
|
`ContextManager: Received async variant [${event.variantId}] for Episode ${event.targetId}.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Safely stops background workers and clears event listeners.
|
|
|
|
|
*/
|
|
|
|
|
shutdown() {
|
2026-04-06 17:59:01 +00:00
|
|
|
this.orchestrator.shutdown();
|
2026-04-06 18:46:21 +00:00
|
|
|
if (this.historyObserver) {
|
|
|
|
|
this.historyObserver.stop();
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subscribes to the core AgentChatHistory to natively track all message events,
|
|
|
|
|
* converting them seamlessly into pristine Episodes.
|
|
|
|
|
*/
|
|
|
|
|
subscribeToHistory(chatHistory: AgentChatHistory) {
|
2026-04-06 18:46:21 +00:00
|
|
|
if (this.historyObserver) {
|
|
|
|
|
this.historyObserver.stop();
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-06 18:46:21 +00:00
|
|
|
this.historyObserver = new HistoryObserver(
|
|
|
|
|
chatHistory,
|
|
|
|
|
this.eventBus,
|
|
|
|
|
this.tracer,
|
|
|
|
|
this.sidecar,
|
|
|
|
|
(episodes) => { this.pristineEpisodes = episodes; },
|
|
|
|
|
() => this.getWorkingBufferView(),
|
|
|
|
|
(episodes) => calculateEpisodeListTokens(episodes)
|
|
|
|
|
);
|
2026-04-06 16:43:04 +00:00
|
|
|
|
2026-04-06 18:46:21 +00:00
|
|
|
this.historyObserver.start();
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generates a computed view of the pristine log.
|
|
|
|
|
* Sweeps backwards (newest to oldest), tracking rolling tokens.
|
2026-04-06 17:44:28 +00:00
|
|
|
* When rollingTokens > retainedTokens, it injects the "best" available ready variant
|
2026-04-06 16:43:04 +00:00
|
|
|
* (snapshot > summary > masked) instead of the raw text.
|
|
|
|
|
* Handles N-to-1 variant skipping automatically.
|
|
|
|
|
*/
|
|
|
|
|
public getWorkingBufferView(): Episode[] {
|
2026-04-06 18:46:21 +00:00
|
|
|
return generateWorkingBufferView(
|
|
|
|
|
this.pristineEpisodes,
|
|
|
|
|
this.sidecar.budget.retainedTokens,
|
|
|
|
|
this.tracer
|
|
|
|
|
);
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns a temporary, compressed Content[] array to be used exclusively for the LLM request.
|
|
|
|
|
* This does NOT mutate the pristine episodic graph.
|
|
|
|
|
*/
|
|
|
|
|
async projectCompressedHistory(): Promise<Content[]> {
|
|
|
|
|
if (!this.sidecar.budget) {
|
2026-04-06 18:46:21 +00:00
|
|
|
return IrProjector.projectAndDump(this.pristineEpisodes, this.env);
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mngConfig = this.sidecar;
|
|
|
|
|
const maxTokens = mngConfig.budget.maxTokens;
|
|
|
|
|
this.tracer.logEvent('ContextManager', 'Projection requested.');
|
|
|
|
|
|
|
|
|
|
// Get the dynamically computed Working Buffer View
|
|
|
|
|
let currentEpisodes = this.getWorkingBufferView();
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 18:46:21 +00:00
|
|
|
let currentTokens = calculateEpisodeListTokens(currentEpisodes);
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
if (currentTokens <= maxTokens) {
|
2026-04-06 17:44:28 +00:00
|
|
|
this.tracer.logEvent('ContextManager', `View is within maxTokens (${currentTokens} <= ${maxTokens}). Returning view.`);
|
2026-04-06 18:46:21 +00:00
|
|
|
return IrProjector.projectAndDump(currentEpisodes, this.env);
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-06 17:44:28 +00:00
|
|
|
this.tracer.logEvent('ContextManager', `View exceeds maxTokens (${currentTokens} > ${maxTokens}). Hitting Synchronous Pressure Barrier. Strategy: ${mngConfig.gcBackstop.strategy}`);
|
2026-04-06 16:43:04 +00:00
|
|
|
// --- The Synchronous Pressure Barrier ---
|
|
|
|
|
// The background eager workers couldn't keep up, or a massive file was pasted.
|
|
|
|
|
// The Working Buffer View is still over the absolute hard limit (maxTokens).
|
|
|
|
|
// We MUST reduce tokens before returning, or the API request will 400.
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
debugLogger.log(
|
|
|
|
|
`Context Manager Synchronous Barrier triggered: View at ${currentTokens} tokens (limit: ${maxTokens}). Strategy: ${mngConfig.gcBackstop.strategy}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Calculate target based on gcTarget
|
|
|
|
|
let targetTokens = maxTokens;
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
if (mngConfig.gcBackstop.target === 'max') {
|
2026-04-06 17:44:28 +00:00
|
|
|
targetTokens = mngConfig.budget.retainedTokens;
|
2026-04-06 16:43:04 +00:00
|
|
|
} else if (mngConfig.gcBackstop.target === 'freeNTokens') {
|
2026-04-06 17:44:28 +00:00
|
|
|
targetTokens = maxTokens - (mngConfig.gcBackstop.freeTokensTarget ?? 10000);
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Structural invariant: We ALWAYS protect the architectural initialization turn (Turn 0)
|
|
|
|
|
// We do NOT arbitrarily protect recent episodes (like currentEpisodes.length - 1)
|
|
|
|
|
// because an episode can be unboundedly large, and protecting it would crash the LLM.
|
2026-04-06 17:44:28 +00:00
|
|
|
const protectedEpisodeId = this.pristineEpisodes.length > 0 ? this.pristineEpisodes[0].id : null;
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
let remainingTokens = currentTokens;
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
const truncated: Episode[] = [];
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
const strategy = mngConfig.gcBackstop.strategy;
|
2026-04-06 17:44:28 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
|
|
|
|
|
for (const ep of currentEpisodes) {
|
2026-04-06 18:46:21 +00:00
|
|
|
const epTokens = calculateEpisodeListTokens([ep]);
|
2026-04-06 16:43:04 +00:00
|
|
|
if (remainingTokens > targetTokens && ep.id !== protectedEpisodeId) {
|
2026-04-06 17:44:28 +00:00
|
|
|
console.log('DROPPING EPISODE:', ep.id, 'rem:', remainingTokens, 'tgt:', targetTokens);
|
|
|
|
|
|
|
|
|
|
remainingTokens -= epTokens;
|
|
|
|
|
if (strategy === 'truncate') {
|
|
|
|
|
this.tracer.logEvent('Barrier', `Truncating episode [${ep.id}].`);
|
|
|
|
|
|
|
|
|
|
debugLogger.log(`Barrier (truncate): Dropped Episode ${ep.id}`);
|
|
|
|
|
} else if (strategy === 'compress') {
|
|
|
|
|
this.tracer.logEvent('Barrier', `Compress fallback to truncate for [${ep.id}].`);
|
|
|
|
|
debugLogger.warn(`Synchronous compress barrier not fully implemented, truncating Episode ${ep.id}.`);
|
|
|
|
|
} else if (strategy === 'rollingSummarizer') {
|
|
|
|
|
this.tracer.logEvent('Barrier', `RollingSummarizer fallback to truncate for [${ep.id}].`);
|
|
|
|
|
debugLogger.warn(`Synchronous rollingSummarizer barrier not fully implemented, truncating Episode ${ep.id}.`);
|
|
|
|
|
}
|
2026-04-06 16:43:04 +00:00
|
|
|
} else {
|
2026-04-06 17:44:28 +00:00
|
|
|
console.log('KEEPING EPISODE:', ep.id, 'rem:', remainingTokens, 'tgt:', targetTokens);
|
|
|
|
|
truncated.push(ep);
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentEpisodes = truncated;
|
|
|
|
|
|
2026-04-06 18:46:21 +00:00
|
|
|
const finalTokens = calculateEpisodeListTokens(currentEpisodes);
|
2026-04-06 17:44:28 +00:00
|
|
|
this.tracer.logEvent('ContextManager', `Finished projection. Final token count: ${finalTokens}.`);
|
2026-04-06 16:43:04 +00:00
|
|
|
debugLogger.log(
|
|
|
|
|
`Context Manager finished. Final actual token count: ${finalTokens}.`,
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-06 18:46:21 +00:00
|
|
|
return IrProjector.projectAndDump(currentEpisodes, this.env);
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
}
|