/**
 * The auto-mode classifier's CANDIDATE LADDER, as one module: which models a classification may be asked on
 * (and which model stands in when no `classifier` role names one), its per-rung reasoning seat and the
 * per-attempt user-message tail, how a rung's reply is read for a re-ask or a failure, and the once-per-run
 * disclosures its first move and its broken contract mint.
 *
 * Beside `derived-route-fallback.ts` and in the same family — both are seats the caps phase's classifier
 * arming reaches DOWN for, and both answer "this leg's model did not work out" one layer apart. Which is why
 * the rung comparison here borrows the route adjudicator's own identity law instead of inventing one.
 *
 * THE TWO LAYERS, since one sentence ("the classifier seat did not work out ⇒ use the run's main model") is
 * spelled in two places. They are ordered, and each has its own trigger and its own record face:
 *
 *   ① PRE-FLIGHT — `derivedRouteFallsBack`, BEFORE any call. Trigger: the classifier model's route differs
 *      from the primary's AND the brain's own route judge finds the credential/URL pairing broken. Effect:
 *      the arming SWAPS the seat, so the ladder below is built from the main model and rung 0 IS the main
 *      model. Record: the once-per-run operator notice `route.fallback_to_primary` (`detail.{seat, from, to,
 *      cause}`) — and only that. `GateDisposition.classifier.modelRequested` reads the MAIN model's id,
 *      because the ladder it describes never carried the configured seat; the gate record answers "which
 *      model was asked", and "what did the deployment configure, and was it swapped" is the notice's
 *      question. `classifierFallback` governs THIS layer too: under `"none"` the pre-flight does not swap —
 *      it keeps the declared seat and announces `route.classifier_unroutable`, so the deployment that said
 *      "deny rather than fall back" gets the denial (the seat's calls are refused at the request gate)
 *      instead of a silent fallback the knob had just declined.
 *   ② THE LADDER — here, AFTER a call. Trigger: the rung produced no RULING. Effect: the next rung is asked.
 *      Record: `GateDisposition.classifier.fallback` on every classification this run's own gate decided (an
 *      inherited-ancestor chain carries the form and no attribution) + the throttled `classifier.fallback`.
 *
 * They FOLD rather than stack: after a ① SWAP the seat and the main model are the same route, so
 * {@link classifierLadderRungs} returns ONE rung and ② cannot fire in the same run (and under `"none"`, where
 * ① does not swap, the ladder is one rung by the knob). The split's cost is the
 * `modelRequested` reading above; the unification (the pre-flight as rung 0 under its own cause word, one
 * record face for both) changes the notice set and is a ticket, not a repair.
 */
import type { AssistantMessage, Model } from "../../internal/llm.js";
import { type AutoModeClassified } from "../auto-mode.js";
import { type ClassifierUnavailableInfo, type ClassifierUnavailableSeat } from "../classifier-unavailable-arm.js";
import type { ApprovalRoute } from "../wiring-manifest.js";
import type { EngineNotice, RunnerDeps } from "../types.js";
/**
 * The rungs a classification walks, in order: the classifier seat, and — under `classifierFallback: "main"` (the
 * default) — the run's main model behind it. The knob is screened by `autoModeClassifierFallbackOf`, here and
 * (idempotently) at the caps phase that reads the same word for the pre-flight's posture, so a value outside
 * the closed set refuses the prepare loudly (`config.auto_mode_classifier_fallback_invalid`) instead of
 * reading as the default.
 *
 * ONE RUNG when the two resolve to the SAME ROUTE. Route identity, not `Model.id`: two catalog entries may
 * legitimately share a wire id while pointing at different endpoints (the route adjudicator's own law, and
 * the split its pre-flight exists for) — an id-only comparison would collapse a healthy main endpoint and a
 * broken classifier endpoint into one rung, turning the ladder off for the deployment that needs it most.
 * The collapse where it does apply is deliberate: re-asking the SAME model is a retry, which is what CC's
 * `unavailableOuterRetries` does and what this ladder is not. A catalog value the identity law cannot
 * normalize (a non-string `baseUrl` — it throws) reads as TWO rungs: being wrong that way costs one extra
 * call on a broken catalog, the other way turns the feature off invisibly, and the bad value still earns its
 * loud refusal at the brain's request gate.
 */
