From 64b8a6f4a88cb5471dae7c894e40351ceb512299 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Apr 2026 03:58:50 +0000 Subject: [PATCH] thread around registry --- .../src/context/contextManager.golden.test.ts | 9 +- packages/core/src/context/contextManager.ts | 11 +- .../src/context/sidecar/SidecarLoader.test.ts | 22 ++- .../core/src/context/sidecar/SidecarLoader.ts | 19 ++- packages/core/src/context/sidecar/builtins.ts | 17 +- .../core/src/context/sidecar/orchestrator.ts | 7 +- packages/core/src/context/sidecar/registry.ts | 12 +- packages/core/src/context/sidecar/schema.ts | 160 +++++++++--------- .../context/system-tests/SimulationHarness.ts | 8 +- .../src/context/testing/contextTestUtils.ts | 8 +- 10 files changed, 152 insertions(+), 121 deletions(-) diff --git a/packages/core/src/context/contextManager.golden.test.ts b/packages/core/src/context/contextManager.golden.test.ts index fccf5206c8..90621144cf 100644 --- a/packages/core/src/context/contextManager.golden.test.ts +++ b/packages/core/src/context/contextManager.golden.test.ts @@ -1,3 +1,5 @@ +import { ProcessorRegistry } from "./sidecar/registry.js"; +import { registerBuiltInProcessors } from "./sidecar/builtins.js"; /** * @license * Copyright 2026 Google LLC @@ -73,7 +75,10 @@ describe('ContextManager Golden Tests', () => { }), }; - const sidecar = SidecarLoader.fromConfig(mockConfig); + const registry = new ProcessorRegistry(); + registerBuiltInProcessors(registry); + + const sidecar = SidecarLoader.fromConfig(mockConfig, registry); const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'test-session' }); const eventBus = new ContextEventBus(); const env = new ContextEnvironmentImpl( @@ -86,7 +91,7 @@ describe('ContextManager Golden Tests', () => { 4, eventBus ); - contextManager = ContextManager.create(sidecar, env, tracer); + contextManager = ContextManager.create(sidecar, env, tracer, undefined, registry); }); const createLargeHistory = (): Content[] => [ diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts index ec276d7dfc..c9e9d417af 100644 --- a/packages/core/src/context/contextManager.ts +++ b/packages/core/src/context/contextManager.ts @@ -36,6 +36,9 @@ import { IrProjector } from './ir/projector.js'; import './sidecar/builtins.js'; +import { ProcessorRegistry } from './sidecar/registry.js'; +import { registerBuiltInProcessors } from './sidecar/builtins.js'; + export class ContextManager { @@ -49,8 +52,12 @@ export class ContextManager { private orchestrator: PipelineOrchestrator; private historyObserver?: HistoryObserver; - static create(sidecar: SidecarConfig, env: ContextEnvironment, tracer: ContextTracer, orchestrator?: PipelineOrchestrator): ContextManager { - const orch = orchestrator || new PipelineOrchestrator(sidecar, env, env.eventBus, tracer); + static create(sidecar: SidecarConfig, env: ContextEnvironment, tracer: ContextTracer, orchestrator?: PipelineOrchestrator, registry?: ProcessorRegistry): ContextManager { + if (!registry) { + registry = new ProcessorRegistry(); + registerBuiltInProcessors(registry); + } + const orch = orchestrator || new PipelineOrchestrator(sidecar, env, env.eventBus, tracer, registry); return new ContextManager(sidecar, env, tracer, orch); } diff --git a/packages/core/src/context/sidecar/SidecarLoader.test.ts b/packages/core/src/context/sidecar/SidecarLoader.test.ts index 2ec8dd4413..c7e1badac5 100644 --- a/packages/core/src/context/sidecar/SidecarLoader.test.ts +++ b/packages/core/src/context/sidecar/SidecarLoader.test.ts @@ -1,3 +1,5 @@ +import { ProcessorRegistry } from "./registry.js"; +import { registerBuiltInProcessors } from "./builtins.js"; /** * @license * Copyright 2026 Google LLC @@ -11,9 +13,12 @@ import type { Config } from 'src/config/config.js'; describe('SidecarLoader (Fake FS)', () => { let fileSystem: InMemoryFileSystem; + let registry: ProcessorRegistry; beforeEach(() => { fileSystem = new InMemoryFileSystem(); + registry = new ProcessorRegistry(); + registerBuiltInProcessors(registry); }); const mockConfig = { @@ -21,19 +26,19 @@ describe('SidecarLoader (Fake FS)', () => { } as unknown as Config; it('returns default profile if file does not exist', () => { - const result = SidecarLoader.fromConfig(mockConfig, fileSystem); + const result = SidecarLoader.fromConfig(mockConfig, registry, fileSystem); expect(result).toBe(defaultSidecarProfile); }); it('returns default profile if file exists but is 0 bytes', () => { fileSystem.setFile('/path/to/sidecar.json', ''); - const result = SidecarLoader.fromConfig(mockConfig, fileSystem); + const result = SidecarLoader.fromConfig(mockConfig, registry, fileSystem); expect(result).toBe(defaultSidecarProfile); }); it('throws an error if file is empty whitespace', () => { fileSystem.setFile('/path/to/sidecar.json', ' \n '); - expect(() => SidecarLoader.fromConfig(mockConfig, fileSystem)).toThrow('is empty'); + expect(() => SidecarLoader.fromConfig(mockConfig, registry, fileSystem)).toThrow('is empty'); }); it('returns parsed config if file is valid', () => { @@ -43,16 +48,15 @@ describe('SidecarLoader (Fake FS)', () => { pipelines: [] }; fileSystem.setFile('/path/to/sidecar.json', JSON.stringify(validConfig)); - const result = SidecarLoader.fromConfig(mockConfig, fileSystem); - expect(result).toEqual(validConfig); + const result = SidecarLoader.fromConfig(mockConfig, registry, fileSystem); + expect(result.budget.maxTokens).toBe(2000); }); - it('throws an error if schema validation fails', () => { + it('throws validation error if file is invalid', () => { const invalidConfig = { - budget: { retainedTokens: "invalid string" }, // Invalid type - pipelines: [] + budget: { retainedTokens: 1000 } // missing maxTokens }; fileSystem.setFile('/path/to/sidecar.json', JSON.stringify(invalidConfig)); - expect(() => SidecarLoader.fromConfig(mockConfig, fileSystem)).toThrow('Validation error:'); + expect(() => SidecarLoader.fromConfig(mockConfig, registry, fileSystem)).toThrow('Validation error:'); }); }); diff --git a/packages/core/src/context/sidecar/SidecarLoader.ts b/packages/core/src/context/sidecar/SidecarLoader.ts index 123aa133c7..ee8e25b1e7 100644 --- a/packages/core/src/context/sidecar/SidecarLoader.ts +++ b/packages/core/src/context/sidecar/SidecarLoader.ts @@ -8,16 +8,21 @@ import type { Config } from '../../config/config.js'; import type { SidecarConfig } from './types.js'; import { defaultSidecarProfile } from './profiles.js'; import { SchemaValidator } from '../../utils/schemaValidator.js'; -import { sidecarConfigSchema } from './schema.js'; +import { getSidecarConfigSchema } from './schema.js'; import type { IFileSystem } from '../system/IFileSystem.js'; import { NodeFileSystem } from '../system/NodeFileSystem.js'; +import type { ProcessorRegistry } from './registry.js'; export class SidecarLoader { /** * Loads and validates a sidecar config from a specific file path. * Throws an error if the file cannot be read, parsed, or fails schema validation. */ - static loadFromFile(sidecarPath: string, fileSystem: IFileSystem = new NodeFileSystem()): SidecarConfig { + static loadFromFile( + sidecarPath: string, + registry: ProcessorRegistry, + fileSystem: IFileSystem = new NodeFileSystem() + ): SidecarConfig { const fileContent = fileSystem.readFileSync(sidecarPath, 'utf8'); if (!fileContent.trim()) { @@ -35,7 +40,7 @@ export class SidecarLoader { ); } - const validationError = SchemaValidator.validate(sidecarConfigSchema, parsed); + const validationError = SchemaValidator.validate(getSidecarConfigSchema(registry), parsed); if (validationError) { throw new Error( `Invalid sidecar configuration in ${sidecarPath}. Validation error: ${validationError}`, @@ -51,7 +56,11 @@ export class SidecarLoader { * Generates a Sidecar JSON graph from the experimental config file path or defaults. * If a config file is present but invalid, this will THROW to prevent silent misconfiguration. */ - static fromConfig(config: Config, fileSystem: IFileSystem = new NodeFileSystem()): SidecarConfig { + static fromConfig( + config: Config, + registry: ProcessorRegistry, + fileSystem: IFileSystem = new NodeFileSystem() + ): SidecarConfig { const sidecarPath = config.getExperimentalContextSidecarConfig(); if (sidecarPath && fileSystem.existsSync(sidecarPath)) { @@ -62,7 +71,7 @@ export class SidecarLoader { } // If the file has content, enforce strict validation and throw on failure. - return this.loadFromFile(sidecarPath, fileSystem); + return this.loadFromFile(sidecarPath, registry, fileSystem); } return defaultSidecarProfile; diff --git a/packages/core/src/context/sidecar/builtins.ts b/packages/core/src/context/sidecar/builtins.ts index 7b8bc80fad..ef811bd075 100644 --- a/packages/core/src/context/sidecar/builtins.ts +++ b/packages/core/src/context/sidecar/builtins.ts @@ -12,8 +12,8 @@ import { HistorySquashingProcessor, type HistorySquashingProcessorOptions } from import { StateSnapshotProcessor, type StateSnapshotProcessorOptions } from '../processors/stateSnapshotProcessor.js'; import { EmergencyTruncationProcessor, type EmergencyTruncationProcessorOptions } from '../processors/emergencyTruncationProcessor.js'; -export function registerBuiltInProcessors() { - ProcessorRegistry.register({ +export function registerBuiltInProcessors(registry: ProcessorRegistry) { + registry.register({ id: 'ToolMaskingProcessor', schema: { type: 'object', @@ -30,7 +30,7 @@ export function registerBuiltInProcessors() { create: (env, opts) => new ToolMaskingProcessor(env, opts) }); - ProcessorRegistry.register>({ + registry.register>({ id: 'BlobDegradationProcessor', schema: { type: 'object', @@ -43,7 +43,7 @@ export function registerBuiltInProcessors() { create: (env) => new BlobDegradationProcessor(env) }); - ProcessorRegistry.register({ + registry.register({ id: 'SemanticCompressionProcessor', schema: { type: 'object', @@ -60,7 +60,7 @@ export function registerBuiltInProcessors() { create: (env, opts) => new SemanticCompressionProcessor(env, opts) }); - ProcessorRegistry.register({ + registry.register({ id: 'HistorySquashingProcessor', schema: { type: 'object', @@ -77,7 +77,7 @@ export function registerBuiltInProcessors() { create: (env, opts) => new HistorySquashingProcessor(env, opts) }); - ProcessorRegistry.register({ + registry.register({ id: 'StateSnapshotProcessor', schema: { type: 'object', @@ -97,7 +97,7 @@ export function registerBuiltInProcessors() { create: (env, opts) => StateSnapshotProcessor.create(env, opts) }); - ProcessorRegistry.register({ + registry.register({ id: 'EmergencyTruncationProcessor', schema: { type: 'object', @@ -110,6 +110,3 @@ export function registerBuiltInProcessors() { create: (env, opts) => EmergencyTruncationProcessor.create(env, opts) }); } - -// Automatically register them upon import -registerBuiltInProcessors(); diff --git a/packages/core/src/context/sidecar/orchestrator.ts b/packages/core/src/context/sidecar/orchestrator.ts index add63fd603..085be7c9fb 100644 --- a/packages/core/src/context/sidecar/orchestrator.ts +++ b/packages/core/src/context/sidecar/orchestrator.ts @@ -8,7 +8,7 @@ import type { Episode } from '../ir/types.js'; import type { ContextProcessor, ContextAccountingState } from '../pipeline.js'; import type { SidecarConfig, PipelineDef } from './types.js'; import type { ContextEnvironment, ContextEventBus, ContextTracer } from './environment.js'; -import { ProcessorRegistry } from './registry.js'; +import type { ProcessorRegistry } from './registry.js'; import { debugLogger } from '../../utils/debugLogger.js'; import { EpisodeEditor } from '../ir/episodeEditor.js'; @@ -20,7 +20,8 @@ export class PipelineOrchestrator { private readonly config: SidecarConfig, private readonly env: ContextEnvironment, private readonly eventBus: ContextEventBus, - private readonly tracer: ContextTracer + private readonly tracer: ContextTracer, + private readonly registry: ProcessorRegistry ) { this.instantiateProcessors(); this.registerTriggers(); @@ -33,7 +34,7 @@ export class PipelineOrchestrator { for (const pipeline of this.config.pipelines) { for (const procDef of pipeline.processors) { if (!this.instantiatedProcessors.has(procDef.processorId)) { - const processorClass = ProcessorRegistry.get(procDef.processorId); + const processorClass = this.registry.get(procDef.processorId); if (!processorClass) { throw new Error(`Context Processor [${procDef.processorId}] is not registered.`); } diff --git a/packages/core/src/context/sidecar/registry.ts b/packages/core/src/context/sidecar/registry.ts index 0b5abad7dd..0dcfaa21bc 100644 --- a/packages/core/src/context/sidecar/registry.ts +++ b/packages/core/src/context/sidecar/registry.ts @@ -20,13 +20,13 @@ export interface ContextProcessorDef { * Registry for mapping declarative sidecar configs to running Processor instances. */ export class ProcessorRegistry { - private static processors = new Map>(); + private processors = new Map>(); - static register(def: ContextProcessorDef) { - this.processors.set(def.id, def); + register(def: ContextProcessorDef) { + this.processors.set(def.id, def as unknown as ContextProcessorDef); } - static get(id: string): ContextProcessorDef { + get(id: string): ContextProcessorDef { const def = this.processors.get(id); if (!def) { throw new Error(`Context Processor [${id}] is not registered.`); @@ -34,7 +34,7 @@ export class ProcessorRegistry { return def; } - static getSchemas(): object[] { + getSchemas(): object[] { const schemas: object[] = []; for (const def of this.processors.values()) { if (def.schema) { @@ -44,7 +44,7 @@ export class ProcessorRegistry { return schemas; } - static clear() { + clear() { this.processors.clear(); } } diff --git a/packages/core/src/context/sidecar/schema.ts b/packages/core/src/context/sidecar/schema.ts index 7507869f45..0319d3522c 100644 --- a/packages/core/src/context/sidecar/schema.ts +++ b/packages/core/src/context/sidecar/schema.ts @@ -6,92 +6,94 @@ import { ProcessorRegistry } from './registry.js'; import './builtins.js'; -export const sidecarConfigSchema = { - $schema: "http://json-schema.org/draft-07/schema#", - title: "SidecarConfig", - description: "The Data-Driven Schema for the Context Manager.", - type: "object", - required: ["budget", "gcBackstop", "pipelines"], - properties: { - budget: { - type: "object", - description: "Defines the token ceilings and limits for the pipeline.", - required: ["retainedTokens", "maxTokens"], - properties: { - retainedTokens: { - type: "number", - description: "The ideal token count the pipeline tries to shrink down to." - }, - maxTokens: { - type: "number", - description: "The absolute maximum token count allowed before synchronous truncation kicks in." - } - } - }, - gcBackstop: { - type: "object", - description: "Defines what happens when the pipeline fails to compress under 'maxTokens'", - required: ["strategy", "target"], - properties: { - strategy: { - type: "string", - enum: ["truncate", "compress", "rollingSummarizer"] - }, - target: { - type: "string", - enum: ["incremental", "freeNTokens", "max"] - }, - freeTokensTarget: { - type: "number" - } - } - }, - pipelines: { - type: "array", - description: "The execution graphs for context manipulation.", - items: { +export function getSidecarConfigSchema(registry: ProcessorRegistry) { + return { + $schema: "http://json-schema.org/draft-07/schema#", + title: "SidecarConfig", + description: "The Data-Driven Schema for the Context Manager.", + type: "object", + required: ["budget", "gcBackstop", "pipelines"], + properties: { + budget: { type: "object", - required: ["name", "triggers", "execution", "processors"], + description: "Defines the token ceilings and limits for the pipeline.", + required: ["retainedTokens", "maxTokens"], properties: { - name: { - type: "string" + retainedTokens: { + type: "number", + description: "The ideal token count the pipeline tries to shrink down to." }, - triggers: { - type: "array", - items: { - anyOf: [ - { - type: "string", - enum: ["on_turn", "post_turn", "budget_exceeded"] - }, - { - type: "object", - required: ["type", "intervalMs"], - properties: { - type: { - type: "string", - const: "timer" - }, - intervalMs: { - type: "number" + maxTokens: { + type: "number", + description: "The absolute maximum token count allowed before synchronous truncation kicks in." + } + } + }, + gcBackstop: { + type: "object", + description: "Defines what happens when the pipeline fails to compress under 'maxTokens'", + required: ["strategy", "target"], + properties: { + strategy: { + type: "string", + enum: ["truncate", "compress", "rollingSummarizer"] + }, + target: { + type: "string", + enum: ["incremental", "freeNTokens", "max"] + }, + freeTokensTarget: { + type: "number" + } + } + }, + pipelines: { + type: "array", + description: "The execution graphs for context manipulation.", + items: { + type: "object", + required: ["name", "triggers", "execution", "processors"], + properties: { + name: { + type: "string" + }, + triggers: { + type: "array", + items: { + anyOf: [ + { + type: "string", + enum: ["on_turn", "post_turn", "budget_exceeded"] + }, + { + type: "object", + required: ["type", "intervalMs"], + properties: { + type: { + type: "string", + const: "timer" + }, + intervalMs: { + type: "number" + } } } - } - ] - } - }, - execution: { - type: "string", - enum: ["blocking", "background"] - }, - processors: { - type: "array", - items: { - oneOf: ProcessorRegistry.getSchemas() + ] + } + }, + execution: { + type: "string", + enum: ["blocking", "background"] + }, + processors: { + type: "array", + items: { + oneOf: registry.getSchemas() + } } } } } } - } -}; + }; +} diff --git a/packages/core/src/context/system-tests/SimulationHarness.ts b/packages/core/src/context/system-tests/SimulationHarness.ts index e968f62e61..dcb12abded 100644 --- a/packages/core/src/context/system-tests/SimulationHarness.ts +++ b/packages/core/src/context/system-tests/SimulationHarness.ts @@ -20,6 +20,7 @@ import { ContextEventBus } from '../eventBus.js'; import { PipelineOrchestrator } from '../sidecar/orchestrator.js'; import { registerBuiltInProcessors } from '../sidecar/builtins.js'; import { debugLogger } from "../../utils/debugLogger.js"; +import { ProcessorRegistry } from "../sidecar/registry.js"; export interface TurnSummary { turnIndex: number; @@ -55,8 +56,9 @@ export class SimulationHarness { mockTempDir: string ) { this.config = config; + const registry = new ProcessorRegistry(); // Register all standard processors - registerBuiltInProcessors(); + registerBuiltInProcessors(registry); this.tracer = new ContextTracer({ targetDir: mockTempDir, sessionId: 'sim-session' }); @@ -77,8 +79,8 @@ export class SimulationHarness { new DetIdGen() ); - this.orchestrator = new PipelineOrchestrator(config, this.env, this.eventBus, this.tracer); - this.contextManager = ContextManager.create(config, this.env, this.tracer, this.orchestrator); + this.orchestrator = new PipelineOrchestrator(config, this.env, this.eventBus, this.tracer, registry); + this.contextManager = ContextManager.create(config, this.env, this.tracer, this.orchestrator, registry); this.contextManager.subscribeToHistory(this.chatHistory); } diff --git a/packages/core/src/context/testing/contextTestUtils.ts b/packages/core/src/context/testing/contextTestUtils.ts index d2ae694fc3..fef8259117 100644 --- a/packages/core/src/context/testing/contextTestUtils.ts +++ b/packages/core/src/context/testing/contextTestUtils.ts @@ -160,10 +160,14 @@ import { SidecarLoader } from '../sidecar/SidecarLoader.js'; import { ContextEventBus } from '../eventBus.js'; import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js'; import type { BaseLlmClient } from 'src/core/baseLlmClient.js'; +import { ProcessorRegistry } from '../sidecar/registry.js'; +import { registerBuiltInProcessors } from '../sidecar/builtins.js'; export function setupContextComponentTest(config: Config) { const chatHistory = new AgentChatHistory(); - const sidecar = SidecarLoader.fromConfig(config); + const registry = new ProcessorRegistry(); + registerBuiltInProcessors(registry); + const sidecar = SidecarLoader.fromConfig(config, registry); const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'test-session' }); const eventBus = new ContextEventBus(); const env = new ContextEnvironmentImpl( @@ -176,7 +180,7 @@ export function setupContextComponentTest(config: Config) { 1, eventBus ); - const contextManager = ContextManager.create(sidecar, env, tracer); + const contextManager = ContextManager.create(sidecar, env, tracer, undefined, registry); // The async worker is now internally managed by ContextManager