@sema-agent/core 7.6.0 → 7.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +2 -2
  3. package/dist/agents/cascade.d.ts +2 -3
  4. package/dist/agents/repair-loop.d.ts +2 -2
  5. package/dist/agents/retain-ledger.d.ts +2 -3
  6. package/dist/agents/send-message-tool.d.ts +2 -2
  7. package/dist/agents/session-util.d.ts +2 -2
  8. package/dist/agents/subagent.d.ts +3 -4
  9. package/dist/agents/teacher.d.ts +2 -2
  10. package/dist/agents/team.d.ts +2 -2
  11. package/dist/agents/verify.d.ts +5 -6
  12. package/dist/core/agent-definition.d.ts +172 -0
  13. package/dist/core/agent-definition.js +1 -0
  14. package/dist/core/delegation-frames.d.ts +298 -0
  15. package/dist/core/delegation-frames.js +21 -0
  16. package/dist/core/engine-notice.d.ts +555 -0
  17. package/dist/core/engine-notice.js +55 -0
  18. package/dist/core/gate-fold.d.ts +12 -0
  19. package/dist/core/gate-fold.js +158 -0
  20. package/dist/core/gate-lanes.d.ts +93 -0
  21. package/dist/core/gate-lanes.js +626 -0
  22. package/dist/core/hands-band.d.ts +134 -0
  23. package/dist/core/hands-band.js +1 -0
  24. package/dist/core/hooks.d.ts +20 -101
  25. package/dist/core/hooks.js +53 -854
  26. package/dist/core/mcp-failure.d.ts +43 -5
  27. package/dist/core/mcp-failure.js +31 -14
  28. package/dist/core/mcp-server-spec.d.ts +217 -0
  29. package/dist/core/mcp-server-spec.js +1 -0
  30. package/dist/core/model-seat.d.ts +99 -0
  31. package/dist/core/model-seat.js +1 -0
  32. package/dist/core/reminder-mint.d.ts +10 -0
  33. package/dist/core/reminder-mint.js +3 -0
  34. package/dist/core/runner/contracts.d.ts +382 -6
  35. package/dist/core/runner/gate-exit.d.ts +177 -9
  36. package/dist/core/runner/gate-exit.js +70 -1
  37. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
  38. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
  39. package/dist/core/runner/prepare-task.d.ts +2 -2
  40. package/dist/core/runner/runtask.d.ts +4 -71
  41. package/dist/core/runner/runtask.js +14 -5
  42. package/dist/core/runner-deps.d.ts +1416 -0
  43. package/dist/core/runner-deps.js +1 -0
  44. package/dist/core/runtime-caps.d.ts +164 -0
  45. package/dist/core/runtime-caps.js +1 -0
  46. package/dist/core/task-event.d.ts +910 -0
  47. package/dist/core/task-event.js +1 -0
  48. package/dist/core/task-limits.d.ts +110 -0
  49. package/dist/core/task-limits.js +1 -0
  50. package/dist/core/task-result.d.ts +809 -0
  51. package/dist/core/task-result.js +1 -0
  52. package/dist/core/task-spec.d.ts +1370 -0
  53. package/dist/core/task-spec.js +1 -0
  54. package/dist/core/task-stream.d.ts +382 -0
  55. package/dist/core/task-stream.js +1 -0
  56. package/dist/core/tool-spec.d.ts +1174 -0
  57. package/dist/core/tool-spec.js +1 -0
  58. package/dist/core/types.d.ts +26 -7691
  59. package/dist/core/types.js +2 -76
  60. package/dist/core/warm-resume.d.ts +2 -2
  61. package/dist/index.d.ts +2 -1
  62. package/dist/index.js +1 -1
  63. package/dist/orchestration/goal.d.ts +2 -2
  64. package/dist/orchestration/run-spec.d.ts +2 -2
  65. package/dist/orchestration/run-workflow-tool.d.ts +3 -3
  66. package/dist/orchestration/workflow.d.ts +4 -4
  67. package/dist/scenarios/scenario-registry.d.ts +3 -3
  68. package/dist/scenarios/teacher-quickstart.d.ts +2 -2
  69. package/dist/server/http.d.ts +2 -2
  70. package/dist/stores/file/fs-atomic.d.ts +88 -12
  71. package/dist/stores/file/fs-atomic.js +184 -55
  72. package/dist/stores/file/index.d.ts +1 -0
  73. package/dist/stores/file/index.js +1 -0
  74. package/package.json +1 -1
  75. package/test/export-surface.snapshot.json +9 -1