export declare function classifierLadderRungs(knob: unknown, classifier: Model, main: Model): readonly Model[];
/**
 * WHY the classifier seat is the model it is when no `classifier` role names one — the closed set behind the
 * `classifier.role_defaulted` notice's `reason`. `tier_below_main` = the nearest directly-bound tier below the
 * run's main model; `tier_table` = the static `flash` row (a main bound to no tier); `role_fallback` = the
 * `summarize → default` chain (no tiers at all); `main_only` = the main model is the only model there is (a
 * single-model catalog, or nothing bound below the main's tier); `role_unresolvable` = a `classifier` role
 * NAMED a model the catalog cannot resolve (a stale or misspelled reference), so the main model stands in —
 * the pre-#805 leg fell back the same way, silently.
 */
export declare const CLASSIFIER_ROLE_DEFAULT_REASONS: readonly ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
export type ClassifierRoleDefaultReason = (typeof CLASSIFIER_ROLE_DEFAULT_REASONS)[number];
/** What {@link classifierSeatModel} returns: the model, and — when no role NAMED it — why it is that one. */
export type ClassifierSeat = {
    readonly model: Model;
    readonly defaulted?: ClassifierRoleDefaultReason;
};
/**
 * The model the classifier SEAT resolves to for this run — ONE resolver for "which model classifies":
 *   · a `classifier` role that NAMES a model (`roleSpecNamesModel`: a string ref, a Model object, `{model}`,
 *     `{select}`) ⇒ the role resolver's answer, exactly as before (a preset-only role — `{thinking}` alone —
 *     does not name one and takes the default below);
 *   · else, with tier bindings declared (`deps.tiers`) and a main model on a tier ⇒ the nearest tier strictly
 *     BELOW the main's whose route differs from the main's — `tier_below_main`; the main's tier is the LOWEST
 *     tier its route sits on (a route bound as both `max` and `flash` is a `flash` main: deterministic, and
 *     the cheaper reading); nothing below ⇒ the main itself, `main_only`. Read off the expanded catalog's
 *     tier keys, the Runner's own resolution of a tier word: a key synthesised for an unbound tier falls DOWN
 *     to the next bound tier, which is the model this walk reaches anyway, and a catalog key spelled like a
 *     tier shadows the binding here as it does everywhere. No thinking preference: whether a model's
 *     thinking can be turned off is the SEAT's business ({@link classifierRungSeat}), not routing's;
 *   · else the role resolver's own fallback (the static tier row `classifier → flash` when tiers are active,
 *     `tier_table`; the `summarize → default` chain otherwise, `role_fallback`); a catalog with no roles at
 *     all ⇒ the main model, `main_only`.
 * The relative pick follows the run's MAIN model on purpose: a side channel that resolved without it is the
 * misfit the ticket names (a `flash` classifier above a `lite` main; two tiers below a `max` main).
 *
 * @contract classifier.seat.one_resolver — this function is the ONE answer to "which model judges this run's
 * gated calls". The engine's arming calls it and a host that DISPLAYS the seat (a capabilities face, an
 * operator pane) calls the same exported function rather than re-deriving the pick from catalog, roles and
 * tiers: a second derivation is a second answer, and the two drift the moment either side's rules move.
 */
export declare function classifierSeatModel(args: {
    deps: Pick<RunnerDeps, "models" | "roles" | "tiers">;
    main: Model;
}): ClassifierSeat;
/**
 * The `classifier.role_defaulted` line — minted ONCE per run by the arming, AFTER the route pre-flight, so it
 * names the seat actually asked (a pre-flight swap to the main model is its own line,
 * `route.fallback_to_primary`, same run). Silent when a role named the model: nothing was defaulted.
 */
