import type { RunInternals, RunnerSelfSeat } from "../core/runner/contracts.js";
import type { ModelRef, TaskResult, TaskSpec, TaskStatus } 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
 * (several independent agents and a judge), 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.
 */
/** 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<Pick<TaskSpec, "limits" | "systemPrompt">>;
}
export interface CascadeConfig {
    /** RB-473 — disclosure channel for CONTAINED host-callback failures in this scope (the RB-463
     *  isolation primitive's bounded first-failure per site). Absent ⇒ failures are counted only. */
    onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
    /** 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).
     *
     * HRD-AGO-10 — a positive verdict is CONJOINED with the rung's status: it counts as a pass only when
     * `result.status === "completed"`, unless {@link CascadeConfig.acceptPartial} is set. A status-blind gate
     * ("the text is non-empty", "the JSON parses") could otherwise keep a blocked/failed/timeout rung, which made
     * `cascadeOutcome: "passed"` disagree with the `status` the same object carries. A verdict rejected on those
     * grounds is recorded as `passed: false` + {@link CascadeAttempt.statusRejected}, and the cascade escalates.
     *
     * @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<GateVerdict>;
    /**
     * HRD-AGO-10 — the explicit opt-out from the require-completed conjunction above. Two states, no middle:
     * - **omitted / `false` (default)** — a {@link gate} pass counts only on a `completed` rung. Unfinished work
     *   is escalated, so `cascadeOutcome: "passed"` always implies the returned rung actually finished.
     * - **`true`** — the gate's verdict alone decides, so a deliberately partial answer can be kept (a blocked
     *   rung whose partial output IS the deliverable, a timeout whose streamed prefix is good enough). The
     *   returned `status` still reports the truth — this only says the gate is allowed to accept it.
     *
     * Ignored by the default gate, which requires `completed` in either case.
     */
    acceptPartial?: boolean;
    /** 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.
     *
     * HRD-AGO-7 — this is a REAL deadline over the run, not just a gate on starting the next rung: each rung is
     * dispatched with an `AbortSignal` that fires at `start + totalTimeoutMs` (combined with the caller's own
     * `spec.signal` when there is one), so a rung already in flight when the budget runs out is ABORTED. What
     * "aborted" buys is bounded by the workload's cooperation with cancellation — the brain request is cancelled
     * and a tool that honors its `ToolExecuteContext.signal` stops at once, but a tool that ignores its signal
     * still runs to its own completion (the run then assembles as failed the moment it returns). Cleanup on a
     * cut: the rung's result is NOT gated (truncated work must never pass), its session IS released (nothing
     * pins it — a durable pause is handled earlier and returns instead), the attempt is recorded with
     * `timedOut: true`, and the ladder stops (`cascadeOutcome: "exhausted"`) rather than escalating past a
     * deadline that has already passed.
     *
     * A budget already spent AT ENTRY (`0`, negative, or a caller that measured elapsed time itself) dispatches
     * NOTHING and returns the cascade's own terminal: `status: "failed"`, `errorCode:
     * "cascade.budget_exhausted"`, empty `attempts`, `finalRung: -1`.
     */
    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;
    /** This rung's OWN+nested spend. ABSENT when the rung ran unpriced (RB-368 knownness batch-4
     *  F4) — a fabricated 0 here both misreported the attempt and let escalation ride under a finite
     *  ceiling the engine could not actually enforce. */
    costMicroUsd?: number;
    status: TaskStatus;
    /** 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;
    /** HRD-AGO-7: this rung was CUT IN FLIGHT by {@link CascadeConfig.totalTimeoutMs}. Its result is truncated
     *  work, so it was never handed to the gate (`passed` is always false) and the ladder stopped here. */
    timedOut?: true;
    /** HRD-AGO-7: the CALLER's own `spec.signal` aborted during this rung (distinct from the cascade deadline).
     *  Same handling — no gate, no escalation onto an already-aborted signal. */
    cancelled?: true;
    /** HRD-AGO-10: the gate returned a PASS but the rung had not `completed`, so the verdict did not count
     *  (`passed: false`, escalate). Set only in the default require-completed mode — never with
     *  {@link CascadeConfig.acceptPartial}. */
    statusRejected?: true;
    /** 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";
    /** F4: present when a finite `costCeilingMicroUsd` was configured but some rung's spend was unpriced —
     *  the ceiling could not be enforced over unknown cost, so the ladder STOPPED escalating (fail-closed)
     *  instead of riding an unbindable budget. Judge spend by the absent cost fields, not by 0. */
    costCeilingUnenforceable?: true;
    /** 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 — or `-1` when NO rung was dispatched (an already
     *  spent {@link CascadeConfig.totalTimeoutMs}); `attempts` is then empty and the result is the cascade's
     *  own synthetic `cascade.budget_exhausted` terminal, not a rung's. */
    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: RunnerSelfSeat, spec: TaskSpec, config: CascadeConfig, 
