Skip to content

Commit fa14f7f

Browse files
committed
feat(memory-router): augmented routing for backend × retrieval-config dispatch
Adds the data plumbing and the MemoryRouter integration for per-query dispatch on a composite (backend × retrieval-config) key, calibrated from the 2026-04-26 LongMemEval-M Phase A N=54 ablation matrix. New primitives: - RetrievalConfigRouter (retrieval-config.ts): six retrieval-config variants spanning HyDE × top-K × rerank-multiplier with per-category accuracy/cost tables and the M-tuned per-category dispatch table. selectBestRetrievalConfig + computeOracleAggregate forecast the per-category-oracle aggregate (68.5% on M Phase A, vs 57.4% static M-tuned). - MemoryDispatchKey + AugmentedRoutingTable + selectAugmentedDispatch (routing-tables.ts): composite dispatch key extending the legacy MemoryBackendId axis with a RetrievalConfigId axis, the MINIMIZE_COST_AUGMENTED_TABLE preset (S backend choices crossed with M retrieval-config choices, every cell empirically justified), and the pure SAFE_FALLBACK_DISPATCH_KEY guard. - MemoryRouter.decideAugmented + decideAndDispatchAugmented: classify the query, resolve the composite dispatch key, optionally dispatch through IMemoryDispatcher with retrievalConfig threaded to the executor as the third-arg MemoryBackendExecutorContext. - Dispatcher contract: optional retrievalConfig field on MemoryDispatchArgs; FunctionMemoryDispatcher forwards it as the third executor arg only when set, preserving the legacy two-arg call shape for existing executors. Tests: 65 new contract tests across retrieval-config, augmented routing primitives, and augmented MemoryRouter integration. Full memory-router suite at 154/154; existing dispatch + classifier behavior unchanged. Spec: packages/agentos-bench/docs/specs/2026-04-26-retrieval-config-router-productionization-plan.md Phase 1 + Phase 2 of the productionization plan; Phase 3 (bench wiring + Phase A validation) follows.
1 parent 1dceecd commit fa14f7f

8 files changed

Lines changed: 1483 additions & 1 deletion

File tree

