/** * router-trajectory.ts — Opt-in DRACO-shaped trajectory recorder for the * cost-optimal model router (ADR-148, phase 5). * * Writes one JSON-line per routing decision and one per outcome to a * shared `.swarm/model-router-trajectories.jsonl`. Outcome rows are * matched to their decision via `task_hash` (FNV-1a-32 of the task text). * * Gated behind `CLAUDE_FLOW_ROUTER_TRAJECTORY=1`. Default: **off** — rows * carry full task text + raw embeddings, which is a PII/retention surface * we do not enable without explicit consent. * * Schema is versioned (`"v": 1`). New required fields bump the version; * additive optional fields do not. * * COMPANION: run-transcript-recorder.ts (weight-eft capture path) * -------------------------------------------------------------- * This recorder captures the routing DECISION only (task, embedding, scalar * quality, tokens, cost) — enough to retrain the router. It deliberately does * NOT carry the full message transcript, the produced patch, or a resolved * boolean. `@metaharness/weight-eft` needs those to build SFT/DPO training * rows, so a SEPARATE opt-in recorder — `run-transcript-recorder.ts` — captures * the full run transcript to `.swarm/run-transcripts.jsonl`. Both share the * `taskHash()` below as their join key, and both are off-by-default for the * same PII/retention reason. Use `unifiedRecorderStatus()` (bottom of this * file) to inspect both at once. Keeping them as two files keeps the routing * hot path free of the heavier transcript payload. * * @module router-trajectory */ import type { ClaudeModel } from './model-router.js'; /** A single routing decision — written at `route()` time. */ export interface TrajectoryDecisionRow { v: 1; type: 'decision'; ts: string; task_hash: string; task: string; embedding?: number[]; complexity: number; model: ClaudeModel; confidence: number; uncertainty: number; routed_by: 'hybrid' | 'bandit-fallback' | 'heuristic'; /** Underlying neural backend when routed_by='hybrid', else absent. */ neural_backend?: 'metaharness-knn' | 'metaharness-krr' | 'fastgrnn'; /** * A/B mode (CLAUDE_FLOW_ROUTER_AB=1) attaches both the bandit-only pick * and the hybrid pick so disagreement rate is measurable over time. */ ab_pair?: { bandit_pick: ClaudeModel; hybrid_pick: ClaudeModel; disagree: boolean; }; /** Execution provider hint (phase 2): 'anthropic' or 'openrouter'. */ provider?: 'anthropic' | 'openrouter'; /** Concrete OpenRouter model slug when provider=openrouter. */ openrouter_model?: string; /** * iter 46 — ensemble-disagreement diagnostic from iter 45. Absolute * difference between unified KRR and bucket specialist predictions for * the picked model. Set when both backends were queried (bucket * specialist available + bucket supplied). Persisting per-decision lets * a future tuner analyze the distribution and recommend an iter 44 * threshold. */ ensemble_disagreement?: number; } /** A routing outcome — written later by the caller via `recordOutcome()`. */ export interface TrajectoryOutcomeRow { v: 1; type: 'outcome'; ts: string; task_hash: string; /** 0..1 measured quality the chosen model achieved. */ quality: number; /** Optional per-model quality if the same query was evaluated against alternates. */ scores?: Record; /** Free-form provenance note (e.g. "manual rating", "benchmark suite"). */ source?: string; /** * iter 31 — token usage from the underlying API response. Optional and * additive: pre-iter-31 readers see undefined; iter-31+ consumers can * sum these for production cost accounting. `modelId` is repeated here * (also in the paired decision row) so cost computation needs only the * outcome row — no JOIN required for streaming aggregation. */ tokens?: { input: number; output: number; }; /** USD spend for this call. Computed at write time from `tokens` + the * shared price table (model-prices.ts) so consumers get a canonical * number even if prices change between write and read. */ cost_usd?: number; /** Concrete model id the call dispatched against (iter 13+ wired this * through agent.modelId). May differ from the bandit's tier label. */ model_id?: string; } export type TrajectoryRow = TrajectoryDecisionRow | TrajectoryOutcomeRow; export declare function taskHash(task: string): string; /** Record one decision. Cheap — a single appendFileSync of a JSONL row. */ export declare function recordDecision(args: { task: string; embedding?: number[]; complexity: number; model: ClaudeModel; confidence: number; uncertainty: number; routedBy: TrajectoryDecisionRow['routed_by']; neuralBackend?: TrajectoryDecisionRow['neural_backend']; abPair?: TrajectoryDecisionRow['ab_pair']; provider?: TrajectoryDecisionRow['provider']; openrouterModel?: TrajectoryDecisionRow['openrouter_model']; /** iter 46 — optional ensemble disagreement diagnostic (iter 45). */ ensembleDisagreement?: number; }): void; /** Record one outcome. Join to a decision by `task_hash`. */ export declare function recordTrajectoryOutcome(args: { task: string; quality: number; scores?: Record; source?: string; /** iter 31 — optional token usage from the underlying API call. */ tokens?: { input: number; output: number; }; /** iter 31 — concrete model id for cost computation against MODEL_PRICES. */ modelId?: string; }): void; /** Diagnostic for status/CLI. */ export declare function trajectoryRecorderStatus(): { enabled: boolean; path: string; taskCharLimit: number; }; /** A training row reconstructed from one decision+outcome pair. Shape matches * the bundled seed-corpus (`seed-rows.json`) so the same train-bundled-krr.mjs * pipeline can consume it. */ export interface PairedTrainingRow { task: string; embedding: number[]; scores: Record; tier: 'cheap' | 'mid' | 'strong'; /** Provenance — useful for filtering low-signal sources at training time. */ source: string; /** ISO timestamp of the outcome row (newer rows can be weighted higher). */ ts: string; } /** Map decision-side complexity ∈ [0,1] back to the corpus tier label. The * buckets are deliberately the same boundaries the bandit uses so a pair * drawn from production lands in the same KRR specialist that served it. */ export declare function tierFromComplexity(complexity: number): 'cheap' | 'mid' | 'strong'; /** Pair decision+outcome rows by task_hash. Returns the rebuilt training rows * + diagnostics so the caller (script or test) can report what was dropped * and why. */ export declare function pairTrajectoryRows(rows: TrajectoryRow[]): { pairs: PairedTrainingRow[]; stats: { totalRows: number; decisions: number; outcomes: number; paired: number; droppedNoEmbedding: number; droppedNoMatch: number; bySource: Record; byTier: Record; }; }; /** Test seam — reset cached config so unit tests can change env vars between cases. */ export declare function __resetTrajectoryRecorderForTests(): void; /** * Unified status for BOTH the routing-decision recorder (this module) and the * companion run-transcript recorder (the weight-eft capture path). The * run-transcript recorder is loaded dynamically so this module has no static * dependency on it (the reverse edge — run-transcript-recorder → taskHash — * is the only static link, keeping the import acyclic). */ export declare function unifiedRecorderStatus(): Promise<{ routerTrajectory: { enabled: boolean; path: string; taskCharLimit: number; }; runTranscripts: { enabled: boolean; path: string; } | { unavailable: true; }; }>; //# sourceMappingURL=router-trajectory.d.ts.map