diff --git a/packages/core/src/context/config/SidecarLoader.ts b/packages/core/src/context/config/SidecarLoader.ts index 8b34e847bc..6d1eb99e65 100644 --- a/packages/core/src/context/config/SidecarLoader.ts +++ b/packages/core/src/context/config/SidecarLoader.ts @@ -52,7 +52,11 @@ export class SidecarLoader { ); } - // Extract strictly what we need since we just validated it. + // Extract strictly what we need. + // Why this unsafe cast is acceptable: + // SchemaValidator just ran \`getSidecarConfigSchema(registry)\` against \`parsed\`. + // That function dynamically maps the \`processorOptions\` to strict JSON schema definitions, + // so we know with absolute certainty at runtime that \`parsed\` conforms to this shape. // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const validConfig = parsed as { budget?: SidecarConfig['budget']; diff --git a/packages/core/src/context/config/profiles.ts b/packages/core/src/context/config/profiles.ts index ec2ae032e9..7ab19c41e9 100644 --- a/packages/core/src/context/config/profiles.ts +++ b/packages/core/src/context/config/profiles.ts @@ -15,6 +15,33 @@ import { createNodeDistillationProcessor } from '../processors/nodeDistillationP import { createStateSnapshotProcessor } from '../processors/stateSnapshotProcessor.js'; import { createStateSnapshotAsyncProcessor } from '../processors/stateSnapshotAsyncProcessor.js'; +/** + * Helper to safely merge static default options with dynamically loaded + * JSON overrides from the SidecarConfig. + * + * Why the unsafe cast is acceptable here: + * Before the \`config\` object ever reaches this function, \`SidecarLoader.ts\` + * passes the raw JSON through \`SchemaValidator\`. The schema dynamically generates + * a \`oneOf\` map linking every \`type\` discriminator to its corresponding processor + * schema definition. By the time we access \`options\` here, its shape has been + * strictly validated against the corresponding Zod/JSONSchema definition at runtime, + * making the generic cast to \`\` structurally safe. + */ +function resolveProcessorOptions( + config: SidecarConfig | undefined, + id: string, + defaultOptions: T, +): T { + if (config?.processorOptions && config.processorOptions[id]) { + return { + ...defaultOptions, + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + ...(config.processorOptions[id].options as T), + }; + } + return defaultOptions; +} + export interface ContextProfile { config: SidecarConfig; buildPipelines: ( @@ -42,20 +69,9 @@ export const defaultSidecarProfile: ContextProfile = { buildPipelines: ( env: ContextEnvironment, config?: SidecarConfig, - ): PipelineDef[] => { + ): PipelineDef[] => // Helper to merge default options with dynamically loaded processorOptions by ID - const getOptions = (id: string, defaultOptions: T): T => { - if (config?.processorOptions && config.processorOptions[id]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return { - ...defaultOptions, - ...(config.processorOptions[id].options as T), - }; - } - return defaultOptions; - }; - - return [ + [ { name: 'Immediate Sanitization', triggers: ['new_message'], @@ -63,7 +79,9 @@ export const defaultSidecarProfile: ContextProfile = { createToolMaskingProcessor( 'ToolMasking', env, - getOptions('ToolMasking', { stringLengthThresholdTokens: 8000 }), + resolveProcessorOptions(config, 'ToolMasking', { + stringLengthThresholdTokens: 8000, + }), ), createBlobDegradationProcessor('BlobDegradation', env), // No options ], @@ -75,12 +93,16 @@ export const defaultSidecarProfile: ContextProfile = { createNodeTruncationProcessor( 'NodeTruncation', env, - getOptions('NodeTruncation', { maxTokensPerNode: 3000 }), + resolveProcessorOptions(config, 'NodeTruncation', { + maxTokensPerNode: 3000, + }), ), createNodeDistillationProcessor( 'NodeDistillation', env, - getOptions('NodeDistillation', { nodeThresholdTokens: 5000 }), + resolveProcessorOptions(config, 'NodeDistillation', { + nodeThresholdTokens: 5000, + }), ), ], }, @@ -91,40 +113,29 @@ export const defaultSidecarProfile: ContextProfile = { createStateSnapshotProcessor( 'StateSnapshotSync', env, - getOptions('StateSnapshotSync', { target: 'max' }), + resolveProcessorOptions(config, 'StateSnapshotSync', { + target: 'max', + }), ), ], }, - ]; - }, - + ], buildAsyncPipelines: ( env: ContextEnvironment, config?: SidecarConfig, - ): AsyncPipelineDef[] => { - const getOptions = (id: string, defaultOptions: T): T => { - if (config?.processorOptions && config.processorOptions[id]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return { - ...defaultOptions, - ...(config.processorOptions[id].options as T), - }; - } - return defaultOptions; - }; - - return [ - { - name: 'Async Background GC', - triggers: ['nodes_aged_out'], - processors: [ - createStateSnapshotAsyncProcessor( - 'StateSnapshotAsync', - env, - getOptions('StateSnapshotAsync', { type: 'accumulate' }), - ), - ], - }, - ]; - }, + ): AsyncPipelineDef[] => [ + { + name: 'Async Background GC', + triggers: ['nodes_aged_out'], + processors: [ + createStateSnapshotAsyncProcessor( + 'StateSnapshotAsync', + env, + resolveProcessorOptions(config, 'StateSnapshotAsync', { + type: 'accumulate', + }), + ), + ], + }, + ], }; diff --git a/packages/core/src/context/pipeline/orchestrator.ts b/packages/core/src/context/pipeline/orchestrator.ts index 032ab31800..ee9a6f5e39 100644 --- a/packages/core/src/context/pipeline/orchestrator.ts +++ b/packages/core/src/context/pipeline/orchestrator.ts @@ -45,10 +45,10 @@ export class PipelineOrchestrator { } private setupTriggers() { - const bindTriggers = ( - pipelines: PipelineDef[] | AsyncPipelineDef[], + const bindTriggers =

( + pipelines: P[], executeFn: ( - pipeline: PipelineDef | AsyncPipelineDef, + pipeline: P, nodes: readonly ConcreteNode[], targets: ReadonlySet, protectedIds: ReadonlySet, @@ -78,9 +78,8 @@ export class PipelineOrchestrator { }; bindTriggers(this.pipelines, (pipeline, nodes, targets, protectedIds) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion void this.executePipelineAsync( - pipeline as PipelineDef, + pipeline, nodes, new Set(targets), new Set(protectedIds),