@sema-agent/core
Version:
Stateless, task-oriented AI agent core
895 lines • 67.8 kB
TypeScript
/**
* 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/verify/repair-loop) 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;
/** The model calls made beside the main loop — the permission classifier's and mods' `$.model.complete` — in
* micro-USD. An **"of which" line**, not a partition member: this cost is part of `costMicroUsd` and stays INSIDE
* {@link llmRootMicroUsd}, so `llmRootMicroUsd + compactionMicroUsd === costMicroUsd` keeps holding. Present once
* at least one such call was booked this leg (absent ⇒ none; a result with no such call is unchanged). A call
* whose reply carried no usage marks `stats.usageMissing` instead of adding here.
* ⚠️ Like {@link compactionMicroUsd}, per-leg on a multi-slice `resource_limit` RESUME: the prior slices' side-call
* spend is inside the exact `costMicroUsd` and `llmRootMicroUsd` but not in this line. */
sideCallMicroUsd?: number;
};
};
}