UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,037 lines • 76.2 kB
/**
 * 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";