import type { AssistantMessage } from "../../internal/llm.js";
import type { HumanReviewGateRow } from "../task-result.js";
import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
import type { TerminalBlockedDetail } from "../terminal-cause.js";
import type { HaltConsumption } from "../../internal/harness.js";
/** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
export interface Stats {
    turns: number;
    tokens: number;
    /** Total tool calls executed (one per `tool_start`). Spread into `TaskResult.stats`. */
    toolCalls: number;
    /** Prompt tokens that MISSED the cache (Anthropic-protocol `input_tokens` shape). */
    promptTokens: number;
    cachedTokens: number;
    cacheWriteTokens: number;
    cacheWriteTokensLong: number;
    outputTokens: number;
    cacheHitRate?: number;
    /** Cache-INCLUSIVE prompt total (`promptTokens + cachedTokens + cacheWriteTokens*`) — the
     *  billing/window/hit-rate denominator. Required (not derived) so a new accumulation site that forgets
     *  it fails the type check instead of silently reporting a total short by its own leg. */
    totalInputTokens: number;
    costMicroUsd: number;
    /** Present (`true`) when at least one model round of this leg reported NO usage frame (the brain's
     *  own `usageMissing` mark on the assistant message: an aborted / errored / usage-less provider reply). The
     *  numeric fields stay required and stay what was COUNTED — with this key present they are a LOWER BOUND,
     *  not a measurement ("0 is unknown, not free"). Absent ⇔ every round reported usage. */
    usageMissing?: true;
    nested?: NestedUsage;
    /** Post-task memory-consolidation usage; set after the result is assembled. Kept out of
     *  the budget gate (`overBudget` reads `costMicroUsd`/`tokens`, never this). */
    memory?: {
        tokens: number;
        costMicroUsd: number;
        applied: number;
    };
    /** Human-review burden. Wall-clock approval-gate wait + count, bucketed by
     *  `gate.kind`, accumulated across resume legs. Set by the run loop from the per-task human-review
     *  accumulator (synchronous `resolveAsk` timing + durable resume latency). **NOT an LLM cost** — never
     *  folded into `costMicroUsd`/`costBreakdown`/the budget gate (same budget-excluded treatment as `memory`).
     *  Flows straight through to `TaskResult.stats.humanReview` via the public-stats spread; absent ⇒ no gate. */
    humanReview?: {
        count: number;
        totalWaitMs: number;
        gates: HumanReviewGateRow[];
    };
    /** TB telemetry B2: engine-mechanism engagement counters. Set by the run loop
     *  only when ≥1 engaged; flows straight through to `TaskResult.stats.mechanisms` via the
     *  public-stats spread. */
    mechanisms?: {
        finalVerifyInjected?: true;
        finalVerifyInjections?: number;
        attachmentsInjected?: number;
        repetitionCuts?: number;
        repetitionSpared?: number;
        repetitionEvents?: Array<{
            turn: number;
            action: "cut" | "spared";
            rule: "char-run" | "unit-loop";
            period: number;
            reps: number;
            segment: string;
        }>;
    };
    /** Within-task compaction LLM cost (micro-USD), accumulated by
     *  `recordCompactionUsage` as a SEPARATE running total because that cost IS folded into `costMicroUsd`
     *  (so it can't be re-derived from `costMicroUsd` alone). The {@link costBreakdown.compactionMicroUsd}
     *  source. Run-loop internal — not a public `TaskResult.stats` field. */
    compactionMicroUsd?: number;
    /**
     * A thin FINANCE TAXONOMY of the LLM-derived costs the engine actually
     * prices, decomposing the task's spend into report categories (no new persisted structure — derived from
     * the cost sources that already exist). CORE = LLM-token-derived ONLY; the SERVICE adds the infra axes it
     * owns (tool-call / sandbox-walltime / egress — it has the k8s cost data) and composes them with this.
     * Each category is ≥ 0 and the parts reconcile to the independently-summed LLM total
     * (`llmRoot + nestedSubagent + compaction` — `memory` is a separate, budget-excluded line, see its doc).
     * Filled in assembleResult for `llmRoot`/`nestedSubagent`/`compaction`; the `memoryConsolidation` line is
     * permanently 0 since B19 retired the runner-integrated consolidation pass (field kept — it is
     * public stats contract; an engine-plane re-mount would fill it again).
     */
    costBreakdown?: {
        /** Root-agent LLM cost (micro-USD): `costMicroUsd` MINUS the sub-categories already folded into it
         *  (compaction). NOT minus nested — nested subagent cost is tracked separately and is NEVER folded into
         *  `costMicroUsd` (it lands only in `stats.nested`), so subtracting it would under-report the root. */
        llmRootMicroUsd: number;
        /** Delegated sub-agent (nested) LLM cost (micro-USD) = `stats.nested?.costMicroUsd ?? 0`. */
        nestedSubagentMicroUsd: number;
        /** Post-task memory-consolidation LLM cost (micro-USD) = `stats.memory?.costMicroUsd ?? 0`. Permanently
         *  0: the runner-integrated consolidation pass is retired (the public `TaskStats` doc is the one home
         *  for what would fill it again). */
        memoryConsolidationMicroUsd: number;
        /** Within-task compaction LLM cost (micro-USD) = the running `compactionMicroUsd` accumulator (the
         *  category Framing-4 missed). This IS part of `costMicroUsd`, so it is subtracted from `llmRoot`. */
        compactionMicroUsd: number;
    };
}
/** The two cumulative budget ceilings a task can trip. Tokens outrank cost when both are
 *  over at the same boundary. */
