mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-04 06:01:15 -07:00
tidy names
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<string>();
|
||||
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,
|
||||
|
||||
@@ -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<string>;
|
||||
}
|
||||
|
||||
export interface ContextConsolidationEvent {
|
||||
ship: readonly ConcreteNode[];
|
||||
nodes: readonly ConcreteNode[];
|
||||
targetDeficit: number;
|
||||
targetNodeIds: Set<string>;
|
||||
}
|
||||
|
||||
export interface IrChunkReceivedEvent {
|
||||
ship: readonly ConcreteNode[];
|
||||
nodes: readonly ConcreteNode[];
|
||||
targetNodeIds: Set<string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<import('./ir/types.js').ConcreteNode> = [];
|
||||
const nodes: Array<import('./ir/types.js').ConcreteNode> = [];
|
||||
for (const ep of pristineEpisodes) {
|
||||
if (ep.concreteNodes) {
|
||||
for (const child of ep.concreteNodes) {
|
||||
ship.push(child);
|
||||
nodes.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newNodes = new Set<string>();
|
||||
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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>,
|
||||
): Promise<Content[]> {
|
||||
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<string>();
|
||||
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<string>();
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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`
|
||||
|
||||
+9
-9
@@ -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;
|
||||
+4
-4
@@ -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
|
||||
});
|
||||
|
||||
+10
-10
@@ -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
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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,
|
||||
});
|
||||
|
||||
+9
-9
@@ -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;
|
||||
@@ -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<readonly ConcreteNode[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Record<string, never>>({
|
||||
@@ -19,22 +20,22 @@ export function registerBuiltInProcessors(registry: ProcessorRegistry) {
|
||||
create: (env) => new BlobDegradationProcessor(env),
|
||||
});
|
||||
|
||||
registry.register<EmergencyTruncationProcessorOptions>({
|
||||
id: 'EmergencyTruncationProcessor',
|
||||
schema: EmergencyTruncationProcessor.schema,
|
||||
create: (env, options) => EmergencyTruncationProcessor.create(env, options),
|
||||
registry.register<HistoryTruncationProcessorOptions>({
|
||||
id: 'HistoryTruncationProcessor',
|
||||
schema: HistoryTruncationProcessor.schema,
|
||||
create: (env, options) => HistoryTruncationProcessor.create(env, options),
|
||||
});
|
||||
|
||||
registry.register<HistorySquashingProcessorOptions>({
|
||||
id: 'HistorySquashingProcessor',
|
||||
schema: HistorySquashingProcessor.schema,
|
||||
create: (env, options) => HistorySquashingProcessor.create(env, options),
|
||||
registry.register<NodeTruncationProcessorOptions>({
|
||||
id: 'NodeTruncationProcessor',
|
||||
schema: NodeTruncationProcessor.schema,
|
||||
create: (env, options) => NodeTruncationProcessor.create(env, options),
|
||||
});
|
||||
|
||||
registry.register<SemanticCompressionProcessorOptions>({
|
||||
id: 'SemanticCompressionProcessor',
|
||||
schema: SemanticCompressionProcessor.schema,
|
||||
create: (env, options) => SemanticCompressionProcessor.create(env, options),
|
||||
registry.register<NodeDistillationProcessorOptions>({
|
||||
id: 'NodeDistillationProcessor',
|
||||
schema: NodeDistillationProcessor.schema,
|
||||
create: (env, options) => NodeDistillationProcessor.create(env, options),
|
||||
});
|
||||
|
||||
registry.register<ToolMaskingProcessorOptions>({
|
||||
@@ -45,13 +46,19 @@ export function registerBuiltInProcessors(registry: ProcessorRegistry) {
|
||||
|
||||
registry.register<StateSnapshotProcessorOptions>({
|
||||
id: 'StateSnapshotProcessor',
|
||||
schema: {}, // Will be added later
|
||||
schema: StateSnapshotProcessor.schema,
|
||||
create: (env, options) => StateSnapshotProcessor.create(env, options),
|
||||
});
|
||||
|
||||
registry.register<StateSnapshotWorkerOptions>({
|
||||
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<RollingSummaryProcessorOptions>({
|
||||
id: 'RollingSummaryProcessor',
|
||||
schema: RollingSummaryProcessor.schema,
|
||||
create: (env, options) => RollingSummaryProcessor.create(env, options),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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<string>,
|
||||
protectedLogicalIds: ReadonlySet<string> = new Set(),
|
||||
): Promise<readonly ConcreteNode[]> {
|
||||
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<string>,
|
||||
protectedLogicalIds: ReadonlySet<string> = 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,
|
||||
);
|
||||
|
||||
@@ -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: {} },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ export const testTruncateProfile: SidecarConfig = {
|
||||
triggers: ['gc_backstop', 'retained_exceeded'],
|
||||
execution: 'blocking',
|
||||
processors: [
|
||||
{ processorId: 'EmergencyTruncationProcessor', options: {} },
|
||||
{ processorId: 'HistoryTruncationProcessor', options: {} },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
| {
|
||||
processorId: 'EmergencyTruncationProcessor';
|
||||
processorId: 'HistoryTruncationProcessor';
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('System Lifecycle Golden Tests', () => {
|
||||
execution: 'blocking',
|
||||
triggers: ['retained_exceeded'],
|
||||
processors: [
|
||||
{ processorId: 'EmergencyTruncationProcessor', options: {} },
|
||||
{ processorId: 'HistoryTruncationProcessor', options: {} },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user