@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,037 lines • 76.2 kB
TypeScript
/**
* The WIRE frames: the `TaskEvent` union every consumer switches on, and every satellite it
* discriminates with — the per-message identity envelope, the brain-status phases and retry classes,
* the tool activity beat, the human-input source/delivery words, the delegation task type, the
* human-input projection and the compaction outcome. This is the file a change to a frame touches, and
* nothing else. Layer 0 vocabulary; `types.ts` re-exports every name below, so no consumer's import
* changes.
*/
import type { ActorAssertion } from "../internal/llm.js";
import type { TaskResult } from "./task-result.js";
import type { TaskNotificationPayload } from "./task-notification.js";
/**
* Identity fields carried by every CONTENT event of a task stream (design/99 §E2 — lightweight message
* identity). Both are OPTIONAL and ADDITIVE; a consumer that ignores them is unchanged.
*
* - `eventId` — a core-minted, time-sortable (uuidv7) unique id stamped on each emitted content event. Gives
* a live UI a stable handle to correlate/dedup streamed events and to map its own client-side message ids
* back to a core anchor. (The durable-anchor mapping for resume-at — which eventIds are restartable
* transcript positions — is finalized by the E18 `resumeAt` seam; this field is the stable handle it builds on.)
* - `parentToolCallId` — present ONLY when this task runs as a sub-agent spawned under a parent task's tool
* call (threaded via the trusted `RunInternals.parentToolCallId`, sourced from the spawning tool's
* `ToolExecuteContext.toolCallId`). Lets a consumer attribute streamed content to the delegation subtree it
* belongs to WITHOUT core physically merging the child stream into the parent (the child's RESULT still
* re-enters the parent MODEL context only through the `delimitUntrusted` fence — observability threading and
* model-context isolation are kept separate, so prompt-cache and the injection boundary are untouched).
*/
export interface TaskEventIdentity {
eventId?: string;
parentToolCallId?: string;
/**
* design/122 S2 (the `WorkflowRun.sourceTaskId` family, NOT a second `taskId` — that
* field already exists on `task_progress` and a duplicate would bite consumers) — the ORIGINATING task's
* canonical id (`spec.taskId ?? sessionId`), stamped ONLY on the content events of a task running AS A
* SUB-AGENT (same condition as {@link parentToolCallId}). A consumer of the C1 forward channel
* (`forwardSubagentEvents`) can key a child's content events by the child task directly, without an
* alias table. A TOP-LEVEL task's own events carry NO `sourceTaskId` (absent = the consumer's current
* task). Additive; a consumer that ignores it is unchanged.
*/
sourceTaskId?: string;
/**
* design/151 S2 (MED-10 live-tail) — the BACKGROUND agent's `a*` registry
* handle, stamped on every event a background child's lane forwards up the host `onForwardEvent`
* channel (content deltas, tool events, progress). THE live-tail routing key: a serving layer
* pipes "what is agent aXX doing right now" straight off the host forward stream — no uuid→a*
* alias table (the BCE spawn frame's sessionId mapping) needed. Sync/steer children carry NO tag
* (no a* row exists); `wa*` workflow rows stay on their own observer lane. Additive; a consumer
* that ignores it is unchanged.
* NESTED bg-in-bg (server[1523] 对等复审 4a): each background boundary OVERWRITES the tag as the
* frame crosses it — on the HOST wire, the whole subtree's frames carry the OUTERMOST a* handle
* (the host-addressable row). Per-frame nesting attribution rides `parentToolCallId`, never this
* field; a consumer splitting an outer stream into per-descendant views keys on that.
*/
bgAgentId?: string;
}
/**
* design/99 §E3/§E10 — a BRAIN-LAYER liveness phase, surfaced so a UI can show why a
* turn is stalled (the brain otherwise absorbs these silently in its connect/retry loop). The PHASE
* itself is provider-NEUTRAL — a graduated bucket this engine mints, never a provider taxonomy or a
* `stop_reason`. The one provider-stated number that does cross this channel is the failing attempt's
* HTTP status, and it travels on its own named seat ({@link BrainStatus.errorStatus}) in the retry
* context only; the phase and the {@link BrainStatus.detail} hint stay free of it.
* A CLOSED union (type-safe) covering the core brain layer; a deployment that surfaces its OWN states
* (e.g. E25 token-refresh `authenticating`) does so on its own channel, not by widening this.
*/
export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "circuit_open"
/**
* TERMINAL frames closing a retry sequence. Without them a consumer that rendered a
* `rate_limited`/`retrying`/`reconnecting` frame has nothing telling it the wait is over, so the
* warning row stays pinned until the next unrelated event. Exactly one terminal frame is emitted,
* and only for a call that actually waited (a retry wait, or a `waiting_first_token` trail —
* #530): `recovered` = the call was served after the wait (a later attempt, or the first token
* finally arriving), `gave_up` = the retry budget ran out (or the call was cancelled) and a
* terminal error follows.
* Both mean the same thing to a renderer — stop showing the retry state.
*/
| "recovered" | "gave_up"
/**
* The first-token WAIT, before any failure: a first-token watchdog is armed
* (`firstTokenTimeoutMs > 0`) and nothing has arrived yet. Emitted once the wait has lasted
* `WAITING_FIRST_TOKEN_AFTER_MS` and re-emitted every `WAITING_FIRST_TOKEN_EVERY_MS` while it goes
* on; a wait whose watchdog fires at the threshold itself is emitted once at that instant, before the
* frame announcing the cut. Carries {@link BrainStatus.elapsedMs} and {@link BrainStatus.timeoutMs}; never carries a
* retry countdown (`retryInMs`/`retryAtMs` — no wait is being announced, a wait is being
* OBSERVED). CLOSED like a retry trail: once a wait frame went out, the call's terminal frame
* follows — `recovered` when the call went on to be served (the first token arrived, on this
* attempt or a later one), `gave_up` when it failed for good — so a renderer that pinned the row
* has its "stop" signal. No frame at all when no watchdog is armed.
*/
| "waiting_first_token";
/**
* WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
* {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
* unattended progress line ("no answer for three minutes") needs the reason. (The status has its own seat,
* {@link BrainStatus.errorStatus}; this bucket is the reason a consumer RENDERS and the only carrier for the
* transport classes, which have no status at all.) Values are about the SHAPE of the failure, never its
* provider taxonomy:
* - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
* at that address, or the name has no address). This is the class the SHORT retry lane serves.
* - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
* tear. No verdict about the target; the full ladder applies.
* - `stall` — no failure was observed on the link at all: THIS process cancelled its own
* request because one of its stall watchdogs reached its deadline while the stream produced
* nothing. The distinction from `transport` is WHO GAVE UP: a `transport` frame reports something
* that happened to the call, a `stall` frame reports a local deadline. It is not a verdict on the
* remedy — a blackholed connection produces silence too, and silence is what a watchdog measures.
* - `rate_limit` — the provider asked the caller to slow down.
* - `server` — the provider reported a failure on its own side.
* - `http` — a response the status predicate calls terminal, retried anyway because the
* provider's own explicit retry verdict said to.
* - `output_cap` — not a failure of the connection at all: the request is being re-sent with a
* lowered output cap after the provider reported the context limit exceeded (no backoff).
*/
export type BrainRetryErrClass = "connect_refused" | "transport" | "stall" | "rate_limit" | "server" | "http" | "output_cap";
/** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
export interface BrainStatus {
phase: BrainStatusPhase;
/** Optional neutral, human-readable hint. Stays free of provider/HTTP detail even now that
* {@link errorStatus} exists: the machine-readable status has its own seat, and interpolating it
* into the prose too would make one fact travel in two spellings a consumer has to reconcile.
* (CC parity: its own retry banner renders a TRANSLATED sentence — "No response from API",
* "Connection dropped" — while the raw status rides the structured key beside it.) */
detail?: string;
/** Seconds until the brain's next retry attempt (from the honored backoff / `Retry-After`), when known. */
retryInSec?: number;
/** Millisecond precision of {@link retryInSec}, for a client that renders a countdown.
* A wait longer than the re-announce slice is re-emitted with the REMAINING time, so a consumer
* that simply overwrites its row always shows a live number instead of a stale one. */
retryInMs?: number;
/**
* The WALL-CLOCK instant the announced wait is expected to end — `Date.now() + retryInMs`, read at
* the moment the frame is emitted, in the EMITTING core process's clock. Stamped centrally at the
* one emit point, so the invariant is total: present exactly when {@link retryInMs} is present, and
* absent on every frame that announces no wait (`recovered` / `gave_up`, and the immediate
* output-cap re-send, which takes no backoff).
*
* WHY IT IS PRODUCED HERE rather than derived by the consumer: `Date.now() + retryInMs` is only
* correct for a reader that received the frame instantly, so a countdown built several process hops
* away restarts on every re-announced slice instead of converging.
*
* CLOCK DOMAIN, stated to remove the ambiguity: WALL CLOCK (`Date.now()`, epoch ms), NOT a monotonic
* reading. A consumer must not compare it against its own monotonic timers, and must treat it as
* approximate across a clock adjustment — {@link retryInMs} stays the authoritative RELATIVE quantity.
*/
retryAtMs?: number;
/** On a `waiting_first_token` frame: how long the current wait has lasted, ms, at the moment the
* frame is emitted (the emitting process's clock; approximate across a hop). The wait is one
* segment's: the response-header wait before headers arrive, the first-token wait after — each
* starts its own count. Absent on every other phase. */
elapsedMs?: number;
/** On a `waiting_first_token` frame: the ceiling of the segment being waited on, ms — the armed
* first-byte window before headers, the armed first-token watchdog after — so a consumer can render
* "45s of 300s". Absent on every other phase. */
timeoutMs?: number;
/** 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
* same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
attempt?: number;
/** The retry budget of THIS lane, so a consumer can render "attempt 3 of 10". Lane, not
* engine: a failure class served by a shorter ladder reports that ladder's budget, so the fraction a
* consumer renders is the one actually in force rather than the engine-wide ceiling. */
maxRetries?: number;
/** Why this wait is happening ({@link BrainRetryErrClass}). Present on retry-wait frames whose cause
* the engine classified; absent on frames that are not a retry wait (`recovered`/`gave_up`) and on a
* `circuit_open` fast-fail, which is a local verdict rather than an observed failure. */
errClass?: BrainRetryErrClass;
/**
* #506 ㋑ — the HTTP status of the attempt that just failed, on the frame announcing the RETRY of it.
* The one provider-stated number this channel carries; everything else about the failure still
* crosses as the engine's own graduated buckets ({@link phase} / {@link errClass}).
*
* WHY IT IS HERE AT ALL, given the channel spent its life refusing it: a UI that must tell an
* operator whether to wait or to go fix something needs the status, and the two neutral buckets
* cannot supply it — `rate_limit` covers a 429 the provider will clear on its own AND a 429 that
* means the account is out of quota, and `server` covers a 500 alongside a 503 behind a load
* balancer. CC states the same number on the same occasion (`system/api_retry.error_status`, read
* off `APIError.status`), so a serving layer forwarding this frame no longer has to choose between
* matching CC's payload and honoring this channel's contract.
*
* PRESENCE — an api_retry context whose attempt got a response that NAMES the failure, and nothing
* else. Present on the connect-ladder retry wait (`rate_limited` / `retrying` / `reconnecting` when
* a response came back) and on the output-cap re-send (the provider's own 400). ABSENT — never
* zeroed, never null — for:
* · a transport failure (`connect_refused` / `transport` at the connect leg): no response existed,
* so no status was ever stated. This is the arm CC spells as an explicit `null`; here it is
* absence, for the same reason every other seat on this interface uses absence, and the fact is
* still TOTAL rather than guessable — {@link errClass} names those two classes by construction.
* · a mid-stream tear (tiers B/C): its response's own status was a SUCCESS, and a status that
* failed nothing must not be published as the cause of a retry.
* · a `circuit_open` fast-fail: a local verdict, nothing was sent, nobody answered.
* · the terminal `recovered` / `gave_up` frames: they announce no attempt.
* Same "only a status that names the failure travels" predicate as the assistant frame's
* {@link AssistantMessage.apiErrorStatus} (one helper, both sites) — a 2xx never travels here either,
* which is reachable on this path via a provider's own `x-should-retry` verdict on a success status.
*
* OPTIONAL, not CC's nullable-REQUIRED, and the difference is CC's own: CC dedicates a whole message
* (`system/api_retry`) to the retry occasion and can therefore require the key on it, while this
* interface is ONE shape shared by every phase — the same geometry as CC's shared
* `control_request_progress` frame, where CC itself makes the key optional.
*/
errorStatus?: number;
}
/**
* design/97 CORE-8 (③): one lightweight TOOL-ACTIVITY beat surfaced from a running task, for a per-agent live
* "last N tool calls" drill-down. A trusted run-scoped sink ({@link RunInternals.onActivity}) receives one at
* each tool start + end. Structural (name/phase/ids) + a SHORT redacted primary `arg` for display — NOT the full
* ARGS or OUTPUT (those carry untrusted/host data; the workflow layer surfaces redacted prompt/output separately).
*/
export interface ToolActivity {
phase: "start" | "end";
toolCallId: string;
toolName: string;
/** wall-clock stamp (epoch ms, `Date.now` read at the emitting Runner where the frame is
* minted). Carried on BOTH beats: the `start` beat stamps the moment the tool call began, the `end`
* beat the moment it settled — so a consumer derives per-call duration (`end.at − start.at`) and
* inter-call idle (an `end.at` → the NEXT `start.at`). Additive/optional: frames recorded by older
* producers simply lack it (a consumer must treat absence as "unknown", never as 0). */
at?: number;
/** design/99 MF-W: a SHORT primary-arg summary for the monitor's `Read(path)` / `Bash(grep …)` display — the
* single most salient input ABBREVIATED (command→name, path→basename, url→origin+path) + SECRET-SCRUBBED +
* truncated (~80 code points). Set on `phase: "start"`. NOT the full args object, and NOT `redactHostLeaks`
* (that over-redacts the path the monitor needs); the abbreviation + scrub is the leak boundary since the value
* is persisted + SSE-forwarded. See `primaryActivityArg`. */
arg?: string;
/** Set on `phase: "end"` — whether the tool call errored. */
isError?: boolean;
}
/**
* design/171 §5.2/§6.2 — which carrier a piece of human input arrived on. `"external"` = the
* notify() event lane; `"system"` = NOT human input (machine-authored text riding a human-input
* code path) — the speaker projection is a guaranteed no-op for it.
*/
export type HumanInputSource = "objective" | "steer" | "next_turn" | "wake" | "external" | "system";
/** design/171 §6.2 — how a human input was disposed of when its event was emitted:
* `"applied"` = delivered into a model turn; `"queued"` = accepted, awaiting the next turn
* boundary; `"parked_for_wake"` = parked for the session's next run (torn-down lane);
* `"blocked"` (design/373 §4.3, additive — closed-set add, consumers named in the ship post) =
* a PARKED entry was withheld at resume redelivery — the screen runs on EVERY resume kind that
* drains parked steers (policy_ask / dry_run_review / plan_review / resource_limit, wake
* included) — by the deployment's `userPromptSubmit` screen (block verdict, or a fail-closed
* non-answer/crash): the row was consumed, the frame never reached the model, and the sibling
* `steering.parked_input_blocked` notice names the same inputId — "park 时收下、redeliver 时被筛"
* is auditable, never a silent disappearance. */
export type HumanInputDelivery = "applied" | "queued" | "parked_for_wake" | "blocked";
/**
* The fleet-task kinds a DELEGATED run can honestly claim, derived from (never a second spelling of)
* {@link TaskNotificationPayload}'s `task_type` vocabulary — the same axis a consumer already keys
* notifications on. The other members of that union name non-agent lanes (a background shell command,
* a monitor, an externally-produced notification), which no delegated run is; deriving rather than
* re-listing is what keeps the two faces from drifting into two vocabularies.
*/
export type DelegationTaskType = Extract<TaskNotificationPayload["task_type"], "background_agent" | "workflow">;
/** Streaming events surfaced from a running task (for live UIs). */
export type TaskEvent = ({
type: "text_delta";
delta: string;
} & TaskEventIdentity) | ({
/**
* The assistant's streaming PROSE SEGMENT is COMPLETE: the model closed the text content
* block whose bytes just streamed as `text_delta`s. This is the explicit segment boundary a
* REMOTE consumer needs so it never has to guess segment ends from wire silence. CC-aligned: block
* completion IS the segmentation signal there (CC's agent stream yields each finished content block at
* the provider's `content_block_stop`), and this event is that same boundary surfaced on the TaskEvent
* wire.
*
* `content` = the authoritative FULL text of the completed segment (byte-equal to that
* segment's accumulated deltas, from the brain's own accumulation) — a consumer commits the
* segment from it instead of trusting its own delta stitching. UNTRUSTED model output for
* display only, same contract as `text_delta`. Emitted only for a segment that holds bytes: an
* empty text block closes silently (a boundary with no segment would render phantom rows).
*
* Additive + ignorable. HONEST ABSENCE: the frame exists only when the serving Brain reports
* block ends (`text_end` on its event stream — all three first-party brains do; a custom brain
* that never emits them yields a wire without this frame). A consumer treats per-segment
* presence as the signal and falls back to its own heuristic only on streams that carry none —
* absence is "unreported", never "the segment did not end".
*/
type: "text_end";
content: string;
} & TaskEventIdentity) | ({
type: "reasoning_delta";
delta: string;
} & TaskEventIdentity) | ({
type: "tool_start";
toolCallId: string;
toolName: string; /** {@link ToolSpec.label} when distinct from `toolName` (design/71 liveness); omitted otherwise. */
label?: string;
args: unknown;
} & TaskEventIdentity) | ({
/**
* A LIVENESS TICK for a tool call that is still running. Emitted on the run's own queue between this
* call's `tool_start` and `tool_end`, under the same ordering law as every other content frame, and
* forwarded to a parent's display sink under the SAME opt-in as those two
* (`TaskSpec.forwardSubagentEvents`).
*
* ONE FRAME, not two: CC 2.1.259 splits this into a per-tool `tool_heartbeat` plus Bash's own
* `bash_progress`; this engine mints the single shape and lets the OUTPUT KEYS be what is conditional.
* Registered divergence.
*
* @contract tool_progress.cadence — AT MOST one frame per second per tool call, enforced at the mint over
* ALL producers rather than inside each one (a producer's own timer is a sampling clock, not the contract),
* and a call's first frame is never earlier than one interval after it entered execution. A call that
* finishes inside one interval emits NO frame at all — this frame's absence is never evidence that a call
* did not run, and a consumer must not expect one per `tool_start`.
*
* @contract tool_progress.ephemeral — LIVE ONLY, like `task_progress`/`status`: core never persists this
* frame (no session-tree entry, no checkpoint state, no resume replay), and a resumed leg re-ticks from
* its own clock. A consumer wanting a DURABLE record of a long call keeps {@link elapsedTimeSeconds} —
* never {@link output}, untrusted process bytes bounded for a pane (the tool's result is the kept face).
*
* Additive and ignorable: a consumer switching on event type and dropping unknown members is unchanged,
* and no gate, policy, memory or model-context path reads this frame.
*/
type: "tool_progress";
/** The still-running call's id — the join key to its own `tool_start`/`tool_end`. */
toolCallId: string;
/** The running tool's name (the `tool_start` value verbatim). */
toolName: string;
/** Whole seconds this tool call has been running, floored and MONOTONIC across its frames. ONE ORIGIN per
* call, stamped by the mint rather than by whichever producer emitted — the call's entry into execution,
* earlier than a shell command's spawn. It does NOT equal the `tool_end` duration. */
elapsedTimeSeconds: number;
/**
* The TAIL of what the command has written so far (stdout and stderr interleaved in arrival order),
* bounded by the mint to a small number of trailing lines — a live pane, never a log.
*
* UNTRUSTED RAW, exactly as `tool_end.output`: process output that may carry secrets, paths or PII;
* core bounds only its SIZE, and a consumer MUST redact before persisting or displaying it. Never
* re-feed it to a model — it is a moving, truncated window.
*
* ABSENT, together with {@link totalLines} and {@link totalBytes}, exactly when the call has produced
* NO output through this environment — it printed nothing (`sleep 3`), or the environment does not
* stream (a buffered remote `exec` never invokes the output callbacks). The three ride or stay away
* together: present = "output has been observed", absent = "none has been observed", never
* "unchanged since the last frame".
*/
output?: string;
/** Newlines observed in the command's output so far (≥ the line count of {@link output}, which is
* only its tail). Rides iff {@link output} does — see its absence law. */
totalLines?: number;
/** Bytes observed in the command's output so far, counted BEFORE the tail bound, so it keeps
* growing after {@link output} stops being able to. Rides iff {@link output} does. */
totalBytes?: number;
} & TaskEventIdentity) | ({
type: "tool_end";
toolCallId: string;
toolName: string;
/** {@link ToolSpec.label} when distinct from `toolName` (design/71 liveness); omitted otherwise. */
label?: string;
isError: boolean;
/**
* design/99 §E1 — the tool's MODEL-FACING result content, surfaced for a live UI
* that renders tool output. It is the harness `AgentToolResult.content` the mapper already produced
* (previously dropped here); the internal `details` payload is NOT exposed (e.g. a sub-agent tool's
* `details` is the entire child `TaskResult`).
*
* **Type is `unknown` and intentionally non-uniform** — within size it passes through AS-IS to keep
* structure a UI wants (a `string`, or `(TextContent|ImageContent)[]` blocks); when the serialized body
* exceeds the core size cap it degrades to a single TRUNCATED STRING (see {@link truncated}). A consumer
* must handle both shapes.
*
* **UNTRUSTED RAW + for OBSERVABILITY ONLY**: it is external tool output (file reads, shell stderr, API
* bodies) and may carry secrets/paths/PII — core bounds only its SIZE; a consumer MUST redact and MAY
* further bound before it persists or displays it (same contract as `tool_start.args`). It is for a live
* UI / trace, NOT for re-feeding to a model: a truncated body would mislead an LLM, and the model already
* has the full, untruncated result in its own context — build model prompts from the session, never by
* replaying this field. Absent when the tool produced no content body.
*/
output?: unknown;
/**
* design/116 (b) — the tool's CC-shaped `toolUseResult` facts (`details.type` ∈ the known card
* set: edit/create/update/bash/text/grep/glob/notebook-edit/notebook/file_unchanged/mcp/image/
* document/worktree/todo/task/task-list/task-output/task-stop/agent/fork/workflow-run/web-fetch/
* web-search/cron-create/cron-delete/cron-list/tool-search/repo-map/monitor-start/
* enter-plan-mode/exit-plan-mode/report-findings/schedule-wakeup/send-message/agent-transcript/
* a2a/path_not_in_root/readonly_out_of_root/read_unestablished), surfaced on the live wire so a CC-faithful shell
* renders rich cards (red/green diff, stdout panes, file cards) without re-parsing the
* model-facing string. Absent when the tool carries no card shape or when the serialized card
* exceeds the size bound (omitted whole; a consumer falls back to `output`).
* UNTRUSTED RAW like {@link output}: a consumer MUST redact before persisting or forwarding.
*
* **The card set is a two-way contract** (structured-card audit, 2026-08-05): every entry has a tool
* that mints it and every minted card is registered, so a consumer switching on `details.type`
* gets neither a promise nothing keeps nor a card it was never told about. That pass ADDED
* report-findings / schedule-wakeup / send-message / agent-transcript / a2a / document /
* readonly_out_of_root and the completed **agent** shape (see below); it REMOVED multiedit /
* memory-saved / memory-recall, three words with no producer anywhere in core (a batch MultiEdit
* replay mints `type:"edit"` with an `edits[]` array; core mounts no memory tool at all).
*
* **Leak boundary**: the allowlist gates by `type`, so what rides is whatever the minting tool put
* in the object — which makes each mint site responsible for its own field set. The Agent family's
* completed card is therefore built from an EXPLICIT whitelist rather than a child-`TaskResult`
* spread: a `TaskResult` can carry `checkpointToken` (the resume CAPABILITY) on the
* `unexpected.suspended` terminal, and that field, with `checkpointGate`, never leaves the process.
*
* **双面契约(CC 2.1.207 @765495 成文,`tool_use_result` describe;CC207-CORE-SWEEP P2-3)** — a tool
* result has TWO faces that never substitute for each other:
* - **模型面** = {@link output} / the tool_result text: the string the MODEL reads. It may carry
* model-directed framing — e.g. a delegation tool's trailer (agentId / usage lines the model uses to
* address SendMessage or budget itself). That framing is FOR THE MODEL; it is not a host data format.
* - **宿主面** = this field: the tool's structured Output object, per-tool shaped, keyed by the matching
* tool call's name. For a delegation/Agent-family tool the completed shape is the subagent's final
* report WITHOUT the model-directed trailer, plus run totals (CC names it `AgentToolCompletedOutput`).
* Both Agent shapes ride `type:"agent"` and are told apart by `status`: `"async_launched"` is the
* background launch RECEIPT (task_id / description / prompt), any other value is the completed
* landing report (`result` / `stats` / `resolvedModel` / `toolStats` / kept `worktreePath` /
* failure attribution). Branch on `status`, never on which keys happen to be present. (Until
* 2026-08-05 only the receipt actually arrived — the completed delivery carried no `type` and was
* dropped by the allowlist, so this paragraph described something a host could not observe.)
* A host/shell MUST render from `structured` and MUST NOT parse the model-facing text back into data
* (trailer wording is model-audience prose and may change without notice); symmetrically, core never
* strips the trailer from the model face just because the structured face exists — the model still needs
* it in-context. When `structured` is absent, the honest fallback is to display `output` as opaque text,
* not to scrape it.
*/
structured?: unknown;
/**
* Present iff {@link isError} is true AND the harness result's `details` carried a string
* discriminator — read as `details.code` first, falling back to `details.errorKind` (each must be
* a string; `code` wins when both are present, as the deliberate tool-chosen spelling). The
* `errorKind` leg is what makes a LOOP-THROWN error's frame classifiable: the loop's thrown-error
* fold and the resume legs write the discriminator under that name. Lifted so a consumer never
* has to parse the (contract-stable) result text. Engine-minted vocabulary today includes
* `"tool.not_found"` (unknown tool name), `"gate.parked"` (an abort short-circuit contaminated
* this call because a durable gate parked the batch — the "Operation aborted" family), and, for a
* failed MCP tool call, the MCP failure class — see {@link import("./mcp-failure.js").McpFailure.kind}
* for the words (the same closed set the per-server status and the wiring manifest's `mcp[]` carry;
* this frame only transports it). Additive: absent on error frames minted before this field
* existed, and on error results whose details carry no string discriminator under either name.
*/
errorCode?: string;
/**
* @contract tool_end.delivered — WHETHER THE REQUEST REACHED ITS SERVER, when this frame closes a
* failed call whose result carried the MCP failure record: the closed
* {@link import("./mcp-failure.js").McpDelivered} (`"yes"` / `"no"` / `"unknown"`), lifted from
* `details.delivered` beside {@link errorCode} and read TOGETHER with it — the same `errorCode`
* (`connection_closed`) is safe to retry at `"no"` (nothing was sent) and must be verified first at
* `"unknown"` (the request may have executed). The meaning of each word is owned by
* {@link import("./mcp-failure.js").McpFailure.delivered}; this frame only transports it. Screened
* against the closed set (an out-of-vocabulary value is not lifted) and never derived from the
* code or the text. ABSENT on every frame whose result carried no such verdict — every executed
* call and every non-MCP failure — and a consumer must not read a semantic out of the absence.
*/
delivered?: import("./mcp-failure.js").McpDelivered;
/**
* WHICH call the gate was holding, when this frame is a park-contamination frame — the tool call
* id of the gated call. Three conditions, ALL required: `errorCode === "gate.parked"`, the run is
* tearing down behind a committed park, and that park binds a tool call at all.
*
* **WHERE IT COMES FROM — never lifted from the result**, and deliberately unlike its neighbour
* {@link errorCode}: that one IS the result's own `details.code`, i.e. tool-authored, because a
* tool classifying its own failure is bounded self-description. This field is an assertion about a
* DIFFERENT call, so a tool able to author it could point an approval UI at a call nobody is
* holding. It is therefore engine-minted — read from the run's committed park holder and handed to
* the frame projection as a parameter, the same posture (and the same reason) as {@link settledBy}.
* `errorCode` only participates in selecting WHETHER the id is placed; it never supplies the
* value, and on its own it is not enough — the run must also be aborting behind a real park, which
* is why a tool declaring `gate.parked` on its own failure cannot conjure this field.
*
* Why it is on the frame: a durable gate parks ONE call and the loop then short-circuits every
* sibling in the batch with an identical "Operation aborted" body, so `gate.parked` alone says
* "something parked this batch" without saying WHAT. A consumer wanting to render "waiting on
* <the gated call>" beside the collateral frames otherwise has to re-derive the pair from batch
* adjacency and timing — an inference that is wrong exactly when it matters (a batch with more
* than one ask candidate, a reordered stream).
*
* ONLY EVER STRUCTURALLY PROVEN, never guessed: the id is read off the committed checkpoint's
* pending tool call, so it is the same value the checkpoint parks on and the same value the
* decide/resume lane answers about. It is therefore ABSENT — not approximated — for the park
* kinds that hold no call (a resource-limit slice, a plan review), and absent for every abort
* that is not a gate park at all (user interrupt, timeout, walltime): those frames are
* byte-unchanged. Absence means "core cannot prove a causal call", never "there wasn't one".
*/
gatedCallId?: string;
/**
* The gate's record of the pass that admitted or refused this call, when the call went through the
* tool gate at all ({@link import("./gate-outcome.js").GateOutcome}): `disposition` (allowed, or
* denied by which LAYER), `settlement` (how the ask this pass consumed ENDED — a person's yes or
* no with the channel's attribution and, on a bare "no", no note; a window that elapsed and whose;
* nobody reachable; the wait aborted…) and `origin` (who ASKED). It is the SAME object the
* `permissionDenied` observer received and the engine's per-call sideband recorded — one mint, three
* faces — so a renderer never branches on prose to tell "a person refused" from "the window closed".
*
* WHERE IT COMES FROM — never inferred here: the gate mints it at its exit, the decide lane mints it
* from the host's decision facts joined with the row's persisted origin.
*
* ABSENT on every frame of a call the gate never saw (a tool call on an unarmed gate, a deferred
* re-issue, a reconcile-recovered orphan). A gated call's frame ALWAYS carries it — an absence is
* not a fact about the call, and a consumer must not read one out of it.
*/
gate?: import("./gate-outcome.js").GateOutcome;
/**
* design/99 §E1 — `true` when {@link output} was SIZE-bounded by core (the full body exceeded the cap and
* was degraded to a truncated string). Lets a consumer detect truncation programmatically instead of
* parsing the `…[+N chars truncated]` marker out of the string. Absent/false = `output` is the complete
* content body. (The full untruncated result still lives in the model's context / `ToolResultStore`.)
*/
truncated?: boolean;
/**
* Present iff {@link truncated}: the TRUE original size (characters) of
* the content this event's {@link output} was degraded from — for a string result, the string's own
* length; for a non-string result (a blocks array), the SUM of each block's own true size (a text
* block's `.text.length`; an image/document block's `.data.length`, i.e. its base64 payload) — never
* `JSON.stringify(content).length`, which is polluted by two different kinds of noise: structural
* overhead (field-name/punctuation bytes that aren't content at all) AND, for text containing quotes/
* backslashes/control characters, escape inflation (the same class of bug this field was created to
* fix in the first place — a real result: 17298 reported vs 13696 true content characters — reappearing
* in a subtler form if the true count were ever taken from the serialized form instead of the blocks).
*
* **Not always arithmetically tied to the marker's own `N`**: the marker counts how much MORE of the
* shown raw-text rendering exists beyond the cut point; `totalChars` counts the true size of the
* ORIGINAL value, which for mixed content includes non-text blocks (e.g. an inlined image) that
* `output` never renders as text at all. The two can legitimately diverge — `totalChars` may run far
* ahead of the cap plus the marker's `N` — and that gap is itself the honest signal that non-text bulk
* was dropped, not an inconsistency to paper over.
*/
totalChars?: number;
} & TaskEventIdentity) | ({
/**
* Context occupancy at a compaction boundary, emitted on EVERY
* boundary (not only when a compaction fires). The three numbers are the engine's own trigger
* inputs, verbatim: `usedTokens > compactAtTokens` IS the predicate `shouldCompact` evaluates, so a
* consumer's "context is N% full" indicator and the moment compaction actually fires can never
* disagree. Re-deriving the numerator downstream is precisely what this exists to prevent.
*
* Deliberately NOT a field on `turn_end`: the boundary check runs AFTER that event, over the
* post-flush message set, so a value attached there would be a different (earlier) measurement
* wearing the same name. `windowTokens` is the AUTOCOMPACT window — a model declaring
* `autoCompactTokens` keeps a LARGER physical request window — so do not present it as "the
* model's context size".
*/
type: "context_usage";
usedTokens: number;
windowTokens: number;
compactAtTokens: number;
/**
* Additive — the leg's system prompt broken down by pack SECTION. Meaning, and the readings it
* forbids, are owned by `contextUsageSections` (`prompt-assembly/section-usage.ts`, `@contract
* context_usage.sections`): PREPARE-TIME estimates in a different coordinate from {@link usedTokens},
* never compared to {@link compactAtTokens}. Absent when the leg assembled no section IR — "no section
* view", never "the sections weigh nothing".
*/
sections?: readonly import("../prompt-assembly/section-usage.js").ContextUsageSection[];
} & TaskEventIdentity) | ({
type: "turn_end";
/**
* Per-turn token usage + cost (the turn that just ended), for live per-turn accounting / a trace UI
* (service [41]). Re-priced in core from injected pricing (authoritative, like `TaskResult.stats`).
* ALWAYS present: a turn that produced no provider usage frame carries the counted ZEROS beside
* `usageMissing: true` — zero is unknown, not free, the one law `TaskResult.stats` states (numbers
* always present, the flag says they under-count), so a consumer handles ONE frame shape.
*
* `inputTokens` is the cache-MISS count (Anthropic-protocol shape, matches
* `stats.promptTokens` / the tracer's `brain.call.promptTokens`); `totalInputTokens` is the
* cache-INCLUSIVE total (`inputTokens + cacheReadTokens + cacheWriteTokens`) that cost is computed
* from. Sum `totalInputTokens` for context/window/spend views; read `inputTokens` only where the
* protocol's `input_tokens` is meant. Up to 2.13.x `inputTokens` carried the total.
*/
usage: {
/** Cache-MISS input tokens for this turn (the Anthropic protocol's `input_tokens` meaning;
* matches `stats.promptTokens` / `brain.call.promptTokens`). **Up to 2.13.x this field
* carried the cache-INCLUSIVE total** (RB-457-a, BREAKING at 3.0.0) — that value now lives
* in `totalInputTokens`. Read this only where the protocol's `input_tokens` is meant. */
inputTokens: number;
/** Cache-INCLUSIVE input total for this turn (`inputTokens + cacheReadTokens +
* cacheWriteTokens`) — what cost is computed from. Sum THIS for context/window/spend
* views; it equals what `inputTokens` carried up to 2.13.x. */
totalInputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
costMicroUsd: number;
};
/** TB 尸检 T1-5: set when the turn ended WITHOUT a provider usage frame (e.g. a degenerate-repetition
* cutoff cancelled the stream before usage arrived) or the brain flagged its frame unknown. Consumers
* must treat `usage` as UNKNOWN — its zeros are not a measured 0 (the provider still served the
* request; `stats` under-counts it honestly rather than polluting the single-source accounting
* with an estimate). Absent on a measured turn (never `false`). */
usageMissing?: true;
/** TB telemetry B1: the NORMALIZED finish reason of the turn's LAST model call —
* the engine's `AssistantMessage.stopReason` (`StopReason`: `"stop"`/`"length"`/`"toolUse"`/
* `"error"`/`"aborted"`). This is post-normalization: e.g. Anthropic `end_turn`/`stop_sequence`
* both fold to `"stop"`, `max_tokens`→`"length"`; the raw provider reason is NOT preserved here.
* The machine signal for "was this turn cut by max_tokens" (`"length"`) — previously stream+trace
* double-blind, which made P1 cap-binding unjudgeable from artifacts. Absent when the turn produced
* no assistant message. Typed `string` (not the internal `StopReason`) to keep the engine's union
* private, but the value set is exactly those five tokens. */
stopReason?: string;
/** The PROVIDER'S OWN name for the model that answered this turn, when it DIFFERS from the id the
* request was sent with — a SECOND fact beside `TaskResult.model` / the trace's `brain.call.model`,
* which stay the ENGINE-SELECTED pool id a resume re-seeds and a catalog resolves, never an
* overwrite of either. PRESENCE: the turn's LAST assistant message carried one
* (`AssistantMessage.responseModel`). ABSENT ⇒ the provider named the requested id or named nothing —
* ONE reading, not two, as on the reference mismatch latch, also written only for a disagreeing pair.
* A mismatch is additionally announced once per leg per pair as `route.response_model_mismatch`. */
responseModel?: string;
} & TaskEventIdentity) | ({
/** design/99 MF-18: a compaction boundary (or, since design/119 #1, mid-turn when the prompt-too-long recovery force-compacts between a failed request and its retry). `trigger` = what caused it: `"auto"` (the context-threshold
* auto-compaction), `"manual"` (the `TaskStream.compact` / `/compact` path), or `"forced"` (RB-398-b
* the prompt-too-long recovery and trim-pressure lanes — compaction was not optional; the
* hook layer's `PreCompactContext.trigger` always had this third state and the wire now carries the
* SAME value the hooks saw instead of flattening it to `"auto"`. Additive value space — a consumer
* switching on auto/manual keeps its exact old behavior). `tokensBefore` = the
* pre-compaction context size. CC parity = `SDKCompactBoundary{trigger, pre_tokens, preserved_segment}`. */
type: "compacted";
trigger: "auto" | "manual" | "forced";
tokensBefore: number;
/** How much context this compaction FREED, as the engine measured it: one structural sum over
* the pre-messages minus the same sum over the post-messages, inside the pass. **The only
* subtractable number on this event**, and what a context meter should render. Clamped ≥ 0 at
* the mint, so `0` means "freed nothing", never "could not tell". Absent ⇒ the post
* measurement failed and no delta exists — never a stand-in. */
freedTokens?: number;
/** DIAGNOSTIC, NOT subtractable. Post-compaction size in the TRIGGER-GATE coordinate
* (structural estimate + the fixed prompt overhead). {@link triggerTokensBefore} is not its
* matching pre-value: the gate reads a usage-ANCHORED figure whenever the provider's last
* usage is fresh, while this is always structural + overhead, so the difference mixes
* coordinates and goes NEGATIVE on ordinary auto passes (3037 − 4591 = −1554, measured on a
* 4k window). Use {@link freedTokens}. Absent when the post measurement failed. */
tokensAfter?: number;
/** DIAGNOSTIC, NOT subtractable. The quantity the gate compared against its threshold —
* usage-anchored when the anchor is fresh, structural + overhead when it is not. It answers
* WHAT tripped the trigger, a different question from how much was freed; left un-clamped and
* un-paired rather than made to look subtractable. Absent on the legacy emitter. */
triggerTokensBefore?: number;
/** TB telemetry B4: wall time of the whole compaction pass (summary call included) — what the
* boundary pause actually cost. Absent on legacy emitters. */
durationMs?: number;
/** CC `SDKCompactBoundary.preserved_segment` parity: `firstKeptEntryId` is the session-tree entry id where
* the surviving KEPT TAIL begins after this compaction. A consumer maps it to a wire message via the
* `entryId` carried on `message_committed` (message-identity Phase 1) — render the compaction divider just
* before that message + relink the preserved tail. (The eventId↔entryId side map service keeps for resumeAt
* also resolves it, but is no longer required.) Absent on a compaction that reported no kept-tail floor. */
preserved_segment?: {
firstKeptEntryId: string;
};
/** Cross-session message frames that sat in the SUMMARIZED prefix of this pass — delivered
* earlier and visible to the model for the turns since. A record still in the session's box is
* handed over again by the engine (once; a second loss is disclosed `resupply_exhausted`), so
* this counts what the summary swallowed, not what is gone. Present iff at least one. */
summarizedCrossSessionMessages?: number;
/** CC post-compact file-restore parity (2026-07-03): the working files the engine
* re-read into the summary (attachWorkingFiles), attachment order. A shell renders CC's
* `Read {path} (…)` cards under the compaction divider from this. Absent when the feature is
* off or nothing was attached. `truncated` = the content was clipped at the per-file cap. */
attachedFiles?: Array<{
path: string;
chars: number;
truncated: boolean;
}>;
/** design/145 §3 (additive): this compaction's summary call FELL BACK from the independent
* `compactionModel` to the main model — the small window would have truncated more than
* `compaction.clampTolerance` of the conversation. Mirror of `compaction.model_fallback`. */
modelFallback?: true;
/** design/145 §3 (additive): why the fallback happened ("window" is the only reason today). */
fallbackReason?: "window";
/** design/145 §3 (additive): the summary INPUT was clamp-truncated on the compaction model by
* this ratio (0..1) — set when the budget gate denied the fallback, or when the
* truncation stayed within tolerance (bounded loss, disclosed either way). */
clampedRatio?: number;
/** design/145 §3 (additive): "budget" = the gate that denied the fallback;
* "tolerance" = within-`clampTolerance` ordinary truncation. */
clampReason?: "budget" | "tolerance";
/** 压缩子阶段遥测 (1.283 批 G / design/145 §6 正交后续批, additive): sub-phase wall-time split
* of THIS pass, mirrored 1:1 from `maybeCompact().phaseDurations` (single source: the
* `compaction.phase_timings` trace frame minus its envelope). Invariant: prepareMs +
* (summaryMs ?? 0) + persistMs === {@link durationMs} (exact tiling; `summaryMs` absent on a
* provider-reuse pass — no summary LLM call happened). Absent on legacy emitters. */
phaseDurations?: import("./auto-compaction.js").CompactionPhaseDurations;
} & TaskEventIdentity) | ({
/**
* RB-398-b④: a compaction pass concluded WITHOUT landing — the non-landing outcomes
* used to exist only as `compaction.*` trace frames, so a stream-only consumer watched a
* context quietly stop being managed (breaker opens, rapid-refill disable, hook blocks —
* all invisible). One lightweight additive event, emitted at the same sites as the trace
* frames; the SUCCESS outcome stays on the `compacted` event (this event never carries it,
* so a consumer treating `compacted` as the only success signal is unchanged).
* `outcome` reuses the {@link CompactOutcome} vocabulary plus `"suppressed"` (over-threshold
* pass suppressed by the anti-thrash floor — a state manual compact can never reach, hence
* outside `CompactOutcome`):
* - `"failed"` — a burned summary attempt failed, or the open breaker drained a manual
* request without an attempt (`reason` says which).
* - `"blocked"` — a `preCompact` hook deliberately blocked the pass.
* - `"suppressed"` — wanted to fire, held under the §25.2 anti-thrash floor.
* - `"noop"` — the pass ran but found nothing to compact (no valid cut point/empty history).
* - `"disabled"` — an explicit (manual/forced) request was refused because compaction is off,
* or the rapid-refill breaker disabled compaction mid-task (`reason` says which). A spec
* that statically disables compaction does NOT produce one of these per boundary (that
* would be per-boundary noise about a static configuration, not a state change).
* - `"mooted"` — a manual request was processed but no boundary could serve it (dying loop /
* caller cancel).
*/
type: "compaction_outcome";
outcome: Exclude<CompactOutcome, "compacted"> | "suppressed";
/** Same three-state vocabulary (and the same value) the pass handed the `preCompact` hook. */
trigger: "auto" | "manual" | "forced";
/** Optional human-readable detail (mirrors the trace frame's reason where one exists). */
reason?: string;
} & TaskEventIdentity) | ({
/**
* TB telemetry B3: the engine injected a SYSTEM steering message (a deadline nudge
* or the design/130 P2 graceful-finalize instruction) into the model's next turn. Previously these
* went through `harness.steer()` with no stream echo — a consumer literally could not tell whether
* the finalize fired (the refute-130 attribution blind spot). Echo only; the model-facing message
* is unchanged. `preview` is the injected text clipped to a display bound.
*
* design/133 (LOW-11/F11): the turn-boundary attachment producers ride the same echo — sources
* `todo_reminder` / `task_reminder` / `changed_files` and the G1/续批 members
* `background_tasks` / `tools_delta` / `agent_listing` / `skills_listing` / `mcp_instructions`
* (one event per attachment; a multi-attachment boundary still coalesces into ONE steer message
* on the model lane). [c209-C]: the listing family's FIRST-FRAME deliveries (initial roster /
* `<skills>` block riding the first user message, not a steer) emit the same echo frames.
*
* `git_status` (env-tail migration, additive member): the git-status frame — the turn-dynamic
* git facts' carrier since they left the system prompt. Emitted at the frame's append RECEIPT
* (first-frame / resume-continuation / compaction re-assert deliveries alike). Its `preview`
* is a CONSTANT wording on purpose (never frame bytes): branch names and status text are
* repo-controlled and must not enter the event telemetry plane through this echo.
*
* `output_style` (additive member): the frame stating the deployment-declared `TaskSpec.outputStyle`, echoed
* at every delivery (a leg's first turn, a resumed leg's continuation, each continuing boundary).
* The permission-posture frames (planning / bypass / auto, and their exits) are model-lane only and have no
* echo; the retired `plan_mode` member was the echo of the opt-in planning reminder they replaced.
*/
type: "steering_injected";
source: "limit_approach" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "output_style" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools" | "final_verification" | "git_status";
preview: string;
} & TaskEventIdentity) | ({
/**
* design/121 (CC 198 `<new-diagnostics>` parity): NEW LSP diagnostics that appeared after the
* agent's edits, drained at a turn boundary. The SAME data was injected into the model as a
* `<new-diagnostics>` context message; this frame is the shell's render source (CC's collapsed
* `Found N new diagnostic issues in M files (ctrl+o to expand)` row). UNTRUSTED RAW (language-
* server output over workspace files) — a consumer redacts/bounds before persisting (§E1).
*/
type: "diagnostics";
files: Array<{
uri: string;
diagnostics: Array<{
message: string;
/** LSP severity: 1=Error 2=Warning 3=Information 4=Hint. */
severity?: number;
range?: {
start: {
line: number;
character: number;
};
end?: {
line: number;
character: number;
};
};
code?: string | number;
source?: string;
}>;
}>;
isNew: true;
} & TaskEventIdentity) | ({
/**
* message-identity Phase 1 (the CC `SDKCompactBoundary` parity enabler): a persisted session-tree entry was
* just COMMITTED for a RENDERABLE message the consumer has already streamed. `entryId` = `SessionTreeEntry.id`
* (uuidv7, persisted, replica-stable) — the SAME id space as `compacted.preserved_segment.firstKeptEntryId`,
* E18 `resumeAt`, and the design/381 `rewindFilesTo`. `role` + (for a tool result) `toolCallId` let a consumer bind it to
* the message it rendered: a tool result by its `toolCallId`; an assistant/user message by stream order (this
* is the terminal boundary for the content just streamed, after its deltas and before its tool results). A
* consumer builds its own `entryId → message` map from these to resolve `preserved_segment` WITHOUT the
* eventId↔entryId side map. Terminal per-message boundary; additive + ignorable. ABSENT for a message that did
* not persist (an empty-failure assistant) and for non-message entries (a compaction summary / model_change —
* never a `message_committed`). The engine still holds no eventId→entryId map (the deployment owns it); this
* only pushes the persisted id onto the wire, the constitution's primitive/profile split (cf. §E2 `eventId`).
*/
type: "message_committed";
entryId: string;
role: "user" | "assistant" | "toolResult";
/** Present iff `role === "toolResult"`: the exact join key to the `tool_start`/`tool_end` already rendered. */
toolCallId?: string;
} & TaskEventIdentity) | ({
/**
* design/171 §5.5/§6.2 — the human-input LIFECYCLE arm: one frame per human input this run
* accepts (WHO fed the run WHAT, per carrier). Additive + ignorable like its siblings.
* Deliberately NOT folded into `message_committed`: that event only fires for messages that
* PERSISTED, while a human input's lifecycle (queued / parked) exists before — and sometimes
* without — a committed message. Constructed via `buildHumanInputEvent` (mints the inputId,
* materializes no absent optional keys); `HumanInputEvent` is the Extract-derived alias.
*
* Attribution semantics are THREE-VALUED, never collapsed:
* - `actor` ABSENT ⇒ the caller supplied none (anonymous — every pre-171 caller);
* - `actor.hostAsserted === false` ⇒ an UNVERIFIED self-claim the host chose to carry;
* - an OLD event stream that predates this arm simply has no `human_input` frames at all —
* a consumer distinguishes "attribution unknowable (pre-arm producer)" by the emitting
* core's version, never by guessing from field absence on some other event.
*/
type: "human_input";
/** Correlation/idempotency key. Caller-supplied when the ingress already minted a message id
* (the parked-steer queue's `inputId` is THE same key); a uuidv7 otherwise. */
inputId: string;
/** Ordinal of this input among the run leg's accepted human inputs (1-based, drain order for
* parked entries). Leg-scoped: a resumed leg restarts at 1 — `inputId` is the global key and
* stream order the global order; a serving layer needing a session-global ordinal mints its
* own. */
sessionSeq: number;
/** Which concrete carrier accepted the input. OPEN set — a serving layer self-reports its own
* carriers (question note / elicitation / decide reason / …) through the same arm; the
* core's five report their {@link HumanInputSource} name. */
carrier: string;
source: HumanInputSource;
/** Who asserted the actor's identity (Slack app id, SSO IdP …) — mirrored from `actor.issuer`
* or supplied alone when the host can name its ingress but not the subject. */
issuer?: string;
actor?: ActorAssertion;
delivery: HumanInputDelivery;
/** Plaintext principal correlation key (deliberately not hashed — the audit consumer joins
* on it). */
principal?: string;
/** The persisted session-tree entry the input landed as, WHEN the emitter knows it (a
* serving layer replaying the ledger); the core's live emissions precede the commit and
* omit it — consumers there join via `message_committed` + stream order. */
entryId?: string;
} & TaskEventIdentity) | ({
/**
* design/99 §E3/§E10 — a live BRAIN-LAYER status the brain would otherwise absorb
* silently (a 429 rate-limit / a 5xx or network retry / a reconnect / an open circuit breaker). Emitted
* the moment the brain decides to wait/retry, carrying the graduated {@link BrainStatusPhase} + an
* optional `retryInSec` so a UI can show "rate limited, retrying in Ns". EPHEMERAL — a pure liveness
* signal, never persisted or replayed on resume. The phase and the hint are provider-neutral (no
* provider taxonomy, no `stop_reason`); the failing attempt's HTTP status rides its own seat in the
* retry context only — see {@link BrainStatus.errorStatus}.
* SCOPE: reflects THIS task's own (top-level) brain calls. A delegated sub-agent's brain liveness routes
* to the SUB-AGENT's own stream (isolated, like every child event — §E2); a background internal brain
* call (memory recall/consolidation) is intentionally not surfaced.
*/
type: "status";
} & BrainStatus & TaskEventIdentity) | ({
/**
* design/115 P2 core slice — SDK-side projection of a background task terminal notification. The model-facing
* projection is the sibling `<task-notification>` XML injected through the runner's system-injection lane when
* the originating harness is still running; durable idle wake/resume is a deployment transport concern.
*/
type: "task_notification";
notification: TaskNotificationPayload;
/** design/373 §3.6 — the frame's injection tier as DELIVERED (additive; consumers tolerate
* absence): on the live lane, the tier the routing used (`next`/`now` → steer lane, `later`
* → followUp lane); on the turn-open redelivery lane, the tier the park CARRIED. ABSENT is a
* fact, not a default: a pre-373 producer event, or a parked record whose tier was not
* carried (the terminal sweep's re-pend) — never read absence as `"later"`. */
priority?: import("./task-notification.js").SystemInjectionPriority;
} & TaskEventIdentity) | ({
/**
* design/99 MF-10 — a SUBAGENT PROGRESS TICK: the delegated sub-run's usage ACCRUING
* over time, emitted at each turn boundary WHILE it runs, so a parent UI animates the Task card's rollup
* (tokens/tool-uses climbing). Emitted ONLY for a subagent (a task with `parentToolCallId`); a top-level
* run's own usage is the consumer's own. `status:"running"` mid-run; the terminal usage is the `done` result.
* Live/ephemeral — NOT persisted (a resumable trace re-derives usage from the result), like `status`.
* Maps to CC `SDKTaskProgress` (camelCase here → snake_case `task_id`/`total_tokens`/`tool_uses`/`duration_ms`
* at the SDK/service projection, the same convention as `toolCallId`→`tool_use_id`).
* `taskId` is the child RUN's id (`spec.taskId ?? sessionId`), NOT the registry handle: the
* background lanes launch with no explicit `spec.taskId`, so it equals the child's session id and
* hence its `transcriptId`. Both domains are tabled in `docs/INTEGRATION-CORE.md` §1.6a.
*/
type: "task_progress";
taskId: string;
/**
* The FLEET-task kind of the run this tick reports on — the type discriminator a consumer needs
* when it folds progress ticks and task notifications into one ledger. Without it the only
* available test is "it appeared on the fleet stream, therefore it is an agent", and that
* inference has already minted an agent-shaped row for a task that was a background shell
* command. Drawn from {@link TaskNotificationPayload}'s `task_type` vocabulary (no second
* spelling of one axis) and narrowed to the members a progress tick can honestly claim.
*
* Present on every tick from a run that HAS a fleet row: `"background_agent"` for a background
* delegation child (its registry `a*` row) and `"workflow"` for a workflow-spawned agent (its
* `wa*` read-face row) — the latter alongside, not in place of, {@link workflowRunId}, which
* names the owning RUN while this names the KIND. ABSENT is a fact, not missing data: a
* SYNCHRONOUS delegated child has no fleet row at all. Additive — tolerate absence, and never
* read absence as "not an agent".
*/
taskType?: DelegationTaskType;
/**
* The registry row's STOP-CYCLE generation this tick reports from (fresh
* spawn = 1, every launched revival bumps it), the same counter `TaskNotificationPayload.seq`
* and `BackgroundChildEvent.seq` already speak — one axis, not a third spelling. It answers
* the one question a fleet consumer cannot otherwise decide when a frame arrives late: "late
* first frame of the cycle I know (same value), or a revived run I have not folded yet
* (higher value)?" Advisory observation, stamped at spawn/revive from the registry's own
* counter; the settle-time ledger stays the authority. ABSENT is a fact, not a gap: a run
* with no `a*` registry row (a SYNCHRONOUS delegated child, a workflow `wa*` agent, a
* top-level run) has no generation concept, and absence must never be read as "cycle 1".
*/
seq?: number;
/** Workflow-lane self-identification (field-proven: the SSE-forwarded tick of a
* WORKFLOW child previously carried a bare uuid with no workflow identity — indistinguishable
* from an unknown nested subagent; the fleet lane had `wa*`+workflowRunId but this lane had
* nothing). Present iff the tick bubbled out of a workflow-spawned agent. Additive. */
workflowRunId?: string;
/** the workflow agent's display label (opts.label / auto `agent-N`), riding with
* {@link workflowRunId} for the same self-identification. Additive. */
workflowAgentLabel?: string;
/** design/99 (nested-subagent live tree): the spawning run's taskId — present on a DEEPER-than-1 subagent tick
* (a subagent spawned by another subagent), forwarded via `RunInternals.onForwardEvent` so a UI nests the
* node directly (this.taskId under parentTaskId) without mapping `parentToolCallId` → owner. The top-level
* run is known to the consumer; its direct children carry the top's taskId here. Absent on a top-level tick. */
parentTaskId?: string;
/** design/99 BC-2 (Service AI [§I]): a short, single-line HUMAN display name for the subagent — its explicit
* `taskName` or the selected agent-type. So a Fleet child row shows a readable label instead of the raw
* `taskId` (the stable row identity stays the taskId). ABSENT when the subagent carries neither a taskName
* nor an agent-type — the child then keeps its taskId (NOT the raw objective, which could leak a delegated
* secret to a progress-scoped consumer — dual-review Q2). Sanitized (control-char-stripped, length-capped). */
name?: string;
/** The EXPLICIT `Agent({name})` spawn identity of the sub-run, when the spawn carried one — the
* teammate KEY, as opposed to {@link name} (the display label): the same string the fleet row's
* spawn frame (`BackgroundChildEvent.spawnName`), the roster row and the child's uplink `from`
* carry, so a consumer binds "@name → row" by string equality. Validated at spawn
* (`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`). ABSENT = an unnamed delegation, never a missing value. */
spawnName?: string;
/**
* The model this sub-run was PREPARED with — the resolved id, read off the leg's prepared model
* at the mint, so it is the same value `TaskResult.model` and the `task.start` trace frame carry.
* (`task.end` reports the run's totals and has never named a model; there is nothing to join to
* there.)
*
* Why it rides the live lane at all: a delegation may name a tier word (`"sonnet"`), an agent
* definition's model, or NOTHING (inherit the caller's current model, or fall to the `subagent`
* role) — every one of those resolves somewhere the consumer cannot see, so a shell badging a
* running child previously had only the REQUESTED word (or nothing) to render, which is a
* different claim from what the child runs on. This is the resolved answer, not the request.
*
* PREPARED, deliberately, and NOT "whatever is serving this turn" — stated in the first sentence
* because the difference is observable. A mid-run DEGRADE switch (`spec.limits.degrade`) or a
* gateway re-route moves the serving model without rewriting this field, exactly as
* `TaskResult.model` behaves; the switch is announced on its own channel (`TaskResult.degraded`),
* and the per-call served id lives on the brain-call telemetry. One name, one meaning, across the
* three faces that use it — at the cost of being the leg's declared model rather than a live one.
*
* Present on every tick this build mints (the frame family is subagent-only by construction, so
* there is no lane where a leg has no prepared model). Declared optional for the ordinary reason:
* a consumer folding frames from a pre-key producer must keep compiling, and absence there means
* "this producer did not state it", never "no model".
*/
model?: string;
/** The child's most recent tool intent as one human line ("Bash npm test",
* "Edit src/x.ts") — the SAME source/value as the registry sink tick's `currentAction` (residual
* observability, lane B), attached to the FORWARDED frame because every client wire projects this
* TaskEvent, not the sink (a deployment's projection legs otherwise see a permanently-absent field).
* UNTRUSTED display hint — redact at the projection boundary. Absent until the child runs a tool. */
currentAction?: string;
usage: {
totalTokens: number;
toolUses: number;
durationMs: number;
};
/** Per-turn ticks are `"running"`. ONE final tick fires at the sub-run's settle with the
* terminal face (`"completed"` | `"failed"` — timeout/blocked fold to failed; a suspended durable
* pause emits no terminal tick), so a uuid-lane consumer sees the row finish instead of a bare
* removal after the last running beat. Additive value space — a consumer switching on "running"
* keeps its exact old behavior. */
status: "running" | "completed" | "failed";
} & TaskEventIdentity) | ({
/**
* design/99 §E13 — the task's tracked working directory CHANGED mid-run (a `bash`
* `cd` moved it). Emitted live after the tool that moved it, carrying the new logical cwd, so a shell UI
* can update its prompt/path display. The tracked cwd is the RAW logical path (a `cd` through a symlink
* reports the raw path navigated, not the resolved target). Only fires when a real shell is mounted
* (absent under `handsReadOnly` / no-exec). The initial cwd is NOT announced (it is the known root); only
* subsequent changes are. A live observability signal — not persisted, and the tracked cwd RESETS to the
* root across a suspend/resume (the per-leg execution cwd is not durable today — a pre-existing limitation
* tracked with the E18/E19 execution-state durability work); within a leg, every `cd` move is reported.
*/
type: "workspace_changed";
cwd: string;
} & TaskEventIdentity) | ({
/**
* design/173 §2.4/§8.1 — this leg's EFFECTIVE wiring manifest: the machine-readable
* disclosure of how the run is assembled around the optional human-interaction seams
* (ask/question/elicit/park/durability/fleet/governance). Emitted ONCE PER PREPARED LEG
* (root/child/resume each get their own — per-leg assembly genuinely differs, e.g. the
* engine-stripped background lanes), after prepare and BEFORE the leg's first model/tool
* interaction; leg identity rides {@link TaskEventIdentity} plus `manifest.leg.kind`.
*
* DISCLOSURE, not verification (§0 guarantee boundary): seam presence/shape as facts —
* liveness (does the callback actually reach a human) is a round-trip probe's job.
* HOST/OPERATOR plane: the manifest never enters model context, and the `governance`
* section carries `audience: "operator"` — a serving layer forwarding this stream to a
* multi-tenant/shared consumer MUST project that section for operators only (its default,
* when it does no projection, is to not disclose the section). Additive: a consumer that
* ignores unknown event types is unchanged; consumers of the manifest itself must ignore
* unknown FIELDS (`schemaVersion` bumps only on a semantic break).
*/
type: "wiring_manifest";
manifest: import("./wiring-manifest.js").WiringManifest;
} & TaskEventIdentity) | ({
/**
* design/388 §2.5 — a RUN-TIME change to this leg's mounted tool set (today: the RefreshMcpTools
* seam re-inserting a server's tools in place). Carries the WHOLE post-change roster (positions
* come from it; a consumer replaces its copy) plus a summary usable only when the consumer holds
* `fromDigest` — otherwise it adopts the snapshot and records a skew (never rejects). The prepared
* roster itself rides `wiring_manifest.manifest.tools`; this frame never precedes that one.
*/
type: "tool_roster_delta";
delta: import("./tool-roster.js").ToolRosterDelta;
} & TaskEventIdentity) | ({
/**
* This leg's DEFERRAL CENSUS: which mounted tools ship as name-only placeholders, which the model has activated, and the policy
* (`RunnerDeps.toolSearch`) that decided it — meaning owned by `ToolDisclosureSnapshot` (`runner/tool-disclosure.ts`). EXISTS ONLY ON A LEG THAT
* DEFERS SOMETHING: an empty deferred set emits no frame, and that absence reads as "nothing was deferred here", never "no disclosure in this build"
* and never "unknown". First frame = the leg-start census, right after `wiring_manifest` — `activated` is EMPTY on a fresh leg and, on a RESUMED one,
* whatever the engine reseeded (never read a non-empty first census as "this leg activated them"); every later frame = a census that CHANGED, i.e. an
* activation landed (either lane — one section, one census). WHOLE SNAPSHOT: replace your copy. `deferred` is frozen at prepare; lists sorted.
*/
type: "tool_disclosure";
policy: import("./tool-search-setting.js").ToolSearchPolicy;
/** Meaning owned by `ToolDisclosureSnapshot.thresholdPercent`: absent under `always` / `never`, where
* no threshold was consulted, and an absence must not be read as "the default percent". */
thresholdPercent?: number;
deferred: readonly string[];
activated: readonly string[];
} & TaskEventIdentity) | {
type: "done";
result: TaskResult;
};
/** design/171 §6.2 — the `human_input` arm as a named type (Extract-derived: ONE source, the union
* above — never a hand-copied second shape). What `buildHumanInputEvent` returns. */
export type HumanInputEvent = Extract<TaskEvent, {
type: "human_input";
}>;
/**
* Outcome of a manual {@link TaskStream.compact} request (the enum names align with
* the diagnosis decision tree's drain exits, so a consumer can tell "202 processed" states apart):
* - `"compacted"` — a compaction landed at the boundary (the `compacted{trigger:"manual"}` event fired).
* - `"failed"` — the attempt ran and threw (summarizer failure / empty summary / oversized summary;
* `compaction.failed` trace frame + `Runner.onError(phase:"compaction")`), or the §17.4 breaker was
* already open so the request was drained without an attempt (`compaction.failed` frame, no attempt).
* - `"mooted"` — the request was processed but the run could no longer compact (dying loop: abort /
* durable suspend / plan review in flight), or the task settled before any boundary arrived.
* - `"noop"` — the attempt ran but there was nothing to compact (no valid cut point / empty history).
* - `"blocked"` — a `preCompact` hook deliberately blocked this compaction (not a failure).
* - `"disabled"` — compaction is disabled by the task spec (`compaction.enabled: false`).
*/
export type CompactOutcome = "compacted" | "failed" | "mooted" | "noop" | "blocked" | "disabled";