Files
waggle-os/packages/agent/src/canary/phase-5-router.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

181 lines
6.8 KiB
TypeScript

/**
* Phase 5 canary router — deterministic per-request routing between
* pre-Phase-5 baseline shapes and GEPA-evolved variants.
*
* Reads WAGGLE_PHASE5_CANARY_PCT (0-100, default 0) via FEATURE_FLAGS, hashes
* the request_id into a stable 0-99 bucket, and routes to the canary variant
* iff bucket < canary_pct AND the base shape has a canary mapping AND the
* canary shape is registered.
*
* BIND (manifest gepa-phase-5/manifest.yaml § canary_toggle):
* - Deterministic per-request_id routing preserves A/B paired-comparison
* validity for §3 monitoring (same request_id always routes the same way).
* - Default canary_pct = 0 until PM canary kick-off ratification (§7.3).
* - Hot reconfig via process restart; no code redeploy required.
* - LOCKED scope (manifest § scope_LOCKED): claude::gen1-v1 +
* qwen-thinking::gen1-v1. Mid-flight scope changes require new LOCKED
* decision memo + Marko ratifikacija.
*
* AUDIT: gepa-phase-5/manifest.yaml § canary_toggle, § scope_LOCKED.
*/
import { FEATURE_FLAGS } from '../feature-flags.js';
import { selectShape, REGISTRY, type SelectShapeOptions } from '../prompt-shapes/selector.js';
import type { PromptShape } from '../prompt-shapes/types.js';
/**
* Phase 5 LOCKED variant scope. Maps a base shape `name` to its evolved
* variant's REGISTRY key. Both directions of the mapping are pinned by the
* scope LOCK (decisions/2026-04-29-phase-5-scope-LOCKED.md).
*
* NOT mapped (intentional, per scope LOCK): qwen-non-thinking, gpt,
* generic-simple. These remain on baseline shapes.
*/
export const BASE_TO_CANARY_VARIANT_MAP: Readonly<Record<string, string>> = Object.freeze({
claude: 'claude::gen1-v1',
'qwen-thinking': 'qwen-thinking::gen1-v1',
});
export interface RouteResult {
/** The PromptShape selected (canary variant or baseline). */
shape: PromptShape;
/** True iff routed to a Phase 5 canary variant. */
isCanary: boolean;
/** Base shape name resolved by selectShape() before canary consideration. */
baseShapeName: string;
/** Canary variant REGISTRY key (only when isCanary === true). */
canaryShapeName?: string;
/** Bucket [0, 99] computed from requestId. Useful for monitoring telemetry. */
bucket: number;
/** Canary percentage at routing time (snapshotted from FEATURE_FLAGS). */
canaryPct: number;
}
export interface RouteOptions extends SelectShapeOptions {
/**
* Override canary_pct for this single call. Useful for tests + replay.
* Production callers should rely on FEATURE_FLAGS.PHASE_5_CANARY_PCT.
* Invalid values fall back to 0 (canary OFF).
*/
canaryPctOverride?: number;
}
/**
* Hash a request id into a stable 0-99 bucket using FNV-1a.
*
* Properties:
* - Deterministic: same input always returns same bucket.
* - Reasonable distribution across short alphanumeric request ids.
* - Fast (no crypto, no allocations beyond input traversal).
*
* Not cryptographically secure — strictly for canary bucketing.
*/
export function hashRequestIdToBucket(requestId: string): number {
if (typeof requestId !== 'string' || requestId.length === 0) {
return 0; // fail-safe: non-strings + empty strings → bucket 0
}
let hash = 0x811c9dc5; // FNV offset basis
for (let i = 0; i < requestId.length; i++) {
hash ^= requestId.charCodeAt(i);
hash = Math.imul(hash, 0x01000193); // FNV prime, with 32-bit truncation via Math.imul
hash >>>= 0; // unsigned 32-bit
}
return hash % 100;
}
/**
* Validate a canary_pct override value. Mirrors feature-flags.ts parser
* semantics (fail-safe to 0 on malformed input).
*/
function clampCanaryPct(raw: number | undefined): number {
if (raw === undefined) return FEATURE_FLAGS.PHASE_5_CANARY_PCT;
if (!Number.isFinite(raw)) return 0;
if (!Number.isInteger(raw)) return 0;
if (raw < 0 || raw > 100) return 0;
return raw;
}
/**
* Resolve the base shape NAME for a model alias by inspecting the result of
* selectShape(). Mirrors selectShape's resolution order; returns the
* `shape.name` string field (which is stable on every PromptShape).
*
* If override is provided, returns the override directly (without resolution).
*/
function resolveBaseShapeName(modelAlias: string, options: SelectShapeOptions): string {
if (options.override) return options.override;
const shape = selectShape(modelAlias, options);
return shape.name;
}
/**
* Route a request to a Phase 5 canary variant or to the baseline shape.
*
* Resolution order:
* 1. Resolve baseline shape name via selectShape() (or options.override).
* 2. Look up canary variant in BASE_TO_CANARY_VARIANT_MAP.
* 3. If no canary mapping: return baseline.
* 4. If canary variant not registered (e.g. shape file missing): return
* baseline (fail-safe — never crash a request because evolved variant
* isn't loaded yet).
* 5. Hash requestId into 0-99 bucket; if bucket < canary_pct: route to
* canary variant; else: return baseline.
*
* Audit: returns full provenance (baseShapeName, canaryShapeName, bucket,
* canaryPct) so §3 monitoring can record per-request routing decisions.
*/
export function routeRequestToVariant(
modelAlias: string,
requestId: string,
options: RouteOptions = {},
): RouteResult {
const canaryPct = clampCanaryPct(options.canaryPctOverride);
const bucket = hashRequestIdToBucket(requestId);
const baseShapeName = resolveBaseShapeName(modelAlias, options);
const baseShape = selectShape(modelAlias, options);
// Canary OFF or no mapping or variant not loaded → return baseline.
if (canaryPct <= 0) {
return { shape: baseShape, isCanary: false, baseShapeName, bucket, canaryPct };
}
const canaryShapeName = BASE_TO_CANARY_VARIANT_MAP[baseShapeName];
if (!canaryShapeName) {
return { shape: baseShape, isCanary: false, baseShapeName, bucket, canaryPct };
}
const canaryShape = REGISTRY[canaryShapeName];
if (!canaryShape) {
// Variant declared in scope but not registered (e.g. shape file deleted or
// not yet loaded). Fail-safe to baseline; surface via return value rather
// than throw so the request still serves.
return { shape: baseShape, isCanary: false, baseShapeName, bucket, canaryPct };
}
// Bucket < canaryPct → route to canary.
if (bucket < canaryPct) {
return {
shape: canaryShape,
isCanary: true,
baseShapeName,
canaryShapeName,
bucket,
canaryPct,
};
}
return { shape: baseShape, isCanary: false, baseShapeName, bucket, canaryPct };
}
/**
* Inspector — list shape NAMES that have an active canary mapping.
* Useful for §3 monitoring + manifest cross-checks.
*/
export function listCanaryEligibleShapes(): string[] {
return Object.keys(BASE_TO_CANARY_VARIANT_MAP);
}
/**
* Inspector — list canary variant REGISTRY keys (in-scope per LOCK).
*/
export function listCanaryVariants(): string[] {
return Object.values(BASE_TO_CANARY_VARIANT_MAP);
}