export declare function announceClassifierRoleDefaulted(args: {
    onNotice: RunnerDeps["onNotice"];
    seat: ClassifierSeat;
    sessionId?: string;
    runId?: string;
}): void;
/**
 * The `classifier.unavailable` notice — a classification that could not RULE (the classifier leg failed, its per-level
 * budget ran out, or it replied with no verdict — `parse_error`; the code name predates that third cause) and where the
 * call went, read off the station's ONE route word (`ApprovalRoute`): `live_approver` = on to the live approver now,
 * `durable_park` = on to the durable park (the notice is minted at the fork: it states the route, never whether the park
 * then commits) — both are `disposition: "asked"` (the handed-on ask carries
 * `requiresRealApproval` and `classifierNoRuling`, so no automatic lane answers it), `none` = `"denied"`, refused — no
 * approver and no park facility (a reply with no verdict is then also counted on the denial limit). The sentence names the
 * route; the detail carries its projection (`disposition`), and the route itself is the run's published seat word
 * (`wiring_manifest.ask.effective`). Audience `user`: the refusal is the session user's own
 * call, and on a lane with no approval face (`-p`) this line is the only place they hear why it did not run. Once
 * per classification that could not answer, from both classifier stations (the leg's gate and an ancestor's frozen
 * classifier in the policy fold) — a call judged by more than one classifier can carry one line per classifier. The line
 * itself is never the count: the unavailable form is never counted; a reply with no verdict refused for want of a seat is
 * counted by the station's ruling path, not by this line.
 *
 * `detail: { toolName, toolCallId, cause, disposition, sessionId?, runId? }` — `cause` is why the classification did not rule,
 * an `AutoModeFallbackCause` word (`timeout` / `error`, or `parse_error` for a reply with no verdict). The sentence names the seat the station forked on — `leg` (this
 * run's own approval seat) or `ancestor` (the frozen approver of the ancestor layer whose classifier it was) — and, on a
 * timeout, the budget the classifier actually held (`budgetMs`, the decider's {@link AutoModeDecider.timeoutMs}; a
 * hand-built decider states none, and the sentence names the knob alone).
 */
export declare function classifierUnavailableNotice(args: ClassifierUnavailableInfo & ClassifierUnavailableStation & {
    sessionId?: string;
    runId?: string;
}): EngineNotice;
/** A classifier station's fork facts, wired once per leg: the route an ask the station hands on takes, and the
 *  `classifier.unavailable` notice through the one throat. Both stations (the gate, the inherited fold) take it. */
export declare function classifierUnavailableSeat(args: ClassifierUnavailableStation & {
    route: ApprovalRoute;
    onNotice: RunnerDeps["onNotice"];
    sessionId?: string;
    runId?: string;
}): ClassifierUnavailableSeat;
/** Which station forked (`leg` — the leg's gate, on its own approval seat; `ancestor` — an ancestor's frozen classifier in
 *  the policy fold, on that ancestor's frozen approver) and the per-level budget its classifier held, when stated.
 *
 *  ONE question at both stations, ONE answer function (`approvalRouteOf`) — where does a handed-on ask go from HERE — asked
 *  of two seats: the leg's gate hands its ask down the leg's own chain (`approvalRouteOf(wiring_manifest.ask)`: its live
 *  approver, or the durable park); the inherited station resolves at the ancestor's frozen approver
 *  (`frozenApproverRoute`: the same table with no park — a blanket or absent seat there ends in a refusal, and the leg's
 *  park is reached only through a live approver's `unavailable`). */
export interface ClassifierUnavailableStation {
    station: "leg" | "ancestor";
    budgetMs?: number;
}
/**
 * The classifier's REASONING SEAT, read off the model a classification is actually asked on — ONE predicate:
 * does the wire SPELL off ({@link thinkingOffSpelled})? Yes ⇒ `off` + the verdict-sized cap (the request pays
 * for the answer alone); no ⇒ `low` + the verdict cap + the low allowance (a model whose thinking the wire
 * cannot turn off is asked for the least it can grade, in the room that costs — CC 2.1.259 `MU`: spell off,
 * or leave thinking to the provider and pay 2048). A model that declares no reasoning at all is on the `low`
 * seat too: its wire carries nothing either way, so the request is the gateway's default, and a default-on
 * gateway thinks — the arm that used to read it as "never thinks" is the one the field report was about.
 * PER RUNG, not once per arming: the second rung is a different model with its own wire.
 *
 * The THINK seat is the RE-ASK seat ({@link classifierReplyAsksAgain}): a first attempt that produced no
 * verdict is asked once more with thinking allowed the room of CC's stage-2 classifier (8192) — an unknown
 * model that answered NOTHING at its cap has just told us it thinks past it, and is re-asked as one that does.
 */