@@ -0,0 +1,910 @@
1
+ /**
2
+ * The WIRE frames: the `TaskEvent` union every consumer switches on, and every satellite it
3
+ * discriminates with — the per-message identity envelope, the brain-status phases and retry classes,
4
+ * the tool activity beat, the human-input source/delivery words, the delegation task type, the
5
+ * human-input projection and the compaction outcome. This is the file a change to a frame touches, and
6
+ * nothing else. Layer 0 vocabulary; `types.ts` re-exports every name below, so no consumer's import
7
+ * changes.
8
+ */
9
+ import type { ActorAssertion } from "../internal/llm.js";
10
+ import type { TaskResult } from "./task-result.js";
11
+ import type { TaskNotificationPayload } from "./task-notification.js";
12
+ /**
13
+ * Identity fields carried by every CONTENT event of a task stream (design/99 §E2 — lightweight message
14
+ * identity). Both are OPTIONAL and ADDITIVE; a consumer that ignores them is unchanged.
15
+ *
16
+ * - `eventId` — a core-minted, time-sortable (uuidv7) unique id stamped on each emitted content event. Gives
17
+ * a live UI a stable handle to correlate/dedup streamed events and to map its own client-side message ids
18
+ * back to a core anchor. (The durable-anchor mapping for resume-at — which eventIds are restartable
19
+ * transcript positions — is finalized by the E18 `resumeAt` seam; this field is the stable handle it builds on.)
20
+ * - `parentToolCallId` — present ONLY when this task runs as a sub-agent spawned under a parent task's tool
21
+ * call (threaded via the trusted `RunInternals.parentToolCallId`, sourced from the spawning tool's
22
+ * `ToolExecuteContext.toolCallId`). Lets a consumer attribute streamed content to the delegation subtree it
23
+ * belongs to WITHOUT core physically merging the child stream into the parent (the child's RESULT still
24
+ * re-enters the parent MODEL context only through the `delimitUntrusted` fence — observability threading and
25
+ * model-context isolation are kept separate, so prompt-cache and the injection boundary are untouched).
26
+ */
27
+ export interface TaskEventIdentity {
28
+ eventId?: string;
29
+ parentToolCallId?: string;
30
+ /**
31
+ * design/122 S2 (the `WorkflowRun.sourceTaskId` family, NOT a second `taskId` — that
32
+ * field already exists on `task_progress` and a duplicate would bite consumers) — the ORIGINATING task's
33
+ * canonical id (`spec.taskId ?? sessionId`), stamped ONLY on the content events of a task running AS A
34
+ * SUB-AGENT (same condition as {@link parentToolCallId}). A consumer of the C1 forward channel
35
+ * (`forwardSubagentEvents`) can key a child's content events by the child task directly, without an
36
+ * alias table. A TOP-LEVEL task's own events carry NO `sourceTaskId` (absent = the consumer's current
37
+ * task). Additive; a consumer that ignores it is unchanged.
38
+ */
39
+ sourceTaskId?: string;
40
+ /**
41
+ * design/151 S2 (MED-10 live-tail) — the BACKGROUND agent's `a*` registry
42
+ * handle, stamped on every event a background child's lane forwards up the host `onForwardEvent`
43
+ * channel (content deltas, tool events, progress). THE live-tail routing key: a serving layer
44
+ * pipes "what is agent aXX doing right now" straight off the host forward stream — no uuid→a*
45
+ * alias table (the BCE spawn frame's sessionId mapping) needed. Sync/steer children carry NO tag
46
+ * (no a* row exists); `wa*` workflow rows stay on their own observer lane. Additive; a consumer
47
+ * that ignores it is unchanged.
48
+ * NESTED bg-in-bg (server[1523] 对等复审 4a): each background boundary OVERWRITES the tag as the
49
+ * frame crosses it — on the HOST wire, the whole subtree's frames carry the OUTERMOST a* handle
50
+ * (the host-addressable row). Per-frame nesting attribution rides `parentToolCallId`, never this
51
+ * field; a consumer splitting an outer stream into per-descendant views keys on that.
52
+ */
53
+ bgAgentId?: string;
54
+ }
55
+ /**
56
+ * design/99 §E3/§E10 — a BRAIN-LAYER liveness phase, surfaced so a UI can show why a
57
+ * turn is stalled (the brain otherwise absorbs these silently in its connect/retry loop). The PHASE
58
+ * itself is provider-NEUTRAL — a graduated bucket this engine mints, never a provider taxonomy or a
59
+ * `stop_reason`. The one provider-stated number that does cross this channel is the failing attempt's
60
+ * HTTP status, and it travels on its own named seat ({@link BrainStatus.errorStatus}) in the retry
61
+ * context only; the phase and the {@link BrainStatus.detail} hint stay free of it.
62
+ * A CLOSED union (type-safe) covering the core brain layer; a deployment that surfaces its OWN states
63
+ * (e.g. E25 token-refresh `authenticating`) does so on its own channel, not by widening this.
64
+ */
65
+ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "circuit_open"
66
+ /**
67
+ * RB-420-c — TERMINAL frames closing a retry sequence. Without them a consumer that rendered a
68
+ * `rate_limited`/`retrying`/`reconnecting` frame has nothing telling it the wait is over, so the
69
+ * warning row stays pinned until the next unrelated event. Exactly one terminal frame is emitted,
70
+ * and only for a call that actually waited (a retry wait, or a `waiting_first_token` trail —
71
+ * #530): `recovered` = the call was served after the wait (a later attempt, or the first token
72
+ * finally arriving), `gave_up` = the retry budget ran out (or the call was cancelled) and a
73
+ * terminal error follows.
74
+ * Both mean the same thing to a renderer — stop showing the retry state.
75
+ */
76
+ | "recovered" | "gave_up"
77
+ /**
78
+ * #530 — the first-token WAIT, before any failure: a first-token watchdog is armed
79
+ * (`firstTokenTimeoutMs > 0`) and nothing has arrived yet. Emitted once the wait has lasted
80
+ * `WAITING_FIRST_TOKEN_AFTER_MS` and re-emitted every `WAITING_FIRST_TOKEN_EVERY_MS` while it goes
81
+ * on, carrying {@link BrainStatus.elapsedMs} and {@link BrainStatus.timeoutMs}; never carries a
82
+ * retry countdown (`retryInMs`/`retryAtMs` — no wait is being announced, a wait is being
83
+ * OBSERVED). CLOSED like a retry trail: once a wait frame went out, the call's terminal frame
84
+ * follows — `recovered` when the call went on to be served (the first token arrived, on this
85
+ * attempt or a later one), `gave_up` when it failed for good — so a renderer that pinned the row
86
+ * has its "stop" signal. No frame at all when no watchdog is armed.
87
+ */
88
+ | "waiting_first_token";
89
+ /**
90
+ * WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
91
+ * {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
92
+ * unattended progress line ("no answer for three minutes") needs the reason, and until this existed the
93
+ * only carriers of it were the HTTP status and the syscall code, neither of which crossed this channel
94
+ * at the time. (The status has since gained its own seat, {@link BrainStatus.errorStatus}; this bucket
95
+ * remains the reason a consumer RENDERS, and it is the only carrier for the transport classes, which
96
+ * have no status at all.) Values are about the SHAPE of the failure, never its provider taxonomy:
97
+ * - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
98
+ * at that address, or the name has no address). This is the class the SHORT retry lane serves.
99
+ * - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
100
+ * tear, a stalled stream. No verdict about the target; the full ladder applies.
101
+ * - `rate_limit` — the provider asked the caller to slow down.
102
+ * - `server` — the provider reported a failure on its own side.
103
+ * - `http` — a response the status predicate calls terminal, retried anyway because the
104
+ * provider's own explicit retry verdict said to.
105
+ * - `output_cap` — not a failure of the connection at all: the request is being re-sent with a
106
+ * lowered output cap after the provider reported the context limit exceeded (no backoff).
107
+ */
108
+ export type BrainRetryErrClass = "connect_refused" | "transport" | "rate_limit" | "server" | "http" | "output_cap";
109
+ /** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
110
+ export interface BrainStatus {
111
+ phase: BrainStatusPhase;
112
+ /** Optional neutral, human-readable hint. Stays free of provider/HTTP detail even now that
113
+ * {@link errorStatus} exists: the machine-readable status has its own seat, and interpolating it
114
+ * into the prose too would make one fact travel in two spellings a consumer has to reconcile.
115
+ * (CC parity: its own retry banner renders a TRANSLATED sentence — "No response from API",
116
+ * "Connection dropped" — while the raw status rides the structured key beside it.) */
117
+ detail?: string;
118
+ /** Seconds until the brain's next retry attempt (from the honored backoff / `Retry-After`), when known. */
119
+ retryInSec?: number;
120
+ /** RB-420-c — millisecond precision of {@link retryInSec}, for a client that renders a countdown.
121
+ * A wait longer than the re-announce slice is re-emitted with the REMAINING time, so a consumer
122
+ * that simply overwrites its row always shows a live number instead of a stale one. */
123
+ retryInMs?: number;
124
+ /**
125
+ * The WALL-CLOCK instant the announced wait is expected to end — `Date.now() + retryInMs`, read at
126
+ * the moment the frame is emitted, in the EMITTING core process's clock. Stamped centrally at the
127
+ * one emit point, so the invariant is total: present exactly when {@link retryInMs} is present, and
128
+ * absent on every frame that announces no wait (`recovered` / `gave_up`, and the immediate
129
+ * output-cap re-send, which takes no backoff).
130
+ *
131
+ * WHY IT IS PRODUCED HERE rather than derived by the consumer: `Date.now() + retryInMs` is only
132
+ * correct for a reader that received the frame instantly. A consumer several process hops away
133
+ * (serving layer → client) recomputes a deadline already shifted by the transit, so a countdown
134
+ * built that way restarts on every re-announced slice instead of converging. The producer is the
135
+ * only party that can state the instant, so it does.
136
+ *
137
+ * CLOCK DOMAIN, stated to remove the ambiguity: WALL CLOCK (`Date.now()`, epoch ms), NOT a
138
+ * monotonic reading. A consumer must not compare it against its own monotonic timers, and must
139
+ * treat it as approximate across a clock adjustment or a machine boundary — {@link retryInMs} stays
140
+ * the authoritative RELATIVE quantity, and this seat is the absolute convenience derived from it.
141
+ */
142
+ retryAtMs?: number;
143
+ /** #530 — on a `waiting_first_token` frame: how long the first token has been awaited, ms, at the
144
+ * moment the frame is emitted (the emitting process's clock; approximate across a hop). Absent on
145
+ * every other phase. */
146
+ elapsedMs?: number;
147
+ /** #530 — on a `waiting_first_token` frame: the armed first-token watchdog, ms — the wait's ceiling,
148
+ * so a consumer can render "45s of 120s". Absent on every other phase. */
149
+ timeoutMs?: number;
150
+ /** RB-420-c — 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
151
+ * same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
152
+ attempt?: number;
153
+ /** RB-420-c — the retry budget of THIS lane, so a consumer can render "attempt 3 of 10". Lane, not
154
+ * engine: a failure class served by a shorter ladder reports that ladder's budget, so the fraction a
155
+ * consumer renders is the one actually in force rather than the engine-wide ceiling. */
156
+ maxRetries?: number;
157
+ /** Why this wait is happening ({@link BrainRetryErrClass}). Present on retry-wait frames whose cause
158
+ * the engine classified; absent on frames that are not a retry wait (`recovered`/`gave_up`) and on a
159
+ * `circuit_open` fast-fail, which is a local verdict rather than an observed failure. */
160
+ errClass?: BrainRetryErrClass;
161
+ /**
162
+ * #506 ㋑ — the HTTP status of the attempt that just failed, on the frame announcing the RETRY of it.
163
+ * The one provider-stated number this channel carries; everything else about the failure still
164
+ * crosses as the engine's own graduated buckets ({@link phase} / {@link errClass}).
165
+ *
166
+ * WHY IT IS HERE AT ALL, given the channel spent its life refusing it: a UI that must tell an
167
+ * operator whether to wait or to go fix something needs the status, and the two neutral buckets
168
+ * cannot supply it — `rate_limit` covers a 429 the provider will clear on its own AND a 429 that
169
+ * means the account is out of quota, and `server` covers a 500 alongside a 503 behind a load
170
+ * balancer. CC states the same number on the same occasion (`system/api_retry.error_status`, read
171
+ * off `APIError.status`), so a serving layer forwarding this frame no longer has to choose between
172
+ * matching CC's payload and honoring this channel's contract.
173
+ *
174
+ * PRESENCE — an api_retry context whose attempt got a response that NAMES the failure, and nothing
175
+ * else. Present on the connect-ladder retry wait (`rate_limited` / `retrying` / `reconnecting` when
176
+ * a response came back) and on the output-cap re-send (the provider's own 400). ABSENT — never
177
+ * zeroed, never null — for:
178
+ * · a transport failure (`connect_refused` / `transport` at the connect leg): no response existed,
179
+ * so no status was ever stated. This is the arm CC spells as an explicit `null`; here it is
180
+ * absence, for the same reason every other seat on this interface uses absence, and the fact is
181
+ * still TOTAL rather than guessable — {@link errClass} names those two classes by construction.
182
+ * · a mid-stream tear (tiers B/C): its response's own status was a SUCCESS, and a status that
183
+ * failed nothing must not be published as the cause of a retry.
184
+ * · a `circuit_open` fast-fail: a local verdict, nothing was sent, nobody answered.
185
+ * · the terminal `recovered` / `gave_up` frames: they announce no attempt.
186
+ * Same "only a status that names the failure travels" predicate as the assistant frame's
187
+ * {@link AssistantMessage.apiErrorStatus} (one helper, both sites) — a 2xx never travels here either,
188
+ * which is reachable on this path via a provider's own `x-should-retry` verdict on a success status.
189
+ *
190
+ * OPTIONAL, not CC's nullable-REQUIRED, and the difference is CC's own: CC dedicates a whole message
191
+ * (`system/api_retry`) to the retry occasion and can therefore require the key on it, while this
192
+ * interface is ONE shape shared by every phase — the same geometry as CC's shared
193
+ * `control_request_progress` frame, where CC itself makes the key optional.
194
+ */
195
+ errorStatus?: number;
196
+ }
197
+ /**
198
+ * design/97 CORE-8 (③): one lightweight TOOL-ACTIVITY beat surfaced from a running task, for a per-agent live
199
+ * "last N tool calls" drill-down. A trusted run-scoped sink ({@link RunInternals.onActivity}) receives one at
200
+ * each tool start + end. Structural (name/phase/ids) + a SHORT redacted primary `arg` for display — NOT the full
201
+ * ARGS or OUTPUT (those carry untrusted/host data; the workflow layer surfaces redacted prompt/output separately).
202
+ */
203
+ export interface ToolActivity {
204
+ phase: "start" | "end";
205
+ toolCallId: string;
206
+ toolName: string;
207
+ /** wall-clock stamp (epoch ms, `Date.now` read at the emitting Runner where the frame is
208
+ * minted). Carried on BOTH beats: the `start` beat stamps the moment the tool call began, the `end`
209
+ * beat the moment it settled — so a consumer derives per-call duration (`end.at − start.at`) and
210
+ * inter-call idle (an `end.at` → the NEXT `start.at`). Additive/optional: frames recorded by older
211
+ * producers simply lack it (a consumer must treat absence as "unknown", never as 0). */
212
+ at?: number;
213
+ /** design/99 MF-W: a SHORT primary-arg summary for the monitor's `Read(path)` / `Bash(grep …)` display — the
214
+ * single most salient input ABBREVIATED (command→name, path→basename, url→origin+path) + SECRET-SCRUBBED +
215
+ * truncated (~80 code points). Set on `phase: "start"`. NOT the full args object, and NOT `redactHostLeaks`
216
+ * (that over-redacts the path the monitor needs); the abbreviation + scrub is the leak boundary since the value
217
+ * is persisted + SSE-forwarded. See `primaryActivityArg`. */
218
+ arg?: string;
219
+ /** Set on `phase: "end"` — whether the tool call errored. */
220
+ isError?: boolean;
221
+ }
222
+ /**
223
+ * design/171 §5.2/§6.2 — which carrier a piece of human input arrived on. `"external"` = the
224
+ * notify() event lane; `"system"` = NOT human input (machine-authored text riding a human-input
225
+ * code path) — the speaker projection is a guaranteed no-op for it.
226
+ */
227
+ export type HumanInputSource = "objective" | "steer" | "next_turn" | "wake" | "external" | "system";
228
+ /** design/171 §6.2 — how a human input was disposed of when its event was emitted:
229
+ * `"applied"` = delivered into a model turn; `"queued"` = accepted, awaiting the next turn
230
+ * boundary; `"parked_for_wake"` = parked for the session's next run (torn-down lane);
231
+ * `"blocked"` (design/373 §4.3, additive — closed-set add, consumers named in the ship post) =
232
+ * a PARKED entry was withheld at resume redelivery — the screen runs on EVERY resume kind that
233
+ * drains parked steers (policy_ask / dry_run_review / plan_review / resource_limit, wake
234
+ * included) — by the deployment's `userPromptSubmit` screen (block verdict, or a fail-closed
235
+ * non-answer/crash): the row was consumed, the frame never reached the model, and the sibling
236
+ * `steering.parked_input_blocked` notice names the same inputId — "park 时收下、redeliver 时被筛"
237
+ * is auditable, never a silent disappearance. */
238
+ export type HumanInputDelivery = "applied" | "queued" | "parked_for_wake" | "blocked";
239
+ /**
240
+ * The fleet-task kinds a DELEGATED run can honestly claim, derived from (never a second spelling of)
241
+ * {@link TaskNotificationPayload}'s `task_type` vocabulary — the same axis a consumer already keys
242
+ * notifications on. The other members of that union name non-agent lanes (a background shell command,
243
+ * a monitor, an externally-produced notification), which no delegated run is; deriving rather than
244
+ * re-listing is what keeps the two faces from drifting into two vocabularies.
245
+ */
246
+ export type DelegationTaskType = Extract<TaskNotificationPayload["task_type"], "background_agent" | "workflow">;
247
+ /** Streaming events surfaced from a running task (for live UIs). */
248
+ export type TaskEvent = ({
249
+ type: "text_delta";
250
+ delta: string;
251
+ } & TaskEventIdentity) | ({
252
+ /**
253
+ * #447 — the assistant's streaming PROSE SEGMENT is COMPLETE: the model closed the text content
254
+ * block whose bytes just streamed as `text_delta`s. This is the explicit segment boundary a
255
+ * REMOTE consumer needs so it never has to guess segment ends from wire silence (the idle-flush
256
+ * heuristic this retires cut one slow-model reply into N fragments). CC-aligned: CC's agent
257
+ * stream yields each finished content block as its own assistant-message unit at the provider's
258
+ * `content_block_stop` — block completion IS the segmentation signal there; a single-process
259
+ * consumer reads it off the provider stream natively, and this event is that same boundary
260
+ * surfaced on the TaskEvent wire (CC pays the byte cost of re-carrying the block at the stop;
261
+ * so does this event's `content`).
262
+ *
263
+ * `content` = the authoritative FULL text of the completed segment (byte-equal to that
264
+ * segment's accumulated deltas, from the brain's own accumulation) — a consumer commits the
265
+ * segment from it instead of trusting its own delta stitching. UNTRUSTED model output for
266
+ * display only, same contract as `text_delta`. Emitted only for a segment that holds bytes: an
267
+ * empty text block closes silently (a boundary with no segment would render phantom rows).
268
+ *
269
+ * Additive + ignorable. HONEST ABSENCE: the frame exists only when the serving Brain reports
270
+ * block ends (`text_end` on its event stream — all three first-party brains do; a custom brain
271
+ * that never emits them yields a wire without this frame). A consumer treats per-segment
272
+ * presence as the signal and falls back to its own heuristic only on streams that carry none —
273
+ * absence is "unreported", never "the segment did not end".
274
+ */
275
+ type: "text_end";
276
+ content: string;
277
+ } & TaskEventIdentity) | ({
278
+ type: "reasoning_delta";
279
+ delta: string;
280
+ } & TaskEventIdentity) | ({
281
+ type: "tool_start";
282
+ toolCallId: string;
283
+ toolName: string; /** {@link ToolSpec.label} when distinct from `toolName` (design/71 liveness); omitted otherwise. */
284
+ label?: string;
285
+ args: unknown;
286
+ } & TaskEventIdentity) | ({
287
+ type: "tool_end";
288
+ toolCallId: string;
289
+ toolName: string;
290
+ /** {@link ToolSpec.label} when distinct from `toolName` (design/71 liveness); omitted otherwise. */
291
+ label?: string;
292
+ isError: boolean;
293
+ /**
294
+ * design/99 §E1 — the tool's MODEL-FACING result content, surfaced for a live UI
295
+ * that renders tool output. It is the harness `AgentToolResult.content` the mapper already produced
296
+ * (previously dropped here); the internal `details` payload is NOT exposed (e.g. a sub-agent tool's
297
+ * `details` is the entire child `TaskResult`).
298
+ *
299
+ * **Type is `unknown` and intentionally non-uniform** — within size it passes through AS-IS to keep
300
+ * structure a UI wants (a `string`, or `(TextContent|ImageContent)[]` blocks); when the serialized body
301
+ * exceeds the core size cap it degrades to a single TRUNCATED STRING (see {@link truncated}). A consumer
302
+ * must handle both shapes.
303
+ *
304
+ * **UNTRUSTED RAW + for OBSERVABILITY ONLY**: it is external tool output (file reads, shell stderr, API
305
+ * bodies) and may carry secrets/paths/PII — core bounds only its SIZE; a consumer MUST redact and MAY
306
+ * further bound before it persists or displays it (same contract as `tool_start.args`). It is for a live
307
+ * UI / trace, NOT for re-feeding to a model: a truncated body would mislead an LLM, and the model already
308
+ * has the full, untruncated result in its own context — build model prompts from the session, never by
309
+ * replaying this field. Absent when the tool produced no content body.
310
+ */
311
+ output?: unknown;
312
+ /**
313
+ * design/116 (b) — the tool's CC-shaped `toolUseResult` facts (`details.type` ∈ the known card
314
+ * set: edit/create/update/bash/text/grep/glob/notebook-edit/notebook/file_unchanged/mcp/image/
315
+ * document/worktree/todo/task/task-list/task-output/task-stop/agent/fork/workflow-run/web-fetch/
316
+ * web-search/cron-create/cron-delete/cron-list/tool-search/repo-map/monitor-start/
317
+ * enter-plan-mode/exit-plan-mode/report-findings/schedule-wakeup/send-message/agent-transcript/
318
+ * a2a/path_not_in_root/readonly_out_of_root), surfaced on the live wire so a CC-faithful shell
319
+ * renders rich cards (red/green diff, stdout panes, file cards) without re-parsing the
320
+ * model-facing string. Absent when the tool carries no card shape or when the serialized card
321
+ * exceeds the size bound (omitted whole; a consumer falls back to `output`).
322
+ * UNTRUSTED RAW like {@link output}: a consumer MUST redact before persisting or forwarding.
323
+ *
324
+ * **The card set is a two-way contract** (structured-card audit, 2026-08-05): every entry has a tool
325
+ * that mints it and every minted card is registered, so a consumer switching on `details.type`
326
+ * gets neither a promise nothing keeps nor a card it was never told about. That pass ADDED
327
+ * report-findings / schedule-wakeup / send-message / agent-transcript / a2a / document /
328
+ * readonly_out_of_root and the completed **agent** shape (see below); it REMOVED multiedit /
329
+ * memory-saved / memory-recall, three words with no producer anywhere in core (a batch MultiEdit
330
+ * replay mints `type:"edit"` with an `edits[]` array; core mounts no memory tool at all).
331
+ *
332
+ * **Leak boundary**: the allowlist gates by `type`, so what rides is whatever the minting tool put
333
+ * in the object — which makes each mint site responsible for its own field set. The Agent family's
334
+ * completed card is therefore built from an EXPLICIT whitelist rather than a child-`TaskResult`
335
+ * spread: a `TaskResult` can carry `checkpointToken` (the resume CAPABILITY) on the
336
+ * `unexpected.suspended` terminal, and that field, with `checkpointGate`, never leaves the process.
337
+ *
338
+ * **双面契约(CC 2.1.207 @765495 成文,`tool_use_result` describe;CC207-CORE-SWEEP P2-3)** — a tool
339
+ * result has TWO faces that never substitute for each other:
340
+ * - **模型面** = {@link output} / the tool_result text: the string the MODEL reads. It may carry
341
+ * model-directed framing — e.g. a delegation tool's trailer (agentId / usage lines the model uses to
342
+ * address SendMessage or budget itself). That framing is FOR THE MODEL; it is not a host data format.
343
+ * - **宿主面** = this field: the tool's structured Output object, per-tool shaped, keyed by the matching
344
+ * tool call's name. For a delegation/Agent-family tool the completed shape is the subagent's final
345
+ * report WITHOUT the model-directed trailer, plus run totals (CC names it `AgentToolCompletedOutput`).
346
+ * Both Agent shapes ride `type:"agent"` and are told apart by `status`: `"async_launched"` is the
347
+ * background launch RECEIPT (task_id / description / prompt), any other value is the completed
348
+ * landing report (`result` / `stats` / `resolvedModel` / `toolStats` / kept `worktreePath` /
349
+ * failure attribution). Branch on `status`, never on which keys happen to be present. (Until
350
+ * 2026-08-05 only the receipt actually arrived — the completed delivery carried no `type` and was
351
+ * dropped by the allowlist, so this paragraph described something a host could not observe.)
352
+ * A host/shell MUST render from `structured` and MUST NOT parse the model-facing text back into data
353
+ * (trailer wording is model-audience prose and may change without notice); symmetrically, core never
354
+ * strips the trailer from the model face just because the structured face exists — the model still needs
355
+ * it in-context. When `structured` is absent, the honest fallback is to display `output` as opaque text,
356
+ * not to scrape it.
357
+ */
358
+ structured?: unknown;
359
+ /**
360
+ * Present iff {@link isError} is true AND the harness result's `details` carried a string
361
+ * discriminator — read as `details.code` first, falling back to `details.errorKind` (each must be
362
+ * a string; `code` wins when both are present, as the deliberate tool-chosen spelling). The
363
+ * `errorKind` leg is what makes a LOOP-THROWN error's frame classifiable: the loop's thrown-error
364
+ * fold and the resume legs write the discriminator under that name. Lifted so a consumer never
365
+ * has to parse the (contract-stable) result text. Engine-minted vocabulary today includes
366
+ * `"tool.not_found"` (unknown tool name), `"gate.parked"` (an abort short-circuit contaminated
367
+ * this call because a durable gate parked the batch — the "Operation aborted" family), and, for a
368
+ * failed MCP tool call, the MCP failure class — see {@link import("./mcp-failure.js").McpFailure.kind}
369
+ * for the words (the same closed set the per-server status and the wiring manifest's `mcp[]` carry;
370
+ * this frame only transports it). Additive: absent on error frames minted before this field
371
+ * existed, and on error results whose details carry no string discriminator under either name.
372
+ */
373
+ errorCode?: string;
374
+ /**
375
+ * @contract tool_end.delivered — WHETHER THE REQUEST REACHED ITS SERVER, when this frame closes a
376
+ * failed call whose result carried the MCP failure record: the closed
377
+ * {@link import("./mcp-failure.js").McpDelivered} (`"yes"` / `"no"` / `"unknown"`), lifted from
378
+ * `details.delivered` beside {@link errorCode} and read TOGETHER with it — the same `errorCode`
379
+ * (`connection_closed`) is safe to retry at `"no"` (nothing was sent) and must be verified first at
380
+ * `"unknown"` (the request may have executed). The meaning of each word is owned by
381
+ * {@link import("./mcp-failure.js").McpFailure.delivered}; this frame only transports it. Screened
382
+ * against the closed set (an out-of-vocabulary value is not lifted) and never derived from the
383
+ * code or the text. ABSENT on every frame whose result carried no such verdict — every executed
384
+ * call and every non-MCP failure — and a consumer must not read a semantic out of the absence.
385
+ */
386
+ delivered?: import("./mcp-failure.js").McpDelivered;
387
+ /**
388
+ * WHICH call the gate was holding, when this frame is a park-contamination frame — the tool call
389
+ * id of the gated call. Three conditions, ALL required: `errorCode === "gate.parked"`, the run is
390
+ * tearing down behind a committed park, and that park binds a tool call at all.
391
+ *
392
+ * **WHERE IT COMES FROM — never lifted from the result**, and deliberately unlike its neighbour
393
+ * {@link errorCode}: that one IS the result's own `details.code`, i.e. tool-authored, because a
394
+ * tool classifying its own failure is bounded self-description. This field is an assertion about a
395
+ * DIFFERENT call, so a tool able to author it could point an approval UI at a call nobody is
396
+ * holding. It is therefore engine-minted — read from the run's committed park holder and handed to
397
+ * the frame projection as a parameter, the same posture (and the same reason) as {@link settledBy}.
398
+ * `errorCode` only participates in selecting WHETHER the id is placed; it never supplies the
399
+ * value, and on its own it is not enough — the run must also be aborting behind a real park, which
400
+ * is why a tool declaring `gate.parked` on its own failure cannot conjure this field.
401
+ *
402
+ * Why it is on the frame: a durable gate parks ONE call and the loop then short-circuits every
403
+ * sibling in the batch with an identical "Operation aborted" body, so `gate.parked` alone says
404
+ * "something parked this batch" without saying WHAT. A consumer wanting to render "waiting on
405
+ * <the gated call>" beside the collateral frames otherwise has to re-derive the pair from batch
406
+ * adjacency and timing — an inference that is wrong exactly when it matters (a batch with more
407
+ * than one ask candidate, a reordered stream).
408
+ *
409
+ * ONLY EVER STRUCTURALLY PROVEN, never guessed: the id is read off the committed checkpoint's
410
+ * pending tool call, so it is the same value the checkpoint parks on and the same value the
411
+ * decide/resume lane answers about. It is therefore ABSENT — not approximated — for the park
412
+ * kinds that hold no call (a resource-limit slice, a plan review), and absent for every abort
413
+ * that is not a gate park at all (user interrupt, timeout, walltime): those frames are
414
+ * byte-unchanged. Absence means "core cannot prove a causal call", never "there wasn't one".
415
+ */
416
+ gatedCallId?: string;
417
+ /**
418
+ * The gate's record of the pass that admitted or refused this call, when the call went through the
419
+ * tool gate at all ({@link import("./gate-outcome.js").GateOutcome}): `disposition` (allowed, or
420
+ * denied by which LAYER), `settlement` (how the ask this pass consumed ENDED — a person's yes or
421
+ * no with the channel's attribution and, on a bare "no", no note; a window that elapsed and whose;
422
+ * nobody reachable; the wait aborted…) and `origin` (who ASKED). It is the SAME object the
423
+ * `permissionDenied` observer received and the engine's per-call sideband recorded — one mint, three
424
+ * faces — so a renderer never branches on prose to tell "a person refused" from "the window closed".
425
+ *
426
+ * WHERE IT COMES FROM — never inferred here: the gate mints it at its exit, the decide lane mints it
427
+ * from the host's decision facts joined with the row's persisted origin. Core does not derive it
428
+ * from `isError`, from the result, or from anything a tool or a post-tool hook can write.
429
+ *
430
+ * ABSENT on every frame of a call the gate never saw (a tool call on an unarmed gate, a deferred
431
+ * re-issue, a reconcile-recovered orphan). A gated call's frame ALWAYS carries it — an absence is
432
+ * not a fact about the call, and a consumer must not read one out of it.
433
+ */
434
+ gate?: import("./gate-outcome.js").GateOutcome;
435
+ /**
436
+ * design/99 §E1 — `true` when {@link output} was SIZE-bounded by core (the full body exceeded the cap and
437
+ * was degraded to a truncated string). Lets a consumer detect truncation programmatically instead of
438
+ * parsing the `…[+N chars truncated]` marker out of the string. Absent/false = `output` is the complete
439
+ * content body. (The full untruncated result still lives in the model's context / `ToolResultStore`.)
440
+ */
441
+ truncated?: boolean;
442
+ /**
443
+ * RB-210 — present iff {@link truncated}: the TRUE original size (characters) of
444
+ * the content this event's {@link output} was degraded from — for a string result, the string's own
445
+ * length; for a non-string result (a blocks array), the SUM of each block's own true size (a text
446
+ * block's `.text.length`; an image/document block's `.data.length`, i.e. its base64 payload) — never
447
+ * `JSON.stringify(content).length`, which is polluted by two different kinds of noise: structural
448
+ * overhead (field-name/punctuation bytes that aren't content at all) AND, for text containing quotes/
449
+ * backslashes/control characters, escape inflation (the same class of bug this field was created to
450
+ * fix in the first place — a real result: 17298 reported vs 13696 true content characters — reappearing
451
+ * in a subtler form if the true count were ever taken from the serialized form instead of the blocks).
452
+ *
453
+ * **Not always arithmetically tied to the marker's own `N`**: the marker counts how much MORE of the
454
+ * shown raw-text rendering exists beyond the cut point; `totalChars` counts the true size of the
455
+ * ORIGINAL value, which for mixed content includes non-text blocks (e.g. an inlined image) that
456
+ * `output` never renders as text at all. The two can legitimately diverge — `totalChars` may run far
457
+ * ahead of the cap plus the marker's `N` — and that gap is itself the honest signal that non-text bulk
458
+ * was dropped, not an inconsistency to paper over.
459
+ */
460
+ totalChars?: number;
461
+ } & TaskEventIdentity) | ({
462
+ /**
463
+ * RB-126 — context occupancy at a compaction boundary, emitted on EVERY
464
+ * boundary (not only when a compaction fires). The three numbers are the engine's own trigger
465
+ * inputs, verbatim: `usedTokens > compactAtTokens` IS the predicate `shouldCompact` evaluates, so a
466
+ * consumer's "context is N% full" indicator and the moment compaction actually fires can never
467
+ * disagree. Re-deriving the numerator downstream is precisely what this exists to prevent.
468
+ *
469
+ * Deliberately NOT a field on `turn_end`: the boundary check runs AFTER that event, over the
470
+ * post-flush message set, so a value attached there would be a different (earlier) measurement
471
+ * wearing the same name. `windowTokens` is the AUTOCOMPACT window — a model declaring
472
+ * `autoCompactTokens` keeps a LARGER physical request window — so do not present it as "the
473
+ * model's context size".
474
+ */
475
+ type: "context_usage";
476
+ usedTokens: number;
477
+ windowTokens: number;
478
+ compactAtTokens: number;
479
+ } & TaskEventIdentity) | ({
480
+ type: "turn_end";
481
+ /**
482
+ * Per-turn token usage + cost (the turn that just ended), for live per-turn accounting / a trace UI
483
+ * (service [41]). Re-priced in core from injected pricing (authoritative, like `TaskResult.stats`).
484
+ * Absent only if the turn produced no assistant usage.
485
+ *
486
+ * RB-457-a: `inputTokens` is the cache-MISS count (Anthropic-protocol shape, matches
487
+ * `stats.promptTokens` / the tracer's `brain.call.promptTokens`); `totalInputTokens` is the
488
+ * cache-INCLUSIVE total (`inputTokens + cacheReadTokens + cacheWriteTokens`) that cost is computed
489
+ * from. Sum `totalInputTokens` for context/window/spend views; read `inputTokens` only where the
490
+ * protocol's `input_tokens` is meant. Up to 2.13.x `inputTokens` carried the total.
491
+ */
492
+ usage?: {
493
+ /** Cache-MISS input tokens for this turn (the Anthropic protocol's `input_tokens` meaning;
494
+ * matches `stats.promptTokens` / `brain.call.promptTokens`). **Up to 2.13.x this field
495
+ * carried the cache-INCLUSIVE total** (RB-457-a, BREAKING at 3.0.0) — that value now lives
496
+ * in `totalInputTokens`. Read this only where the protocol's `input_tokens` is meant. */
497
+ inputTokens: number;
498
+ /** Cache-INCLUSIVE input total for this turn (`inputTokens + cacheReadTokens +
499
+ * cacheWriteTokens`) — what cost is computed from. Sum THIS for context/window/spend
500
+ * views; it equals what `inputTokens` carried up to 2.13.x. */
501
+ totalInputTokens: number;
502
+ outputTokens: number;
503
+ cacheReadTokens: number;
504
+ cacheWriteTokens: number;
505
+ costMicroUsd: number;
506
+ };
507
+ /** TB 尸检 T1-5: set when the turn ended WITHOUT a provider usage frame (e.g. a degenerate-repetition
508
+ * cutoff cancelled the stream before usage arrived). Consumers must treat the missing usage as
509
+ * UNKNOWN — not zero (the provider still served the request; `stats` under-counts it honestly rather
510
+ * than polluting the single-source accounting with an estimate). */
511
+ usageMissing?: true;
512
+ /** TB telemetry B1: the NORMALIZED finish reason of the turn's LAST model call —
513
+ * the engine's `AssistantMessage.stopReason` (`StopReason`: `"stop"`/`"length"`/`"toolUse"`/
514
+ * `"error"`/`"aborted"`). This is post-normalization: e.g. Anthropic `end_turn`/`stop_sequence`
515
+ * both fold to `"stop"`, `max_tokens`→`"length"`; the raw provider reason is NOT preserved here.
516
+ * The machine signal for "was this turn cut by max_tokens" (`"length"`) — previously stream+trace
517
+ * double-blind, which made P1 cap-binding unjudgeable from artifacts. Absent when the turn produced
518
+ * no assistant message. Typed `string` (not the internal `StopReason`) to keep the engine's union
519
+ * private, but the value set is exactly those five tokens. */
520
+ stopReason?: string;
521
+ } & TaskEventIdentity) | ({
522
+ /** 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
523
+ * auto-compaction), `"manual"` (the `TaskStream.compact` / `/compact` path), or `"forced"` (RB-398-b
524
+ * the prompt-too-long recovery and trim-pressure lanes — compaction was not optional; the
525
+ * hook layer's `PreCompactContext.trigger` always had this third state and the wire now carries the
526
+ * SAME value the hooks saw instead of flattening it to `"auto"`. Additive value space — a consumer
527
+ * switching on auto/manual keeps its exact old behavior). `tokensBefore` = the
528
+ * pre-compaction context size. CC parity = `SDKCompactBoundary{trigger, pre_tokens, preserved_segment}`. */
529
+ type: "compacted";
530
+ trigger: "auto" | "manual" | "forced";
531
+ tokensBefore: number;
532
+ /** TB telemetry B4: post-compaction context size in the TRIGGER-GATE coordinate
533
+ * (`postTriggerTokens` = estimate + anchor-less overhead). Pairs with {@link triggerTokensBefore}
534
+ * — NOT with `tokensBefore` (which is the CC `pre_tokens` value in prepareCompaction's coordinate,
535
+ * lacking the overhead term; subtracting across the two coordinates would mis-state the delta,
536
+ * the audit-B-1 / P-13 class). Absent when the post measurement failed (best-effort, never fails a
537
+ * successful compaction). */
538
+ tokensAfter?: number;
539
+ /** TB telemetry B4: pre-compaction size in the SAME trigger-gate coordinate as {@link tokensAfter}
540
+ * (`triggerTokens` — the exact quantity the compaction gate compared). `triggerTokensBefore −
541
+ * tokensAfter` is the self-consistent freed delta. Absent on the legacy emitter. */
542
+ triggerTokensBefore?: number;
543
+ /** TB telemetry B4: wall time of the whole compaction pass (summary call included) — what the
544
+ * boundary pause actually cost. Absent on legacy emitters. */
545
+ durationMs?: number;
546
+ /** CC `SDKCompactBoundary.preserved_segment` parity: `firstKeptEntryId` is the session-tree entry id where
547
+ * the surviving KEPT TAIL begins after this compaction. A consumer maps it to a wire message via the
548
+ * `entryId` carried on `message_committed` (message-identity Phase 1) — render the compaction divider just
549
+ * before that message + relink the preserved tail. (The eventId↔entryId side map service keeps for resumeAt
550
+ * also resolves it, but is no longer required.) Absent on a compaction that reported no kept-tail floor. */
551
+ preserved_segment?: {
552
+ firstKeptEntryId: string;
553
+ };
554
+ /** CC post-compact file-restore parity (2026-07-03): the working files the engine
555
+ * re-read into the summary (attachWorkingFiles), attachment order. A shell renders CC's
556
+ * `Read {path} (…)` cards under the compaction divider from this. Absent when the feature is
557
+ * off or nothing was attached. `truncated` = the content was clipped at the per-file cap. */
558
+ attachedFiles?: Array<{
559
+ path: string;
560
+ chars: number;
561
+ truncated: boolean;
562
+ }>;
563
+ /** design/145 §3 (additive): this compaction's summary call FELL BACK from the independent
564
+ * `compactionModel` to the main model — the small window would have truncated more than
565
+ * `compaction.clampTolerance` of the conversation. Mirror of `compaction.model_fallback`. */
566
+ modelFallback?: true;
567
+ /** design/145 §3 (additive): why the fallback happened ("window" is the only reason today). */
568
+ fallbackReason?: "window";
569
+ /** design/145 §3 (additive): the summary INPUT was clamp-truncated on the compaction model by
570
+ * this ratio (0..1) — set when the budget gate denied the fallback, or when the
571
+ * truncation stayed within tolerance (bounded loss, disclosed either way). */
572
+ clampedRatio?: number;
573
+ /** design/145 §3 (additive): "budget" = the gate that denied the fallback;
574
+ * "tolerance" = within-`clampTolerance` ordinary truncation. */
575
+ clampReason?: "budget" | "tolerance";
576
+ /** 压缩子阶段遥测 (1.283 批 G / design/145 §6 正交后续批, additive): sub-phase wall-time split
577
+ * of THIS pass, mirrored 1:1 from `maybeCompact().phaseDurations` (single source: the
578
+ * `compaction.phase_timings` trace frame minus its envelope). Invariant: prepareMs +
579
+ * (summaryMs ?? 0) + persistMs === {@link durationMs} (exact tiling; `summaryMs` absent on a
580
+ * provider-reuse pass — no summary LLM call happened). Absent on legacy emitters. */
581
+ phaseDurations?: import("./auto-compaction.js").CompactionPhaseDurations;
582
+ } & TaskEventIdentity) | ({
583
+ /**
584
+ * RB-398-b④: a compaction pass concluded WITHOUT landing — the non-landing outcomes
585
+ * used to exist only as `compaction.*` trace frames, so a stream-only consumer watched a
586
+ * context quietly stop being managed (breaker opens, rapid-refill disable, hook blocks —
587
+ * all invisible). One lightweight additive event, emitted at the same sites as the trace
588
+ * frames; the SUCCESS outcome stays on the `compacted` event (this event never carries it,
589
+ * so a consumer treating `compacted` as the only success signal is unchanged).
590
+ * `outcome` reuses the {@link CompactOutcome} vocabulary plus `"suppressed"` (over-threshold
591
+ * pass suppressed by the anti-thrash floor — a state manual compact can never reach, hence
592
+ * outside `CompactOutcome`):
593
+ * - `"failed"` — a burned summary attempt failed, or the open breaker drained a manual
594
+ * request without an attempt (`reason` says which).
595
+ * - `"blocked"` — a `preCompact` hook deliberately blocked the pass.
596
+ * - `"suppressed"` — wanted to fire, held under the §25.2 anti-thrash floor.
597
+ * - `"noop"` — the pass ran but found nothing to compact (no valid cut point/empty history).
598
+ * - `"disabled"` — an explicit (manual/forced) request was refused because compaction is off,
599
+ * or the rapid-refill breaker disabled compaction mid-task (`reason` says which). A spec
600
+ * that statically disables compaction does NOT produce one of these per boundary (that
601
+ * would be per-boundary noise about a static configuration, not a state change).
602
+ * - `"mooted"` — a manual request was processed but no boundary could serve it (dying loop /
603
+ * caller cancel).
604
+ */
605
+ type: "compaction_outcome";
606
+ outcome: Exclude<CompactOutcome, "compacted"> | "suppressed";
607
+ /** Same three-state vocabulary (and the same value) the pass handed the `preCompact` hook. */
608
+ trigger: "auto" | "manual" | "forced";
609
+ /** Optional human-readable detail (mirrors the trace frame's reason where one exists). */
610
+ reason?: string;
611
+ } & TaskEventIdentity) | ({
612
+ /**
613
+ * TB telemetry B3: the engine injected a SYSTEM steering message (a deadline nudge
614
+ * or the design/130 P2 graceful-finalize instruction) into the model's next turn. Previously these
615
+ * went through `harness.steer()` with no stream echo — a consumer literally could not tell whether
616
+ * the finalize fired (the refute-130 attribution blind spot). Echo only; the model-facing message
617
+ * is unchanged. `preview` is the injected text clipped to a display bound.
618
+ *
619
+ * design/133 (LOW-11/F11): the turn-boundary attachment producers ride the same echo — sources
620
+ * `todo_reminder` / `task_reminder` / `changed_files` / `plan_mode` and the G1/续批 members
621
+ * `background_tasks` / `tools_delta` / `agent_listing` / `skills_listing` / `mcp_instructions`
622
+ * (one event per attachment; a multi-attachment boundary still coalesces into ONE steer message
623
+ * on the model lane). [c209-C]: the listing family's FIRST-FRAME deliveries (initial roster /
624
+ * `<skills>` block riding the first user message, not a steer) emit the same echo frames.
625
+ *
626
+ * `git_status` (env-tail migration, additive member): the git-status frame — the turn-dynamic
627
+ * git facts' carrier since they left the system prompt. Emitted at the frame's append RECEIPT
628
+ * (first-frame / resume-continuation / compaction re-assert deliveries alike). Its `preview`
629
+ * is a CONSTANT wording on purpose (never frame bytes): branch names and status text are
630
+ * repo-controlled and must not enter the event telemetry plane through this echo.
631
+ */
632
+ type: "steering_injected";
633
+ source: "limit_approach" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "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";
634
+ preview: string;
635
+ } & TaskEventIdentity) | ({
636
+ /**
637
+ * design/121 (CC 198 `<new-diagnostics>` parity): NEW LSP diagnostics that appeared after the
638
+ * agent's edits, drained at a turn boundary. The SAME data was injected into the model as a
639
+ * `<new-diagnostics>` context message; this frame is the shell's render source (CC's collapsed
640
+ * `Found N new diagnostic issues in M files (ctrl+o to expand)` row). UNTRUSTED RAW (language-
641
+ * server output over workspace files) — a consumer redacts/bounds before persisting (§E1).
642
+ */
643
+ type: "diagnostics";
644
+ files: Array<{
645
+ uri: string;
646
+ diagnostics: Array<{
647
+ message: string;
648
+ /** LSP severity: 1=Error 2=Warning 3=Information 4=Hint. */
649
+ severity?: number;
650
+ range?: {
651
+ start: {
652
+ line: number;
653
+ character: number;
654
+ };
655
+ end?: {
656
+ line: number;
657
+ character: number;
658
+ };
659
+ };
660
+ code?: string | number;
661
+ source?: string;
662
+ }>;
663
+ }>;
664
+ isNew: true;
665
+ } & TaskEventIdentity) | ({
666
+ /**
667
+ * message-identity Phase 1 (the CC `SDKCompactBoundary` parity enabler): a persisted session-tree entry was
668
+ * just COMMITTED for a RENDERABLE message the consumer has already streamed. `entryId` = `SessionTreeEntry.id`
669
+ * (uuidv7, persisted, replica-stable) — the SAME id space as `compacted.preserved_segment.firstKeptEntryId`,
670
+ * E18 `resumeAt`, and the design/381 `rewindFilesTo`. `role` + (for a tool result) `toolCallId` let a consumer bind it to
671
+ * the message it rendered: a tool result by its `toolCallId`; an assistant/user message by stream order (this
672
+ * is the terminal boundary for the content just streamed, after its deltas and before its tool results). A
673
+ * consumer builds its own `entryId → message` map from these to resolve `preserved_segment` WITHOUT the
674
+ * eventId↔entryId side map. Terminal per-message boundary; additive + ignorable. ABSENT for a message that did
675
+ * not persist (an empty-failure assistant) and for non-message entries (a compaction summary / model_change —
676
+ * never a `message_committed`). The engine still holds no eventId→entryId map (the deployment owns it); this
677
+ * only pushes the persisted id onto the wire, the constitution's primitive/profile split (cf. §E2 `eventId`).
678
+ */
679
+ type: "message_committed";
680
+ entryId: string;
681
+ role: "user" | "assistant" | "toolResult";
682
+ /** Present iff `role === "toolResult"`: the exact join key to the `tool_start`/`tool_end` already rendered. */
683
+ toolCallId?: string;
684
+ } & TaskEventIdentity) | ({
685
+ /**
686
+ * design/171 §5.5/§6.2 — the human-input LIFECYCLE arm: one frame per human input this run
687
+ * accepts (WHO fed the run WHAT, per carrier). Additive + ignorable like its siblings.
688
+ * Deliberately NOT folded into `message_committed`: that event only fires for messages that
689
+ * PERSISTED, while a human input's lifecycle (queued / parked) exists before — and sometimes
690
+ * without — a committed message. Constructed via `buildHumanInputEvent` (mints the inputId,
691
+ * materializes no absent optional keys); `HumanInputEvent` is the Extract-derived alias.
692
+ *
693
+ * Attribution semantics are THREE-VALUED, never collapsed:
694
+ * - `actor` ABSENT ⇒ the caller supplied none (anonymous — every pre-171 caller);
695
+ * - `actor.hostAsserted === false` ⇒ an UNVERIFIED self-claim the host chose to carry;
696
+ * - an OLD event stream that predates this arm simply has no `human_input` frames at all —
697
+ * a consumer distinguishes "attribution unknowable (pre-arm producer)" by the emitting
698
+ * core's version, never by guessing from field absence on some other event.
699
+ */
700
+ type: "human_input";
701
+ /** Correlation/idempotency key. Caller-supplied when the ingress already minted a message id
702
+ * (the parked-steer queue's `inputId` is THE same key); a uuidv7 otherwise. */
703
+ inputId: string;
704
+ /** Ordinal of this input among the run leg's accepted human inputs (1-based, drain order for
705
+ * parked entries). Leg-scoped: a resumed leg restarts at 1 — `inputId` is the global key and
706
+ * stream order the global order; a serving layer needing a session-global ordinal mints its
707
+ * own. */
708
+ sessionSeq: number;
709
+ /** Which concrete carrier accepted the input. OPEN set — a serving layer self-reports its own
710
+ * carriers (question note / elicitation / decide reason / …) through the same arm; the
711
+ * core's five report their {@link HumanInputSource} name. */
712
+ carrier: string;
713
+ source: HumanInputSource;
714
+ /** Who asserted the actor's identity (Slack app id, SSO IdP …) — mirrored from `actor.issuer`
715
+ * or supplied alone when the host can name its ingress but not the subject. */
716
+ issuer?: string;
717
+ actor?: ActorAssertion;
718
+ delivery: HumanInputDelivery;
719
+ /** Plaintext principal correlation key (deliberately not hashed — the audit consumer joins
720
+ * on it). */
721
+ principal?: string;
722
+ /** The persisted session-tree entry the input landed as, WHEN the emitter knows it (a
723
+ * serving layer replaying the ledger); the core's live emissions precede the commit and
724
+ * omit it — consumers there join via `message_committed` + stream order. */
725
+ entryId?: string;
726
+ } & TaskEventIdentity) | ({
727
+ /**
728
+ * design/99 §E3/§E10 — a live BRAIN-LAYER status the brain would otherwise absorb
729
+ * silently (a 429 rate-limit / a 5xx or network retry / a reconnect / an open circuit breaker). Emitted
730
+ * the moment the brain decides to wait/retry, carrying the graduated {@link BrainStatusPhase} + an
731
+ * optional `retryInSec` so a UI can show "rate limited, retrying in Ns". EPHEMERAL — a pure liveness
732
+ * signal, never persisted or replayed on resume. The phase and the hint are provider-neutral (no
733
+ * provider taxonomy, no `stop_reason`); the failing attempt's HTTP status rides its own seat in the
734
+ * retry context only — see {@link BrainStatus.errorStatus}.
735
+ * SCOPE: reflects THIS task's own (top-level) brain calls. A delegated sub-agent's brain liveness routes
736
+ * to the SUB-AGENT's own stream (isolated, like every child event — §E2); a background internal brain
737
+ * call (memory recall/consolidation) is intentionally not surfaced.
738
+ */
739
+ type: "status";
740
+ } & BrainStatus & TaskEventIdentity) | ({
741
+ /**
742
+ * design/115 P2 core slice — SDK-side projection of a background task terminal notification. The model-facing
743
+ * projection is the sibling `<task-notification>` XML injected through the runner's system-injection lane when
744
+ * the originating harness is still running; durable idle wake/resume is a deployment transport concern.
745
+ */
746
+ type: "task_notification";
747
+ notification: TaskNotificationPayload;
748
+ /** design/373 §3.6 — the frame's injection tier as DELIVERED (additive; consumers tolerate
749
+ * absence): on the live lane, the tier the routing used (`next`/`now` → steer lane, `later`
750
+ * → followUp lane); on the turn-open redelivery lane, the tier the park CARRIED. ABSENT is a
751
+ * fact, not a default: a pre-373 producer event, or a parked record whose tier was not
752
+ * carried (the terminal sweep's re-pend) — never read absence as `"later"`. */
753
+ priority?: import("./task-notification.js").SystemInjectionPriority;
754
+ } & TaskEventIdentity) | ({
755
+ /**
756
+ * design/99 MF-10 — a SUBAGENT PROGRESS TICK: the delegated sub-run's usage ACCRUING
757
+ * over time, emitted at each turn boundary WHILE it runs, so a parent UI animates the Task card's rollup
758
+ * (tokens/tool-uses climbing). Emitted ONLY for a subagent (a task with `parentToolCallId`); a top-level
759
+ * run's own usage is the consumer's own. `status:"running"` mid-run; the terminal usage is the `done` result.
760
+ * Live/ephemeral — NOT persisted (a resumable trace re-derives usage from the result), like `status`.
761
+ * Maps to CC `SDKTaskProgress` (camelCase here → snake_case `task_id`/`total_tokens`/`tool_uses`/`duration_ms`
762
+ * at the SDK/service projection, the same convention as `toolCallId`→`tool_use_id`).
763
+ */
764
+ type: "task_progress";
765
+ taskId: string;
766
+ /**
767
+ * The FLEET-task kind of the run this tick reports on — the type discriminator a consumer needs
768
+ * when it folds progress ticks and task notifications into one ledger. Without it the only
769
+ * available test is "it appeared on the fleet stream, therefore it is an agent", and that
770
+ * inference has already minted an agent-shaped row for a task that was a background shell
771
+ * command. Drawn from {@link TaskNotificationPayload}'s `task_type` vocabulary (no second
772
+ * spelling of one axis) and narrowed to the members a progress tick can honestly claim.
773
+ *
774
+ * Present on every tick from a run that HAS a fleet row: `"background_agent"` for a background
775
+ * delegation child (its registry `a*` row) and `"workflow"` for a workflow-spawned agent (its
776
+ * `wa*` read-face row) — the latter alongside, not in place of, {@link workflowRunId}, which
777
+ * names the owning RUN while this names the KIND. ABSENT is a fact, not missing data: a
778
+ * SYNCHRONOUS delegated child has no fleet row at all. Additive — tolerate absence, and never
779
+ * read absence as "not an agent".
780
+ */
781
+ taskType?: DelegationTaskType;
782
+ /**
783
+ * #258 — the registry row's STOP-CYCLE generation this tick reports from (fresh
784
+ * spawn = 1, every launched revival bumps it), the same counter `TaskNotificationPayload.seq`
785
+ * and `BackgroundChildEvent.seq` already speak — one axis, not a third spelling. It answers
786
+ * the one question a fleet consumer cannot otherwise decide when a frame arrives late: "late
787
+ * first frame of the cycle I know (same value), or a revived run I have not folded yet
788
+ * (higher value)?" Advisory observation, stamped at spawn/revive from the registry's own
789
+ * counter; the settle-time ledger stays the authority. ABSENT is a fact, not a gap: a run
790
+ * with no `a*` registry row (a SYNCHRONOUS delegated child, a workflow `wa*` agent, a
791
+ * top-level run) has no generation concept, and absence must never be read as "cycle 1".
792
+ */
793
+ seq?: number;
794
+ /** Workflow-lane self-identification (field-proven: the SSE-forwarded tick of a
795
+ * WORKFLOW child previously carried a bare uuid with no workflow identity — indistinguishable
796
+ * from an unknown nested subagent; the fleet lane had `wa*`+workflowRunId but this lane had
797
+ * nothing). Present iff the tick bubbled out of a workflow-spawned agent. Additive. */
798
+ workflowRunId?: string;
799
+ /** the workflow agent's display label (opts.label / auto `agent-N`), riding with
800
+ * {@link workflowRunId} for the same self-identification. Additive. */
801
+ workflowAgentLabel?: string;
802
+ /** design/99 (nested-subagent live tree): the spawning run's taskId — present on a DEEPER-than-1 subagent tick
803
+ * (a subagent spawned by another subagent), forwarded via `RunInternals.onForwardEvent` so a UI nests the
804
+ * node directly (this.taskId under parentTaskId) without mapping `parentToolCallId` → owner. The top-level
805
+ * run is known to the consumer; its direct children carry the top's taskId here. Absent on a top-level tick. */
806
+ parentTaskId?: string;
807
+ /** design/99 BC-2 (Service AI [§I]): a short, single-line HUMAN display name for the subagent — its explicit
808
+ * `taskName` or the selected agent-type. So a Fleet child row shows a readable label instead of the raw
809
+ * `taskId` (the stable row identity stays the taskId). ABSENT when the subagent carries neither a taskName
810
+ * nor an agent-type — the child then keeps its taskId (NOT the raw objective, which could leak a delegated
811
+ * secret to a progress-scoped consumer — dual-review Q2). Sanitized (control-char-stripped, length-capped). */
812
+ name?: string;
813
+ /**
814
+ * The model this sub-run was PREPARED with — the resolved id, read off the leg's prepared model
815
+ * at the mint, so it is the same value `TaskResult.model` and the `task.start` trace frame carry.
816
+ * (`task.end` reports the run's totals and has never named a model; there is nothing to join to
817
+ * there.)
818
+ *
819
+ * Why it rides the live lane at all: a delegation may name a tier word (`"sonnet"`), an agent
820
+ * definition's model, or NOTHING (inherit the caller's current model, or fall to the `subagent`
821
+ * role) — every one of those resolves somewhere the consumer cannot see, so a shell badging a
822
+ * running child previously had only the REQUESTED word (or nothing) to render, which is a
823
+ * different claim from what the child runs on. This is the resolved answer, not the request.
824
+ *
825
+ * PREPARED, deliberately, and NOT "whatever is serving this turn" — stated in the first sentence
826
+ * because the difference is observable. A mid-run DEGRADE switch (`spec.limits.degrade`) or a
827
+ * gateway re-route moves the serving model without rewriting this field, exactly as
828
+ * `TaskResult.model` behaves; the switch is announced on its own channel (`TaskResult.degraded`),
829
+ * and the per-call served id lives on the brain-call telemetry. One name, one meaning, across the
830
+ * three faces that use it — at the cost of being the leg's declared model rather than a live one.
831
+ *
832
+ * Present on every tick this build mints (the frame family is subagent-only by construction, so
833
+ * there is no lane where a leg has no prepared model). Declared optional for the ordinary reason:
834
+ * a consumer folding frames from a pre-key producer must keep compiling, and absence there means
835
+ * "this producer did not state it", never "no model".
836
+ */
837
+ model?: string;
838
+ /** The child's most recent tool intent as one human line ("Bash npm test",
839
+ * "Edit src/x.ts") — the SAME source/value as the registry sink tick's `currentAction` (residual
840
+ * observability, lane B), attached to the FORWARDED frame because every client wire projects this
841
+ * TaskEvent, not the sink (a deployment's projection legs otherwise see a permanently-absent field).
842
+ * UNTRUSTED display hint — redact at the projection boundary. Absent until the child runs a tool. */
843
+ currentAction?: string;
844
+ usage: {
845
+ totalTokens: number;
846
+ toolUses: number;
847
+ durationMs: number;
848
+ };
849
+ /** Per-turn ticks are `"running"`. ONE final tick fires at the sub-run's settle with the
850
+ * terminal face (`"completed"` | `"failed"` — timeout/blocked fold to failed; a suspended durable
851
+ * pause emits no terminal tick), so a uuid-lane consumer sees the row finish instead of a bare
852
+ * removal after the last running beat. Additive value space — a consumer switching on "running"
853
+ * keeps its exact old behavior. */
854
+ status: "running" | "completed" | "failed";
855
+ } & TaskEventIdentity) | ({
856
+ /**
857
+ * design/99 §E13 — the task's tracked working directory CHANGED mid-run (a `bash`
858
+ * `cd` moved it). Emitted live after the tool that moved it, carrying the new logical cwd, so a shell UI
859
+ * can update its prompt/path display. The tracked cwd is the RAW logical path (a `cd` through a symlink
860
+ * reports the raw path navigated, not the resolved target). Only fires when a real shell is mounted
861
+ * (absent under `handsReadOnly` / no-exec). The initial cwd is NOT announced (it is the known root); only
862
+ * subsequent changes are. A live observability signal — not persisted, and the tracked cwd RESETS to the
863
+ * root across a suspend/resume (the per-leg execution cwd is not durable today — a pre-existing limitation
864
+ * tracked with the E18/E19 execution-state durability work); within a leg, every `cd` move is reported.
865
+ */
866
+ type: "workspace_changed";
867
+ cwd: string;
868
+ } & TaskEventIdentity) | ({
869
+ /**
870
+ * design/173 §2.4/§8.1 — this leg's EFFECTIVE wiring manifest: the machine-readable
871
+ * disclosure of how the run is assembled around the optional human-interaction seams
872
+ * (ask/question/elicit/park/durability/fleet/governance). Emitted ONCE PER PREPARED LEG
873
+ * (root/child/resume each get their own — per-leg assembly genuinely differs, e.g. the
874
+ * engine-stripped background lanes), after prepare and BEFORE the leg's first model/tool
875
+ * interaction; leg identity rides {@link TaskEventIdentity} plus `manifest.leg.kind`.
876
+ *
877
+ * DISCLOSURE, not verification (§0 guarantee boundary): seam presence/shape as facts —
878
+ * liveness (does the callback actually reach a human) is a round-trip probe's job.
879
+ * HOST/OPERATOR plane: the manifest never enters model context, and the `governance`
880
+ * section carries `audience: "operator"` — a serving layer forwarding this stream to a
881
+ * multi-tenant/shared consumer MUST project that section for operators only (its default,
882
+ * when it does no projection, is to not disclose the section). Additive: a consumer that
883
+ * ignores unknown event types is unchanged; consumers of the manifest itself must ignore
884
+ * unknown FIELDS (`schemaVersion` bumps only on a semantic break).
885
+ */
886
+ type: "wiring_manifest";
887
+ manifest: import("./wiring-manifest.js").WiringManifest;
888
+ } & TaskEventIdentity) | {
889
+ type: "done";
890
+ result: TaskResult;
891
+ };
892
+ /** design/171 §6.2 — the `human_input` arm as a named type (Extract-derived: ONE source, the union
893
+ * above — never a hand-copied second shape). What `buildHumanInputEvent` returns. */
894
+ export type HumanInputEvent = Extract<TaskEvent, {
895
+ type: "human_input";
896
+ }>;
897
+ /**
898
+ * Outcome of a manual {@link TaskStream.compact} request (the enum names align with
899
+ * the diagnosis decision tree's drain exits, so a consumer can tell "202 processed" states apart):
900
+ * - `"compacted"` — a compaction landed at the boundary (the `compacted{trigger:"manual"}` event fired).
901
+ * - `"failed"` — the attempt ran and threw (summarizer failure / empty summary / oversized summary;
902
+ * `compaction.failed` trace frame + `Runner.onError(phase:"compaction")`), or the §17.4 breaker was
903
+ * already open so the request was drained without an attempt (`compaction.failed` frame, no attempt).
904
+ * - `"mooted"` — the request was processed but the run could no longer compact (dying loop: abort /
905
+ * durable suspend / plan review in flight), or the task settled before any boundary arrived.
906
+ * - `"noop"` — the attempt ran but there was nothing to compact (no valid cut point / empty history).
907
+ * - `"blocked"` — a `preCompact` hook deliberately blocked this compaction (not a failure).
908
+ * - `"disabled"` — compaction is disabled by the task spec (`compaction.enabled: false`).
909
+ */
910
+ export type CompactOutcome = "compacted" | "failed" | "mooted" | "noop" | "blocked" | "disabled";