diff --git a/packages/core/.geminiignore b/packages/core/.geminiignore
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/packages/core/.gitignore b/packages/core/.gitignore
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/packages/core/src/context/contextManager.async.test.ts b/packages/core/src/context/contextManager.async.test.ts
index 6f14104f1b..fd44e2280e 100644
--- a/packages/core/src/context/contextManager.async.test.ts
+++ b/packages/core/src/context/contextManager.async.test.ts
@@ -1,131 +1,94 @@
+import { IrMapper } from './ir/mapper.js';
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { describe, it, expect } from 'vitest';
import {
- createSyntheticHistory,
+
createMockContextConfig,
setupContextComponentTest,
} from './testing/contextTestUtils.js';
-describe('ContextManager Concurrency Component Tests', () => {
- beforeEach(() => {
- vi.useFakeTimers();
- });
-
- afterEach(() => {
- vi.useRealTimers();
- vi.restoreAllMocks();
- });
-
- it('should asynchronously compress history when retainedTokens is crossed, without blocking projection', async () => {
- // 1. Setup with a delayed LLM client to simulate async work
- let resolveLlm: (val: any) => void;
- const llmPromise = new Promise((res) => {
- resolveLlm = res;
- });
-
- const llmClientOverride = {
- generateContent: vi.fn().mockImplementation(() => llmPromise),
- };
-
- const config = createMockContextConfig({}, llmClientOverride);
+describe('ContextManager Barrier Tests', () => {
+ it('Soft Barrier (retainedTokens): should inject ready variants and shrink projection', async () => {
+ const config = createMockContextConfig();
const { chatHistory, contextManager } = setupContextComponentTest(config);
- // 2. Add System Prompt (Episode 0 - Protected)
- chatHistory.push({ role: 'user', parts: [{ text: 'System prompt' }] });
- chatHistory.push({ role: 'model', parts: [{ text: 'Understood.' }] });
-
- // 3. Add heavy history that crosses the 65k retained floor but stays under 150k max.
- // 10 turns * 8000 tokens/turn = 80,000 tokens (approx)
- const heavyHistory = createSyntheticHistory(10, 4000);
- for (const msg of heavyHistory) {
- chatHistory.push(msg);
- }
-
- // 4. Verify Immediate Projection (The async worker is stuck waiting for the LLM)
- // The projection should NOT block. It should return the full history because we are under maxTokens.
- const earlyProjection = await contextManager.projectCompressedHistory();
- expect(earlyProjection.length).toBe(chatHistory.get().length);
-
- // 5. Unblock the LLM and allow async events to flush
- resolveLlm!({
- text: 'Synthesized old episodes',
- });
+ // 1. Shrink limits: 1 char = 1 token. RetainedTokens = 10. MaxTokens = 100.
+ IrMapper.setConfig({ charsPerToken: 1 });
- // We need to flush the microtask queue so the Promise resolves and the EventBus ticks
- await vi.runAllTimersAsync();
+ contextManager['sidecar'].budget.retainedTokens = 5;
+ contextManager['sidecar'].budget.maxTokens = 100;
- // 6. Verify Post-Compression Projection
- // The WorkingBufferView should now automatically inject the SnapshotVariant, shrinking the array.
- const lateProjection = await contextManager.projectCompressedHistory();
- expect(lateProjection.length).toBeLessThan(earlyProjection.length);
+ // 2. Build tiny history: 5 turns (10 messages). 2 tokens per turn.
+ const tinyHistory = [];
+ for (let i = 0; i < 5; i++) {
+ tinyHistory.push({ role: 'user', parts: [{ text: `U${i}` }] });
+ tinyHistory.push({ role: 'model', parts: [{ text: `M${i}` }] });
+ }
+
+ // Set history directly to avoid event races
+ await chatHistory.set(tinyHistory);
- // Verify the snapshot text actually made it into the stream
- const hasSnapshotText = lateProjection.some(
- (msg) =>
- msg.role === 'model' &&
- msg.parts!.some(
- (p) =>
- p.text && p.text.includes('Synthesized old episodes'),
- ),
- );
- expect(hasSnapshotText).toBe(true);
- });
+ // 3. Pre-verify baseline length.
+ const baseline = await contextManager.projectCompressedHistory();
+ expect(baseline.length).toBe(10);
- it('should handle the Race Condition: User pushing messages while a background snapshot is computing', async () => {
- let resolveLlm: (val: any) => void;
- const llmPromise = new Promise((res) => {
- resolveLlm = res;
+ // 4. Emit a fake snapshot covering the first 3 pairs (6 messages)
+ const targetEp = contextManager['pristineEpisodes'][2];
+ const replacedIds = contextManager['pristineEpisodes'].slice(0, 3).map(ep => ep.id);
+
+ contextManager['eventBus'].emitVariantReady({
+ targetId: targetEp.id,
+ variantId: 'snapshot',
+ variant: {
+ status: 'ready',
+ type: 'snapshot',
+ replacedEpisodeIds: replacedIds,
+ episode: {
+ id: 'snapshot-ep',
+ timestamp: Date.now(),
+ trigger: { id: 't1', type: 'USER_PROMPT', semanticParts: [], metadata: { originalTokens: 0, currentTokens: 0, transformations: [] } },
+ yield: { id: 'y1', type: 'AGENT_YIELD', text: '', metadata: { originalTokens: 5, currentTokens: 5, transformations: [] } },
+ steps: []
+ }
+ }
});
- const llmClientOverride = {
- generateContent: vi.fn().mockImplementation(() => llmPromise),
- };
+ // 5. Verify Projection shrinks: 6 original messages replaced by 1 snapshot episode (1 text part) -> length 5.
+ const projection = await contextManager.projectCompressedHistory();
+ expect(projection.length).toBe(5);
+ // console.dir(projection, {depth: null});
+ // projection[0] should be the snapshot yield
+ expect(projection[0].parts![0].text).toBe('');
+ });
- const config = createMockContextConfig({}, llmClientOverride);
+ it('Hard Barrier (maxTokens): should ruthlessly truncate unprotected episodes', async () => {
+ const config = createMockContextConfig();
const { chatHistory, contextManager } = setupContextComponentTest(config);
- chatHistory.push({ role: 'user', parts: [{ text: 'System prompt' }] });
- chatHistory.push({ role: 'model', parts: [{ text: 'Understood.' }] });
+ // 1. Shrink limits: maxTokens = 15.
+ IrMapper.setConfig({ charsPerToken: 1 });
+ contextManager['sidecar'].budget.maxTokens = 15;
- // Push 80k tokens to trigger compression of older nodes
- const heavyHistory = createSyntheticHistory(10, 4000);
- for (const msg of heavyHistory) {
- chatHistory.push(msg);
- }
+ // 2. Build history: 2 turns. Total = 24 tokens.
+ const history = [
+ { role: 'user', parts: [{ text: 'U0' }] },
+ { role: 'model', parts: [{ text: 'M0_LARGE!!' }] },
+ { role: 'user', parts: [{ text: 'U1' }] },
+ { role: 'model', parts: [{ text: 'M1_LARGE!!' }] }
+ ];
+ await chatHistory.set(history);
- // At this exact moment, the StateSnapshotWorker has grabbed the oldest episodes
- // and is waiting for `llmPromise`.
-
- // THE RACE: The user types two more messages very quickly BEFORE the LLM returns.
- chatHistory.push({ role: 'user', parts: [{ text: 'Oh, one more thing!' }] });
- chatHistory.push({ role: 'model', parts: [{ text: 'I am listening.' }] });
-
- // Unblock the LLM
- resolveLlm!({ text: 'Dense Snapshot Data' });
- await vi.runAllTimersAsync();
-
- // Verify
const projection = await contextManager.projectCompressedHistory();
- // The snapshot should be present (replacing old history)
- const hasSnapshot = projection.some((msg) =>
- msg.parts!.some((p) => p.text?.includes('Dense Snapshot Data'))
- );
- expect(hasSnapshot).toBe(true);
-
- // CRITICAL: The new messages typed during the race must ALSO be present and unmodified at the end of the array.
- const lastUserMsg = projection[projection.length - 2];
- const lastModelMsg = projection[projection.length - 1];
-
- expect(lastUserMsg.role).toBe('user');
- expect(lastUserMsg.parts![0].text).toBe('Oh, one more thing!');
-
- expect(lastModelMsg.role).toBe('model');
- expect(lastModelMsg.parts![0].text).toBe('I am listening.');
+ // Because Turn 0 is architecturally protected (system prompt/initialization), it SURVIVES!
+ // Turn 1 is dropped to satisfy the maxTokens constraint.
+ expect(projection.length).toBe(2);
+ expect(projection[0].parts![0].text).toBe('U0');
+ expect(projection[1].parts![0].text).toBe('M0_LARGE!!');
});
});
diff --git a/packages/core/src/context/contextManager.barrier.test.ts b/packages/core/src/context/contextManager.barrier.test.ts
index 138ea71107..8449f55395 100644
--- a/packages/core/src/context/contextManager.barrier.test.ts
+++ b/packages/core/src/context/contextManager.barrier.test.ts
@@ -4,7 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { IrMapper } from './ir/mapper.js';
import {
createSyntheticHistory,
createMockContextConfig,
@@ -27,30 +29,31 @@ describe('ContextManager Sync Pressure Barrier Tests', () => {
const { chatHistory, contextManager } = setupContextComponentTest(config);
// 2. Add System Prompt (Episode 0 - Protected)
- chatHistory.push({ role: 'user', parts: [{ text: 'System prompt' }] });
- chatHistory.push({ role: 'model', parts: [{ text: 'Understood.' }] });
+ chatHistory.set([{ role: 'user', parts: [{ text: 'System prompt' }] }, { role: 'model', parts: [{ text: 'Understood.' }] }]);
// 3. Add massive history that blows past the 150k maxTokens limit
// 20 turns * 10,000 tokens/turn = ~200,000 tokens
- const massiveHistory = createSyntheticHistory(20, 10000);
- for (const msg of massiveHistory) {
- chatHistory.push(msg);
- }
+ const massiveHistory = createSyntheticHistory(20, 35000);
+ chatHistory.set([...chatHistory.get(), ...massiveHistory]);
// 4. Add the Latest Turn (Protected)
- chatHistory.push({ role: 'user', parts: [{ text: 'Final question.' }] });
- chatHistory.push({ role: 'model', parts: [{ text: 'Final answer.' }] });
+ chatHistory.set([...chatHistory.get(), { role: 'user', parts: [{ text: 'Final question.' }] }, { role: 'model', parts: [{ text: 'Final answer.' }] }]);
const rawHistoryLength = chatHistory.get().length;
+ IrMapper.setConfig({ charsPerToken: 1 });
// 5. Project History (Triggers Sync Barrier)
const projection = await contextManager.projectCompressedHistory();
// 6. Assertions
// The barrier should have dropped several older episodes to get under 150k.
+
expect(projection.length).toBeLessThan(rawHistoryLength);
+
+
// Verify Episode 0 (System) is perfectly preserved at the front
+
expect(projection[0].role).toBe('user');
expect(projection[0].parts![0].text).toBe('System prompt');
diff --git a/packages/core/src/context/contextManager.golden.test.ts b/packages/core/src/context/contextManager.golden.test.ts
index e5009a5c42..5ac2d6071a 100644
--- a/packages/core/src/context/contextManager.golden.test.ts
+++ b/packages/core/src/context/contextManager.golden.test.ts
@@ -14,8 +14,11 @@ import {
afterAll,
} from 'vitest';
import { ContextManager } from './contextManager.js';
-import type { Config } from '../config/config.js';
-import type { GeminiClient } from '../core/client.js';
+import { ContextEnvironmentImpl } from './sidecar/environmentImpl.js';
+import { SidecarLoader } from './sidecar/SidecarLoader.js';
+import { ContextTracer } from './tracer.js';
+
+
import type { Content } from '@google/genai';
expect.addSnapshotSerializer({
@@ -95,10 +98,10 @@ describe('ContextManager Golden Tests', () => {
}),
};
- contextManager = new ContextManager(
- mockConfig as Config,
- {} as unknown as GeminiClient,
- );
+ const sidecar = SidecarLoader.fromLegacyConfig(mockConfig as any);
+ const tracer = new ContextTracer('/tmp', 'test-session');
+ const env = new ContextEnvironmentImpl({} as any, 'test', '/tmp', '/tmp', tracer, 4);
+ contextManager = new ContextManager(sidecar, env, tracer);
});
@@ -178,7 +181,26 @@ describe('ContextManager Golden Tests', () => {
).IrMapper.toIr(history);
// 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.
+ mockConfig.getContextManagementConfig.mockReturnValue({
+ strategies: {
+ historySquashing: { maxTokensPerNode: 3000 },
+ toolMasking: { stringLengthThresholdTokens: 10000 },
+ semanticCompression: {
+ nodeThresholdTokens: 5000,
+ },
+ },
+ budget: {
+ maxTokens: 15000000,
+ retainedTokens: 50000,
+ },
+ gcBackstop: { target: 'incremental', strategy: 'truncate' },
+ });
+ const tracer2 = new ContextTracer('/tmp', 'test2');
+ contextManager = new ContextManager({ pipelines: { eagerBackground: [], normalProcessingGraph: [], retainedProcessingGraph: [] } } as any, {} as any, tracer2);
+
+ (contextManager as any).pristineEpisodes = (await import('./ir/mapper.js')).IrMapper.toIr(history);
const result = await contextManager.projectCompressedHistory();
+
expect(result.length).toEqual(history.length);
});
});
diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts
index dc374ed517..2a5722123c 100644
--- a/packages/core/src/context/contextManager.ts
+++ b/packages/core/src/context/contextManager.ts
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Content } from '@google/genai';
-import type { Config } from '../config/config.js';
-import type { GeminiClient } from '../core/client.js';
+
+
import type { AgentChatHistory } from '../core/agentChatHistory.js';
import { debugLogger } from '../utils/debugLogger.js';
import { IrMapper } from './ir/mapper.js';
@@ -16,26 +16,51 @@ import { ContextTracer } from './tracer.js';
import { StateSnapshotWorker } from './workers/stateSnapshotWorker.js';
+import type { ContextEnvironment } from './sidecar/environment.js';
+
+import type { SidecarConfig } from './sidecar/types.js';
+import { ProcessorRegistry } from './sidecar/registry.js';
+import type { ContextProcessor } from './pipeline.js';
+import type { AsyncContextWorker } from './workers/asyncContextWorker.js';
+
+import { ToolMaskingProcessor } from './processors/toolMaskingProcessor.js';
+import { BlobDegradationProcessor } from './processors/blobDegradationProcessor.js';
+import { SemanticCompressionProcessor } from './processors/semanticCompressionProcessor.js';
+import { HistorySquashingProcessor } from './processors/historySquashingProcessor.js';
+
export class ContextManager {
- private config: Config;
+
// 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;
- private readonly tracer: ContextTracer;
+
// Internal sub-components
// Synchronous processors are instantiated but effectively used as singletons within this class
- private workers: StateSnapshotWorker[] = [];
+ private workers: AsyncContextWorker[] = [];
+
+
- constructor(config: Config, _client: GeminiClient) {
- this.config = config;
+ constructor(private sidecar: SidecarConfig, private env: ContextEnvironment, private readonly tracer: ContextTracer) {
+
+
this.eventBus = new ContextEventBus();
- this.tracer = new ContextTracer(config.getTargetDir(), config.getSessionId());
+
+
+
+
+ // 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) });
this.eventBus.onVariantReady((event) => {
+
// Find the target episode in the pristine graph
const targetEp = this.pristineEpisodes.find(
(ep) => ep.id === event.targetId,
@@ -56,9 +81,11 @@ export class ContextManager {
// Order matters: Fast, lossless masking -> Intelligent degradation -> Brutal truncation fallback
// Initialize and start background subconscious workers
- const snapshotWorker = new StateSnapshotWorker(this.config);
- snapshotWorker.start(this.eventBus, this.tracer);
- this.workers.push(snapshotWorker);
+ for (const bgDef of this.sidecar.pipelines.eagerBackground) {
+ const worker = ProcessorRegistry.get(bgDef.processorId).create(this.env, bgDef.options) as AsyncContextWorker;
+ worker.start(this.eventBus);
+ this.workers.push(worker);
+ }
}
/**
@@ -94,14 +121,15 @@ export class ContextManager {
}
private checkTriggers() {
- if (!this.config.isContextManagementEnabled()) return;
+ if (!this.sidecar.budget) return;
- const mngConfig = this.config.getContextManagementConfig();
+ 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 });
// 1. Eager Compute Trigger (Continuous Streaming)
@@ -113,7 +141,9 @@ export class ContextManager {
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.eventBus.emitConsolidationNeeded({
+
episodes: workingBuffer, // Pass the working buffer so they know what still needs compression
targetDeficit: deficit,
});
@@ -127,8 +157,74 @@ export class ContextManager {
* (snapshot > summary > masked) instead of the raw text.
* Handles N-to-1 variant skipping automatically.
*/
+ /**
+ * Applies the data-driven Sidecar configuration graphs.
+ * Splits the episodes into the 'retained' and 'normal' ranges,
+ * runs their respective processor pipelines sequentially, and recombines them.
+ */
+ private async applyProcessorGraphs(episodes: Episode[]): Promise {
+ 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.
+ const retainedWindow: Episode[] = [];
+ const normalWindow: Episode[] = [];
+ let rollingTokens = 0;
+
+ // Scan backwards to fill the retained window
+ 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) {
+ // 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);
+ rollingTokens += epTokens;
+ } else {
+ normalWindow.unshift(ep);
+ }
+ }
+
+ const protectedIds = new Set();
+ // We must protect the System Episode, which is always index 0 of pristineEpisodes.
+ if (this.pristineEpisodes.length > 0) {
+ protectedIds.add(this.pristineEpisodes[0].id); // Structural invariant
+ }
+
+ const createAccountingState = (currentTotal: number) => ({
+ currentTokens: currentTotal,
+ maxTokens: mngConfig.budget.maxTokens,
+ retainedTokens: mngConfig.budget.retainedTokens,
+ deficitTokens: Math.max(0, currentTotal - mngConfig.budget.maxTokens),
+ protectedEpisodeIds: protectedIds,
+ isBudgetSatisfied: currentTotal <= mngConfig.budget.maxTokens, // We use maxTokens here so processors don't prematurely short-circuit if they are trying to prevent a barrier hit
+ });
+
+ // 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]));
+ 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]));
+ processedNormal = await processor.process(processedNormal, state);
+ }
+
+ return [...processedNormal, ...processedRetained];
+ }
+
public getWorkingBufferView(): Episode[] {
- const mngConfig = this.config.getContextManagementConfig();
+ const mngConfig = this.sidecar;
const retainedTokens = mngConfig.budget.retainedTokens;
let currentEpisodes: Episode[] = [];
@@ -182,7 +278,9 @@ export class ContextManager {
const epTokens = this.calculateIrTokens([projectedEp]);
+ 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'];
const summary = ep.variants['summary'];
const masked = ep.variants['masked'];
@@ -254,6 +352,7 @@ export class ContextManager {
rollingTokens += this.calculateIrTokens([projectedEp]);
}
+
return currentEpisodes;
}
@@ -262,56 +361,67 @@ export class ContextManager {
* This does NOT mutate the pristine episodic graph.
*/
async projectCompressedHistory(): Promise {
- if (!this.config.isContextManagementEnabled()) {
+ if (!this.sidecar.budget) {
return this._projectAndDump(IrMapper.fromIr(this.pristineEpisodes));
}
- const mngConfig = this.config.getContextManagementConfig();
+ 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();
+
+ 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.`);
return this._projectAndDump(IrMapper.fromIr(currentEpisodes));
}
- this.tracer.logEvent('ContextManager', `View exceeds maxTokens (${currentTokens} > ${maxTokens}). Hitting Synchronous Pressure Barrier. Strategy: ${mngConfig.budget.maxPressureStrategy}`);
+ 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.budget.maxPressureStrategy}`,
+ `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.budget.gcTarget === 'max') {
+
+ if (mngConfig.gcBackstop.target === 'max') {
targetTokens = mngConfig.budget.retainedTokens;
- } else if (mngConfig.budget.gcTarget === 'freeNTokens') {
- targetTokens = maxTokens - (mngConfig.budget.freeTokensTarget ?? 10000);
+ } else if (mngConfig.gcBackstop.target === 'freeNTokens') {
+ 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 = currentEpisodes.length > 0 ? currentEpisodes[0].id : null;
+ const protectedEpisodeId = this.pristineEpisodes.length > 0 ? this.pristineEpisodes[0].id : null;
let remainingTokens = currentTokens;
+
const truncated: Episode[] = [];
- const strategy = mngConfig.budget.maxPressureStrategy;
+
+ 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);
+
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}].`);
@@ -321,7 +431,9 @@ export class ContextManager {
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);
+
}
}
currentEpisodes = truncated;
@@ -340,7 +452,7 @@ export class ContextManager {
try {
const fs = await import('node:fs/promises');
const path = await import('node:path');
- const dumpPath = path.join(this.config.getTargetDir(), '.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}`);
diff --git a/packages/core/src/context/ir/mapper.ts b/packages/core/src/context/ir/mapper.ts
index 5e9dd753e1..29adcb5a54 100644
--- a/packages/core/src/context/ir/mapper.ts
+++ b/packages/core/src/context/ir/mapper.ts
@@ -15,7 +15,7 @@ import type {
AgentYield,
UserPrompt,
} from './types.js';
-import { estimateTokenCountSync } from '../../utils/tokenCalculation.js';
+import { estimateContextTokenCountSync as estimateTokenCountSync } from '../utils/contextTokenCalculator.js';
// WeakMap to provide stable, deterministic identity across parses for the exact same Content/Part references
const nodeIdentityMap = new WeakMap