/**
 * The TRUSTED, run-scoped internals channel — the same object `Runner.runTask` takes as its second
 * argument — forwarded to EVERY rung under the composition forward contract (identity, with the one
 * placement-axis exception stated where the shape is named in `src/index.ts`).
 *
 * WHY THE SEAT EXISTS: `runCascade` returns a `TaskResult` and DRAINS each rung's event stream
 * internally, so the host never holds a `TaskStream` for any rung. Without this parameter a
 * deployment observes nothing a rung did — tool beats, forwarded child progress, the resolved
 * workspace — only the per-rung {@link CascadeAttempt} audit rows and the final result. An
 * observation sink here (`onActivity`, `onForwardEvent`, `onWorkspaceResolved`) is the only way in.
 *
 * ATTRIBUTION: rungs run SEQUENTIALLY with the SAME object, so one sink receives the rungs' beats
 * CONCATENATED in ladder order. `ToolActivity` carries no rung identity — two rungs running the same
 * tool are indistinguishable — so the boundary a host keys on is {@link CascadeConfig.onRung}, which
 * fires only after its own rung's beats have been delivered (the sole rung ending without one is a
 * durable pause, which returns immediately). Two honesty notes: SUBAGENT events forwarded through
 * `onForwardEvent` carry their own `sourceTaskId`, and a BACKGROUND child outlives the rung that
 * spawned it — its frames can arrive inside the next rung's segment.
 *
 * FAILURE-ISOLATED, by the same primitive as {@link CascadeConfig.onRung}: engine-invoked through a
 * run-scoped safe notifier — a throwing `onActivity` is contained (first failure per site disclosed
 * via `console.warn`, later ones counted) and never faults the rung. Uncontained, the fault
 * AMPLIFIES: the faulted rung is rejected by the gate, the cascade escalates, and a non-idempotent
 * tool the rung had ALREADY executed runs AGAIN on the next rung — the MEASURED HAZARD pin holds
 * containment.
 *
 * FORWARDED WHOLE, NOT FILTERED: every field rides every rung, including the ref-valued ones
 * (`peerSelfRef`, `ownOrgAdmissionRef`) whose own contract is one ref per RUN — wiring those here
 * makes the ladder's rungs share one carrier. The seat is meant for observation sinks; anything
 * else is a choice the caller is making explicitly, not a default this layer applies.
 *
 * `isolation: "worktree"` in particular rides through here, where the verification gate REFUSES it —
 * an asymmetry with a reason. That field mints a NEW workspace per RUN: the gate's runs are one
 * artifact judged and repaired by several legs, so separate workspaces would have the verifier
 * grading files the implementation never wrote to, while a ladder's rungs are INDEPENDENT COLD
 * RE-RUNS, so one workspace per rung is the coherent reading. What a host must then know: the
 * returned result is ONE rung's, its work lives in THAT rung's workspace, and no field of the result
 * names it — `onWorkspaceResolved` is the only place the path appears, once per rung in ladder
 * order. Whether it is still readable afterwards is the FACTORY's contract: each rung's
 * factory-minted env is torn down at the end of that rung's run.
 *
 * Absent ⇒ every rung is dispatched with `undefined`, with ONE design/380 O1② placement delta either
 * way: when neither `placementRoot` nor `rootSessionId` is present, rungs after the first get a
 * fresh shallow copy carrying `placementRoot` = the first rung's resolved sessionId (the ladder's
 * placement fixed point); every other field still rides by the same reference, and an internals
 * object that already carries a fixed point is forwarded verbatim.
 */
internals?: RunInternals): Promise<CascadeRunResult>;
