This commit is contained in:
Your Name
2026-04-08 20:44:56 +00:00
parent f726de12e5
commit 9287159ccc
22 changed files with 106 additions and 41 deletions
+4 -4
View File
@@ -20,8 +20,8 @@ import { ProcessorRegistry } from './sidecar/registry.js';
export class ContextManager {
// The stateful, pristine flat graph.
private pristineShip: ReadonlyArray<ConcreteNode> = [];
private currentShip: ReadonlyArray<ConcreteNode> = [];
private pristineShip: readonly ConcreteNode[] = [];
private currentShip: readonly ConcreteNode[] = [];
private readonly eventBus: ContextEventBus;
// Internal sub-components
@@ -154,7 +154,7 @@ export class ContextManager {
* Useful for internal tool rendering (like the trace viewer).
* Note: This is an expensive, deep clone operation.
*/
getPristineGraph(): ReadonlyArray<ConcreteNode> {
getPristineGraph(): readonly ConcreteNode[] {
return [...this.pristineShip];
}
@@ -163,7 +163,7 @@ export class ContextManager {
* up to the configured token budget.
* This is the view that will eventually be projected back to the LLM.
*/
getShip(): ReadonlyArray<ConcreteNode> {
getShip(): readonly ConcreteNode[] {
return [...this.currentShip];
}
+3 -3
View File
@@ -8,18 +8,18 @@ import { EventEmitter } from 'node:events';
import type { ConcreteNode } from './ir/types.js';
export interface PristineHistoryUpdatedEvent {
ship: ReadonlyArray<ConcreteNode>;
ship: readonly ConcreteNode[];
newNodes: Set<string>;
}
export interface ContextConsolidationEvent {
ship: ReadonlyArray<ConcreteNode>;
ship: readonly ConcreteNode[];
targetDeficit: number;
targetNodeIds: Set<string>;
}
export interface IrChunkReceivedEvent {
ship: ReadonlyArray<ConcreteNode>;
ship: readonly ConcreteNode[];
targetNodeIds: Set<string>;
}
+1 -1
View File
@@ -45,7 +45,7 @@ export class HistoryObserver {
this.tokenCalculator,
);
const ship: import('./ir/types.js').ConcreteNode[] = [];
const ship: Array<import('./ir/types.js').ConcreteNode> = [];
for (const ep of pristineEpisodes) {
if (ep.concreteNodes) {
for (const child of ep.concreteNodes) {
+1 -1
View File
@@ -21,7 +21,7 @@ export class IrProjector {
* applies the Immediate Sanitization pipeline, and enforces token boundaries.
*/
static async project(
ship: ReadonlyArray<ConcreteNode>,
ship: readonly ConcreteNode[],
orchestrator: PipelineOrchestrator,
sidecar: SidecarConfig,
tracer: ContextTracer,
+3 -3
View File
@@ -159,7 +159,7 @@ function parseToolResponses(
if (callId) pendingCallParts.delete(callId);
}
}
return currentEpisode as Partial<Episode>;
return currentEpisode;
}
function parseUserParts(
@@ -231,7 +231,7 @@ function parseModelParts(
];
}
}
return currentEpisode as Partial<Episode>;
return currentEpisode;
}
function finalizeYield(currentEpisode: Partial<Episode>) {
@@ -247,7 +247,7 @@ function finalizeYield(currentEpisode: Partial<Episode>) {
},
};
const existingNodes =
currentEpisode.concreteNodes as import('./types.js').ConcreteNode[];
currentEpisode.concreteNodes as Array<import('./types.js').ConcreteNode>;
currentEpisode.concreteNodes = [...existingNodes, yieldNode];
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ export interface ContextWorker {
onInboxTopics?: string[];
};
execute(args: {
targets: ReadonlyArray<ConcreteNode>;
targets: readonly ConcreteNode[];
inbox: InboxSnapshot;
}): Promise<void>;
}
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { BlobDegradationProcessor } from './blobDegradationProcessor.js';
import {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ProcessArgs, ContextProcessor } from '../pipeline.js';
import type { ConcreteNode, UserPrompt } from '../ir/types.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
@@ -22,7 +27,7 @@ export class BlobDegradationProcessor implements ContextProcessor {
this.env = env;
}
async process({ targets, state }: ProcessArgs): Promise<ReadonlyArray<ConcreteNode>> {
async process({ targets, state }: ProcessArgs): Promise<readonly ConcreteNode[]> {
if (state.isBudgetSatisfied) {
return targets;
}
@@ -58,7 +63,7 @@ export class BlobDegradationProcessor implements ContextProcessor {
continue;
}
const prompt = node as UserPrompt;
const prompt = node;
let modified = false;
const newParts = [...prompt.semanticParts];
@@ -50,7 +50,7 @@ export class EmergencyTruncationProcessor implements ContextProcessor {
async process({
targets,
state,
}: ProcessArgs): Promise<ReadonlyArray<ConcreteNode>> {
}: ProcessArgs): Promise<readonly ConcreteNode[]> {
// Calculate how many tokens we need to remove based on the configured knob
let targetTokensToRemove = 0;
const strategy = this.options.target ?? 'max';
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { HistorySquashingProcessor } from './historySquashingProcessor.js';
import {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ContextProcessor, ProcessArgs } from '../pipeline.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
import { truncateProportionally } from '../truncation.js';
@@ -67,7 +72,7 @@ export class HistorySquashingProcessor implements ContextProcessor {
return null;
}
async process({ targets, state }: ProcessArgs): Promise<ReadonlyArray<ConcreteNode>> {
async process({ targets, state }: ProcessArgs): Promise<readonly ConcreteNode[]> {
if (state.isBudgetSatisfied) {
return targets;
}
@@ -86,7 +91,7 @@ export class HistorySquashingProcessor implements ContextProcessor {
// 1. Squash User Prompts
if (node.type === 'USER_PROMPT') {
const prompt = node as UserPrompt;
const prompt = node;
let modified = false;
const newParts = [...prompt.semanticParts];
@@ -126,7 +131,7 @@ export class HistorySquashingProcessor implements ContextProcessor {
// 2. Squash Model Thoughts
if (node.type === 'AGENT_THOUGHT') {
const thought = node as AgentThought;
const thought = node;
const squashResult = this.tryApplySquash(thought.text, limitChars, currentDeficit);
if (squashResult) {
@@ -152,7 +157,7 @@ export class HistorySquashingProcessor implements ContextProcessor {
// 3. Squash Agent Yields
if (node.type === 'AGENT_YIELD') {
const agentYield = node as AgentYield;
const agentYield = node;
const squashResult = this.tryApplySquash(agentYield.text, limitChars, currentDeficit);
if (squashResult) {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { SemanticCompressionProcessor } from './semanticCompressionProcessor.js';
import {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ContextProcessor, ProcessArgs } from '../pipeline.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
import { debugLogger } from '../../utils/debugLogger.js';
@@ -74,7 +79,7 @@ export class SemanticCompressionProcessor implements ContextProcessor {
}
}
async process({ targets, state }: ProcessArgs): Promise<ReadonlyArray<ConcreteNode>> {
async process({ targets, state }: ProcessArgs): Promise<readonly ConcreteNode[]> {
if (state.isBudgetSatisfied) {
return targets;
}
@@ -95,7 +100,7 @@ export class SemanticCompressionProcessor implements ContextProcessor {
// 1. Compress User Prompts
if (node.type === 'USER_PROMPT') {
const prompt = node as UserPrompt;
const prompt = node;
let modified = false;
const newParts = [...prompt.semanticParts];
@@ -140,7 +145,7 @@ export class SemanticCompressionProcessor implements ContextProcessor {
// 2. Compress Model Thoughts
if (node.type === 'AGENT_THOUGHT') {
const thought = node as AgentThought;
const thought = node;
if (thought.text.length > thresholdChars) {
const summary = await this.generateSummary(thought.text, 'Agent Thought');
const newTokens = this.env.tokenCalculator.estimateTokensForParts([{ text: summary }]);
@@ -171,7 +176,7 @@ export class SemanticCompressionProcessor implements ContextProcessor {
// 3. Compress Tool Observations
if (node.type === 'TOOL_EXECUTION') {
const tool = node as ToolExecution;
const tool = node;
const rawObs = tool.observation;
let stringifiedObs = '';
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { StateSnapshotProcessor } from './stateSnapshotProcessor.js';
import {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ContextProcessor, ProcessArgs, BackstopTargetOptions, ContextWorker } from '../pipeline.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
import type { ConcreteNode, Snapshot } from '../ir/types.js';
@@ -32,7 +37,7 @@ export class StateSnapshotProcessor implements ContextProcessor, ContextWorker {
}
// --- ContextWorker Interface (Proactive Accumulation) ---
async execute({ targets, inbox }: { targets: ReadonlyArray<ConcreteNode>; inbox: import('../pipeline.js').InboxSnapshot }): Promise<void> {
async execute({ targets, inbox }: { targets: readonly ConcreteNode[]; inbox: import('../pipeline.js').InboxSnapshot }): Promise<void> {
// We only care about nodes that have aged out past retainedTokens
// To calculate this precisely, we'd need the ContextAccountingState, but for V0
@@ -46,7 +51,7 @@ export class StateSnapshotProcessor implements ContextProcessor, ContextWorker {
}
// --- ContextProcessor Interface (Sync Backstop / Cache Application) ---
async process({ targets, state, inbox }: ProcessArgs): Promise<ReadonlyArray<ConcreteNode>> {
async process({ targets, state, inbox }: ProcessArgs): Promise<readonly ConcreteNode[]> {
if (state.isBudgetSatisfied) {
return targets;
}
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { ToolMaskingProcessor } from './toolMaskingProcessor.js';
import {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ContextProcessor, ProcessArgs } from '../pipeline.js';
import type { ConcreteNode, ToolExecution } from '../ir/types.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
@@ -91,7 +96,7 @@ export class ToolMaskingProcessor implements ContextProcessor {
return text.includes('<tool_output_masked>');
}
async process({ targets, state }: ProcessArgs): Promise<ReadonlyArray<ConcreteNode>> {
async process({ targets, state }: ProcessArgs): Promise<readonly ConcreteNode[]> {
const maskingConfig = this.options;
if (!maskingConfig) return targets;
if (state.isBudgetSatisfied) return targets;
@@ -148,7 +153,7 @@ export class ToolMaskingProcessor implements ContextProcessor {
continue;
}
const step = node as ToolExecution;
const step = node;
const toolName = step.toolName;
if (toolName && UNMASKABLE_TOOLS.has(toolName)) {
returnedNodes.push(node);
@@ -1,4 +1,9 @@
import { ProcessorRegistry } from './registry.js';
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ProcessorRegistry } from './registry.js';
import { EmergencyTruncationProcessor, type EmergencyTruncationProcessorOptions } from '../processors/emergencyTruncationProcessor.js';
import { BlobDegradationProcessor } from '../processors/blobDegradationProcessor.js';
import { HistorySquashingProcessor, type HistorySquashingProcessorOptions } from '../processors/historySquashingProcessor.js';
+8 -3
View File
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { InboxMessage, InboxSnapshot } from '../pipeline.js';
export class LiveInbox {
@@ -12,7 +17,7 @@ export class LiveInbox {
});
}
getMessages(): ReadonlyArray<InboxMessage> {
getMessages(): readonly InboxMessage[] {
return [...this.messages];
}
@@ -22,10 +27,10 @@ export class LiveInbox {
}
export class InboxSnapshotImpl implements InboxSnapshot {
private messages: ReadonlyArray<InboxMessage>;
private messages: readonly InboxMessage[];
private consumedIds = new Set<string>();
constructor(messages: ReadonlyArray<InboxMessage>) {
constructor(messages: readonly InboxMessage[]) {
this.messages = messages;
}
@@ -58,7 +58,7 @@ export class PipelineOrchestrator {
const instance = factory.create(
this.env,
procDef.options || {},
) as ContextProcessor;
);
this.instantiatedProcessors.set(procDef.processorId, instance);
}
}
@@ -153,10 +153,10 @@ export class PipelineOrchestrator {
}
applyProcessorDiff(
ship: ReadonlyArray<ConcreteNode>,
targets: ReadonlyArray<ConcreteNode>,
returnedNodes: ReadonlyArray<ConcreteNode>,
): ReadonlyArray<ConcreteNode> {
ship: readonly ConcreteNode[],
targets: readonly ConcreteNode[],
returnedNodes: readonly ConcreteNode[],
): readonly ConcreteNode[] {
const mutableShip = [...ship];
const targetSet = new Set(targets.map((n) => n.id));
const returnedMap = new Map(returnedNodes.map((n) => [n.id, n]));
@@ -204,10 +204,10 @@ export class PipelineOrchestrator {
async executeTriggerSync(
trigger: PipelineTrigger,
ship: ReadonlyArray<ConcreteNode>,
ship: readonly ConcreteNode[],
triggerTargets: ReadonlySet<string>,
state: ContextAccountingState,
): Promise<ReadonlyArray<ConcreteNode>> {
): Promise<readonly ConcreteNode[]> {
let currentShip = ship;
const pipelines = this.config.pipelines.filter((p) =>
p.triggers.includes(trigger),
@@ -262,7 +262,7 @@ export class PipelineOrchestrator {
private async executePipelineAsync(
pipeline: PipelineDef,
ship: ReadonlyArray<ConcreteNode>,
ship: readonly ConcreteNode[],
triggerTargets: Set<string>,
state: ContextAccountingState,
) {
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { SidecarConfig } from './types.js';
export const testTruncateProfile: SidecarConfig = {
@@ -36,7 +36,7 @@ export class ContextTokenCalculator {
* Calculates the total token count for a flat array of ConcreteNodes (The Ship).
* This is fast because it relies on pre-computed metadata where available.
*/
calculateConcreteListTokens(ship: ReadonlyArray<ConcreteNode>): number {
calculateConcreteListTokens(ship: readonly ConcreteNode[]): number {
let tokens = 0;
for (const node of ship) {
tokens += node.metadata.currentTokens;