export type BudgetAxis = "tokens" | "cost";
/**
 * The terminal-state flags the run loop hands {@link assembleResult} — the typed contract between the
 * loop and result assembly (exported so it's grep-able and tests can construct it).
 */
export interface ResultFlags {
    threw: unknown;
    /** The run's ENGINE-minted identity (`Prepared.runId`), echoed verbatim on
     *  `TaskResult.runId`. Pure pass-through on every terminal: assembly neither mints nor derives it
     *  (a caller reaching assembly at all has completed prepare, so the value exists), and a
     *  hand-constructed flags bag without one simply produces a result without one. */
    runId?: string;
    /** The EFFECTIVE (resolved) model id that served the task — echoed on `TaskResult.model`. */
    model?: string;
    /** Some spend was accounted while the serving model had NO configured price table
     *  (no `RunnerDeps.pricing` entry, no `Model.cost`). The numeric total is then a fabricated
     *  undercount — `stats.costMicroUsd`/`costBreakdown` are OMITTED from the public result instead of
     *  reporting a 0 indistinguishable from "declared free". The internal accumulator (budget gate's
     *  coordinate) is untouched. */
    unpricedSpend?: boolean;
    /** Prepare-time rewind disclosures to echo on `TaskResult.rewindNotes`.
     *  Pure pass-through — assembly neither adds nor filters (a rewind that FAILED never reaches here; it
     *  throws at prepare and lands in the `threw` slot as a terminal errorCode). */
    rewindNotes?: TaskResult["rewindNotes"];
    /** The run's OWN edited-file ledger, echoed on `TaskResult.editedFiles`. Pure pass-through on
     *  EVERY terminal — what this run's hands changed is a fact about the leg that ran, whatever
     *  terminal it reached (a failed/aborted run that edited files is exactly the case a "restore the
     *  code this message changed" affordance is for). Undefined ⇒ the key is omitted. */
    editedFiles?: TaskResult["editedFiles"];
    /** The run's final turn was halted by a person's BARE rejection of a tool call (the parent-thread
     *  control-flow boundary) — echoed on `TaskResult.haltedOnUserRejection`. Pure pass-through on
     *  every terminal: the fact is about the leg that ran, whatever terminal it reached (on the normal
     *  path the terminal is `completed`, and this is what tells that completion apart from a natural
     *  one — the model did not finish; the person stopped it and the run awaits their direction). */
    haltedOnUserRejection?: boolean;
    /** the harness's STATEMENT of what the run's halt did (see `HaltConsumption`):
     *  the loop's report that the halt's cut settled the final turn, and/or the boundary consult that
     *  stopped the loop for the halt's request. This is the ONLY input to `TaskResult.haltedByUser`
     *  (`haltConsumed`, halt-attribution.ts) and to slot 9.7 — the assembly never infers causation from the
     *  final's shape or from the verb's acceptance. A halt that was accepted but consumed nothing (the run
     *  ended for its own reason first) leaves this empty: the original terminal stands and the seat is
     *  NOT signed (the settle lane mints `task.halt_unconsumed` for it). */
    haltConsumption?: HaltConsumption;
    /** Call ids of answered-but-never-collected questions, echoed on
     *  `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
     *  optional `onError` alert is NOT the disclosure — this mandatory result face is. */
    strandedHumanAnswers?: TaskResult["strandedHumanAnswers"];
    /** The run's remote-workspace lifecycle failures, echoed on `TaskResult.remoteEnvFailures`.
     *  Pure pass-through (assembly neither adds nor filters) and INDEPENDENT of the status/errorCode chain:
     *  a refused `suspendVM` still ends the task exactly the way it did before — this only stops the eleven
     *  distinct codes from arriving as one anonymous `limits.max_turns_exceeded`. Empty/absent ⇒ the field is omitted. */
    remoteEnvFailures?: TaskResult["remoteEnvFailures"];
    /** The run's effective read posture (resolved face + normalized deny
     *  additions), echoed on `TaskResult.effectiveReadFace` / `.effectiveWriteFace` / `.effectiveReadDenyPatterns`. Pure
     *  pass-through (assembly neither adds nor filters) and INDEPENDENT of the status/errorCode chain:
     *  the posture is a fact about the leg that ran, whatever terminal it reached — a post-completion
     *  spawner folds it stricter-wins into follow-on legs; every other consumer may ignore it. */
    effectiveReadFace?: TaskResult["effectiveReadFace"];
    effectiveWriteFace?: TaskResult["effectiveWriteFace"];
    effectiveReadDenyPatterns?: TaskResult["effectiveReadDenyPatterns"];
    /** The memory-visibility observation, echoed on
     *  `TaskResult.effectiveMemoryScopes`. Pure pass-through on every terminal, same law as the
     *  read-posture seats above: present iff prepare completed (the memory-less states are their own
     *  values — absence means only "prepare never ran to completion"). */
    effectiveMemoryScopes?: TaskResult["effectiveMemoryScopes"];
    /** The leg's effective reasoning resolution, echoed on `TaskResult.effectiveReasoning`.
     *  Pure pass-through on every terminal (same law as the read-posture seats): the SAME object the
     *  `reasoning.resolved` trace frame carried (runtask resolves once per leg — two faces, one mint);
     *  absent when thinking was off/unset for the leg, so the key is omitted. */
    effectiveReasoning?: TaskResult["effectiveReasoning"];
    /** The run's compaction record (see `TaskResult.compaction`); assembly omits the key when absent. */
    compaction?: TaskResult["compaction"];
    /** ruled 2026-08-04 — the usage-governance wait hint carried by the platform terminal the run adopted
     *  (`undefined` for every other cause: an expiring environment has no return time to give, and a store
     *  failure is not a window). Passed as data rather than read back off `threw` so the seat has a typed
     *  producer. The assembler still gates it on the SETTLED errorCode, so a hint that arrived alongside a
     *  higher-ranked terminal is dropped instead of being filed under a code that does not name it —
     *  see `TaskResult.retryAfterMs` for the in-presence condition this preserves. */
    retryAfterMs?: number;
    abortedForTimeout: boolean;
    abortedForTurns: boolean;
    /** audit A-1: the abort signal fired DURING the run (user interrupt / timeout / max-turns / budget),
     *  captured before the run loop's unconditional release-abort. Since the loop now exits CLEANLY on an
     *  abort (no synthetic stopReason:"aborted" assistant is appended), `final` is the last REAL assistant
     *  message — this flag is what keeps an interrupted run from being mistaken for a completed one. */
    abortedLive?: boolean;
    /** The leg's ONE evaluation of {@link answerSettled} (a clean `"stop"` final with no queued continuation
     *  lost): a live abort that found this true landed on the run's tail, not on work — slot 9 stands down
     *  and the run takes the completed floor. Absent/false = the abort's ordinary reading. */
    answerSettled?: boolean;
    budgetHit?: "exceeded" | "precall";
    /** Which budget ceiling the hit belongs to. Set together with {@link budgetHit} at every
     *  site that records one; it picks the terminal code (`limits.max_tokens_exceeded` vs
     *  `limits.max_cost_exceeded`), so a hit without an axis would be an unattributable terminal. */
    budgetAxis?: BudgetAxis;
    blockedReason?: string;
    blockedDetail?: TerminalBlockedDetail;
    conflict?: boolean;
    /** env-tail migration F5: the request-build guard threw the irreducible-core terminal this run —
     *  the loop folded it into a text-only failure message, so the typed code rides this flag. */
    gitCoreOverBudget?: boolean;
    outputInvalid?: boolean;
    /** A re-suspend was refused because the task already suspended `maxSuspends` times
     *  (a resume/restart loop). It aborted the run (no `threw`) but must read as `failed`/`suspend.loop`,
     *  NOT `suspended` — slotted ABOVE `suspendRef` and the aborted branches (this suspend never minted a
     *  checkpoint, so `suspendRef` is unset anyway; the ordering makes the intent explicit). */
    suspendLoop?: boolean;
    /** The run's ONE committed durable pause (the holder's `pausedRef.current.cause`): the run stopped at a
     *  gate — an approval or content ask, a resource slice, a platform pause, a plan or dry-run review —
     *  and a checkpoint is durably committed. It used an abort to stop the loop (`final.stopReason ===
     *  "aborted"`) but set no `threw`/timeout/turns, so it is slotted ABOVE the `!final`/aborted arms (a
     *  clean pause, not a failure) and BELOW the real budget/threw failures (a limit that coincided still
     *  owns the ending). WHICH terminal status it reads (`suspended` / `needs_review`) is the pause's own
     *  `gate.kind` registry row, not a second flag: the two same-shaped refs that used to split the approval
     *  family from the review family — and the slot order between them that only ever mattered if both were
     *  somehow set — are gone with the commit-side routing that fed them. */
    paused?: PausedCause;
}
/**
 * Extract a machine-readable code from a thrown error (e.g. `SessionError.code`, a Node error code).
 * Walks the `cause` chain because the harness wraps lower-level errors (e.g. a `SessionError("conflict")`
 * append failure surfaces as an `AgentHarnessError("unknown")` whose cause is the real one) — so we
 * prefer the first specific code over a generic `"unknown"` wrapper.
 */
