diff --git a/packages/core/src/context/DESIGN.md b/packages/core/src/context/DESIGN.md index fc9baac5bd..c408728f77 100644 --- a/packages/core/src/context/DESIGN.md +++ b/packages/core/src/context/DESIGN.md @@ -41,7 +41,7 @@ The orchestrator reads the `SidecarConfig`. It manages the lifecycle of the pipe ### The Workers: `ContextProcessor`s Small, highly-focused classes that implement context reduction strategies. They do not mutate the graph directly; instead, they are given an `EpisodeEditor` which provides a safe, scoped API to attach `Variant`s and append metadata. -* *Examples:* `ToolMaskingProcessor`, `SemanticCompressionProcessor`, `BlobDegradationProcessor`. +* *Examples:* `ToolMaskingProcessor`, `NodeDistillationProcessor`, `BlobDegradationProcessor`. ### The Glue: `ContextEventBus` A Pub/Sub bus that decouples the components. It enables the `HistoryObserver` to notify the system of new messages, and allows background processors to notify the `ContextManager` when a new compressed variant is ready to be used. diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts index 1bc7b2b1e8..5a4816a700 100644 --- a/packages/core/src/context/contextManager.ts +++ b/packages/core/src/context/contextManager.ts @@ -20,8 +20,8 @@ import { ProcessorRegistry } from './sidecar/registry.js'; export class ContextManager { // The stateful, pristine flat graph. - private pristineShip: readonly ConcreteNode[] = []; - private currentShip: readonly ConcreteNode[] = []; + private pristineNodes: readonly ConcreteNode[] = []; + private currentNodes: readonly ConcreteNode[] = []; private readonly eventBus: ContextEventBus; // Internal sub-components @@ -56,15 +56,15 @@ export class ContextManager { this.orchestrator = orchestrator; this.eventBus.onPristineHistoryUpdated((event) => { - this.pristineShip = event.ship; - // In V2, we assume currentShip updates sequentially via Orchestrator patches. + this.pristineNodes = event.nodes; + // In V2, we assume currentNodes 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)); + // For now, simple fallback: if the current nodes doesn't have the new nodes, append them. + // A more robust implementation would diff the nodes, but for now we'll just track. + const existingIds = new Set(this.currentNodes.map((n) => n.id)); + const addedNodes = event.nodes.filter((n) => !existingIds.has(n.id)); if (addedNodes.length > 0) { - this.currentShip = [...this.currentShip, ...addedNodes]; + this.currentNodes = [...this.currentNodes, ...addedNodes]; } this.evaluateTriggers(event.newNodes); @@ -103,19 +103,19 @@ export class ContextManager { if (newNodes.size > 0) { this.eventBus.emitChunkReceived({ - ship: this.currentShip, + nodes: this.currentNodes, targetNodeIds: newNodes }); } - const currentTokens = this.env.tokenCalculator.calculateConcreteListTokens(this.currentShip); + const currentTokens = this.env.tokenCalculator.calculateConcreteListTokens(this.currentNodes); if (currentTokens > this.sidecar.budget.retainedTokens) { const agedOutNodes = new Set(); let rollingTokens = 0; // 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]; + for (let i = this.currentNodes.length - 1; i >= 0; i--) { + const node = this.currentNodes[i]; rollingTokens += this.env.tokenCalculator.calculateConcreteListTokens([node]); if (rollingTokens > this.sidecar.budget.retainedTokens) { agedOutNodes.add(node.id); @@ -124,7 +124,7 @@ export class ContextManager { if (agedOutNodes.size > 0) { this.eventBus.emitConsolidationNeeded({ - ship: this.currentShip, + nodes: this.currentNodes, targetDeficit: currentTokens - this.sidecar.budget.retainedTokens, targetNodeIds: agedOutNodes, }); @@ -156,7 +156,7 @@ export class ContextManager { * Note: This is an expensive, deep clone operation. */ getPristineGraph(): readonly ConcreteNode[] { - return [...this.pristineShip]; + return [...this.pristineNodes]; } /** @@ -164,8 +164,8 @@ export class ContextManager { * up to the configured token budget. * This is the view that will eventually be projected back to the LLM. */ - getShip(): readonly ConcreteNode[] { - return [...this.currentShip]; + getNodes(): readonly ConcreteNode[] { + return [...this.currentNodes]; } /** @@ -182,7 +182,7 @@ export class ContextManager { ); // Apply final GC Backstop pressure barrier synchronously before mapping const finalHistory = await IrProjector.project( - this.currentShip, + this.currentNodes, this.orchestrator, this.sidecar, this.tracer, diff --git a/packages/core/src/context/eventBus.ts b/packages/core/src/context/eventBus.ts index 1507b09457..1e0192c052 100644 --- a/packages/core/src/context/eventBus.ts +++ b/packages/core/src/context/eventBus.ts @@ -8,18 +8,18 @@ import { EventEmitter } from 'node:events'; import type { ConcreteNode } from './ir/types.js'; export interface PristineHistoryUpdatedEvent { - ship: readonly ConcreteNode[]; + nodes: readonly ConcreteNode[]; newNodes: Set; } export interface ContextConsolidationEvent { - ship: readonly ConcreteNode[]; + nodes: readonly ConcreteNode[]; targetDeficit: number; targetNodeIds: Set; } export interface IrChunkReceivedEvent { - ship: readonly ConcreteNode[]; + nodes: readonly ConcreteNode[]; targetNodeIds: Set; } diff --git a/packages/core/src/context/historyObserver.ts b/packages/core/src/context/historyObserver.ts index e8898b7e65..3ac9eb6eee 100644 --- a/packages/core/src/context/historyObserver.ts +++ b/packages/core/src/context/historyObserver.ts @@ -40,23 +40,23 @@ export class HistoryObserver { (_event: HistoryEvent) => { // Rebuild the pristine IR graph from the full source history on every change. // Wait, toIr still returns an Episode[]. - // We actually need to map the Episode[] to a flat ConcreteNode[] here to form the 'ship'. + // We actually need to map the Episode[] to a flat ConcreteNode[] here to form the 'nodes'. const pristineEpisodes = this.irMapper.toIr( this.chatHistory.get(), this.tokenCalculator, ); - const ship: Array = []; + const nodes: Array = []; for (const ep of pristineEpisodes) { if (ep.concreteNodes) { for (const child of ep.concreteNodes) { - ship.push(child); + nodes.push(child); } } } const newNodes = new Set(); - for (const node of ship) { + for (const node of nodes) { if (!this.seenNodeIds.has(node.id)) { newNodes.add(node.id); this.seenNodeIds.add(node.id); @@ -66,11 +66,11 @@ export class HistoryObserver { this.tracer.logEvent( 'HistoryObserver', 'Rebuilt pristine graph from chat history update', - { shipSize: ship.length, newNodesCount: newNodes.size }, + { nodesSize: nodes.length, newNodesCount: newNodes.size }, ); this.eventBus.emitPristineHistoryUpdated({ - ship, + nodes, newNodes, }); }, diff --git a/packages/core/src/context/ir/fromIr.ts b/packages/core/src/context/ir/fromIr.ts index 4f48d154da..479794a44d 100644 --- a/packages/core/src/context/ir/fromIr.ts +++ b/packages/core/src/context/ir/fromIr.ts @@ -33,9 +33,9 @@ class IrSerializer implements IrSerializationWriter { } } -export function fromIr(ship: readonly ConcreteNode[], registry: IrNodeBehaviorRegistry): Content[] { +export function fromIr(nodes: readonly ConcreteNode[], registry: IrNodeBehaviorRegistry): Content[] { const writer = new IrSerializer(); - for (const node of ship) { + for (const node of nodes) { const behavior = registry.get(node.type); behavior.serialize(node, writer); } diff --git a/packages/core/src/context/ir/mapper.ts b/packages/core/src/context/ir/mapper.ts index f32e2fe507..641929acce 100644 --- a/packages/core/src/context/ir/mapper.ts +++ b/packages/core/src/context/ir/mapper.ts @@ -17,7 +17,7 @@ export class IrMapper { return toIr(history, tokenCalculator, this.nodeIdentityMap); } - fromIr(ship: readonly ConcreteNode[]): Content[] { - return fromIr(ship, this.registry); + fromIr(nodes: readonly ConcreteNode[]): Content[] { + return fromIr(nodes, this.registry); } } diff --git a/packages/core/src/context/ir/projector.ts b/packages/core/src/context/ir/projector.ts index 6fccc786a2..881bf6ed1a 100644 --- a/packages/core/src/context/ir/projector.ts +++ b/packages/core/src/context/ir/projector.ts @@ -16,11 +16,11 @@ import type { SidecarConfig } from '../sidecar/types.js'; export class IrProjector { /** - * Orchestrates the final projection: takes a working buffer view (The Ship), + * Orchestrates the final projection: takes a working buffer view (The Nodes), * applies the Immediate Sanitization pipeline, and enforces token boundaries. */ static async project( - ship: readonly ConcreteNode[], + nodes: readonly ConcreteNode[], orchestrator: PipelineOrchestrator, sidecar: SidecarConfig, tracer: ContextTracer, @@ -28,7 +28,7 @@ export class IrProjector { protectedIds: Set, ): Promise { if (!sidecar.budget) { - const contents = env.irMapper.fromIr(ship); + const contents = env.irMapper.fromIr(nodes); tracer.logEvent('IrProjector', 'Projected Context to LLM (No Budget)', { projectedContext: contents, }); @@ -36,14 +36,14 @@ export class IrProjector { } const maxTokens = sidecar.budget.maxTokens; - const currentTokens = env.tokenCalculator.calculateConcreteListTokens(ship); + const currentTokens = env.tokenCalculator.calculateConcreteListTokens(nodes); // V0: Always protect the first node (System Prompt) and the last turn - if (ship.length > 0) { - protectedIds.add(ship[0].id); - if (ship[0].logicalParentId) protectedIds.add(ship[0].logicalParentId); + if (nodes.length > 0) { + protectedIds.add(nodes[0].id); + if (nodes[0].logicalParentId) protectedIds.add(nodes[0].logicalParentId); - const lastNode = ship[ship.length - 1]; + const lastNode = nodes[nodes.length - 1]; protectedIds.add(lastNode.id); if (lastNode.logicalParentId) protectedIds.add(lastNode.logicalParentId); } @@ -53,7 +53,7 @@ export class IrProjector { 'IrProjector', `View is within maxTokens (${currentTokens} <= ${maxTokens}). Returning view.`, ); - const contents = env.irMapper.fromIr(ship); + const contents = env.irMapper.fromIr(nodes); tracer.logEvent('IrProjector', 'Projected Context to LLM', { projectedContext: contents, }); @@ -72,8 +72,8 @@ export class IrProjector { const agedOutNodes = new Set(); let rollingTokens = 0; // Start from newest and count backwards - for (let i = ship.length - 1; i >= 0; i--) { - const node = ship[i]; + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i]; const nodeTokens = env.tokenCalculator.calculateConcreteListTokens([node]); rollingTokens += nodeTokens; if (rollingTokens > sidecar.budget.retainedTokens) { @@ -81,15 +81,15 @@ export class IrProjector { } } - const processedShip = await orchestrator.executeTriggerSync( + const processedNodes = await orchestrator.executeTriggerSync( 'gc_backstop', - ship, + nodes, agedOutNodes, protectedIds, ); const finalTokens = - env.tokenCalculator.calculateConcreteListTokens(processedShip); + env.tokenCalculator.calculateConcreteListTokens(processedNodes); tracer.logEvent( 'IrProjector', `Finished projection. Final token count: ${finalTokens}.`, @@ -100,15 +100,15 @@ export class IrProjector { // Apply skipList logic to abstract over summarized nodes const skipList = new Set(); - for (const node of processedShip) { + for (const node of processedNodes) { if (node.abstractsIds) { for (const id of node.abstractsIds) skipList.add(id); } } - const visibleShip = processedShip.filter((n) => !skipList.has(n.id)); + const visibleNodes = processedNodes.filter((n) => !skipList.has(n.id)); - const contents = env.irMapper.fromIr(visibleShip); + const contents = env.irMapper.fromIr(visibleNodes); tracer.logEvent('IrProjector', 'Projected Sanitized Context to LLM', { projectedContextSanitized: contents, }); diff --git a/packages/core/src/context/ir/types.ts b/packages/core/src/context/ir/types.ts index 9e65aaa84c..bbe91260c9 100644 --- a/packages/core/src/context/ir/types.ts +++ b/packages/core/src/context/ir/types.ts @@ -31,7 +31,7 @@ export interface IrNode { /** * Concrete Nodes: The atomic, renderable pieces of data. - * These are the actual "planks" of the Ship of Theseus. + * These are the actual "planks" of the Nodes of Theseus. */ export interface BaseConcreteNode extends IrNode { /** The ID of the Logical Node (e.g., Episode) that structurally owns this node */ diff --git a/packages/core/src/context/migration-plan.md b/packages/core/src/context/migration-plan.md index 168ddb0feb..6cdf27435a 100644 --- a/packages/core/src/context/migration-plan.md +++ b/packages/core/src/context/migration-plan.md @@ -1,4 +1,4 @@ -# The Ship of Theseus Migration Checklist +# The Nodes of Theseus Migration Checklist - [x] **Phase 1: Core Types (`ir/types.ts`)** - [x] Add `ConcreteNode` and `LogicalNode` types. @@ -15,13 +15,13 @@ - [x] **Phase 3: The Reducer (`sidecar/orchestrator.ts`)** - [x] Update `executePipeline` and `executeTriggerSync` to act as a reducer. - - [x] Map `ContextPatch` results onto the flat Ship array. + - [x] Map `ContextPatch` results onto the flat Nodes array. - [x] **Phase 4: Pristine Graph & Mapping (`contextManager.ts` & `ir/toIr.ts`)** - [x] Update `toIr` to produce a flat list of `ConcreteNode`s and a tree of `LogicalNode`s. - [x] Make `ContextManager` track the Pristine Graph and instantiate the flat - Ship. + Nodes. - [x] Commit patches to the Pristine Graph history. - [x] **Phase 5: The Walker (`ir/projector.ts`)** @@ -30,8 +30,8 @@ - [ ] **Phase 6: Refactoring Processors** - [ ] `ToolMaskingProcessor` - - [ ] `SemanticCompressionProcessor` + - [ ] `NodeDistillationProcessor` - [ ] `BlobDegradationProcessor` - - [ ] `EmergencyTruncationProcessor` - - [ ] `HistorySquashingProcessor` + - [ ] `HistoryTruncationProcessor` + - [ ] `NodeTruncationProcessor` - [ ] `StateSnapshotProcessor` diff --git a/packages/core/src/context/processors/emergencyTruncationProcessor.ts b/packages/core/src/context/processors/historyTruncationProcessor.ts similarity index 80% rename from packages/core/src/context/processors/emergencyTruncationProcessor.ts rename to packages/core/src/context/processors/historyTruncationProcessor.ts index ec68ddaeb2..dd8dddf5ef 100644 --- a/packages/core/src/context/processors/emergencyTruncationProcessor.ts +++ b/packages/core/src/context/processors/historyTruncationProcessor.ts @@ -12,14 +12,14 @@ import type { import type { ConcreteNode } from '../ir/types.js'; import type { ContextEnvironment } from '../sidecar/environment.js'; -export type EmergencyTruncationProcessorOptions = BackstopTargetOptions; +export type HistoryTruncationProcessorOptions = BackstopTargetOptions; -export class EmergencyTruncationProcessor implements ContextProcessor { +export class HistoryTruncationProcessor implements ContextProcessor { static create( env: ContextEnvironment, - options: EmergencyTruncationProcessorOptions, - ): EmergencyTruncationProcessor { - return new EmergencyTruncationProcessor(env, options); + options: HistoryTruncationProcessorOptions, + ): HistoryTruncationProcessor { + return new HistoryTruncationProcessor(env, options); } static readonly schema = { @@ -37,13 +37,13 @@ export class EmergencyTruncationProcessor implements ContextProcessor { }, }; - readonly id = 'EmergencyTruncationProcessor'; - readonly name = 'EmergencyTruncationProcessor'; + readonly id = 'HistoryTruncationProcessor'; + readonly name = 'HistoryTruncationProcessor'; private readonly env: ContextEnvironment; - readonly options: EmergencyTruncationProcessorOptions; + readonly options: HistoryTruncationProcessorOptions; constructor( env: ContextEnvironment, - options: EmergencyTruncationProcessorOptions, + options: HistoryTruncationProcessorOptions, ) { this.env = env; this.options = options; diff --git a/packages/core/src/context/processors/semanticCompressionProcessor.test.ts b/packages/core/src/context/processors/nodeDistillationProcessor.test.ts similarity index 93% rename from packages/core/src/context/processors/semanticCompressionProcessor.test.ts rename to packages/core/src/context/processors/nodeDistillationProcessor.test.ts index c33787b550..da5a566fe0 100644 --- a/packages/core/src/context/processors/semanticCompressionProcessor.test.ts +++ b/packages/core/src/context/processors/nodeDistillationProcessor.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, vi } from 'vitest'; -import { SemanticCompressionProcessor } from './semanticCompressionProcessor.js'; +import { NodeDistillationProcessor } from './nodeDistillationProcessor.js'; import { createMockEnvironment, createDummyNode, @@ -13,7 +13,7 @@ import { } from '../testing/contextTestUtils.js'; import type { UserPrompt, AgentThought, ToolExecution } from '../ir/types.js'; -describe('SemanticCompressionProcessor', () => { +describe('NodeDistillationProcessor', () => { it('should trigger summarization via LLM for long text parts', async () => { const mockLlmClient = { generateContent: vi.fn().mockResolvedValue(createMockGenerateContentResponse('Mocked Summary!')), // length = 15 @@ -23,7 +23,7 @@ describe('SemanticCompressionProcessor', () => { llmClient: mockLlmClient as any, }); - const processor = SemanticCompressionProcessor.create(env, { + const processor = NodeDistillationProcessor.create(env, { nodeThresholdTokens: 10, }); @@ -81,7 +81,7 @@ describe('SemanticCompressionProcessor', () => { llmClient: mockLlmClient as any, }); - const processor = SemanticCompressionProcessor.create(env, { + const processor = NodeDistillationProcessor.create(env, { nodeThresholdTokens: 100, // Very high threshold }); diff --git a/packages/core/src/context/processors/semanticCompressionProcessor.ts b/packages/core/src/context/processors/nodeDistillationProcessor.ts similarity index 91% rename from packages/core/src/context/processors/semanticCompressionProcessor.ts rename to packages/core/src/context/processors/nodeDistillationProcessor.ts index a6419fd9e2..1f959ed2ed 100644 --- a/packages/core/src/context/processors/semanticCompressionProcessor.ts +++ b/packages/core/src/context/processors/nodeDistillationProcessor.ts @@ -9,16 +9,16 @@ import type { ContextEnvironment } from '../sidecar/environment.js'; import { debugLogger } from '../../utils/debugLogger.js'; import { getResponseText } from '../../utils/partUtils.js'; -export interface SemanticCompressionProcessorOptions { +export interface NodeDistillationProcessorOptions { nodeThresholdTokens: number; } -export class SemanticCompressionProcessor implements ContextProcessor { +export class NodeDistillationProcessor implements ContextProcessor { static create( env: ContextEnvironment, - options: SemanticCompressionProcessorOptions, - ): SemanticCompressionProcessor { - return new SemanticCompressionProcessor(env, options); + options: NodeDistillationProcessorOptions, + ): NodeDistillationProcessor { + return new NodeDistillationProcessor(env, options); } static readonly schema = { @@ -32,14 +32,14 @@ export class SemanticCompressionProcessor implements ContextProcessor { required: ['nodeThresholdTokens'], }; - readonly id = 'SemanticCompressionProcessor'; - readonly name = 'SemanticCompressionProcessor'; - readonly options: SemanticCompressionProcessorOptions; + readonly id = 'NodeDistillationProcessor'; + readonly name = 'NodeDistillationProcessor'; + readonly options: NodeDistillationProcessorOptions; private env: ContextEnvironment; constructor( env: ContextEnvironment, - options: SemanticCompressionProcessorOptions, + options: NodeDistillationProcessorOptions, ) { this.env = env; this.options = options; @@ -74,7 +74,7 @@ export class SemanticCompressionProcessor implements ContextProcessor { ); return getResponseText(response) || text; } catch (e) { - debugLogger.warn(`SemanticCompressionProcessor failed to summarize ${contextInfo}`, e); + debugLogger.warn(`NodeDistillationProcessor failed to summarize ${contextInfo}`, e); return text; // Fallback to original text on API failure } } diff --git a/packages/core/src/context/processors/historySquashingProcessor.test.ts b/packages/core/src/context/processors/nodeTruncationProcessor.test.ts similarity index 94% rename from packages/core/src/context/processors/historySquashingProcessor.test.ts rename to packages/core/src/context/processors/nodeTruncationProcessor.test.ts index 9ec57576ee..7fbf288a01 100644 --- a/packages/core/src/context/processors/historySquashingProcessor.test.ts +++ b/packages/core/src/context/processors/nodeTruncationProcessor.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, vi } from 'vitest'; -import { HistorySquashingProcessor } from './historySquashingProcessor.js'; +import { NodeTruncationProcessor } from './nodeTruncationProcessor.js'; import { createMockEnvironment, createDummyNode, @@ -12,7 +12,7 @@ import { import type { UserPrompt, AgentThought, AgentYield } from '../ir/types.js'; import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js'; -describe('HistorySquashingProcessor', () => { +describe('NodeTruncationProcessor', () => { it('should truncate nodes that exceed maxTokensPerNode', async () => { const env = createMockEnvironment(); const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any; @@ -26,7 +26,7 @@ describe('HistorySquashingProcessor', () => { (env as any).tokenCalculator = mockTokenCalculator; - const processor = HistorySquashingProcessor.create(env, { + const processor = NodeTruncationProcessor.create(env, { maxTokensPerNode: 1, // Will equal 10 chars limit }); @@ -88,7 +88,7 @@ describe('HistorySquashingProcessor', () => { (env as any).tokenCalculator = mockTokenCalculator; - const processor = HistorySquashingProcessor.create(env, { + const processor = NodeTruncationProcessor.create(env, { maxTokensPerNode: 100, }); diff --git a/packages/core/src/context/processors/historySquashingProcessor.ts b/packages/core/src/context/processors/nodeTruncationProcessor.ts similarity index 89% rename from packages/core/src/context/processors/historySquashingProcessor.ts rename to packages/core/src/context/processors/nodeTruncationProcessor.ts index c86f888430..99aedc53ca 100644 --- a/packages/core/src/context/processors/historySquashingProcessor.ts +++ b/packages/core/src/context/processors/nodeTruncationProcessor.ts @@ -8,16 +8,16 @@ import type { ContextEnvironment } from '../sidecar/environment.js'; import { truncateProportionally } from '../truncation.js'; import type { ConcreteNode } from '../ir/types.js'; -export interface HistorySquashingProcessorOptions { +export interface NodeTruncationProcessorOptions { maxTokensPerNode: number; } -export class HistorySquashingProcessor implements ContextProcessor { +export class NodeTruncationProcessor implements ContextProcessor { static create( env: ContextEnvironment, - options: HistorySquashingProcessorOptions, - ): HistorySquashingProcessor { - return new HistorySquashingProcessor(env, options); + options: NodeTruncationProcessorOptions, + ): NodeTruncationProcessor { + return new NodeTruncationProcessor(env, options); } static readonly schema = { @@ -31,14 +31,14 @@ export class HistorySquashingProcessor implements ContextProcessor { required: ['maxTokensPerNode'], }; - readonly id = 'HistorySquashingProcessor'; - readonly name = 'HistorySquashingProcessor'; - readonly options: HistorySquashingProcessorOptions; + readonly id = 'NodeTruncationProcessor'; + readonly name = 'NodeTruncationProcessor'; + readonly options: NodeTruncationProcessorOptions; private env: ContextEnvironment; constructor( env: ContextEnvironment, - options: HistorySquashingProcessorOptions, + options: NodeTruncationProcessorOptions, ) { this.env = env; this.options = options; diff --git a/packages/core/src/context/processors/rollingSummaryProcessor.ts b/packages/core/src/context/processors/rollingSummaryProcessor.ts new file mode 100644 index 0000000000..30f2b1e5a7 --- /dev/null +++ b/packages/core/src/context/processors/rollingSummaryProcessor.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import type { ContextProcessor, ProcessArgs, BackstopTargetOptions } from '../pipeline.js'; +import type { ContextEnvironment } from '../sidecar/environment.js'; +import type { ConcreteNode, RollingSummary } from '../ir/types.js'; +import { SnapshotGenerator } from '../utils/snapshotGenerator.js'; +import { debugLogger } from '../../utils/debugLogger.js'; + +export interface RollingSummaryProcessorOptions extends BackstopTargetOptions { + systemInstruction?: string; +} + +export class RollingSummaryProcessor implements ContextProcessor { + static readonly schema = { + type: 'object', + properties: { + target: { type: 'string', enum: ['incremental', 'freeNTokens', 'max'] }, + freeTokensTarget: { type: 'number' }, + systemInstruction: { type: 'string' }, + }, + }; + + static create( + env: ContextEnvironment, + options: RollingSummaryProcessorOptions, + ): RollingSummaryProcessor { + return new RollingSummaryProcessor(env, options); + } + + readonly id = 'RollingSummaryProcessor'; + readonly name = 'RollingSummaryProcessor'; + readonly options: RollingSummaryProcessorOptions; + private readonly env: ContextEnvironment; + private readonly generator: SnapshotGenerator; + + constructor(env: ContextEnvironment, options: RollingSummaryProcessorOptions) { + this.env = env; + this.options = options; + this.generator = new SnapshotGenerator(env); + } + + async process({ targets }: ProcessArgs): Promise { + if (targets.length === 0) return targets; + + const strategy = this.options.target ?? 'max'; + let targetTokensToRemove = 0; + + if (strategy === 'incremental') { + // A rolling summary should target a small chunk. For now, since state isn't passed, + // we'll default to a fixed threshold, like 10000 tokens, to avoid eating the whole history. + // Ideally, the orchestrator should pass `tokensToRemove` explicitly. + targetTokensToRemove = 10000; + } else if (strategy === 'freeNTokens') { + targetTokensToRemove = this.options.freeTokensTarget ?? Infinity; + } else if (strategy === 'max') { + targetTokensToRemove = Infinity; + } + + if (targetTokensToRemove <= 0) return targets; + + let deficitAccumulator = 0; + const nodesToSummarize: ConcreteNode[] = []; + + // Scan oldest to newest to find the oldest block that exceeds the token requirement + for (const node of targets) { + if (node.id === targets[0].id && node.type === 'USER_PROMPT') { + // Keep system prompt if it's the very first node + continue; + } + + nodesToSummarize.push(node); + deficitAccumulator += this.env.tokenCalculator.getTokenCost(node); + + if (deficitAccumulator >= targetTokensToRemove) break; + } + + if (nodesToSummarize.length < 2) return targets; // Not enough context to summarize + + try { + // Synthesize the rolling summary synchronously + const snapshotText = await this.generator.synthesizeSnapshot(nodesToSummarize, this.options.systemInstruction); + const newId = this.env.idGenerator.generateId(); + + const summaryNode: RollingSummary = { + id: newId, + logicalParentId: newId, + type: 'ROLLING_SUMMARY', + timestamp: Date.now(), + text: snapshotText, + }; + + const consumedIds = nodesToSummarize.map(n => n.id); + const returnedNodes = targets.filter(t => !consumedIds.includes(t.id)); + const firstRemovedIdx = targets.findIndex(t => consumedIds.includes(t.id)); + + if (firstRemovedIdx !== -1) { + const idx = Math.max(0, firstRemovedIdx); + returnedNodes.splice(idx, 0, summaryNode); + } else { + returnedNodes.unshift(summaryNode); + } + + return returnedNodes; + + } catch (e) { + debugLogger.error('RollingSummaryProcessor failed sync backstop', e); + return targets; + } + } +} diff --git a/packages/core/src/context/processors/stateSnapshotProcessor.ts b/packages/core/src/context/processors/stateSnapshotProcessor.ts index a7e5170488..1f86cc36f4 100644 --- a/packages/core/src/context/processors/stateSnapshotProcessor.ts +++ b/packages/core/src/context/processors/stateSnapshotProcessor.ts @@ -15,6 +15,16 @@ export interface StateSnapshotProcessorOptions extends BackstopTargetOptions { } export class StateSnapshotProcessor implements ContextProcessor { + static readonly schema = { + type: 'object', + properties: { + target: { type: 'string', enum: ['incremental', 'freeNTokens', 'max'] }, + freeTokensTarget: { type: 'number' }, + model: { type: 'string' }, + systemInstruction: { type: 'string' }, + }, + }; + static create( env: ContextEnvironment, options: StateSnapshotProcessorOptions, diff --git a/packages/core/src/context/processors/stateSnapshotWorker.ts b/packages/core/src/context/processors/stateSnapshotWorker.ts index 4d637d8229..3cca9c8e52 100644 --- a/packages/core/src/context/processors/stateSnapshotWorker.ts +++ b/packages/core/src/context/processors/stateSnapshotWorker.ts @@ -15,6 +15,14 @@ export interface StateSnapshotWorkerOptions { } export class StateSnapshotWorker implements ContextWorker { + static readonly schema = { + type: 'object', + properties: { + type: { type: 'string', enum: ['accumulate', 'point-in-time'] }, + systemInstruction: { type: 'string' }, + }, + }; + static create( env: ContextEnvironment, options: StateSnapshotWorkerOptions, diff --git a/packages/core/src/context/sidecar/builtins.ts b/packages/core/src/context/sidecar/builtins.ts index 02892c064e..476b17b440 100644 --- a/packages/core/src/context/sidecar/builtins.ts +++ b/packages/core/src/context/sidecar/builtins.ts @@ -4,13 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ import type { ProcessorRegistry } from './registry.js'; -import { EmergencyTruncationProcessor, type EmergencyTruncationProcessorOptions } from '../processors/emergencyTruncationProcessor.js'; +import { HistoryTruncationProcessor, type HistoryTruncationProcessorOptions } from '../processors/historyTruncationProcessor.js'; import { BlobDegradationProcessor } from '../processors/blobDegradationProcessor.js'; -import { HistorySquashingProcessor, type HistorySquashingProcessorOptions } from '../processors/historySquashingProcessor.js'; -import { SemanticCompressionProcessor, type SemanticCompressionProcessorOptions } from '../processors/semanticCompressionProcessor.js'; +import { NodeTruncationProcessor, type NodeTruncationProcessorOptions } from '../processors/nodeTruncationProcessor.js'; +import { NodeDistillationProcessor, type NodeDistillationProcessorOptions } from '../processors/nodeDistillationProcessor.js'; import { ToolMaskingProcessor, type ToolMaskingProcessorOptions } from '../processors/toolMaskingProcessor.js'; import { StateSnapshotProcessor, type StateSnapshotProcessorOptions } from '../processors/stateSnapshotProcessor.js'; import { StateSnapshotWorker, type StateSnapshotWorkerOptions } from '../processors/stateSnapshotWorker.js'; +import { RollingSummaryProcessor, type RollingSummaryProcessorOptions } from '../processors/rollingSummaryProcessor.js'; export function registerBuiltInProcessors(registry: ProcessorRegistry) { registry.register>({ @@ -19,22 +20,22 @@ export function registerBuiltInProcessors(registry: ProcessorRegistry) { create: (env) => new BlobDegradationProcessor(env), }); - registry.register({ - id: 'EmergencyTruncationProcessor', - schema: EmergencyTruncationProcessor.schema, - create: (env, options) => EmergencyTruncationProcessor.create(env, options), + registry.register({ + id: 'HistoryTruncationProcessor', + schema: HistoryTruncationProcessor.schema, + create: (env, options) => HistoryTruncationProcessor.create(env, options), }); - registry.register({ - id: 'HistorySquashingProcessor', - schema: HistorySquashingProcessor.schema, - create: (env, options) => HistorySquashingProcessor.create(env, options), + registry.register({ + id: 'NodeTruncationProcessor', + schema: NodeTruncationProcessor.schema, + create: (env, options) => NodeTruncationProcessor.create(env, options), }); - registry.register({ - id: 'SemanticCompressionProcessor', - schema: SemanticCompressionProcessor.schema, - create: (env, options) => SemanticCompressionProcessor.create(env, options), + registry.register({ + id: 'NodeDistillationProcessor', + schema: NodeDistillationProcessor.schema, + create: (env, options) => NodeDistillationProcessor.create(env, options), }); registry.register({ @@ -45,13 +46,19 @@ export function registerBuiltInProcessors(registry: ProcessorRegistry) { registry.register({ id: 'StateSnapshotProcessor', - schema: {}, // Will be added later + schema: StateSnapshotProcessor.schema, create: (env, options) => StateSnapshotProcessor.create(env, options), }); registry.register({ id: 'StateSnapshotWorker', - schema: {}, // Will be added later + schema: StateSnapshotWorker.schema, create: (env, options) => StateSnapshotWorker.create(env, options) as any, // ContextWorker instead of ContextProcessor }); + + registry.register({ + id: 'RollingSummaryProcessor', + schema: RollingSummaryProcessor.schema, + create: (env, options) => RollingSummaryProcessor.create(env, options), + }); } diff --git a/packages/core/src/context/sidecar/orchestrator.test.ts b/packages/core/src/context/sidecar/orchestrator.test.ts index 2d24bef776..7b8c74be90 100644 --- a/packages/core/src/context/sidecar/orchestrator.test.ts +++ b/packages/core/src/context/sidecar/orchestrator.test.ts @@ -165,12 +165,12 @@ describe('PipelineOrchestrator (Component)', () => { registry, ); - const ship = [createDummyNode('not-protected-ep', 'USER_PROMPT', 100, undefined, 'not-protected-id')]; + const nodes = [createDummyNode('not-protected-ep', 'USER_PROMPT', 100, undefined, 'not-protected-id')]; const result = await orchestrator.executeTriggerSync( 'new_message', - ship, - new Set(ship.map((s) => s.id)), + nodes, + new Set(nodes.map((s) => s.id)), new Set(), ); @@ -199,13 +199,13 @@ describe('PipelineOrchestrator (Component)', () => { registry, ); - const ship = [createDummyNode('not-protected-ep', 'USER_PROMPT', 100, undefined, 'not-protected-id')]; + const nodes = [createDummyNode('not-protected-ep', 'USER_PROMPT', 100, undefined, 'not-protected-id')]; // This should resolve immediately with the UNMODIFIED array because execution is background const result = await orchestrator.executeTriggerSync( 'new_message', - ship, - new Set(ship.map((s) => s.id)), + nodes, + new Set(nodes.map((s) => s.id)), new Set(), ); @@ -237,18 +237,18 @@ describe('PipelineOrchestrator (Component)', () => { registry, ); - const ship = [createDummyNode('not-protected-ep', 'USER_PROMPT', 100, undefined, 'not-protected-id')]; + const nodes = [createDummyNode('not-protected-ep', 'USER_PROMPT', 100, undefined, 'not-protected-id')]; // It should not throw! It should swallow the error and return the unmodified array. const result = await orchestrator.executeTriggerSync( 'new_message', - ship, - new Set(ship.map((s) => s.id)), + nodes, + new Set(nodes.map((s) => s.id)), new Set(), ); expect(result).toHaveLength(1); - expect(result).toStrictEqual(ship); + expect(result).toStrictEqual(nodes); }); it('automatically binds to retained_exceeded trigger via EventBus', () => { @@ -272,11 +272,11 @@ describe('PipelineOrchestrator (Component)', () => { new PipelineOrchestrator(config, env, eventBus, env.tracer, registry); - const ship = [createDummyNode('1', 'USER_PROMPT')]; + const nodes = [createDummyNode('1', 'USER_PROMPT')]; // Emit the trigger eventBus.emitConsolidationNeeded({ - ship, + nodes, targetDeficit: 100, targetNodeIds: new Set(), }); diff --git a/packages/core/src/context/sidecar/orchestrator.ts b/packages/core/src/context/sidecar/orchestrator.ts index 9dd78e0614..5752f37645 100644 --- a/packages/core/src/context/sidecar/orchestrator.ts +++ b/packages/core/src/context/sidecar/orchestrator.ts @@ -125,7 +125,7 @@ export class PipelineOrchestrator { this.eventBus.onConsolidationNeeded((event) => { void this.executePipelineAsync( pipeline, - event.ship, + event.nodes, event.targetNodeIds, new Set(), // protected IDs ); @@ -134,7 +134,7 @@ export class PipelineOrchestrator { this.eventBus.onChunkReceived((event) => { void this.executePipelineAsync( pipeline, - event.ship, + event.nodes, event.targetNodeIds, new Set(), // protected IDs ); @@ -151,7 +151,7 @@ export class PipelineOrchestrator { const inboxSnapshot = new InboxSnapshotImpl( this.env.inbox?.getMessages() || [], ); - const targets = event.ship.filter(n => event.targetNodeIds.has(n.id)); + const targets = event.nodes.filter(n => event.targetNodeIds.has(n.id)); // Fire and forget worker.execute({ targets, inbox: inboxSnapshot }).catch((e) => { debugLogger.error(`Worker ${worker.name} failed onNodesAdded:`, e); @@ -167,7 +167,7 @@ export class PipelineOrchestrator { const inboxSnapshot = new InboxSnapshotImpl( this.env.inbox?.getMessages() || [], ); - const targets = event.ship.filter(n => event.targetNodeIds.has(n.id)); + const targets = event.nodes.filter(n => event.targetNodeIds.has(n.id)); // Fire and forget worker.execute({ targets, inbox: inboxSnapshot }).catch((e) => { debugLogger.error(`Worker ${worker.name} failed onNodesAgedOut:`, e); @@ -187,11 +187,11 @@ export class PipelineOrchestrator { } applyProcessorDiff( - ship: readonly ConcreteNode[], + nodes: readonly ConcreteNode[], targets: readonly ConcreteNode[], returnedNodes: readonly ConcreteNode[], ): readonly ConcreteNode[] { - const mutableShip = [...ship]; + const mutableNodes = [...nodes]; const targetSet = new Set(targets.map((n) => n.id)); const returnedMap = new Map(returnedNodes.map((n) => [n.id, n])); @@ -215,34 +215,34 @@ export class PipelineOrchestrator { } if (removedIds.size === 0 && newNodes.length === 0) { - return ship; + return nodes; } - let earliestRemovalIdx = mutableShip.length; + let earliestRemovalIdx = mutableNodes.length; let i = 0; - while (i < mutableShip.length) { - if (removedIds.has(mutableShip[i].id)) { + while (i < mutableNodes.length) { + if (removedIds.has(mutableNodes[i].id)) { if (i < earliestRemovalIdx) earliestRemovalIdx = i; - mutableShip.splice(i, 1); + mutableNodes.splice(i, 1); } else { i++; } } if (newNodes.length > 0) { - mutableShip.splice(earliestRemovalIdx, 0, ...newNodes); + mutableNodes.splice(earliestRemovalIdx, 0, ...newNodes); } - return mutableShip; + return mutableNodes; } async executeTriggerSync( trigger: PipelineTrigger, - ship: readonly ConcreteNode[], + nodes: readonly ConcreteNode[], triggerTargets: ReadonlySet, protectedLogicalIds: ReadonlySet = new Set(), ): Promise { - let currentShip = ship; + let currentNodes = nodes; const pipelines = this.config.pipelines.filter((p) => p.triggers.includes(trigger), ); @@ -263,18 +263,18 @@ export class PipelineOrchestrator { `Executing processor synchronously: ${procDef.processorId}`, ); - const allowedTargets = currentShip.filter((n) => + const allowedTargets = currentNodes.filter((n) => this.isNodeAllowed(n, triggerTargets, protectedLogicalIds), ); const returnedNodes = await processor.process({ - buffer: new ContextWorkingBufferImpl(currentShip), + buffer: new ContextWorkingBufferImpl(currentNodes), targets: allowedTargets, inbox: inboxSnapshot, }); - currentShip = this.applyProcessorDiff( - currentShip, + currentNodes = this.applyProcessorDiff( + currentNodes, allowedTargets, returnedNodes, ); @@ -290,12 +290,12 @@ export class PipelineOrchestrator { // Success! Drain consumed messages this.env.inbox?.drainConsumed(inboxSnapshot.getConsumedIds()); - return currentShip; + return currentNodes; } private async executePipelineAsync( pipeline: PipelineDef, - ship: readonly ConcreteNode[], + nodes: readonly ConcreteNode[], triggerTargets: Set, protectedLogicalIds: ReadonlySet = new Set(), ) { @@ -303,9 +303,9 @@ export class PipelineOrchestrator { 'Orchestrator', `Triggering async pipeline: ${pipeline.name}`, ); - if (!ship || ship.length === 0) return; + if (!nodes || nodes.length === 0) return; - let currentShip = ship; + let currentNodes = nodes; const inboxSnapshot = new InboxSnapshotImpl( this.env.inbox?.getMessages() || [], ); @@ -320,18 +320,18 @@ export class PipelineOrchestrator { `Executing processor: ${procDef.processorId} (async)`, ); - const allowedTargets = currentShip.filter((n) => + const allowedTargets = currentNodes.filter((n) => this.isNodeAllowed(n, triggerTargets, protectedLogicalIds), ); const returnedNodes = await processor.process({ - buffer: new ContextWorkingBufferImpl(currentShip), + buffer: new ContextWorkingBufferImpl(currentNodes), targets: allowedTargets, inbox: inboxSnapshot, }); - currentShip = this.applyProcessorDiff( - currentShip, + currentNodes = this.applyProcessorDiff( + currentNodes, allowedTargets, returnedNodes, ); diff --git a/packages/core/src/context/sidecar/profiles.ts b/packages/core/src/context/sidecar/profiles.ts index eafc3845bf..da98ce4193 100644 --- a/packages/core/src/context/sidecar/profiles.ts +++ b/packages/core/src/context/sidecar/profiles.ts @@ -42,11 +42,11 @@ export const defaultSidecarProfile: SidecarConfig = { execution: 'background', processors: [ { - processorId: 'HistorySquashingProcessor', + processorId: 'NodeTruncationProcessor', options: { maxTokensPerNode: 3000 }, }, { - processorId: 'SemanticCompressionProcessor', + processorId: 'NodeDistillationProcessor', options: { nodeThresholdTokens: 5000 }, }, ], @@ -60,7 +60,6 @@ export const defaultSidecarProfile: SidecarConfig = { processorId: 'StateSnapshotProcessor', options: { target: 'max' } }, - { processorId: 'EmergencyTruncationProcessor', options: {} }, ], }, ], diff --git a/packages/core/src/context/sidecar/testProfile.ts b/packages/core/src/context/sidecar/testProfile.ts index 60ca215c11..68641b7d76 100644 --- a/packages/core/src/context/sidecar/testProfile.ts +++ b/packages/core/src/context/sidecar/testProfile.ts @@ -16,7 +16,7 @@ export const testTruncateProfile: SidecarConfig = { triggers: ['gc_backstop', 'retained_exceeded'], execution: 'blocking', processors: [ - { processorId: 'EmergencyTruncationProcessor', options: {} }, + { processorId: 'HistoryTruncationProcessor', options: {} }, ], }, ], diff --git a/packages/core/src/context/sidecar/types.ts b/packages/core/src/context/sidecar/types.ts index 81db3f45e0..6b7615e16c 100644 --- a/packages/core/src/context/sidecar/types.ts +++ b/packages/core/src/context/sidecar/types.ts @@ -14,11 +14,11 @@ export type ProcessorConfig = } | { processorId: 'BlobDegradationProcessor'; options?: object } | { - processorId: 'SemanticCompressionProcessor'; + processorId: 'NodeDistillationProcessor'; options: { nodeThresholdTokens: number }; } | { - processorId: 'HistorySquashingProcessor'; + processorId: 'NodeTruncationProcessor'; options: { maxTokensPerNode: number }; } | { @@ -26,7 +26,7 @@ export type ProcessorConfig = options?: Record; } | { - processorId: 'EmergencyTruncationProcessor'; + processorId: 'HistoryTruncationProcessor'; options?: Record; }; diff --git a/packages/core/src/context/system-tests/SimulationHarness.ts b/packages/core/src/context/system-tests/SimulationHarness.ts index 3c0b4564bb..c519f14d29 100644 --- a/packages/core/src/context/system-tests/SimulationHarness.ts +++ b/packages/core/src/context/system-tests/SimulationHarness.ts @@ -106,7 +106,7 @@ export class SimulationHarness { // 2. Measure tokens immediately after append (Before background processing) const tokensBefore = this.env.tokenCalculator.calculateConcreteListTokens( - this.contextManager.getShip(), + this.contextManager.getNodes(), ); debugLogger.log( `[Turn ${this.currentTurnIndex}] Tokens BEFORE: ${tokensBefore}`, @@ -116,7 +116,7 @@ export class SimulationHarness { await new Promise((resolve) => setTimeout(resolve, 50)); // 3.1 Simulate what projectCompressedHistory does with the sync handlers - let currentView = this.contextManager.getShip(); + let currentView = this.contextManager.getNodes(); const currentTokens = this.env.tokenCalculator.calculateConcreteListTokens(currentView); if (this.config.budget && currentTokens > this.config.budget.maxTokens) { @@ -136,7 +136,7 @@ export class SimulationHarness { const ep = currentView[i]; if ( !this.contextManager - .getShip() + .getNodes() .find((c) => c.id === ep.id) ) { this.eventBus.emitVariantReady({ @@ -157,7 +157,7 @@ export class SimulationHarness { // 4. Measure tokens after background processors have (hopefully) emitted variants const tokensAfter = this.env.tokenCalculator.calculateConcreteListTokens( - this.contextManager.getShip(), + this.contextManager.getNodes(), ); debugLogger.log( `[Turn ${this.currentTurnIndex}] Tokens AFTER: ${tokensAfter}`, diff --git a/packages/core/src/context/system-tests/lifecycle.golden.test.ts b/packages/core/src/context/system-tests/lifecycle.golden.test.ts index 1209120983..2c56f8119b 100644 --- a/packages/core/src/context/system-tests/lifecycle.golden.test.ts +++ b/packages/core/src/context/system-tests/lifecycle.golden.test.ts @@ -52,7 +52,7 @@ describe('System Lifecycle Golden Tests', () => { execution: 'blocking', triggers: ['retained_exceeded'], processors: [ - { processorId: 'EmergencyTruncationProcessor', options: {} }, + { processorId: 'HistoryTruncationProcessor', options: {} }, ], }, ], diff --git a/packages/core/src/context/typed-context-ir.md b/packages/core/src/context/typed-context-ir.md index f69ba5b9e9..0abf4b8b16 100644 --- a/packages/core/src/context/typed-context-ir.md +++ b/packages/core/src/context/typed-context-ir.md @@ -1,20 +1,20 @@ -# Context Manager: The Pure Functional "Ship of Theseus" IR +# Context Manager: The Pure Functional "Nodes of Theseus" IR This document outlines the architectural transition from the V0 Mutating Editor pattern to the V1 Pure Functional, Immutable Episodic IR, designed to scale into a multi-agent, async state transformation system. -## 1. Core Philosophy: The Ship of Theseus +## 1. Core Philosophy: The Nodes of Theseus The primary constraint of deep immutable trees is the cascading cost of cloning parent nodes when a leaf node changes. To solve this, we decouple the structural hierarchy of the context from the actual data sent to the LLM. The IR is divided into two distinct domains: 1. **Logical Nodes:** Structural boundaries that define the hierarchy (e.g., `Task`, `Episode`). These nodes **do not render** to the LLM. They exist to group related interactions and provide semantic meaning. -2. **Concrete Nodes:** The atomic, renderable pieces of data (e.g., `UserPrompt`, `ToolExecution`, `Snapshot`, `RollingSummary`). These are the actual "planks" of the ship. +2. **Concrete Nodes:** The atomic, renderable pieces of data (e.g., `UserPrompt`, `ToolExecution`, `Snapshot`, `RollingSummary`). These are the actual "planks" of the nodes. Because Concrete Nodes carry a reference to their Logical Parent (e.g., `episodeId`), they can be stored and processed as a **Flat List**. ## 2. The Autonomous `ContextWorkingBuffer` -The "Ship" is no longer a dumb array; it is encapsulated in a rich `ContextWorkingBuffer` entity. +The "Nodes" is no longer a dumb array; it is encapsulated in a rich `ContextWorkingBuffer` entity. ### Encapsulation of History The Buffer manages its own audit trail and lineage. If a processor needs the pristine, unaltered data of a deeply compressed node (e.g., a Snapshotter summarizing masked tools), it queries the Buffer directly: diff --git a/packages/core/src/context/utils/contextTokenCalculator.ts b/packages/core/src/context/utils/contextTokenCalculator.ts index a4237ae29d..2d0126e5db 100644 --- a/packages/core/src/context/utils/contextTokenCalculator.ts +++ b/packages/core/src/context/utils/contextTokenCalculator.ts @@ -62,12 +62,12 @@ export class ContextTokenCalculator { } /** - * Fast calculation for a flat array of ConcreteNodes (The Ship). + * Fast calculation for a flat array of ConcreteNodes (The Nodes). * It relies entirely on the O(1) sidecar token cache. */ - calculateConcreteListTokens(ship: readonly ConcreteNode[]): number { + calculateConcreteListTokens(nodes: readonly ConcreteNode[]): number { let tokens = 0; - for (const node of ship) { + for (const node of nodes) { tokens += this.getTokenCost(node); } return tokens;