import type { Runner } from "../core/runner/runtask.js"; import type { ModelRef, TaskResult, TaskSpec } from "../core/types.js"; /** * Quality-gate cascade (design/27). Run the SAME task across a ladder of models (cheapest → strongest); * after each rung a caller-supplied **decidable gate** judges "good enough" — pass = keep it, fail = * escalate to the next rung. Classic FrugalGPT cascade, as a thin function over `runner.runTask` * (no Runner-core changes). * * APPLICABILITY: cascade only helps when the gate is **decidable** (a schema check, a verifier model, a * concrete assertion). Open-ended "completeness" tasks (find every bug, writing quality) have no oracle — * the gate keeps passing the first plausible cheap answer and escalation idles. Those want breadth / * adversarial debate (a `team`), not a depth ladder. * * Cost note (design/27 §2): each rung is an independent COLD `runTask` — every escalation repays the * full input cost. Cascade wins when (a) the cheap rung usually passes and (b) the prompt isn't so large * that the restart tax dominates. It is NOT always cheaper than running the strongest model directly. * * Boundary vs teacher mode: teacher injects advice and continues the SAME session (recovery); cascade * does independent re-runs on a model ladder (breadth). Orthogonal, not merged. */ /** A gate's verdict: a bare boolean, or `{ pass, diagnostics }` to attach reasoning to `attempts[]`. */ export type GateVerdict = boolean | { pass: boolean; diagnostics?: string; }; export interface CascadeRung { /** The model this rung runs on. */ model: ModelRef; /** Optional per-rung overrides (default: inherit the task's). */ overrides?: Partial>; } export interface CascadeConfig { /** Model ladder, cheapest → strongest. ≥1 rung; the last rung is the fallback (returned even if it doesn't pass). */ ladder: CascadeRung[]; /** * Quality gate: given a rung's result, return whether it is good enough (true/`{pass:true}` = keep, * false = escalate). Default: `status === "completed"` — and, if `spec.outputSchema` is set, also * requires a valid `structuredOutput` (so a schema auto-gives "only keep a rung that actually produced * valid structured output"; a prose-only completion escalates). The gate can be a pure check (structured * output / fields), a verifier model, semantic agreement, etc. **A throw is treated as "did not pass"** * (fail-open to escalate) and recorded as `attempts[].gateError` — so a broken gate can't strand the run, * but a verifier-gate's own failure is still visible. The gate must be decidable (see APPLICABILITY). * * @warning unredacted — the gate receives the UNREDACTED `TaskResult` (model output, structured output, * stats); this is prose advisory, not an enforced boundary (design/54 §3.5, [46] DESIGN1). If your gate * forwards output to an external service, ensure it has no sensitive data or sanitize first. */ gate?: (result: TaskResult, rung: { index: number; model: ModelRef; }) => GateVerdict | Promise; /** Max escalations (rungs run ≤ maxEscalations + 1). Default = `ladder.length - 1` (the whole ladder). */ maxEscalations?: number; /** Stop escalating once cumulative cost reaches this; return the current best. Optional. */ costCeilingMicroUsd?: number; /** Overall wall-clock ceiling for the whole cascade (per-rung time is `spec.limits.timeout`). Optional. */ totalTimeoutMs?: number; /** Per-rung callback (observability). */ onRung?: (info: { index: number; model: ModelRef; passed: boolean; result: TaskResult; }) => void; } export interface CascadeAttempt { index: number; model: ModelRef; passed: boolean; costMicroUsd: number; status: TaskResult["status"]; /** The rung's own failure code (e.g. "auth") if its `runTask` failed — distinct from "gate rejected it". */ errorCode?: string; /** Set when the gate THREW (vs returned false) — surfaces a broken verifier-gate (expired key, etc.). */ gateError?: string; /** Optional reasoning the gate attached to its verdict. */ diagnostics?: string; } export interface CascadeRunResult extends TaskResult { /** * The cascade's conclusion, **distinct from the inherited `status`**: `"passed"` = some rung passed the * gate; `"exhausted"` = ladder/guards ran out with no rung passing (the last rung's result is returned, * and its `status` may still be "completed" — judge the cascade by `cascadeOutcome`, not `status`). */ cascadeOutcome: "passed" | "exhausted"; /** True if the returned result came from a rung after the first (i.e. an escalation happened). */ escalated: boolean; /** Which rung (0-based) produced the returned result. */ finalRung: number; /** Per-rung audit detail (cost, pass/fail, why). Each attempt's `costMicroUsd` includes that rung's nested * (delegated) cost; the top-level `stats.costMicroUsd` is the sum of the rungs' OWN cost (nested totals live * in `stats.nested.costMicroUsd`), so it is NOT the plain sum of `attempts[].costMicroUsd`. */ attempts: CascadeAttempt[]; } /** * Side-effect note: `runCascade` runs the task up to N times. If your tools have non-idempotent effects * (`effect: "write"`), those execute once PER rung — ensure idempotency, or gate before the side effect * (e.g. a structured-output gate). `TaskSpec` never guaranteed idempotency; cascade makes it explicit. */ export declare function runCascade(runner: Runner, spec: TaskSpec, config: CascadeConfig): Promise; //# sourceMappingURL=cascade.d.ts.map