export declare function errorCodeOf(err: unknown): string | undefined;
/**
 * WHY the run ended — the ONE terminal arbitration ({@link TerminalCause}). First match wins, in the
 * slot order documented on {@link assembleResult} (a public contract: do not reorder). Every plane
 * field a result shows (`status`, `errorCode`, `errorMessage`, `blockedReason`, the checkpoint seats)
 * is projected from the value this returns by {@link terminalProjection}, so a terminal that carries a
 * token cannot be reported under a status that does not, and a review pause cannot be reported as an
 * approval pause: the pairing is the cause's shape, not the arbitration's discipline.
 *
 * `apiFailure` rides beside the cause rather than inside it on purpose: it is the message's own
 * provider-failure disclosure, filled only when a provider failure IS the terminal (the same
 * in-presence rule the usage-window wait hint states for itself), and it is not part of the cause's
 * public projection.
 */
/**
 * Has the run's ANSWER settled — is `final` the model's own clean end of its work? A run whose last turn
 * ended `stopReason:"stop"` has said everything it was going to say; whatever ran after that turn (the
 * final boundary's compaction, a stop hook, the teardown) is the run's tail, not its work. A hard stop
 * that lands in that tail — `TaskStream.interrupt()`, the task's `signal`, the walltime or turns limit —
 * cuts nothing the person or the caller wanted, so the run is NOT cancellable there: it ends `completed`
 * with the answer as `result` (slot 9 yields to the completed floor), no interruption marker is written
 * for it, and the end-of-task hook seats keep their full deadline. One predicate, read by every site that
 * used to decide the question for itself; there is no second spelling.
 *
 * What does NOT count as settled — each is "the model was still working" and stays cancellable:
 *  - `stopReason:"length"` — the output was truncated; the auto-continue chain owns what follows;
 *  - `stopReason:"toolUse"` — a batch was in flight (the orphan reconcile owns the abort);
 *  - `stopReason:"aborted"` / `"error"` — the turn itself was cut or failed;
 *  - a `partialFinalized` `"stop"` — a mid-stream loss promoted to a final; the loop's bounded
 *    continue nudge owns it, so the stop the caller sees is a salvage, not the model's own end;
 *  - no `final` at all;
 *  - `continuationCut` — the run was cut with a continuation still owed: a steer / follow-up the person
 *    had already queued was still pending (queued, or drained at the final boundary and never served —
 *    the harness's agent_end account, which also mints `task.user_steer_undrained` /
 *    `task.user_followup_undrained`), or the loop itself had decided to run another turn (a drained
 *    frame, a truncated-output / thinking-only / degenerate-output nudge) and the abort caught it before
 *    the dispatch (`Prepared.workCutRef.continuation`, from the loop's own trace steps) — in both, a
 *    stop landing between the decision and the next stream returns the PREVIOUS clean answer as
 *    `final` (the loop exits at its pre-dispatch guard), so `final` alone cannot tell, and a
 *    thinking-only "stop" the loop was about to nudge into a visible answer is an empty result, not
 *    an answer; or the abort was already in force when the last turn ENDED
 *    (`Prepared.workCutRef.abortedBeforeTurnEnd`) — a brain that ignored its signal and delivered a
 *    full "stop" through the abort still answered a run somebody had stopped mid-work.
 * Three axes, one law: the `abortedLive` flag alone decides that a stop landed; this decides whether it
 * landed on work or on the tail. Evaluated ONCE per leg (the terminal-adoption lane, where both facts
 * are in hand) and handed on as a value — {@link ResultFlags.answerSettled} — so the arbitration, the
 * marker gate, the hook-seat grace, the teardown attribution and the notice read the same verdict.
 * (A limit that is NOT a stop — a budget ceiling crossed by the last turn's own spend — is arbitrated
 * above slot 9 and is unaffected.)
 */
