not working

This commit is contained in:
Your Name
2026-04-06 17:44:28 +00:00
parent ac6dc1d477
commit aa0deb05a4
14 changed files with 857 additions and 475 deletions
+79 -188
View File
@@ -5,6 +5,7 @@
*/
import type { Content } from '@google/genai';
import type { AgentChatHistory } from '../core/agentChatHistory.js';
import { debugLogger } from '../utils/debugLogger.js';
import { IrMapper } from './ir/mapper.js';
@@ -13,7 +14,7 @@ import type { Episode } from './ir/types.js';
import { ContextEventBus } from './eventBus.js';
import { ContextTracer } from './tracer.js';
import { StateSnapshotWorker } from './workers/stateSnapshotWorker.js';
import type { ContextEnvironment } from './sidecar/environment.js';
@@ -28,46 +29,38 @@ import { SemanticCompressionProcessor } from './processors/semanticCompressionPr
import { HistorySquashingProcessor } from './processors/historySquashingProcessor.js';
export class ContextManager {
// 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 unsubscribeHistory?: () => void;
private readonly eventBus: ContextEventBus;
// Internal sub-components
// Synchronous processors are instantiated but effectively used as singletons within this class
private workers: AsyncContextWorker[] = [];
constructor(
private sidecar: SidecarConfig,
private env: ContextEnvironment,
private readonly tracer: ContextTracer,
) {
constructor(private sidecar: SidecarConfig, private env: ContextEnvironment, private readonly tracer: ContextTracer) {
this.eventBus = new ContextEventBus();
// Register built-ins
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),
});
ProcessorRegistry.register({
id: 'StateSnapshotWorker',
create: (env, opts) => new StateSnapshotWorker(env),
});
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) });
ProcessorRegistry.register({ id: 'StateSnapshotWorker', create: (env, opts) => new StateSnapshotWorker(env) });
this.eventBus.onVariantReady((event) => {
// Find the target episode in the pristine graph
const targetEp = this.pristineEpisodes.find(
(ep) => ep.id === event.targetId,
@@ -77,10 +70,7 @@ export class ContextManager {
targetEp.variants = {};
}
targetEp.variants[event.variantId] = event.variant;
this.tracer.logEvent(
'ContextManager',
`Received async variant [${event.variantId}] for Episode ${event.targetId}`,
);
this.tracer.logEvent('ContextManager', `Received async variant [${event.variantId}] for Episode ${event.targetId}`);
debugLogger.log(
`ContextManager: Received async variant [${event.variantId}] for Episode ${event.targetId}.`,
);
@@ -92,10 +82,7 @@ export class ContextManager {
// Initialize and start background subconscious workers
for (const bgDef of this.sidecar.pipelines.eagerBackground) {
const worker = ProcessorRegistry.get(bgDef.processorId).create(
this.env,
bgDef.options,
) as AsyncContextWorker;
const worker = ProcessorRegistry.get(bgDef.processorId).create(this.env, bgDef.options) as AsyncContextWorker;
worker.start(this.eventBus);
this.workers.push(worker);
}
@@ -128,11 +115,7 @@ export class ContextManager {
// function calls and responses into unified Episodes. Pushing messages
// individually would shatter these episodic boundaries.
this.pristineEpisodes = IrMapper.toIr(chatHistory.get());
this.tracer.logEvent(
'ContextManager',
'Rebuilt pristine graph from chat history update',
{ episodeCount: this.pristineEpisodes.length },
);
this.tracer.logEvent('ContextManager', 'Rebuilt pristine graph from chat history update', { episodeCount: this.pristineEpisodes.length });
this.checkTriggers();
});
}
@@ -141,16 +124,13 @@ export class ContextManager {
if (!this.sidecar.budget) return;
const mngConfig = this.sidecar;
// Calculate tokens based on the *Working Buffer View*, not the raw pristine log.
// This solves Bug 2: The View shrinks when variants are applied, preventing infinite GC loops.
const workingBuffer = this.getWorkingBufferView();
const currentTokens = this.calculateIrTokens(workingBuffer);
this.tracer.logEvent('ContextManager', 'Evaluated triggers', {
currentTokens,
retainedTokens: mngConfig.budget.retainedTokens,
});
this.tracer.logEvent('ContextManager', 'Evaluated triggers', { currentTokens, retainedTokens: mngConfig.budget.retainedTokens });
// 1. Eager Compute Trigger (Continuous Streaming)
// Broadcast the full pristine log to the async workers so they can proactively summarize partial massive files.
@@ -160,18 +140,10 @@ export class ContextManager {
// If we exceed 65k, tell the background processors to opportunistically synthesize the oldest nodes.
if (currentTokens > mngConfig.budget.retainedTokens) {
const deficit = currentTokens - mngConfig.budget.retainedTokens;
this.tracer.logEvent(
'ContextManager',
'Budget crossed. Emitting ConsolidationNeeded',
{ deficit },
);
console.log(
'EMITTING CONSOLIDATION. Buffer:',
workingBuffer.length,
'Deficit:',
deficit,
);
this.tracer.logEvent('ContextManager', 'Budget crossed. Emitting ConsolidationNeeded', { deficit });
console.log('EMITTING CONSOLIDATION. Buffer:', workingBuffer.length, 'Deficit:', deficit);
this.eventBus.emitConsolidationNeeded({
episodes: workingBuffer, // Pass the working buffer so they know what still needs compression
targetDeficit: deficit,
});
@@ -181,7 +153,7 @@ export class ContextManager {
/**
* Generates a computed view of the pristine log.
* Sweeps backwards (newest to oldest), tracking rolling tokens.
* When rollingTokens > retainedTokens, it injects the "best" available ready variant
* When rollingTokens > retainedTokens, it injects the "best" available ready variant
* (snapshot > summary > masked) instead of the raw text.
* Handles N-to-1 variant skipping automatically.
*/
@@ -193,6 +165,7 @@ export class ContextManager {
private async applyProcessorGraphs(episodes: Episode[]): Promise<Episode[]> {
const mngConfig = this.sidecar;
const retainedLimit = mngConfig.budget.retainedTokens;
// If we're incredibly small, maybe we just run the retained graph on everything?
// Let's divide the episodes exactly at the retained boundary.
@@ -204,11 +177,7 @@ export class ContextManager {
for (let i = episodes.length - 1; i >= 0; i--) {
const ep = episodes[i];
const epTokens = this.calculateIrTokens([ep]);
if (
(rollingTokens + epTokens <= retainedLimit &&
normalWindow.length === 0) ||
retainedWindow.length === 0
) {
if ((rollingTokens + epTokens <= retainedLimit && normalWindow.length === 0) || retainedWindow.length === 0) {
// We always put at least the latest episode in the retained window.
// We only add to retainedWindow if we haven't already started the normalWindow (contiguous block).
retainedWindow.unshift(ep);
@@ -236,34 +205,18 @@ export class ContextManager {
// Run Retained Graph
let processedRetained = [...retainedWindow];
for (const def of mngConfig.pipelines.retainedProcessingGraph) {
const processor = ProcessorRegistry.get(def.processorId).create(
this.env,
def.options,
) as ContextProcessor;
this.tracer.logEvent(
'ContextManager',
`Running ${processor.name} on retained window.`,
);
const state = createAccountingState(
this.calculateIrTokens([...normalWindow, ...processedRetained]),
);
const processor = ProcessorRegistry.get(def.processorId).create(this.env, def.options) as ContextProcessor;
this.tracer.logEvent('ContextManager', `Running ${processor.name} on retained window.`);
const state = createAccountingState(this.calculateIrTokens([...normalWindow, ...processedRetained]));
processedRetained = await processor.process(processedRetained, state);
}
// Run Normal Graph
let processedNormal = [...normalWindow];
for (const def of mngConfig.pipelines.normalProcessingGraph) {
const processor = ProcessorRegistry.get(def.processorId).create(
this.env,
def.options,
) as ContextProcessor;
this.tracer.logEvent(
'ContextManager',
`Running ${processor.name} on normal window.`,
);
const state = createAccountingState(
this.calculateIrTokens([...processedNormal, ...processedRetained]),
);
const processor = ProcessorRegistry.get(def.processorId).create(this.env, def.options) as ContextProcessor;
this.tracer.logEvent('ContextManager', `Running ${processor.name} on normal window.`);
const state = createAccountingState(this.calculateIrTokens([...processedNormal, ...processedRetained]));
processedNormal = await processor.process(processedNormal, state);
}
@@ -273,7 +226,7 @@ export class ContextManager {
public getWorkingBufferView(): Episode[] {
const mngConfig = this.sidecar;
const retainedTokens = mngConfig.budget.retainedTokens;
let currentEpisodes: Episode[] = [];
let rollingTokens = 0;
const skippedIds = new Set<string>();
@@ -281,14 +234,11 @@ export class ContextManager {
for (let i = this.pristineEpisodes.length - 1; i >= 0; i--) {
const ep = this.pristineEpisodes[i];
// If this episode was already replaced by an N-to-1 Snapshot injected earlier in the sweep, skip it entirely!
// This solves Bug 1 (Duplicate Projection).
if (skippedIds.has(ep.id)) {
this.tracer.logEvent(
'ViewGenerator',
`Skipping episode [${ep.id}] due to N-to-1 replacement.`,
);
this.tracer.logEvent('ViewGenerator', `Skipping episode [${ep.id}] due to N-to-1 replacement.`);
continue;
}
@@ -328,16 +278,7 @@ export class ContextManager {
const epTokens = this.calculateIrTokens([projectedEp]);
if (ep.variants) {
console.log(
'Checking variants for',
ep.id,
'rollingTokens:',
rollingTokens,
'retained:',
retainedTokens,
);
}
if (ep.variants) { console.log('Checking variants for', ep.id, 'rollingTokens:', rollingTokens, 'retained:', retainedTokens); }
if (rollingTokens > retainedTokens && ep.variants) {
console.log('EVALUATING VARIANTS FOR', ep.id);
const snapshot = ep.variants['snapshot'];
@@ -354,10 +295,7 @@ export class ContextManager {
for (const id of snapshot.replacedEpisodeIds) {
skippedIds.add(id);
}
this.tracer.logEvent(
'ViewGenerator',
`Episode [${ep.id}] has SnapshotVariant. Selecting variant over raw text. Added [${snapshot.replacedEpisodeIds.join(',')}] to skippedIds.`,
);
this.tracer.logEvent('ViewGenerator', `Episode [${ep.id}] has SnapshotVariant. Selecting variant over raw text. Added [${snapshot.replacedEpisodeIds.join(',')}] to skippedIds.`);
debugLogger.log(
`Opportunistically swapped Episodes [${snapshot.replacedEpisodeIds.join(', ')}] for pre-computed Snapshot variant.`,
);
@@ -385,10 +323,7 @@ export class ContextManager {
},
] as any;
projectedEp.yield = undefined;
this.tracer.logEvent(
'ViewGenerator',
`Episode [${ep.id}] has SummaryVariant. Selecting variant over raw text.`,
);
this.tracer.logEvent('ViewGenerator', `Episode [${ep.id}] has SummaryVariant. Selecting variant over raw text.`);
debugLogger.log(
`Opportunistically swapped Episode ${ep.id} for pre-computed Summary variant.`,
);
@@ -406,10 +341,7 @@ export class ContextManager {
tokens: masked.recoveredTokens || 10,
};
}
this.tracer.logEvent(
'ViewGenerator',
`Episode [${ep.id}] has MaskedVariant. Selecting variant over raw text.`,
);
this.tracer.logEvent('ViewGenerator', `Episode [${ep.id}] has MaskedVariant. Selecting variant over raw text.`);
debugLogger.log(
`Opportunistically swapped Episode ${ep.id} for pre-computed Masked variant.`,
);
@@ -420,6 +352,7 @@ export class ContextManager {
rollingTokens += this.calculateIrTokens([projectedEp]);
}
return currentEpisodes;
}
@@ -438,107 +371,75 @@ export class ContextManager {
// Get the dynamically computed Working Buffer View
let currentEpisodes = this.getWorkingBufferView();
currentEpisodes = await this.applyProcessorGraphs(currentEpisodes);
let currentTokens = this.calculateIrTokens(currentEpisodes);
if (currentTokens <= maxTokens) {
this.tracer.logEvent(
'ContextManager',
`View is within maxTokens (${currentTokens} <= ${maxTokens}). Returning view.`,
);
this.tracer.logEvent('ContextManager', `View is within maxTokens (${currentTokens} <= ${maxTokens}). Returning view.`);
return this._projectAndDump(IrMapper.fromIr(currentEpisodes));
}
this.tracer.logEvent(
'ContextManager',
`View exceeds maxTokens (${currentTokens} > ${maxTokens}). Hitting Synchronous Pressure Barrier. Strategy: ${mngConfig.gcBackstop.strategy}`,
);
this.tracer.logEvent('ContextManager', `View exceeds maxTokens (${currentTokens} > ${maxTokens}). Hitting Synchronous Pressure Barrier. Strategy: ${mngConfig.gcBackstop.strategy}`);
// --- 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.
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;
if (mngConfig.gcBackstop.target === 'max') {
targetTokens = mngConfig.budget.retainedTokens;
targetTokens = mngConfig.budget.retainedTokens;
} else if (mngConfig.gcBackstop.target === 'freeNTokens') {
targetTokens =
maxTokens - (mngConfig.gcBackstop.freeTokensTarget ?? 10000);
targetTokens = maxTokens - (mngConfig.gcBackstop.freeTokensTarget ?? 10000);
}
// 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.
const protectedEpisodeId =
this.pristineEpisodes.length > 0 ? this.pristineEpisodes[0].id : null;
const protectedEpisodeId = this.pristineEpisodes.length > 0 ? this.pristineEpisodes[0].id : null;
let remainingTokens = currentTokens;
const truncated: Episode[] = [];
const strategy = mngConfig.gcBackstop.strategy;
for (const ep of currentEpisodes) {
const epTokens = this.calculateIrTokens([ep]);
if (remainingTokens > targetTokens && ep.id !== protectedEpisodeId) {
console.log(
'DROPPING EPISODE:',
ep.id,
'rem:',
remainingTokens,
'tgt:',
targetTokens,
);
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}.`,
);
}
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}.`);
}
} else {
console.log(
'KEEPING EPISODE:',
ep.id,
'rem:',
remainingTokens,
'tgt:',
targetTokens,
);
truncated.push(ep);
console.log('KEEPING EPISODE:', ep.id, 'rem:', remainingTokens, 'tgt:', targetTokens);
truncated.push(ep);
}
}
currentEpisodes = truncated;
const finalTokens = this.calculateIrTokens(currentEpisodes);
this.tracer.logEvent(
'ContextManager',
`Finished projection. Final token count: ${finalTokens}.`,
);
this.tracer.logEvent('ContextManager', `Finished projection. Final token count: ${finalTokens}.`);
debugLogger.log(
`Context Manager finished. Final actual token count: ${finalTokens}.`,
);
@@ -551,20 +452,10 @@ export class ContextManager {
try {
const fs = await import('node:fs/promises');
const path = await import('node:path');
const dumpPath = path.join(
this.env.getTraceDir(),
'.gemini',
'projected_context.json',
);
const dumpPath = path.join(this.env.getTraceDir(), '.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}`,
);
await fs.writeFile(dumpPath, JSON.stringify(contents, null, 2), 'utf-8');
debugLogger.log(`[Observability] Context successfully dumped to ${dumpPath}`);
} catch (e) {
debugLogger.error(`Failed to dump context: ${e}`);
}