src/memory-router/MemoryRouter.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import {
2828
} from './backend-costs.js';
2929
import {
3030
PRESET_TABLES,
31+
selectAugmentedDispatch,
32+
type AugmentedRoutingTable,
3133
type MemoryBackendId,
34+
type MemoryDispatchKey,
3235
type MemoryQueryCategory,
3336
type MemoryRouterPreset,
3437
type RoutingTable,
@@ -111,6 +114,17 @@ export interface MemoryRouterOptions {
111114
* execute the chosen backend themselves.
112115
*/
113116
readonly dispatcher?: IMemoryDispatcher<unknown, unknown>;
117+
/**
118+
* Optional augmented routing table. When supplied,
119+
* {@link MemoryRouter.decideAugmented} and
120+
* {@link MemoryRouter.decideAndDispatchAugmented} resolve each query
121+
* to a composite {@link MemoryDispatchKey} (backend × retrieval-config)
122+
* instead of the legacy single-axis backend pick.
123+
*
124+
* Existing consumers see no behavioral change unless they call the
125+
* new augmented methods.
126+
*/
127+
readonly augmentedTable?: AugmentedRoutingTable;
114128
}
115129

116130
/**
@@ -151,6 +165,26 @@ export interface MemoryRouterDispatchedDecision<TTrace> {
151165
readonly backend: MemoryBackendId;
152166
}
153167

168+
/**
169+
* Bundled result of a `decideAugmented()` call. Carries the classifier
170+
* result and the composite {@link MemoryDispatchKey} (backend ×
171+
* retrieval-config) the augmented router resolved.
172+
*/
173+
export interface MemoryRouterAugmentedDecision {
174+
readonly classifier: MemoryClassifierResult;
175+
readonly dispatch: MemoryDispatchKey;
176+
}
177+
178+
/**
179+
* Bundled result of a `decideAndDispatchAugmented()` call. Combines
180+
* the augmented decision with the dispatched traces.
181+
*/
182+
export interface MemoryRouterAugmentedDispatchedDecision<TTrace> {
183+
readonly decision: MemoryRouterAugmentedDecision;
184+
readonly traces: TTrace[];
185+
readonly dispatch: MemoryDispatchKey;
186+
}
187+
154188
/**
155189
* Thrown when `decideAndDispatch` is called on a router that was
156190
* constructed without a dispatcher.
@@ -165,6 +199,22 @@ export class MemoryRouterDispatcherMissingError extends Error {
165199
}
166200
}
167201

202+
/**
203+
* Thrown when `decideAugmented` or `decideAndDispatchAugmented` is
204+
* called on a router that was constructed without an
205+
* {@link AugmentedRoutingTable}.
206+
*/
207+
export class MemoryRouterAugmentedTableMissingError extends Error {
208+
constructor() {
209+
super(
210+
'MemoryRouter.decideAugmented requires an augmentedTable. ' +
211+
'Pass one in MemoryRouterOptions (e.g. MINIMIZE_COST_AUGMENTED_TABLE), ' +
212+
'or use the legacy `decide` / `decideAndDispatch` methods.',
213+
);
214+
this.name = 'MemoryRouterAugmentedTableMissingError';
215+
}
216+
}
217+
168218
// ============================================================================
169219
// Class
170220
// ============================================================================
@@ -202,6 +252,7 @@ export class MemoryRouter {
202252
private readonly classifier: IMemoryClassifier;
203253
private readonly preset: MemoryRouterPreset;
204254
private readonly routingTable: RoutingTable;
255+
private readonly augmentedTable: AugmentedRoutingTable | null;
205256
private readonly budgetPerQuery: number | null;
206257
private readonly budgetMode: MemoryBudgetMode;
207258
private readonly backendCosts: Readonly<
@@ -214,6 +265,7 @@ export class MemoryRouter {
214265
this.classifier = options.classifier;
215266
this.preset = options.preset ?? 'minimize-cost';
216267
this.dispatcher = options.dispatcher ?? null;
268+
this.augmentedTable = options.augmentedTable ?? null;
217269

218270
// Resolve routing table: explicit > preset's default.
219271
const baseTable = options.routingTable ?? PRESET_TABLES[this.preset];
@@ -313,4 +365,94 @@ export class MemoryRouter {
313365
backend: dispatched.backend,
314366
};
315367
}
368+
369+
/**
370+
* Decide-only routing using the augmented table. Classifies the
371+
* query, resolves a composite {@link MemoryDispatchKey} (backend ×
372+
* retrieval-config) from the configured
373+
* {@link AugmentedRoutingTable}, and returns both pieces.
374+
*
375+
* Does NOT execute the recall — pair with {@link IMemoryDispatcher}
376+
* for the end-to-end flow, or call
377+
* {@link MemoryRouter.decideAndDispatchAugmented} when both an
378+
* augmented table and a dispatcher are wired.
379+
*
380+
* @param query - The user's memory-recall query text.
381+
* @param options - Per-call overrides (ground-truth telemetry,
382+
* prompt variant). The `groundTruthCategory` field is unused on
383+
* this path because augmented routing has no `selectBackend`-style
384+
* telemetry surface; tests pass it through anyway for parity.
385+
* @returns The classifier result + composite dispatch key.
386+
*
387+
* @throws {@link MemoryRouterAugmentedTableMissingError} when no
388+
* augmented table was supplied at construction.
389+
*/
390+
async decideAugmented(
391+
query: string,
392+
options?: MemoryRouterDecideOptions,
393+
): Promise<MemoryRouterAugmentedDecision> {
394+
if (!this.augmentedTable) {
395+
throw new MemoryRouterAugmentedTableMissingError();
396+
}
397+
const useFewShot =
398+
options?.useFewShotPrompt ?? this.defaultUseFewShotPrompt;
399+
const classifierOptions = useFewShot
400+
? { useFewShotPrompt: true }
401+
: undefined;
402+
403+
const classifier = await this.classifier.classify(query, classifierOptions);
404+
const dispatch = selectAugmentedDispatch(
405+
classifier.category,
406+
this.augmentedTable,
407+
);
408+
409+
return { classifier, dispatch };
410+
}
411+
412+
/**
413+
* Decide + dispatch in one call, using the augmented table.
414+
* Requires the router to have been constructed with both an
415+
* {@link AugmentedRoutingTable} and a {@link IMemoryDispatcher}.
416+
*
417+
* The selected {@link RetrievalConfigId} is forwarded to the
418+
* dispatcher's `retrievalConfig` arg; consumer-defined executors
419+
* read it via the third-arg
420+
* {@link MemoryBackendExecutorContext} parameter.
421+
*
422+
* @typeParam TTrace - Caller's trace shape (passed through verbatim).
423+
* @typeParam TPayload - Caller's payload shape for the dispatcher.
424+
* @param query - User memory-recall query.
425+
* @param dispatchPayload - Optional payload forwarded to the
426+
* per-backend executor (e.g. topK, retrieval policy).
427+
* @param options - Per-call overrides (ground-truth telemetry,
428+
* prompt variant).
429+
*
430+
* @throws {@link MemoryRouterAugmentedTableMissingError} when no
431+
* augmented table was supplied at construction.
432+
* @throws {@link MemoryRouterDispatcherMissingError} when no
433+
* dispatcher was supplied at construction.
434+
*/
435+
async decideAndDispatchAugmented<TTrace, TPayload = undefined>(
436+
query: string,
437+
dispatchPayload?: TPayload,
438+
options?: MemoryRouterDecideOptions,
439+
): Promise<MemoryRouterAugmentedDispatchedDecision<TTrace>> {
440+
if (!this.dispatcher) {
441+
throw new MemoryRouterDispatcherMissingError();
442+
}
443+
const decision = await this.decideAugmented(query, options);
444+
445+
const dispatched = (await this.dispatcher.dispatch({
446+
backend: decision.dispatch.backend,
447+
retrievalConfig: decision.dispatch.retrievalConfig,
448+
query,
449+
payload: dispatchPayload as unknown,
450+
})) as MemoryDispatchResult<TTrace>;
451+
452+
return {
453+
decision,
454+
traces: dispatched.traces,
455+
dispatch: decision.dispatch,
456+
};
457+
}
316458
}

0 commit comments

Comments
 (0)