UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

887 lines • 66.9 kB
/**
 * The TERMINAL record of one run: `TaskResult`, the plane word it still projects (`TaskStatus`), the
 * memory-scope summary it carries (`EffectiveMemoryScopes`) and the remote-workspace failure note
 * (`RemoteEnvFailureNote`). The cause itself lives one module over in `terminal-cause.ts`, which owns
 * the closed set and its replay table. Layer 0 vocabulary; `types.ts` re-exports every name below, so
 * no consumer's import changes.
 */
import type { TerminalCause } from "./terminal-cause.js";
import type { NestedUsage } from "./tool-spec.js";
/**
 * `"suspended"` (design/45) is a **non-terminal** outcome: the task hit a durable tool-policy gate
 * (`ask` with a `CheckpointStore` wired) and persisted a checkpoint instead of finishing — resume it
 * with `runner.resume(checkpointToken, outcome)`. v1 contract: it is only safe at the **top-level
 * `runTask` boundary**; the orchestrators (cascade/teacher/verify/team) map an unexpected `suspended`
 * to `failed` + `errorCode:"unexpected.suspended"` and pass the token up (§11 Q6, hard boundary).
 *
 * `"needs_review"` (design/76 §2.5, dry-run / shadow; design/80 D-B, plan-gate) is also a **non-terminal,
 * resumable** outcome, but a DISTINCT one: it is the REVIEW-PAUSE terminal a human/judge must clear. Two
 * disjoint gate kinds share it (carrying a `reviewRef`, errorCode `"review.pending"`): a `{kind:"needs_review"}`
 * checkpoint — a profile's dry-run interception produced a predicted state-diff to REVIEW before it is applied,
 * resumed with a `dry_run_review` outcome — and a `{kind:"plan_review"}` checkpoint (design/80 D-B) — a
 * profile's plan-gate produced a proposed PLAN a human must approve/edit/reject BEFORE any step runs, resumed
 * with a `plan_review` outcome. Both are DISJOINT from `"suspended"`: that is a PRE-ACTION approval of a TOOL
 * CALL (resumed with `policy_ask`); these are human REVIEWS (the distinct gate.kind picks the resume outcome).
 * Orchestrators that map an unexpected `suspended` should map an unexpected `needs_review` the same
 * hard-boundary way (it is, like `suspended`, only safe at the top-level `runTask` boundary).
 */
/**
 * Terminal vs waiting. Three words are TERMINAL — the leg is over and nobody is expected to act: `completed`,
 * `failed`, and `blocked` (the AGENT's own report, through `report_blocked`, that it cannot finish and why —
 * a self-report, not a pause; it waits for nobody). Two words are WAITING — a person's decision resumes the
 * same leg through `runner.resume(token, …)`: `suspended` (a durable pre-action approval of a tool call) and
 * `needs_review` (a durable review pause). The task-registry face spells the two waiting words as `parked`.
 * A consumer that files `blocked` beside `suspended` is waiting on a decision that never comes.
 */
export type TaskStatus = "completed" | "blocked" | "failed" | "suspended" | "needs_review";
/**
 * One remote-workspace lifecycle failure, reported as data on {@link TaskResult.remoteEnvFailures}.
 * See that field for why the eleven-code taxonomy needed a structured channel to reach a caller at all.
 */
/**
 * ONE `humanReview` ledger row — what `stats.humanReview.gates[]` carries, what the runner's live
 * accumulator holds, and what a checkpoint persists. It lived as six structurally-identical inline
 * copies, the persisted one NARROWER than the rest, so "does a row carry the call it gated?" had six
 * answers and the persisted one said no while the bytes it stored said yes. One declaration now.
 */
export interface HumanReviewGateRow {
    /** `kind` / `decision` / `toolName` / `toolArg` are documented once, on `stats.humanReview.gates`. */
    kind: string;
    waitMs: number;
    decision?: string;
    toolName?: string;
    toolArg?: string;
    /**
     * The id of the tool call this row gated — the row's IDENTITY, not a display field. Minted with the
     * row at each of the four mint sites, never re-derived downstream: a run pushes rows from several
     * mints while only one passes through the host's `onAsk`, so the two ledgers' counts are legitimately
     * unequal and there was no key to reconcile on. Absent means one thing — the mint had no call id to
     * give (a probe row; a durable gate whose checkpoint carries a non-`tool_approval` pending action,
     * since a `resource_limit` / `needs_review` / `plan_review` pause gates no call; or an older engine).
     * Never a placeholder.
     */
    toolCallId?: string;
}
export interface RemoteEnvFailureNote {
    /** Which VM-lifecycle call failed. */
    op: "suspendVM" | "resumeVM" | "postResumeInit";
    /** The adapter's verbatim code — the whole point of this note. */
    code: import("./remote-env.js").RemoteExecutionErrorCode;
    /** `code` is in the retryable family (`RETRYABLE_REMOTE_ERROR_CODES`) — a re-attempt can plausibly work. */
    retryable: boolean;
    /** How many times the engine called `op` before giving up (1 = it did not retry). */
    attempts: number;
    /** The adapter's message. Human-facing; branch on `code`, not on this. */
    message: string;
}
/** Result returned when a task finishes or gets stuck. Designed to be machine-readable for an external AI. */
/**
 * design/178 v2 §2.3 (件①) — the value of {@link TaskResult.effectiveMemoryScopes}: the memory
 * visibility face a leg ACTUALLY ran under, as a DISCRIMINATED three-state union (deliberately no
 * `"partial"` state — the engine session is built only after every plane materialized, so a
 * half-mounted scene is a fail-open `memoryless` with residue, never a served half-face):
 *
 *  - `"mounted"` — the memory engine session mounted. `scopes` is the EFFECTIVE VISIBILITY set in
 *    the effective serving order, both exactly as the engine mounted them: per plane, the
 *    admission-projected read layering PLUS a distinct write-only scope where one exists (the
 *    engine registers the write scope's dir and its entries ride the injected index — a write-only
 *    scope IS visible; the common writeScope∈scopes form dedups to the plain layering), project
 *    plane first on a dual root. Each row carries its admission ORIGIN (`"deployment"` = the
 *    operator's own declared set; `"request"` = the caller's spec — the two trust planes of the
 *    org admission door). `writeScope` is the effective write face as granted (an org writeScope
 *    not explicitly granted reads `null` here, exactly as it ran). `contract` names the
 *    scope-identity contract in force (`"v2"` typed keys / `"legacy"` opaque strings).
 *    SCOPE OF THE CLAIM (stated precisely): the rows are the MEMORY-ENGINE scope set this leg
 *    mounted — the dirs the engine registered and indexed. They deliberately do NOT cover the
 *    separate memory-adjacent faces (the shared-memory store pair, the project-context memory
 *    layer — each its own surface with its own disclosure), and two REGISTERED engine-plane
 *    channels can carry another scope's bytes without a row here: a root-owning scope's non-empty
 *    on-disk index is served verbatim to a later mount of that root ("live file wins"),
 *    and control-plane announcements carry no producing scope and drain plane-wide. Both are
 *    engine mount semantics under their own tickets, disclosed here so this face is never read as
 *    a complete cross-face visibility proof.
 *  - `"memoryless"` — memory was configured but did not mount. `reason: "mount-failed"` = the
 *    fail-open mount arm caught a fault anywhere in the mount phase (directory resolution through
 *    materialize — named for the whole captured span, not one step); `reason: "no-backend"` = the
 *    spec enables memory but no `RunnerDeps.memoryBackend` is configured. `materializedResidue`
 *    (mount-failed only) lists plane scopes whose PHYSICAL materialize had already completed when
 *    the fault hit — a LOWER bound (a plane that threw mid-materialize is not listed; on-disk
 *    residue is ≥ this list), and explicitly NOT a visibility face: an auditor must never read
 *    residue as mounted scopes, which is why it is a separate seat from the always-empty `scopes`.
 *  - `"none"` — memory was not in play at all: `"no-spec"` = the task carries no usable memory
 *    spec; `"disabled"` = a spec is present with `enabled: false`.
 *
 * Deliberate-refusal configurations (`config.memory_*` codes) fail the whole prepare and produce
 * NO observation — this union never dresses a refusal as a state. Honesty note (design r7): the
 * `?: never` members forbid non-`undefined` values at the type level; the repo does not compile
 * with `exactOptionalPropertyTypes`, so explicit-`undefined` presence is a wire-validator concern,
 * not a type-level one.
 */
