2026-04-06 16:43:04 +00:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2026 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
2026-04-07 04:46:04 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
import type { Content } from '@google/genai';
|
|
|
|
|
import type { AgentChatHistory } from '../core/agentChatHistory.js';
|
|
|
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
2026-04-08 02:34:06 +00:00
|
|
|
import type { ConcreteNode } from './ir/types.js';
|
2026-04-07 02:16:06 +00:00
|
|
|
import type { ContextEventBus } from './eventBus.js';
|
|
|
|
|
import type { ContextTracer } from './tracer.js';
|
2026-04-06 16:43:04 +00:00
|
|
|
import type { ContextEnvironment } from './sidecar/environment.js';
|
|
|
|
|
import type { SidecarConfig } from './sidecar/types.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 { IrProjector } from './ir/projector.js';
|
2026-04-07 03:58:50 +00:00
|
|
|
import { registerBuiltInProcessors } from './sidecar/builtins.js';
|
2026-04-07 04:03:54 +00:00
|
|
|
import { ProcessorRegistry } from './sidecar/registry.js';
|
2026-04-07 03:58:50 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
export class ContextManager {
|
2026-04-08 02:34:06 +00:00
|
|
|
// The stateful, pristine flat graph.
|
|
|
|
|
private pristineShip: ReadonlyArray<ConcreteNode> = [];
|
|
|
|
|
private currentShip: ReadonlyArray<ConcreteNode> = [];
|
2026-04-06 16:43:04 +00:00
|
|
|
private readonly eventBus: ContextEventBus;
|
2026-04-07 04:46:04 +00:00
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
// Internal sub-components
|
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 16:43:04 +00:00
|
|
|
|
2026-04-07 04:46:04 +00:00
|
|
|
static create(
|
|
|
|
|
sidecar: SidecarConfig,
|
|
|
|
|
env: ContextEnvironment,
|
|
|
|
|
tracer: ContextTracer,
|
|
|
|
|
orchestrator?: PipelineOrchestrator,
|
|
|
|
|
registry?: ProcessorRegistry,
|
|
|
|
|
): ContextManager {
|
|
|
|
|
if (!registry) {
|
|
|
|
|
registry = new ProcessorRegistry();
|
|
|
|
|
registerBuiltInProcessors(registry);
|
|
|
|
|
}
|
|
|
|
|
const orch =
|
|
|
|
|
orchestrator ||
|
|
|
|
|
new PipelineOrchestrator(sidecar, env, env.eventBus, tracer, registry);
|
|
|
|
|
return new ContextManager(sidecar, env, tracer, orch);
|
2026-04-07 03:13:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use ContextManager.create() instead
|
|
|
|
|
private constructor(
|
2026-04-07 04:46:04 +00:00
|
|
|
private sidecar: SidecarConfig,
|
|
|
|
|
private env: ContextEnvironment,
|
|
|
|
|
private readonly tracer: ContextTracer,
|
|
|
|
|
orchestrator: PipelineOrchestrator,
|
2026-04-07 03:13:14 +00:00
|
|
|
) {
|
2026-04-06 19:18:17 +00:00
|
|
|
this.eventBus = env.eventBus;
|
2026-04-07 03:13:14 +00:00
|
|
|
this.orchestrator = orchestrator;
|
2026-04-06 16:43:04 +00:00
|
|
|
|
2026-04-06 19:54:09 +00:00
|
|
|
this.eventBus.onPristineHistoryUpdated((event) => {
|
2026-04-08 02:34:06 +00:00
|
|
|
this.pristineShip = event.ship;
|
|
|
|
|
// In V2, we assume currentShip updates sequentially via Orchestrator patches.
|
|
|
|
|
// But if pristine changes, we must ensure our current view incorporates new nodes.
|
|
|
|
|
// For now, simple fallback: if the current ship doesn't have the new nodes, append them.
|
|
|
|
|
// A more robust implementation would diff the ship, but for now we'll just track.
|
|
|
|
|
const existingIds = new Set(this.currentShip.map((n) => n.id));
|
|
|
|
|
const addedNodes = event.ship.filter((n) => !existingIds.has(n.id));
|
|
|
|
|
if (addedNodes.length > 0) {
|
|
|
|
|
this.currentShip = [...this.currentShip, ...addedNodes];
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 20:59:26 +00:00
|
|
|
this.evaluateTriggers(event.newNodes);
|
2026-04-06 19:54:09 +00:00
|
|
|
});
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
this.eventBus.onVariantReady((event) => {
|
2026-04-08 02:34:06 +00:00
|
|
|
// In V2, async workers write back patches.
|
|
|
|
|
// The old variant dict logic is replaced by the orchestrator applying patches directly.
|
|
|
|
|
// For now we log it.
|
|
|
|
|
this.tracer.logEvent(
|
|
|
|
|
'ContextManager',
|
|
|
|
|
`Received async variant [${event.variantId}] for Node ${event.targetId}`,
|
|
|
|
|
);
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
`ContextManager: Received async variant [${event.variantId}] for Node ${event.targetId}.`,
|
2026-04-06 16:43:04 +00:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 19:54:09 +00:00
|
|
|
/**
|
|
|
|
|
* Evaluates if the current working buffer exceeds configured budget thresholds,
|
|
|
|
|
* firing consolidation events if necessary.
|
|
|
|
|
*/
|
2026-04-07 20:59:26 +00:00
|
|
|
private evaluateTriggers(newNodes: Set<string>) {
|
2026-04-06 19:54:09 +00:00
|
|
|
if (!this.sidecar.budget) return;
|
|
|
|
|
|
2026-04-07 20:59:26 +00:00
|
|
|
if (newNodes.size > 0) {
|
2026-04-08 02:34:06 +00:00
|
|
|
this.eventBus.emitChunkReceived({
|
|
|
|
|
ship: this.currentShip,
|
|
|
|
|
targetNodeIds: newNodes
|
|
|
|
|
});
|
2026-04-07 20:59:26 +00:00
|
|
|
}
|
2026-04-06 19:54:09 +00:00
|
|
|
|
2026-04-08 02:34:06 +00:00
|
|
|
const currentTokens = this.env.tokenCalculator.calculateConcreteListTokens(this.currentShip);
|
|
|
|
|
|
2026-04-06 19:54:09 +00:00
|
|
|
if (currentTokens > this.sidecar.budget.retainedTokens) {
|
2026-04-07 20:59:26 +00:00
|
|
|
const agedOutNodes = new Set<string>();
|
|
|
|
|
let rollingTokens = 0;
|
2026-04-08 02:34:06 +00:00
|
|
|
// Walk backwards finding nodes that fall out of the retained budget
|
|
|
|
|
for (let i = this.currentShip.length - 1; i >= 0; i--) {
|
|
|
|
|
const node = this.currentShip[i];
|
|
|
|
|
rollingTokens += node.metadata.currentTokens;
|
2026-04-07 20:59:26 +00:00
|
|
|
if (rollingTokens > this.sidecar.budget.retainedTokens) {
|
2026-04-08 02:34:06 +00:00
|
|
|
agedOutNodes.add(node.id);
|
2026-04-07 20:59:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 02:34:06 +00:00
|
|
|
if (agedOutNodes.size > 0) {
|
|
|
|
|
this.eventBus.emitConsolidationNeeded({
|
|
|
|
|
ship: this.currentShip,
|
|
|
|
|
targetDeficit: currentTokens - this.sidecar.budget.retainedTokens,
|
|
|
|
|
targetNodeIds: agedOutNodes,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-06 19:54:09 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 16:43:04 +00:00
|
|
|
/**
|
2026-04-08 02:34:06 +00:00
|
|
|
* Starts tracking the raw agent history and translating it to Episodic IR.
|
2026-04-06 16:43:04 +00:00
|
|
|
*/
|
|
|
|
|
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,
|
2026-04-06 19:54:09 +00:00
|
|
|
this.env.tokenCalculator,
|
2026-04-06 18:46:21 +00:00
|
|
|
);
|
|
|
|
|
this.historyObserver.start();
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-08 02:34:06 +00:00
|
|
|
* Retrieves the raw, uncompressed Episodic IR graph.
|
|
|
|
|
* Useful for internal tool rendering (like the trace viewer).
|
|
|
|
|
* Note: This is an expensive, deep clone operation.
|
2026-04-06 16:43:04 +00:00
|
|
|
*/
|
2026-04-08 02:34:06 +00:00
|
|
|
getPristineGraph(): ReadonlyArray<ConcreteNode> {
|
|
|
|
|
return [...this.pristineShip];
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-08 02:34:06 +00:00
|
|
|
* Generates a virtual view of the pristine graph, substituting in variants
|
|
|
|
|
* up to the configured token budget.
|
|
|
|
|
* This is the view that will eventually be projected back to the LLM.
|
2026-04-06 16:43:04 +00:00
|
|
|
*/
|
2026-04-08 02:34:06 +00:00
|
|
|
getShip(): ReadonlyArray<ConcreteNode> {
|
|
|
|
|
return [...this.currentShip];
|
|
|
|
|
}
|
2026-04-07 04:46:04 +00:00
|
|
|
|
2026-04-08 02:34:06 +00:00
|
|
|
/**
|
|
|
|
|
* Executes the final 'gc_backstop' pipeline if necessary, enforcing the token budget,
|
|
|
|
|
* and maps the Episodic IR back into a raw Gemini Content[] array for transmission.
|
|
|
|
|
* This is the primary method called by the agent framework before sending a request.
|
|
|
|
|
*/
|
|
|
|
|
async projectCompressedHistory(
|
|
|
|
|
activeTaskIds: Set<string> = new Set(),
|
|
|
|
|
): Promise<Content[]> {
|
|
|
|
|
this.tracer.logEvent(
|
|
|
|
|
'ContextManager',
|
|
|
|
|
'Starting projection to LLM context',
|
|
|
|
|
);
|
|
|
|
|
// Apply final GC Backstop pressure barrier synchronously before mapping
|
|
|
|
|
const finalHistory = await IrProjector.project(
|
|
|
|
|
this.currentShip,
|
2026-04-06 18:58:49 +00:00
|
|
|
this.orchestrator,
|
|
|
|
|
this.sidecar,
|
|
|
|
|
this.tracer,
|
|
|
|
|
this.env,
|
2026-04-08 02:34:06 +00:00
|
|
|
activeTaskIds,
|
2026-04-06 16:43:04 +00:00
|
|
|
);
|
2026-04-08 02:34:06 +00:00
|
|
|
|
|
|
|
|
this.tracer.logEvent('ContextManager', 'Finished projection');
|
|
|
|
|
|
|
|
|
|
return finalHistory;
|
2026-04-06 16:43:04 +00:00
|
|
|
}
|
|
|
|
|
}
|