/**
 * The task LIMITS: the allowance axes a caller sets on one run (turns, tokens, cost, wall-clock, the
 * tool-call and idle bounds, the per-model overrides). Its own module rather than a section of
 * `task-spec.ts` because it is the one shape a caller edits without touching anything else in the
 * spec. Layer 0 vocabulary; `types.ts` re-exports the name, so no consumer's import changes.
 */
import type { ModelRef } from "./model-seat.js";
/**
 * design/164 — the task's complete limit surface (see {@link TaskSpec.limits}).
 *
 * **Window semantics.** `maxTokens` / `maxCostUsd` / `maxWalltimeMs` are PER-SLICE windows, not
 * whole-task totals: a task that opted into {@link TaskSpec.resourceSuspend} suspends when a window is
 * exhausted and its resume gets the NEXT window of the same size. Whole-task ceilings live on
 * `resourceSuspend` (`totalTokens` / `totalBudgetUsd`) and are frozen on the ledger, so a resume can
 * never refresh them.
 *
 * **Two-tier hit semantics.** When a window is exhausted:
 *  - no `resourceSuspend` (the default) ⇒ a LOUD TERMINAL: `status:"failed"` with
 *    `limits.max_tokens_exceeded` / `limits.max_cost_exceeded` / `limits.max_turns_exceeded` /
 *    `limits.max_walltime_exceeded`, salvaged text attached where there is any. This is the
 *    "must have a definite outcome by the deadline" shape — express it by NOT opting in.
 *  - `resourceSuspend` set and the durable infrastructure present ⇒ a resumable `suspended` checkpoint.
 *
 * **Priority.** Several axes may be armed at once; the first to trip stops the run. When more than one
 * trips at the same turn boundary the attribution order is tokens > cost > turns > walltime.
 */
export interface TaskLimits {
    /**
     * Cumulative prompt+completion tokens this slice may spend. The PRIMARY budget axis. Unset ⇒ no token
     * ceiling. Must be a finite, non-negative number (a bad value is a fail-loud config error, never folded
     * to a default) — see the two-tier hit semantics on {@link TaskLimits}.
     */
    maxTokens?: number;
    /**
     * Cumulative spend ceiling for this slice, in USD (requires `model.cost` or `RunnerDeps.pricing`).
     * Checked pre-call and at each turn boundary. Unset ⇒ no cost ceiling. Money and tokens are two
     * genuine mental models and both are kept; documentation leads with `maxTokens`.
     *
     * NOTE: the pre-call check is **best-effort** — it estimates the first call's input from the objective
     * length only (it cannot see the assembled system prompt / history / memory), so it may under-estimate
     * and let an oversized first turn run; the turn-boundary gate then catches it after the spend.
     */
    maxCostUsd?: number;
    /** Turn ceiling for this slice. Unset ⇒ no turn ceiling. `0` is the explicit "unbounded" sentinel
     *  (kept for callers that write it deliberately); any other non-positive / non-finite value is a
     *  fail-loud config error. */
    maxTurns?: number;
    /**
     * Wall-clock ceiling for this slice, in MILLISECONDS. A PER-SLICE ACTIVE clock: it starts at the
     * slice's first turn on the monotonic clock, suspended time does not count, and a resume opens a
     * fresh window (a new process, a new anchor — no cross-process clock is ever compared). Unset ⇒ no
     * wall-clock ceiling, which is the default: time answers "how long did it take", not "how much work
     * is this allowed to be", so it is the axis of last resort.
     */
    maxWalltimeMs?: number;
    /**
     * design/119 #2: per-REQUEST output-token cap (the provider `max_tokens` / `max_completion_tokens`
     * field). NOT a budget — it bounds each model reply, while {@link maxTokens} bounds the whole slice.
     * Unset ⇒ the brain falls back to `model.maxTokens` (both families; the OpenAI lane omits the
     * wire field — provider default — only when `model.maxTokens` is also absent). A reply
     * that hits it stops with `length` and the loop's truncated-output auto-continue takes over.
     * HARD bound on Anthropic budget-thinking models too: a cap too small to host a legal thinking
     * budget (< 2048) skips thinking for the request rather than silently raising the cap.
     */
    maxOutputTokens?: number;
    /**
     * design/164 — the LIMIT-APPROACH notice. While any of `maxTokens`/`maxCostUsd`/`maxTurns` is armed,
     * the engine injects at most two one-shot reminders at turn boundaries: one when the highest axis
     * ratio crosses the first threshold ("start converging"), one when it crosses the second ("deliver
     * now"). Both are advisory prompt frames — they never stop the run, and they never fire on a
     * resource-suspend task (a slice boundary is not a delivery deadline).
     *
     * Default ON whenever an axis is armed; `false` switches it off. `{ at: [first, second] }` overrides
     * the thresholds (each a fraction in `(0, 1]`, first ≤ second).
     */
    approachNotice?: false | {
        at?: [number, number];
    };
    /** Cancel an in-flight stream the moment cumulative cost/tokens cross their window. Defaults to `true`
     *  when {@link maxCostUsd} is set (hard control), `false` otherwise. */
    budgetStreamCancel?: boolean;
    /**
     * **Near-budget degradation** (design/21 §5C). When cumulative cost reaches `atCostFraction` of the
     * effective cost window, **switch the active model to the cheaper `to`** for the rest of the task to
     * stretch the remaining budget — accepting lower quality instead of stopping at the ceiling. One-way
     * (never switches back); sets `TaskResult.degraded = { …, reason: "budget" }`. Requires
     * {@link maxCostUsd} (the fraction is of it); ignored otherwise. This is the proactive, Runner-level
     * complement to the reactive brain-level `createDegradingBrain` (rate_limit / breaker-open).
     */
    degrade?: {
        to: ModelRef;
        atCostFraction: number;
    };
    /**
     * The OUTER wall on how long the engine waits for a single brain
     * call, in ms. Default {@link import("../brain/timeout.js").BRAIN_CALL_GUARDRAIL_DEFAULT_MS}
     * (one hour); `false` or `0` switches it off; any other non-positive / non-finite value is a
     * config error (fail-loud, never folded to a default). Overrides `RunnerDeps.brainCallGuardrailMs`.
     *
     * This is a GUARDRAIL, not a budget: it answers "is the system hung", so unlike every axis above it
     * is armed by default, and it does NOT depend on the brain cooperating — on expiry the engine aborts
     * the signal it handed the brain AND stops awaiting the call, ending the task fail-loud with
     * `errorCode` `"brain.call_guardrail_timeout"`. That is what makes it the backstop for a custom
     * `Brain` that ignores `options.signal` or never settles.
     *
     * It is deliberately COARSE. A task that also sets {@link maxWalltimeMs} runs both clocks in parallel
     * and the tighter one expires first — the guardrail only shows up when everything finer failed to bind.
     */
    brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
}