export type EffectiveMemoryScopes = {
    state: "mounted";
    reason?: never;
    contract: "v2" | "legacy";
    /** `shadowed` = how many of this scope's entries the session's write home retires from the
     *  DEFAULT read face by name (they stay reachable by id and through `includeShadowed`).
     *  PRESENT-IFF non-zero, and never on the write home's own row — absence is "none", which is
     *  why zero is not spelled. A mount-time snapshot: the tool faces re-derive it per call, so a
     *  later challenge or tombstone can make the live answer smaller than this number. */
    scopes: Array<{
        scope: string;
        origin: "deployment" | "request";
        shadowed?: number;
    }>;
    writeScope: string | null;
    /** The project identity the assembly point DECLARED for this task, echoed verbatim
     *  ({@link import("./memory.js").MemoryProjectKey}). PRESENT-IFF the spec carried one:
     *  absence means the deployment said nothing, which is not the same as `{ absent }`, which
     *  means it looked and found none. */
    projectKey?: import("./memory.js").MemoryProjectKey;
    materializedResidue?: never;
} | {
    state: "memoryless";
    reason: "mount-failed";
    contract?: "v2" | "legacy";
    scopes: [];
    writeScope: null;
    materializedResidue?: string[];
} | {
    state: "memoryless";
    reason: "no-backend";
    contract?: "v2" | "legacy";
    scopes: [];
    writeScope: null;
} | {
    state: "none";
    reason: "no-spec" | "disabled";
    contract?: never;
    scopes: [];
    writeScope: null;
    materializedResidue?: never;
};
export interface TaskResult {
    taskId: string;
    /**
     * The ENGINE-minted identity of THIS run (uuidv7), minted once at the top of prepare and
     * never rewritten. The third id of the trio, and the only one the engine owns:
     *  - {@link taskId} — the HOST's task identity, or (when the host named none) the session id: it is
     *    `spec.taskId ?? sessionId` and therefore answers the SAME value for every run of one session;
     *  - {@link sessionId} — the conversation, shared by every run that continues it;
     *  - `runId` — this call. Two runs of one session ALWAYS differ here; a host-supplied `taskId` does
     *    not change it (the host's handle and the engine's run are different questions).
     *
     * Use it to join a run's per-run disclosures (`task.user_steer_undrained` /
     * `task.user_followup_undrained` / `route.fallback_to_primary` / `steering.parked_input_blocked` /
     * `task.turn_interrupted` all carry `detail.runId`) and its `task.start`/`task.end` trace pair to
     * the result they belong to. Do NOT use it as an aggregation key across runs — that is what
     * `taskId`/`taskSignature` are for.
     *
     * **In-presence condition**: present on every terminal of a run that ENTERED prepare — a prepare-time
     * refusal included. The id is minted and published at prepare's FIRST statement precisely so this
     * holds: prepare itself emits run-scoped disclosures (`route.fallback_to_primary`), and a disclosure
     * naming a run that no terminal ever names would be unjoinable on exactly the failure path it is
     * about. It is absent only where no run was entered — a throw above the prepare call — and on
     * stub/synthesized results a host assembles itself. Optional for that reason, never fabricated, and
     * never the `"unknown"` sentinel `taskId`/`sessionId` fall back to on that path.
     */
    runId?: string;
    /** Use this to continue the same conversation on the next call. */
    sessionId: string;
    /**
     * WHY the run ended — the ONE tagged cause ({@link TerminalCause}), and the ONLY terminal record on this
     * result: there is no `status` word and no `errorCode`/`errorMessage`/`blockedReason`/`checkpointToken`/
     * `checkpointId`/`checkpointGate`/`workspaceRestoreMode` beside it (a second spelling of the same fact
     * would be the one a reader reaches for, and the two would have to be kept equal forever). Branch on
     * `terminal.kind`: `"paused"` carries the token and the gate together (a suspended run with no token
     * cannot be constructed), `"failed"` carries the code and the message together, `"blocked"` its reason.
     * A face that still speaks the five-word {@link TaskStatus} derives it with
     * {@link import("./runner/terminal-projection.js").terminalProjection} — the one cause→word derivation.
     */
    terminal: TerminalCause;
    /** design/99 MF-25: the EFFECTIVE model id this task ran on — the ENGINE-SELECTED, RESOLVED `Model.id`, not
     *  the requested `TaskSpec.model` ref (a role / name / `@mention`). A mid-run degradation is observed
     *  separately (the degraded-model fields); what the PROVIDER called it rides {@link responseModel}. */
    model?: string;
    /** The PROVIDER'S OWN name for the model that answered this run's FINAL turn, when it DIFFERS from the
     *  id the request was sent with — the result-face twin of `turn_end.responseModel`. {@link model} above
     *  stays the ENGINE-SELECTED id a resume re-seeds and a catalog resolves; a UI echoing "served by X"
     *  prints this one when it is here and {@link model} when it is not. **In-presence condition**: the final
     *  assistant message carried one. ABSENT ⇒ the provider named the requested id or named nothing (one
     *  reading, not two), there was no final assistant message, or the final turn AGREED while an earlier one
     *  did not — the per-turn frames are the complete record, this is the terminal one. Never present on the
     *  engine's synthetic messages, which carry no provider report; a mismatch is announced either way as
     *  `route.response_model_mismatch`. */
    responseModel?: string;
    /**
     * The leg's effective REASONING resolution: the result-face twin of {@link model} for the thinking
     * knob. How the requested tier resolved against the serving model's real capability
     * (`requested`/`effective`/`graded`/`clamped`/`format`/`endpoint`, plus `dropped:true` when a
     * non-reasoning model dropped the request entirely — field semantics on
     * {@link import("../brain/reasoning.js").ResolvedReasoning}). It is the SAME resolver output the
     * `reasoning.resolved` trace frame carries — the two faces cannot tell different stories; this seat
     * serves consumers without a tracer (the trace frame is the deployment-observability face, this is the
     * caller face).
     *
     * **In-presence condition** — mirrors the trace frame exactly: present on every terminal of a leg that ran
     * with a REQUESTED thinking tier other than off/unset (the `spec > role > model.defaultThinking` chain);
     * absent when thinking was off/unset for the leg, and on prepare failures (the resolution is minted after
     * prepare). On a resumed task each leg re-resolves against the leg's own serving model.
     *
     * **Per-request correction** — the leg-entry mint runs before any request exists, so it can only
     * describe the model's CAPABILITY. When the leg's FIRST committed request reports wire facts that change
     * that answer, the seat is re-resolved against them by the same resolver and the trace twin is re-emitted
     * with it. Today that is one arm: the anthropic budget path's cap-wins skip — a HARD per-request output
     * cap too small to host a legal thinking budget deletes the thinking block, so a leg capped that way now
     * reports `effective:"off"`/`dropped:true` instead of claiming the gradient the wire never carried. Only
     * the FIRST committed request is consumed: the seat stays a leg-ENTRY snapshot (see the degradation law
     * below), and the leg's nested internal brain calls (compaction summary, tool-invoked side query) carry
     * their own caps and must never restate the leg's posture.
     *
     * **Degradation law** — same as {@link model}, whose resolution this is: the seat describes the leg's
     * PRIMARY serving model at leg entry. A mid-run degradation (reactive fallback / near-budget switch)
     * changes the serving model WITHOUT re-minting this seat or its trace twin — read {@link degraded} to see
     * the switch; the fallback's own reasoning capability is deliberately NOT re-reported here (re-resolving
     * one face would desync it from the task-start `reasoning.resolved` frame). A consumer needing the
     * fallback's reasoning posture resolves `degraded.to` itself (`resolveReasoning` is exported).
     */
    effectiveReasoning?: import("../brain/reasoning.js").ResolvedReasoning;
    /** Final assistant text. */
    result: string;
    /**
     * Best-effort salvaged assistant text from a terminal FAILURE. Consumption recipe: on a non-`completed`
     * status read `result` FIRST — most failure terminals (max-turns, budget, tool errors, …) still carry
     * the final assistant text there — and fall back to `salvagedOutput` only when `result` is empty:
     * `const out = r.result || r.salvagedOutput || ""`. Do NOT treat `salvagedOutput` as "the output on
     * failure": it is populated on one CLOSED list of terminals, and on all of them the assembler fills it
     * from the SAME final-message text that `result` carries, so a consumer that reads only `salvagedOutput`
     * on failure discards real work on every other failure path.
     *
     * The populating terminals, and what the text means on each:
     * - `errorCode === "output.degenerate"` (degenerate-repetition cutoff): the model's text up to the
     *   loop, with the degenerate tail TRIMMED at the brain stream layer (byte-identical repeats dropped,
     *   one instance of the repeating unit kept — see `trimDegenerateTail`). Declared LOSSY normalization:
     *   the trim never drops unique bytes, but the repetition COUNT is not preserved — a payload whose
     *   meaning lives in how many times a unit repeats (fixed-length padding, "output N copies") comes
     *   back as one instance; this is not the raw failure snapshot.
     * - the `limits.max_*_exceeded` family (a per-slice window was exhausted on a task that did not opt
     *   into resource-suspend): whatever text the run had produced before the loud terminal.
     * - `"env.lifetime_expired"` / `"usage.window_exhausted"` (ruled 2026-08-04): the same, for the two
     *   EXTERNAL stop causes — an expiring environment or a closed deployment window that could not be
     *   suspended into a checkpoint. Nothing about the run itself failed, so its work is handed back on the
     *   same seat the ceilings use. (An entry-time `usage.window_exhausted` refusal ran nothing, so there
     *   is nothing to salvage and the field is absent.)
     *
     * Undefined on every other terminal and when there was nothing to salvage.
     */
    salvagedOutput?: string;
    /**
     * Present iff this run compacted its context at least once at a turn boundary or a prompt-too-long
     * recovery, or opened its compaction breaker: `passes` = landed compactions, `kind` = the last landed
     * pass's trigger (`reactive` is the wire's `forced`), `breakerOpen` = compaction was disabled for the
     * rest of the run (later context was trimmed away, not summarized). The end-of-task settle pass is
     * NOT counted (ruled): it summarizes AFTER the work, for the next run, and says nothing about what this
     * run's model saw. Projected onto a delegated child's terminal frame so
     * the parent can tell that a child forgot.
     */
    compaction?: {
        passes: number;
        breakerOpen: boolean;
        kind?: import("./task-notification.js").CompactionKind;
    };
    /**
     * The remote-workspace lifecycle failures this run hit, as DATA. The seam declares eleven
     * distinct `RemoteExecutionErrorCode`s, but every one of them used to reach the caller as the same
     * thing: a `suspendVM` refusal was swallowed into the `onError` side channel and the result read
     * `limits.max_turns_exceeded` (identical for a transient auth blip and a permanently-unsupported adapter), while
     * a failed restore reported the single code `resume.env_failed` with the real code buried in prose. A
     * deployment that wants to distinguish "refresh the credential and re-resume" from "this adapter can
     * never do it" had nothing but a string to parse.
     *
     * - `op` — which lifecycle call failed.
     * - `code` — the adapter's verbatim `RemoteExecutionErrorCode`.
     * - `retryable` — whether `code` is in the retryable family (`RETRYABLE_REMOTE_ERROR_CODES`). For the
     *   IDEMPOTENT restore ops the engine has already spent its own bounded retries (see `attempts`); this
     *   flag is what a caller acts on for the ops the engine deliberately does not retry — a `suspendVM`
     *   takes a snapshot, so re-driving it is the caller's decision, not the engine's.
     * - `attempts` — how many times the engine called it (1 = no retry was made or none was warranted).
     * - `message` — the adapter's message, for humans.
     *
     * Present only when at least one lifecycle call failed (absent = nothing to report), so a consumer that
     * ignores the field is unchanged. It is DIAGNOSTIC: {@link terminal} keeps its existing meaning —
     * a failed suspend still ends the task the way it always did, it is just no longer anonymous.
     */
    remoteEnvFailures?: RemoteEnvFailureNote[];
    /**
     * Present when this run's terminal failure came from the PROVIDER BOUNDARY — the transport or the
     * provider failed the model call, as opposed to this deployment refusing to send one, a limit being
     * reached, or the model's own output being unusable. Its PRESENCE is the assertion; the members are
     * whatever the failing attempt actually stated about itself:
     *   - `status`    — the failing HTTP status, when the provider answered with one. Absent for a
     *     connect failure, a stall, a mid-stream tear, and an in-band error frame delivered inside a
     *     200 — never zeroed, since 0 reads as a number a consumer may format.
     *   - `requestId` — the provider's own request identifier for that attempt, when it stated one in a
     *     shape worth carrying. This is the handle a provider's support channel asks for.
     * Both may be absent together: that is a provider failure nobody could label further, which is a
     * different statement from "not a provider failure" (the field itself absent).
     *
     * Nested rather than three loose keys so the assertion and its evidence cannot drift apart — there
     * is no state in which a consumer sees the claim beside a status belonging to some other attempt.
     * The same family rides the assistant message itself as `isApiErrorMessage` / `apiErrorStatus` /
     * `requestId`; this seat is its RUN-LEVEL projection, lifted from that message at the one assembly
     * point rather than re-derived, and the two spellings are named here so the mapping is written down
     * instead of inferred.
     *
     * PRESENCE, deliberately narrow: filled only when the provider failure is the terminal that NAMED
     * this result. A run that hit a provider failure and then a higher-ranked terminal (a budget
     * ceiling, a walltime cut) reports that other cause and leaves this absent — the same rule
     * {@link usageWindowRetryAfterMs} states for itself, and for the same reason: a diagnostic seat
     * beside a cause it does not belong to is worse than a missing one.
     */
    apiFailure?: {
        status?: number;
        requestId?: string;
    };
    /**
     * How long (milliseconds) until the deployment usage window that stopped this task frees up — the wait
     * hint a scheduler needs to decide WHEN to re-submit, rather than polling.
     *
     * **In-presence condition:** set on exactly two terminals — `errorCode === "usage.window_exhausted"`
     * (either moment: the entry refusal that ran nothing, or the running terminal that could not suspend)
     * and `errorCode === "memory.admission_required"` (the prepare-throw path sets it there too — see
     * assemble-result's accepted-code pair). Absent everywhere else, INCLUDING when a usage window did
     * stop the run but a higher-ranked terminal
     * named the result (a budget ceiling crossed on the same turn): the reported cause is then that other
     * code, and a wait filed under it would describe something the code does not name.
     *
     * **Do not branch on its presence** to work out WHY a task stopped — read `errorCode` for that (the
     * standing rule for every optional field on this type). A consumer treating "has `retryAfterMs`" as
     * "was rate-limited" misreads both directions: the same code can arrive without one, and the field
     * says nothing about which of the two governance moments produced it (`stats.turns`/`result` do).
     *
     * A DURABLE pause carries its own wait elsewhere: a `suspended` result's hint rides the checkpoint gate
     * (`resumeAfterMs`), which the resume path consumes — the two are deliberately separate seats for the
     * two different resumption stories (re-submit the task vs resume the checkpoint), never aliases.
     *
     * The same number is also delivered to `RunnerDeps.onError` on the typed error (unchanged) — this seat
     * is for the caller that holds the result and never wired that sink.
     */
    retryAfterMs?: number;
    /**
     * Present when the task was served — at least once — by a **degraded** (cheaper/different) model
     * because the primary was rate-limited, its circuit breaker was open, or it failed with a
     * `server_error`/`last_resort` class error (design/126 fallback chain, opt-in via
     * `createDegradingBrain`). Lets a caller know the result was produced at reduced quality and alert/
     * track it. Records the **first** degradation only (degradation is one-way within a task). Set even
     * when the fallback also failed — so `status:"failed"` + `degraded` means "primary down, fallback
     * down too". `reason="budget"` is reserved for the v2 near-budget trigger.
     */
    degraded?: {
        from: string;
        to: string;
        /** design/126 widens the union: `server_error` (5xx/529 escaped the retry budget) and `last_resort`
         *  (404/other non-retryable statuses) — the CC 批 β fallback-chain triggers. Additive on this
         *  OUTPUT field; consumers switching exhaustively should add the two arms. */
        reason: "breaker_open" | "rate_limit" | "budget" | "server_error" | "last_resort";
        /** design/126 — the fallback-chain model ids attempted after the primary, in order (absent on the
         *  single-hop legacy path's pre-126 markers and on the budget path). */
        chain?: string[];
        /** 1-based turn during which degradation was triggered. Reactive (rate_limit/breaker): the turn the
         *  cheaper model first served. Budget: the turn whose cumulative cost crossed the threshold (the
         *  switch takes effect from the next turn). */
        atTurn: number;
    };
    /** The validated object the model submitted via `submit_output` when `TaskSpec.outputSchema` was set
     *  (1.41). Undefined if the model answered in prose instead, or the task didn't finish via the output
     *  tool. Cast it with `Static<typeof yourSchema>`. (Exhausting the output-retry cap → `failed` +
     *  `errorCode="output.invalid"`, and this stays undefined.) */
    structuredOutput?: unknown;
    /**
     * design/101 §E19 + design/381 — NON-FATAL rewind disclosures for this run: the legs where the rewind
     * machinery did something other than what the request's plain reading implies, reported as data instead
     * of silence. Present only when at least one applies (absent = nothing to disclose), so a consumer that
     * ignores the field is unchanged. A rewind that could NOT be delivered as asked is NOT a note — it is a
     * terminal failure (`rewind_snapshot.unresolvable` / `rewind.store_unconfigured` /
     * `rewind.restore_failed` / `rewind.invalid_spec` / `rewind.rewind_files_retired` /
     * `rewind.conflicting_targets` / `rewind.child_scope_unsupported` / `rewind.capture_disabled`).
     *
     * - `conversation_only` — {@link TaskSpec.resumeAt} branched the transcript WITHOUT
     *   {@link TaskSpec.restoreFiles}, so the working tree was deliberately left where it was (CC's
     *   "Restore conversation" mode). Legal and useful; the note is what makes the two axes' divergence
     *   visible.
     * - `files_env_unsupported` — a file restore was requested but this deployment mounts no
     *   filesystem-capable `ExecutionEnv` (stub env), so the file axis was inert. Not an error: such a
     *   deployment has no working tree to rewind.
     * - `restore_partial` — {@link TaskSpec.acceptPartialRestore} tolerated a partial restore: ≥1 tracked
     *   file was refused or failed while the others converged; the message carries the per-file ledger
     *   summary (applied / identical / refused / failed). Present ONLY under that explicit opt-in — the
     *   default posture is the loud terminal `rewind.restore_failed` (DV-15).
     *
     * (design/381 closed-set surgery: `snapshot_store_unconfigured` is RETIRED — its only producer was
     * the per-task capture request, and capture is now always-on when a store is wired; a restore request
     * with no store keeps the fail-loud terminal instead.)
     */
    rewindNotes?: Array<{
        code: "conversation_only" | "files_env_unsupported" | "restore_partial";
        /** Human-readable statement of what did NOT happen and why — safe to show a user verbatim. */
        message: string;
    }>;
    /**
     * The files THIS RUN's hands actually mutated, with how many times each — the same
     * `{path, edits}` shape the delegated-child projection publishes as
     * {@link import("../agents/subagent-steps.js").SubagentEditedFile}, so a host can read the two
     * seats side by side. A run is one host-side user message, which is the granularity a
     * "restore the code this message changed" affordance needs; the per-model-turn beat
     * (`turn_end`) deliberately has no such seat.
     *
     * **Source**: the hands band's own mutation lane. One entry is bumped when a `Write`/`Edit`/
     * `NotebookEdit` call's FINAL env write returns success — not when a tool call starts, not from a
     * tool-name table over the event stream. The consequences are the contract:
     *  - a refused, gated or FAILED write is not counted (a write whose bytes never landed is not an
     *    edit — the same lane point that retracts its first-touch history record);
     *  - a `Bash` command that changed a file is not counted: it does not go through those lanes.
     *    This matches the reference implementation's own trigger set;
     *  - one tool CALL counts once, so a batch `Edit` (multiple `edits[]` against one file in one
     *    call) is one edit here, exactly as the child projection counts it.
     *
     * **`path` form**: the model-supplied path argument, verbatim — the same coordinate
     * `SubagentEditedFile.path` uses, deliberately not the canonical containment key, so both seats
     * spell a file the way the transcript does. IDENTITY, however, is the canonical FILE, not the
     * spelling: a relative argument resolves against the run's LIVE cwd, so one spelling can name two
     * different files across a `cd` (two entries, each showing that spelling) and two spellings can
     * name one file (ONE entry, showing the first spelling that reached it). Insertion order = order
     * of first edit.
     *
     * **Three-valued on the failure edge, upper bound never lower**: a write failure is three-valued.
     * A failure whose error code's contract states nothing was written is a proven no-op and is NOT
     * counted. An AMBIGUOUS failure or a thrown write IS counted: an `ExecutionEnv` whose write is not
     * atomic can truncate a file and then report an error, that file may really have changed, and the
     * first-touch history record for it is kept for exactly that reason — so this list names it too,
     * and a `rewindFilesTo` will restore it. (The reference env replaces whole files by stage+rename —
     * fully old or fully new — so this edge belongs to its in-place fallback arms and to third-party
     * envs.) A host therefore reads this list as "files that may differ from before this run", never
     * as "writes the model saw succeed"; the tool answers carry that.
     *
     * **In-presence condition**: present iff this run's hands landed ≥1 such write — zero edits means
     * the key is ABSENT, never an empty array. It does NOT depend on whether a
     * `RunnerDeps.fileHistoryStore` is wired: this is an observation of what this run did, not a
     * durable history, and a deployment with no history store still gets it (what a store adds is the
     * ability to REWIND, not the ability to say what changed). It covers this run's OWN band only — a
     * delegated child's writes ride the child's own result and the delegation projection, not this
     * key.
     *
     * **Bound**: at most 1000 distinct files. Past that, already-listed files keep counting and new
     * ones are not added — a result seat cannot be unbounded, and a single user message reaching a
     * thousand distinct files is already outside what this observation is for. A consumer that must
     * know whether it is reading a saturated list can compare the length against that ceiling.
     */
    editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
    /**
     * Present (`true`) exactly when the run's FINAL turn was halted by a person's BARE rejection of a
     * tool call — the parent-thread control-flow boundary: the rejected call's same-message siblings
     * that had not started were settled un-executed (each an error result coded `gate.batch_halted`),
     * and the engine deliberately did NOT re-invoke the model, so the run ends awaiting the user's
     * direction. On this path `status` is still `"completed"` (nothing failed, nothing is suspended,
     * the session is continuable as ever) — this field is what tells such an end apart from a natural
     * finish: `result` text is whatever the model had produced BEFORE the rejection (often empty), and
     * a consumer surface should read the state as "stopped by the user, awaiting their direction",
     * never as "the task finished its work". Absent everywhere else — including when queued user input
     * (a steer/follow-up) continued the run past the rejection and it later ended naturally.
     */
    haltedOnUserRejection?: true;
    /**
     * Present (`true`) exactly when a {@link TaskStream.halt}
     * (the bare user interrupt, the CC Esc form — cut the in-flight turn, stop at that boundary, wait
     * for the person's next input) was CONSUMED by this run: the engine's own record says the run
     * ended FOR it. The attribution law, one sentence: signed ⇔ the halt's cut settled a turn (the loop
     * reported the interrupt settlement it took for the seat the halt aborted — stream shape, batch
     * shape, or the recovery lane's arms) OR the halt's stop request was among the sources a boundary
     * consult stopped the loop for (the pre-turn guard that refuses the next turn — the birth-window
     * halt included — or the post-turn consult). The MINTER states the fact; the result assembly reads
     * it and never infers causation from the final's shape (a brain minting its own `aborted` final
     * beside a late halt reads as the failure it is) nor from the verb's acceptance.
     *
     * The ruled terminal form is completed-with-marker, this seat being the marker: `status` is
     * `"completed"` on the ordinary path (a clean, resumable ending — the session continues via the
     * ordinary next-run front door), and this field is what tells a person-stopped ending apart from a
     * natural finish — same reading discipline as {@link haltedOnUserRejection}, its sibling seat
     * ("stopped by the user, awaiting their direction", never "the task finished its work"). `result` is
     * whatever the model had produced before the halt (possibly empty). Rides EVERY terminal a
     * consumed halt reaches — a halt whose cut settled a turn and then raced a real failure/limit, or
     * whose boundary stop coincided with a review/resource park (`needs_review` / `suspended`),
     * truthfully says a person also stopped it (the slice-2 status-keyed suppression is retired).
     *
     * ABSENT (never `false`) whenever the halt was not consumed, however it was answered: a halt
     * landing after the run's abort already fired (first-writer-wins, the `interrupt()` attribution
     * law — that ending belongs to the abort); a halt accepted while the run was already ending for its
     * own reason (a natural completion past the loop's final commit point, a brain's own terminal, a
     * durable approval park whose commit won the race and whose abort then owned the ending); a
     * steer-now cut (its stamp is `steer`). In every such case the halt's trace is its verb receipt plus
     * one `task.halt_unconsumed` notice — the seat is never signed on a guess.
     */
    haltedByUser?: true;
    /**
     * The deliveries of AskUserQuestion calls a person ANSWERED but whose call never executed to collect
     * the answer (the leg ended first — abort, batch teardown, or a loop failure). Rides EVERY terminal
     * when non-empty, because the alternative is an answer a human produced vanishing with nothing to
     * show for it: the answers were NOT delivered to the model and are gone with this leg — re-ask if
     * the decision is still needed. `RunnerDeps.onError` (classification `"unconsumed-human-answer"`)
     * carries the same fact as an additional alert; this field is the mandatory face.
     */
    strandedHumanAnswers?: ReadonlyArray<{
        /** The engine-minted per-delivery identity (AskQuestionRequest.deliveryId) — the KEY: call ids can
         *  repeat across deliveries, so two lost answers on one call id are still two records here. */
        deliveryId: string;
        toolCallId: string;
    }>;
    /**
     * HOST-SUPPLIED: the approval plane's report that this leg ended with human approvals still
     * OUTSTANDING (the ask identities it is holding, and when the OLDEST of them was created).
     *
     * **Who writes it, and why not the engine.** Core mints no ask identity — an ask id belongs to the
     * deployment's approval store, and `resolveAsk` only awaits the host's `onAsk` — and core starts no
     * approval clock. So this seat is filled by the `Runner` a deployment hands to
     * `runWorkflow`/`startWorkflow`: that call both RAN the leg and owns the pending-approval map, which
     * is the only place the two facts meet without inventing an attribution. Core's own Runner never
     * writes it. ABSENT is therefore the ordinary world — an older deployment, or one whose approvals are
     * always answered inline — and every such leg behaves exactly as it did before this seat existed.
     *
     * **Three-state, deliberately.** Absent = "this deployment is not reporting outstanding approvals, or
     * there are none"; present = "these asks are outstanding". An EMPTY `askIds` is neither and is
     * refused: it would let a half-written record read as "waiting on nothing", which is precisely the
     * misdiagnosis the seat exists to prevent. A present-but-malformed value is announced and then
     * treated as no report — never silently folded into absence, and never granted the exemption below.
     *
     * **What core does with it** (`src/orchestration/workflow.ts`): a workflow agent leg whose progress
     * watchdog fired while an approval was outstanding was NOT stalled — it was waiting on a person, by
     * design — so that attempt is not charged against the stall-retry budget. The run's own
     * `totalTimeoutMs` remains the backstop for a wait nobody ever answers.
     */
    pendingApproval?: {
        askIds: readonly string[];
        oldestCreatedAtMs: number;
    };
    /**
     * HOST-SUPPLIED: how long this leg spent BLOCKED on human approvals, in ms, as the deployment's
     * approval plane measured it. Same writer and same absence contract as {@link pendingApproval}: the
     * approval clock belongs to whoever holds the ask, so the figure is SINGLE-SOURCED there and core neither
     * estimates it nor accumulates it (two sides publishing two numbers for one wait is the drift this seat
     * exists to avoid). ABSENT is the ordinary world.
     *
     * Already CUMULATIVE for the leg — a receipt states the leg's total wait so far, not that attempt's
     * increment — so a later receipt SUPERSEDES an earlier one. Adding successive receipts double-counts.
     *
     * **Absence is not zero.** `0` is "nobody was waited on"; absent is "this deployment does not supply the
     * fact". A consumer that defaults absence to `0` reports a deployment's silence as a measurement.
     *
     * What it is FOR: a workflow that hit its total timeout cannot otherwise say whether it was SLOW or
     * WAITING. `WorkflowRun.timeoutInterruption.approvalWaitedMs` carries the largest leg's figure onto the
     * terminal disclosure for exactly that reading — near `timeoutMs` ⇒ the window went on a person, not on
     * work.
     */
    approvalWaitedMs?: number;
    /**
     * The READ-face this leg's read surfaces actually judged under, as an
     * engine-filled OBSERVATION (never a knob: writing it on a spec does nothing). It is the run's ONE
     * resolved face — the same value the hands toolkit enforced and the delegation carriers rode — with
     * the hands-less resume pin folded in (a row resumed on a hands-less worker still reports the
     * checkpoint-frozen `"roots"`).
     *
     * **In-presence condition (#242 widened):** present on every leg that completes prepare — hands
     * mounted (any run with the built-in fs band) AND every hands-less leg (resume legs fold the
     * checkpoint seed stricter-wins; non-resume legs report the live resolution). Absent only on
     * prepare-failure terminals (nothing ran). Consumers that keyed on absence = "no posture" must
     * re-key: absence now means only "prepare never completed". Rides every OTHER terminal, not just
     * `completed` — the posture is a fact about the leg that ran, whatever ended it.
     *
     * Purpose: a POST-COMPLETION follow-on leg spawned OUTSIDE the delegation tree (the verify gate's
     * verifier and fix legs are the canonical case) cannot see the checkpoint fold the completed leg ran
     * under — its spawner holds only the resume-side config, which never contained the frozen posture.
     * Folding this seat into the follow-on spec (stricter-wins: only `"roots"` ever tightens; an
     * `"open"` observation is never forwarded) closes that escape for ANY follow-on, not just the
     * verifier. See {@link effectiveReadDenyPatterns} for the deny-set half.
     */
    effectiveReadFace?: import("../tools/fs/containment-face.js").ContainmentFace;
    /**
     * The WRITE-face containment state in force on this leg, the read seat's twin as an engine-filled
     * OBSERVATION. Same in-presence condition as {@link effectiveReadFace} (every leg that completes
     * prepare; absent only on prepare-failure terminals) and the same purpose: a POST-COMPLETION follow-on
     * leg folds it stricter-wins into its own spec. Only `"roots"` is ever worth forwarding; an `"open"`
     * observation never widens anything by itself.
     */
    effectiveWriteFace?: import("../tools/fs/containment-face.js").ContainmentFace;
    /**
     * The sensitive-path deny ADDITIONS in force on this leg beyond the
     * built-in table, normalized (deployment ∪ task ∪ checkpoint-frozen seed), as an engine-filled
     * OBSERVATION. "In force" = judged by this leg's own read surfaces where they mounted, and carried
     * to its delegation subtree either way (a hands-less resume leg reports the entries its children
     * judge under).
     * This seat is the only place a checkpoint-FROZEN deny entry becomes visible after the resumed leg
     * completes: the resume-side caller's own config never contained it. A follow-on leg's spawner
     * unions these into the child's `readDenyPatterns` (add-only; the child's compile folds exact
     * duplicates, so re-supplying deployment-shared entries is idempotent) — that is how the frozen
     * posture rides every leg that continues this work, not just the delegation subtree.
     *
     * **In-presence condition:** present iff non-empty. Built-in entries are never listed — they are the
     * DEPLOYMENT's own selection (the default tiers unless it says otherwise), re-compiled per leg and reported on its wiring
     * manifest ({@link import("./wiring-manifest.js").WiringManifest.readDeny}). Same terminal coverage as
     * {@link effectiveReadFace}: rides any terminal of a leg that ran; absent on prepare failures.
     *
     * On a DURABLE-PAUSE hand-back these seats are additionally the RE-SUPPLY hint: a checkpoint
     * minted by a leg with no face resolution (a hands-less non-resume continuation) carries no
     * frozen face section of its own, so a caller resuming that token must fold these entries back
     * into the resume-side config (`readDenyPatterns` / `readFace: "roots"`). The resumed leg unions
     * them into its own judgment and delegation carriers; the union is NOT serialized into any
     * further checkpoint that leg may mint (a hands-less RESUME mint re-freezes only its original seed; a non-resume hands-less pause mints its live-resolved posture, #242), so
     * fold them on EVERY resume — the standing resume contract. The seats describe the posture
     * governing the WORK, never the contents of any checkpoint row.
     */
    effectiveReadDenyPatterns?: readonly import("../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
    /**
     * design/178 v2 §2.3 (件①) — the memory VISIBILITY face this leg actually ran under, as an
     * engine-filled OBSERVATION (never a knob: writing it on a spec does nothing). This is the
     * EFFECTIVE face, not the requested one: a refused request never reaches a terminal at all (the
     * whole prepare fails with its governance code), so what this seat answers is "what was actually
     * given" — the delta against the request is directly readable (an org writeScope not explicitly
     * granted collapses to `null` here as it did in the run; a fail-open mount failure reads
     * `memoryless`, never a dressed-up mount).
     *
     * **In-presence condition** (same law as {@link effectiveReadFace}): present on every terminal of
     * a leg that COMPLETED prepare — the memory-less states are answered as their own values
     * (`none` / `memoryless`), so consumers must never read ABSENCE as "no memory"; absence means
     * only "prepare never completed". Delegated children mint their own on their own legs (their
     * request plane can only narrow the parent's frozen org verdict); a resume leg's value is the
     * re-adjudication at resume time (admission runs in every prepare — the current-policy reading,
     * same axis as the read-face seats).
     *
     * Minted AFTER the materialize outcome, not after the admission verdict — the fail-open mount
     * arm sits between the two, and stamping earlier would report a mount that never happened.
     * See {@link EffectiveMemoryScopes} for the per-state field law.
     */
    effectiveMemoryScopes?: EffectiveMemoryScopes;
    /**
     * `turns`/`tokens`/`costMicroUsd` are this task's OWN model usage. `nested` is the summed usage of any
     * delegated sub-runs (sub-agents) it spawned — present only when it delegated. The true total
     * cost is `tokens + (nested?.tokens ?? 0)`; a large `nested` is the multi-agent "~15×" made visible.
     */
    stats: {
        turns: number;
        tokens: number;
        /** design/97 CORE-8 (②): total TOOL CALLS executed (one per `tool_start`) — distinct from `turns` (model
         * rounds; a turn may issue several parallel tool calls). For a CC-style "N tool calls" per-agent row.
         * **v1 fidelity (audit MINOR)**: counted in THIS run leg via the harness dispatch path, so on a DURABLE
         * RESUME it under-reports — the resumed gated call (executed by the resume engine, not the harness) and any
         * cross-slice resource-suspend legs are not folded (turns/tokens/cost ARE folded; toolCalls is per-leg, like
         * the compaction subtotal). OBSERVE-ONLY (never gates budget). The workflow display is unaffected (its agents
         * fail-on-suspend, never durably resume). A `spentToolCalls` ledger fold is a follow-on. */
        toolCalls?: number;
        /** #692 C-b (additive): present (`true`) when the usage figures on this face are a LOWER BOUND rather than a
         *  measurement — at least one model round of the leg reported no usage frame (the brain's `usageMissing`
         *  mark; the same word the per-turn `turn_end` frame carries), or the result was minted by a host / an
         *  orchestration lane that never ran a model round it could measure (a refused spawn's journal entry, a
         *  prepare-time refusal, a synthetic abort). The numeric members stay required and stay what was counted:
         *  `tokens: 0` beside this key reads "unknown", never "free". Absent ⇔ every round reported usage. */
        usageMissing?: true;
        /** Prompt (input) tokens that MISSED the prefix cache, summed across the task's turns and normalized
         * across providers (Anthropic already reports `input_tokens` this way; OpenAI/vLLM include the cache
         * in theirs, so the subsets are removed). Mutually exclusive with `cachedTokens`/`cacheWriteTokens*`;
         * the three of them sum to `totalInputTokens`.
         *
         * **RB-457-a (BREAKING at 3.0.0)**: this used to carry the cache-INCLUSIVE total, which collides with
         * the Anthropic protocol's identically-named quantity — a consumer computing
         * `cachedTokens / (promptTokens + cachedTokens)` double-counted the cache subset and reported
         * `h/(1+h)` (a 98% hit rate surfaced as 49.5%). Use `totalInputTokens` for the old value; use
         * `cacheHitRate` rather than recomputing a ratio. */
        promptTokens?: number;
        /** Prompt tokens served from the model's prefix cache (a cache HIT), summed over the task's
         * turns — when the gateway reports it (vLLM/OpenAI `prompt_tokens_details.cached_tokens`,
         * DeepSeek `prompt_cache_hit_tokens`, Anthropic `cache_read_input_tokens`). */
        cachedTokens?: number;
        /** Prompt tokens written to the cache (Anthropic `cache_creation_input_tokens`); 0 elsewhere. */
        cacheWriteTokens?: number;
        /** `cachedTokens / totalInputTokens` ∈ [0,1] — the prefix-cache hit rate for this task. Cost-critical;
         * a low value on a multi-turn task means the cacheable prefix is unstable (see `design/09`).
         * Track it per task and trend it per session. Undefined when the gateway reports no usage.
         * **Prefer this over recomputing a ratio** — it is the one place the denominator is defined. */
        cacheHitRate?: number;
        /** The cache-INCLUSIVE prompt total across the task's turns — `promptTokens + cachedTokens +
         *  cacheWriteTokens + cacheWriteTokensLong`, matching OTel `gen_ai.usage.input_tokens`. This is the
         *  quantity cost is computed from, the denominator of `cacheHitRate`, and the figure to use for
         *  "how much context did this task present" (window/budget views). `promptTokens` carried
         *  this value up to 2.13.x; it now carries the cache MISS count and this field is its own
         *  accumulator rather than an alias. Optional BY DESIGN, like every usage field on this stats
         *  face: undefined means the gateway reported no usage (and pre-3.0.0 persisted rows never had
         *  it) — the event-face `turn_end.usage.totalInputTokens` is required because there the whole
         *  `usage` object is already conditional (intentional asymmetry, not a gap). */
        totalInputTokens?: number;
        /** Completion (output) tokens summed over the task's turns. */
        outputTokens?: number;
        /** Cache-write tokens at the 1-hour TTL (Anthropic); 0 unless 1h caching is in use. */
        cacheWriteTokensLong?: number;
        /** Authoritative (and only) cost figure: **integer micro-USD** (1e-6 USD), computed in core from
         *  injected pricing — integer to avoid float-accumulation error in billing. (design/157 B19: the
         *  float-USD twin was removed; divide by 1e6 at the display edge if you need USD.)
         *  **ABSENT when any spend was unpriced** (RB-368): the serving model had neither a
         *  `RunnerDeps.pricing` entry nor a `Model.cost` declaration — an unpriced run reports NO cost
         *  rather than a fabricated 0, so "no price table" stays distinguishable from "declared free"
         *  (an explicit all-zero `Model.cost` still reports 0). `costBreakdown` is omitted with it. */
        costMicroUsd?: number;
        nested?: NestedUsage;
        /**
         * Post-task memory-consolidation usage (design/41), when consolidation ran. Its own line item —
         * **kept out of `tokens`/`costMicroUsd` and excluded from the budget gate** — so a completed task is
         * never retroactively flipped to a budget terminal by reconcile cost. `tokens` is 0 when every note
         * took a cheap (no-LLM) path; `applied` counts store mutations the pass made (cheap-path
         * UPDATE/DELETE + applied LLM decisions) so callers can observe reconcile efficacy without re-reading
         * the store. Absent when consolidation was disabled / no-op / produced no notes. Populated
         * asynchronously by whatever engine-plane pass produced it (consolidation is fire-and-forget).
         */
        memory?: {
            tokens: number;
            costMicroUsd: number;
            applied: number;
        };
        /**
         * design/100 §E12 — the prompt-suggestion pass's usage, when it ran. Same budget-excluded side-observable
         * treatment as {@link memory}: **kept out of `tokens`/`costMicroUsd` and excluded from the budget gate**, so
         * the (fire-and-forget, post-completion) suggestion pass can never retroactively flip a done task to
         * a budget terminal. Absent when `suggestNextPrompts` was off / produced nothing. Populated asynchronously —
         * see {@link TaskStream.suggestions}.
         */
        suggestions?: {
            tokens: number;
            costMicroUsd: number;
        };
        /**
         * TB telemetry B2: counters for the engine's wall-clock/deadline mechanisms, so a
         * bench/monitoring consumer can judge from the RESULT whether they actually engaged (refute-130
         * lesson: "did P1/P2 fire" was unanswerable from artifacts). Present only when at least one
         * mechanism engaged this leg; absent = none did (byte-compatible).
         */
        mechanisms?: {
            /** design/132: the natural-end final-verification reminder was injected this run. */
            finalVerifyInjected?: true;
            /** oracle-grounding gate (opus LOW 复审, additive): HOW MANY final-verification injections
             *  fired this run — 1 = the R9 one-shot only, 2 = R9 + the grounding re-entry (hard cap).
             *  Present iff `finalVerifyInjected` is (the boolean stays for compatibility). */
            finalVerifyInjections?: number;
            /** design/133: number of attachment blocks injected this run — turn-boundary bundles
             *  (todo/task reminders, changed-files notes, plan-mode re-reminders, listing drift frames;
             *  each bundled block counts once) PLUS the [c209-C] first-frame listing deliveries riding the
             *  first user message (agent_listing / skills_listing — counted only when the first prompt was
             *  actually sent, R2 C5: a hook-blocked / budget-rejected run counts zero). */
            attachmentsInjected?: number;
            /** design/164: how many LIMIT-APPROACH notices were injected this run (0..2 — the
             *  "start converging" frame and the "deliver now" frame, each one-shot). Absent when none fired,
             *  which is also the answer for a task that armed no limit axis at all. */
            approachNoticesSent?: number;
            /** Repetition telemetry (2026-07-10 observability): brain-side degenerate-repetition
             *  stream CUTS this run (each pairs with an `output.degenerate`-classified turn). */
            repetitionCuts?: number;
            /** Repetitions that landed in a detection window but were SPARED by a 2e1c161 structural
             *  allowance (code-line shape / divider run) — the raw material for judging the allowance. */
            repetitionSpared?: number;
            /** Per-event details (turn + rule + period/reps + a ≤120-char segment sample), capped at 20
             *  entries — `repetitionCuts`/`repetitionSpared` keep the true totals past the cap. */
            repetitionEvents?: Array<{
                turn: number;
                action: "cut" | "spared";
                rule: "char-run" | "unit-loop";
                period: number;
                reps: number;
                segment: string;
            }>;
            /** design/319 (B ticket, G9② observation seat) — reminder-disclosure trigger counts for the
             *  leg, keyed `<outlet>.<form>`: outlets `read` / `notebook` / `pdf` / `mcp` / `webFetch` /
             *  `webSearch`; forms
             *  · `bare` (bare-form trailer appended — reminder-shaped text without this session's mark),
             *  · `marked` (marked-form trailer — never throttled),
             *  · `bare_throttled` (a bare trailer suppressed by the 60s per-key window),
             *  · `defused` (an MCP/web segment's exact-mark bytes were rewritten — the lane's one sanctioned
             *    byte change, always paired with a `marked` disclosure),
             *  · `envelope` (text shaped like one of the engine's OTHER authority envelopes — the DISCLOSED
             *    subset is `task-notification` / `new-diagnostics` / `user_memory` / `skills` /
             *    `total_tokens` / `instruction-files`; `scope` is
             *    fenced but not disclosed, since `<scope>…</scope>` is also an ordinary build-file element.
             *    That family carries no mark, so its sentence is positional rather than byte-testable. It
             *    rides ON the reminder copy when both families hit, so `envelope` can be bumped alongside
             *    `marked`/`bare` on one result),
             *  · `envelope_throttled` (an envelope sentence suppressed by its OWN 60s window — the two
             *    families throttle independently, so one may emit while the other reports suppression),
             *  · `mark_echo` (observation ONLY, never a disclosure: the session's mark VALUE appeared in
             *    external bytes with no reminder-shaped tag around it, so the tag-grammar detector cannot
             *    reach it. Nothing is rewritten and no trailer is appended; it is an upper bound on model
             *    exposure for the lanes that do not defuse).
             *  Present only when ≥1 key is non-zero. The map is OPEN by type — fold by key rather than
             *  switching on a closed set. This is the D-4/D-6 re-ruling data (defuse/trailer widening to
             *  Read/Bash/Grep): a reading, never a gate. */
            reminderDisclosures?: Record<string, number>;
        };
        /**
         * design/91 — **human-review burden** (design/89 §2.4 C2 axis). The wall-clock time a task spent waiting
         * on a human at an approval gate, plus the count, bucketed by `gate.kind`. Both human-review paths are
         * captured: the **synchronous** `onAsk`/`resolveAsk` await (`t_return − t_call`) and the **durable**
         * suspend→resume wait (`resumedAt − checkpoint.suspendedAt`, including the human's offline time); a
         * multi-leg suspend/resume chain **accumulates across legs**. All timestamps come from {@link RunnerDeps.now}.
         *
         * **NOT an LLM cost (load-bearing, design/91 §1):** human-review time is wall-clock latency, not token
         * spend — it is **NEVER folded into `costMicroUsd`, `costBreakdown`, or the budget gate** (the same
         * budget-excluded side-observable treatment as {@link memory}). Adding human seconds to token µUSD is the
         * design/89 §2.1 "apples + oranges" trap; the supervisor Pareto frontier keeps `(token cost, human seconds,
         * correctness)` as three SEPARATE axes. Absent (`undefined`) when the task hit no approval gate OR no clock
         * is injectable to measure — byte-compatible with a task that never paused for a human.
         */
        humanReview?: {
            /** Number of human decisions awaited (each synchronous `resolveAsk` return + each durable resume of an
             *  approval suspend). Equals `gates.length`. */
            count: number;
            /** Cumulative wall-clock the approval gates were alive (ms), summed across all legs. Equals `Σ gates[].waitMs`. */
            totalWaitMs: number;
            /** One entry per human decision, in order. `kind` = the gate's discriminant (`human` /
             *  `irreversible_ask` for durable; `human` for a synchronous ask). `decision` = the adjudication when
             *  known (`allow`/`deny`). design/99 MF-24: `toolName` = the gated tool; `toolArg` = a
             *  SHORT, SECRET-SCRUBBED one-line summary of its input (the same `primaryActivityArg` boundary as the MF-W
             *  activity arg) — the permission-denial ledger's "denied: Bash(rm …)" display. `toolArg` rides every mint
             *  that has the input in scope: the synchronous ask from the request, the durable-resume gate from the
             *  checkpoint's `pendingAction`; `toolCallId` names the call the row gated (absent = not joinable). */
            gates: HumanReviewGateRow[];
        };
        /**
         * design/80 D-E-core: a thin FINANCE TAXONOMY of the LLM-derived costs the engine actually prices,
         * decomposing the task's spend into report categories (derived from existing cost sources — no new
         * persisted structure). **CORE = LLM-token-derived ONLY** — the deployment SERVICE adds the infra axes it
         * owns (tool-call / sandbox-walltime / egress; it has the k8s cost data) and composes them with this for
         * the supervisor surface. Each category is ≥ 0 and the LLM parts reconcile to the independently-summed
         * LLM total: `llmRootMicroUsd + compactionMicroUsd === costMicroUsd` and the fully-reconciled
         * spend is `costMicroUsd + nested.costMicroUsd` — `costMicroUsd` deliberately EXCLUDES nested
         * subagent cost (it lands in `nested`, see assemble-result.ts), so `nestedSubagentMicroUsd` sits
         * OUTSIDE the costMicroUsd identity, not inside it (measured correction: the previous wording
         * `llmRoot + nested + compaction === costMicroUsd` only held when nested was 0).
         * (`memoryConsolidationMicroUsd` is a separate, budget-excluded line, permanently 0 since the
         * runner-integrated consolidation pass was retired — design/157 B19 — and NOT part of any identity).
         * Present whenever stats are assembled, EXCEPT when `costMicroUsd` is absent for unpriced spend
         * (RB-368) — the taxonomy would partition a fabricated total, so it is omitted together with it.
         */
        costBreakdown?: {
            /** Root-agent LLM cost (micro-USD) = `costMicroUsd − compactionMicroUsd` (the sub-category folded into
             *  it). NOT minus nested — nested cost is never folded into `costMicroUsd` (it lands in `nested`).
             *  ⚠️ APPROXIMATE on a multi-slice `resource_limit` RESUME (design/80 D-E-core): the
             *  durable resource ledger persists only the AGGREGATE prior spend, not its compaction subtotal, so
             *  prior slices' compaction folds in HERE rather than into `compactionMicroUsd`. The TOTAL (`costMicroUsd`)
             *  stays exact; only this llmRoot-vs-compaction split is per-leg-approximate across resource slices. */
            llmRootMicroUsd: number;
            /** Delegated sub-agent (nested) LLM cost (micro-USD) = `nested?.costMicroUsd ?? 0`. */
            nestedSubagentMicroUsd: number;
            /** Post-task memory-consolidation LLM cost (micro-USD). Permanently 0 since design/157 B19 retired
             *  the runner-integrated consolidation pass (kept: public stats contract; an engine-plane re-mount —
             *  design/138 §7 — would fill it). A separate, budget-excluded line. */
            memoryConsolidationMicroUsd: number;
            /** design/100 §E12 — prompt-suggestion pass LLM cost (micro-USD) = `suggestions?.costMicroUsd ?? 0`; filled
             *  asynchronously after the (post-completion) pass (0 until then). A separate, budget-excluded line. */
            suggestionsMicroUsd?: number;
            /** Within-task compaction LLM cost (micro-USD) — part of `costMicroUsd`, so subtracted from root.
             *  ⚠️ See {@link llmRootMicroUsd}: under-reports prior slices' compaction on a multi-slice resource resume. */
            compactionMicroUsd: number;
        };
    };
}