export declare function answerSettled(final: AssistantMessage | undefined, continuationCut?: boolean): boolean;
export declare function terminalCauseOf(final: AssistantMessage | undefined, stats: Pick<Stats, "humanReview">, flags: ResultFlags): {
    terminal: TerminalCause;
    apiFailure: TaskResult["apiFailure"];
};
/**
 * Build the final {@link TaskResult} from the run's terminal state. **The errorCode priority chain is a
 * public contract** (callers `switch` on these strings) — first match wins, in this exact order
 * (red-line #3, do not reorder):
 *   1. `outputInvalid`  → `"output.invalid"`
 *   2. degenerate       → `"output.degenerate"` + `salvagedOutput` (wins over budget/timeout)
 *   3. `budgetHit`      → `"limits.max_tokens_exceeded"` | `"limits.max_cost_exceeded"` (by `budgetAxis`;
 *       the pre-call estimate rejection and the after-spend crossing share the code — the distinction
 *       lives in `errorMessage`, not in a second code)
 *   3.5 `suspendLoop`  → `"suspend.loop"` (the re-suspend cap; deterministic root
 *       cause, above `threw` because it owns the abort)
 *   4. `threw` + walltime → `"limits.max_walltime_exceeded"`
 *   5. `threw` + turns   → `"limits.max_turns_exceeded"`
 *   6. `threw` + conflict→ `"conflict"`
 *   7. `threw` (other)   → `errorCodeOf(threw)`
 *   8. `blockedReason`   → status `"blocked"` (no errorCode)
 *   8.5 `paused`         → status by the pause's registry row (`suspended` for an approval / resource
 *       slice / platform pause, `needs_review` + `"review.pending"` for a plan or dry-run review) +
 *       checkpointToken/gate (a deliberate durable pause beats the abort it used, but
 *       yields to a real budget/threw failure above)
 *   9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
 *       (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
 *       normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
 *       EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`: the
 *       loop reported the halt's cut settled the final turn — never shape inference),
 *       and EXCEPT when the answer had already settled (`answerSettled`: a clean `"stop"` final, not
 *       partial) — a stop landing on the run's tail cuts no work, so the run falls through to 12
 *       (`completed`, the answer as `result`, no `limits.*` code for the axis; three axes, one law)
 *   9.7 halt CONSUMED (`haltConsumed(flags.haltConsumption)`) + (no `final` | the halt's own aborted
 *       `final`) → status `"completed"` (a bare user halt — the verb working, not a defect; below the
 *       aborts on purpose, a real abort/limit that also fired owns the terminal and the halt rides as
 *       the pass-through seat; an accepted-but-unconsumed halt never reaches this slot)
 *  10. no `final`        → status `"failed"`
 *  11. stopReason error  → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
 *  12. else              → `"completed"`
 */
export declare function assembleResult(spec: TaskSpec, sessionId: string, final: AssistantMessage | undefined, stats: Stats, flags: ResultFlags): TaskResult;