export declare const CLASSIFIER_OFF_SEAT: {
    reasoning: "off";
    maxTokens: number;
};
export declare const CLASSIFIER_LOW_SEAT: {
    reasoning: "low";
    maxTokens: number;
};
export declare const CLASSIFIER_THINK_SEAT: {
    reasoning: "low";
    maxTokens: number;
};
export type ClassifierSeatRequest = typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT | typeof CLASSIFIER_THINK_SEAT;
/**
 * The seat for one rung's FIRST attempt — see the seat table above.
 *
 * @contract classifier.budget_is_room — every classifier output cap is ROOM the request asks for, never a
 * bound the engine can vouch for: a gateway may exclude reasoning tokens from `max_tokens` (a vLLM-fronted
 * Qwen gateway answered a cap of 128 with 938 output tokens and `stop`), so an over-long think is cut by the
 * round-trip timeout (`unavailable` / `timeout`), not by the cap (`parse_error` on an empty reply), and the
 * two are attested as the two different facts they are.
 */
export declare function classifierRungSeat(model: Model): typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT;
/**
 * The user-message TAIL of each attempt — CC 2.1.259's classifier suffixes, verbatim: the FIRST attempt
 * carries the fast-mode suffix (`$ns`: the answer must begin with `<block>`, no preamble), the RE-ASK carries
 * the thinking-stage suffix (`Uns`: think in `<thinking>` first, then answer). The base prompt (2.1.250 `KRg`)
 * already says "no preamble" once; upstream repeats it at the END of the user message, and it is the repeat
 * that lands — live 2026-09-15, a Qwen 3.8 with thinking off went from 70% tagged replies (1.2–3.9k chars of
 * "Looking at this action…" prose) to 90% at 8 output tokens with the first suffix, and the re-ask suffix
 * had it write its reasoning inside `<thinking>` (stripped by the reader) before the tag.
 */
export declare const CLASSIFIER_ATTEMPT_SUFFIX: {
    readonly first: "\nErr on the side of blocking. Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No \"Looking at...\" or similar preamble.";
    readonly reask: "\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>. Think longer on ambiguous or borderline actions; keep reasoning brief for clear-cut ones.";
};
/** The classifier reply's answer text — every text block joined; thinking blocks are not the verdict. */
export declare function classifierReplyText(reply: AssistantMessage): string;
/**
 * CC 2.1.259 `$Tt`: an EMPTY reply that stopped at the output cap is "unparseable" — a reply to ask for again,
 * not a refusal and not a verdict. The engine's brains report that cut two ways: `stopReason:"length"` (an
 * engine-shrunk cap) or `stopReason:"error"` with `errorKind:"length_empty"` (a static cap — the classifier's
 * own). An empty reply with any OTHER stop is CC's `policy_refusal` arm: not re-asked.
 */
export declare function classifierReplyEmptyAtCap(reply: AssistantMessage): boolean;
/**
 * A reply that may answer in the TAG form only: its text is not a complete answer of its own. Output cut by the cap
 * (`stopReason: "length"`), a dropped connection or a stalled stream (`partialFinalized`, with a synthesised
 * `stop`), and a reply that CALLED A TOOL (`stopReason: "toolUse"`, or a tool-call block in the content — the
 * classifier is given no tools, and the openai brain reports a length cut with a surviving call as `toolUse`):
 * whatever text arrived is a prefix or a fragment — a `No` on its way to `No, this must be blocked` — so it may
 * carry a verdict only through a `<block>` tag, which reads a `<block>no` prefix as CC does; the bare-answer arm
 * never sees it.
 */
