thread around registry

This commit is contained in:
Your Name
2026-04-07 03:58:50 +00:00
parent a9cc61349e
commit 64b8a6f4a8
10 changed files with 152 additions and 121 deletions
@@ -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[] => [
+9 -2
View File
@@ -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);
}
@@ -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:');
});
});
@@ -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;
+7 -10
View File
@@ -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<ToolMaskingProcessorOptions>({
export function registerBuiltInProcessors(registry: ProcessorRegistry) {
registry.register<ToolMaskingProcessorOptions>({
id: 'ToolMaskingProcessor',
schema: {
type: 'object',
@@ -30,7 +30,7 @@ export function registerBuiltInProcessors() {
create: (env, opts) => new ToolMaskingProcessor(env, opts)
});
ProcessorRegistry.register<Record<string, never>>({
registry.register<Record<string, never>>({
id: 'BlobDegradationProcessor',
schema: {
type: 'object',
@@ -43,7 +43,7 @@ export function registerBuiltInProcessors() {
create: (env) => new BlobDegradationProcessor(env)
});
ProcessorRegistry.register<SemanticCompressionProcessorOptions>({
registry.register<SemanticCompressionProcessorOptions>({
id: 'SemanticCompressionProcessor',
schema: {
type: 'object',
@@ -60,7 +60,7 @@ export function registerBuiltInProcessors() {
create: (env, opts) => new SemanticCompressionProcessor(env, opts)
});
ProcessorRegistry.register<HistorySquashingProcessorOptions>({
registry.register<HistorySquashingProcessorOptions>({
id: 'HistorySquashingProcessor',
schema: {
type: 'object',
@@ -77,7 +77,7 @@ export function registerBuiltInProcessors() {
create: (env, opts) => new HistorySquashingProcessor(env, opts)
});
ProcessorRegistry.register<StateSnapshotProcessorOptions>({
registry.register<StateSnapshotProcessorOptions>({
id: 'StateSnapshotProcessor',
schema: {
type: 'object',
@@ -97,7 +97,7 @@ export function registerBuiltInProcessors() {
create: (env, opts) => StateSnapshotProcessor.create(env, opts)
});
ProcessorRegistry.register<EmergencyTruncationProcessorOptions>({
registry.register<EmergencyTruncationProcessorOptions>({
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();
@@ -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.`);
}
@@ -20,13 +20,13 @@ export interface ContextProcessorDef<TOptions = object> {
* Registry for mapping declarative sidecar configs to running Processor instances.
*/
export class ProcessorRegistry {
private static processors = new Map<string, ContextProcessorDef<unknown>>();
private processors = new Map<string, ContextProcessorDef<unknown>>();
static register<TOptions>(def: ContextProcessorDef<TOptions>) {
this.processors.set(def.id, def);
register<TOptions>(def: ContextProcessorDef<TOptions>) {
this.processors.set(def.id, def as unknown as ContextProcessorDef<unknown>);
}
static get(id: string): ContextProcessorDef {
get(id: string): ContextProcessorDef<unknown> {
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();
}
}
+81 -79
View File
@@ -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()
}
}
}
}
}
}
}
};
};
}
@@ -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);
}
@@ -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