/** * Error/budget-aware **degradation cascade** (1.39, design/21, design/17 §5C). * * Unlike failover (a *peer* backup whose goal is "same quality, a gateway that's actually up"), * degradation **deliberately downgrades** to a cheaper / different-provider model to KEEP SERVING when * the primary is rate-limited or its circuit breaker is open — accepting some quality loss instead of * failing the task. Opt-in; the served turn is tagged so the Runner can surface `TaskResult.degraded`. * * v1 (this file) handles the two **reactive** triggers visible at the brain layer — `rate_limit` and * `breaker_open`. The proactive **near-budget** trigger needs the task budget (only the Runner sees it) * and is deferred to v2 (council O1); `"budget"` is reserved in `DegradeReason` for it. * * Detection is **structural**, not literal string-matching: the breaker-open signal is * `CIRCUIT_OPEN_MARKER` (exported from circuit-breaker.ts) and the code comes from the shared * `extractErrorCode`. Degradation must be the **outermost** brain in a stack (so it sees the inner * failover/breaker/timeout outcomes and can downgrade on them): * `createDegradingBrain({ primary: failover(breaker(timeout(brain))), fallback, fallbackModel })`. */ import { type AssistantMessageDiagnostic, type Model } from "../internal/llm.js"; import type { Brain } from "../core/types.js"; /** Diagnostic `type` the decorator stamps on a degraded turn's message; the Runner reads it back. */ export declare const DEGRADED_DIAGNOSTIC_TYPE = "degraded"; /** Why a turn was degraded. `"budget"` is reserved for the v2 near-budget trigger (Runner-level). * design/126 (CC 批 β) widens the reactive set: `"server_error"` = a `server`-class failure (5xx incl. * 529 overloaded, 408/409) that ESCAPED the inner brain's own retry budget (CC parity: retry in place * first, then fall back — CC :600552); `"last_resort"` = an `http`-class status error (404 * model-not-found and other non-retryable statuses — CC :600513/:600588 folds both into "try the next * model instead of dying"). Deliberate deviations from CC, documented in design/126 D2: `rate_limit` * stays a trigger (CC waits in place; an unattended headless task waiting forever is a dead task), and * `auth` (401/403) NEVER triggers (CC falls back on 403; silently swapping models over a * permission/key misconfiguration is worse than failing loud for headless). */ export type DegradeReason = "breaker_open" | "rate_limit" | "budget" | "server_error" | "last_resort"; export interface DegradationInfo { /** The model id that was meant to serve (the primary). */ from: string; /** The cheaper model id that actually served (or the LAST model attempted when the chain failed too). */ to: string; /** The FIRST trigger that started the downgrade walk (later hops may fail for other reasons). */ reason: DegradeReason; /** design/126: the model ids attempted after the primary, in order (length 1 = the classic single * hop). Optional — absent on markers written by pre-chain versions. */ chain?: string[]; } interface DegradingBrainBaseOptions { /** The preferred brain (typically `failover(breaker(timeout(openai)))`). */ primary: Brain; /** * Which reactive triggers cause a downgrade. **Form-dependent default (codex 126 审 M3)**: the * design/126 `fallbacks` chain form defaults to all four reactive classes * (`rate_limit`/`breaker_open`/`server_error`/`last_resort`); the legacy `fallback`+`fallbackModel` * form keeps its pre-126 two-trigger default (`rate_limit`/`breaker_open`) so existing deployments * don't silently start swapping models on 5xx. Pass it explicitly to override either default. */ downgradeOn?: DegradeReason[]; } /** * The two forms are type-level EXCLUSIVE (codex 126 审 M2 — omitting both must not compile): * design/126 `fallbacks` chain, or the legacy single `fallback`+`fallbackModel` pair (chain-of-one). */ export type DegradingBrainOptions = (DegradingBrainBaseOptions & { /** * design/126 (CC 批 β) — the ORDERED downgrade chain (up to 3 entries, CC `Kjd` parity; excess is * truncated, duplicate model ids deduped, and an entry whose model id equals the model that just * failed is skipped at walk time). Each hop carries its OWN brain (own credentials — the decorator * clears the primary's per-call `apiKey`/`headers`/`metadata` for every hop, same discipline as the * single `fallback`). */ fallbacks: Array<{ brain: Brain; model: Model; }>; fallback?: never; fallbackModel?: never; }) | (DegradingBrainBaseOptions & { fallbacks?: never; /** The cheaper / different-provider brain to fall back to. **Carry its own credentials in its config** * (e.g. `createOpenAIBrain({ apiKey, baseUrl })`) — the decorator clears the primary's per-call * `apiKey`/`headers`/`metadata` so the fallback uses its own and doesn't leak the primary's identity * to a different provider. */ fallback: Brain; /** The model the fallback serves (different from the requested model). Resolve it from your role map * (e.g. `roles.cheap`) for consistency with the rest of model resolution (council O2). */ fallbackModel: Model; }); /** Pull a degradation marker off a served message (used by the Runner to fill `TaskResult.degraded`). */ export declare function readDegradation(msg: { diagnostics?: AssistantMessageDiagnostic[]; } | undefined): DegradationInfo | undefined; /** * design/131 (codex 131 审 A) — resilience-standdown pass-through: forward one brain's stream * verbatim, but keep the decorator discipline that a REJECTING inner brain (throw instead of an * error event) still leaves a terminal on the returned stream — otherwise `result()` never * resolves and the harness hangs (the same class as the degradation-brain blocker). */ export declare function passthroughStream(brain: Brain, model: Model, context: Parameters[1], options: Parameters[2]): ReturnType; export declare function createDegradingBrain(opts: DegradingBrainOptions): Brain; export {}; //# sourceMappingURL=degrading.d.ts.map