export declare function classifierReplyTagOnly(reply: AssistantMessage): boolean;
/**
 * The text the DECIDER reads for its verdict: the reply's answer text, except that a tag-only reply
 * ({@link classifierReplyTagOnly}) whose text carries NO tagged answer reads as EMPTY — a cut prefix must not
 * reach the bare-answer arm as if it were the whole reply (adversarial passes 2–3: a connection cut after `No`,
 * and a scratch `<block>` mention before a cut `No`, executed the gated tool). Judged by the reader itself with
 * its bare arm closed, so "no tagged verdict" and "no verdict" cannot drift. Only the MISSING form is emptied:
 * a tag-only reply that carries a CONFLICT keeps its text, so the decider records the conflict and the leg
 * does not re-ask it (adversarial pass 4: emptying every parse failure had turned a cut conflict into a
 * missing reply, and the re-ask into an allow). The decider then records `parse_error` / `missing` for the
 * emptied text, the same word an empty reply gets.
 */
export declare function classifierReplyVerdictText(reply: AssistantMessage): string;
/**
 * Whether a first attempt is asked AGAIN on the THINK seat — ONE rule: the reply is MISSING a verdict, either
 * empty at the cap ({@link classifierReplyEmptyAtCap}), cut or fragmentary without a tagged verdict
 * ({@link classifierReplyTagOnly}), or text in which the reader finds no answer at all (`parse_error` of form
 * `missing`: prose, a thinking-only reply). Not a CONFLICT (two `<block>` answers — it carries a `yes`, and a second attempt that answered `no`
 * would erase it: the fail-closed `parse_error` it is stands) and not an empty reply with a non-cap stop
 * (CC's refusal arm). CC 2.1.259 `Vrr` re-asks an unparseable reply up to `maxRetries` times; sema re-asks
 * ONCE, on the stage-2 room, so a rung costs at most two calls. A failed reply ({@link classifierReplyFailure})
 * is judged before this and never reaches it.
 */
export declare function classifierReplyAsksAgain(reply: AssistantMessage): boolean;
/**
 * A reply that is a LEG FAILURE rather than an answer — the brains RETURN a provider error as a message
 * (`stopReason: "error"` with the provider's text on `errorMessage`: an HTTP 4xx/5xx, a quota refusal, an
 * overloaded gateway) and a cancelled request as `"aborted"`; neither is a reply the reader may judge. Read as
 * text they were `parse_error` — a card saying "the classifier's reply carried no verdict" about a model that
 * never answered, a ladder record blaming `parse_error` instead of `error`. The leg THROWS what this returns
 * and the decider maps the throw to `unavailable` (`error`), as it maps every leg failure; the sibling
 * side-channel legs (compaction, consolidation) already read an error stop this way. The cap-cut form
 * (`errorKind: "length_empty"`) is not a failure: it is the empty-at-cap reply the re-ask owns.
 */
export declare function classifierReplyFailure(reply: AssistantMessage): Error | undefined;
/**
 * The per-decision READ of a classification, as one seat: the `auto_mode.classified` trace frame (wall time,
 * verdict, cause), the ladder's throttled `classifier.fallback` disclosure and the per-model
 * `classifier.contract_broken` streak. All are observers — a throwing sink never reaches the gate (the decider
 * swallows it), and none changes a verdict.
 *
 * The frame's `model` is the model the classification RAN ON, which is what the field has always meant (its
 * declaration reads "the resolved classifier role, or the main model it fell back to"): the route
 * pre-flight's swap and the ladder's walk are two ways to reach that same sentence. `seatModelId` is the
 * fallback for a classification that carries no attribution at all.
 */
export declare function classifierRoundObserver(args: {
    tracer: RunnerDeps["tracer"];
    onNotice: RunnerDeps["onNotice"];
    taskId: string;
    seatModelId: string;
    sessionId?: string;
    runId?: string;
}): (info: AutoModeClassified) => void;
