@platless/axom-cli 0.1.0
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.
- package/README.md +64 -0
- package/dist/args.js +29 -0
- package/dist/cli.js +76 -0
- package/dist/commands/auth.js +52 -0
- package/dist/commands/execute.js +40 -0
- package/dist/commands/guide.js +38 -0
- package/dist/commands/record.js +99 -0
- package/dist/commands/skills.js +69 -0
- package/dist/commands/struct.js +50 -0
- package/dist/commands/update.js +88 -0
- package/dist/commands/whoami.js +8 -0
- package/dist/commands/workstation.js +171 -0
- package/dist/config.js +131 -0
- package/dist/credentialBroker.js +340 -0
- package/dist/credentials.js +211 -0
- package/dist/install/agentsDir.js +35 -0
- package/dist/install/claude/commands.js +89 -0
- package/dist/install/claude/materialize.js +42 -0
- package/dist/install/installer.js +45 -0
- package/dist/install/targets/claude.js +154 -0
- package/dist/mcp.js +152 -0
- package/dist/output.js +37 -0
- package/dist/workstation/acp/acpAgents.js +1287 -0
- package/dist/workstation/acp/acpClient.js +691 -0
- package/dist/workstation/acp/acpSession.js +1047 -0
- package/dist/workstation/acp/adoption.js +396 -0
- package/dist/workstation/acp/auth.js +80 -0
- package/dist/workstation/acp/authUrlSniffer.js +52 -0
- package/dist/workstation/acp/binaryInstall.js +411 -0
- package/dist/workstation/acp/configOptions.js +257 -0
- package/dist/workstation/acp/contract.js +494 -0
- package/dist/workstation/acp/draftInterceptor.js +154 -0
- package/dist/workstation/acp/draftSubagents.js +38 -0
- package/dist/workstation/acp/installErrors.js +89 -0
- package/dist/workstation/acp/installProbe.js +363 -0
- package/dist/workstation/acp/permission.js +327 -0
- package/dist/workstation/acp/registry/catalog.js +29 -0
- package/dist/workstation/acp/registry/launch.js +128 -0
- package/dist/workstation/acp/registry/registryClient.js +160 -0
- package/dist/workstation/acp/registry/registryTypes.js +8 -0
- package/dist/workstation/acp/sessionUpdateMapper.js +602 -0
- package/dist/workstation/acp/test/fakeAgent.js +435 -0
- package/dist/workstation/acp/test/sessionUpdateFixtures.js +165 -0
- package/dist/workstation/acp/timedCommand.js +47 -0
- package/dist/workstation/acp/uvxInstall.js +248 -0
- package/dist/workstation/acp/validate.js +134 -0
- package/dist/workstation/agentSessions.js +1628 -0
- package/dist/workstation/channels/agent.js +96 -0
- package/dist/workstation/channels/computer.js +91 -0
- package/dist/workstation/channels/management.js +231 -0
- package/dist/workstation/channels/url.js +42 -0
- package/dist/workstation/config.js +47 -0
- package/dist/workstation/connection/encryptedChannel.js +261 -0
- package/dist/workstation/connection/retryPolicy.js +14 -0
- package/dist/workstation/connection/supervisor.js +353 -0
- package/dist/workstation/crypto.js +63 -0
- package/dist/workstation/daemon.js +225 -0
- package/dist/workstation/daemonctl.js +95 -0
- package/dist/workstation/exec.js +112 -0
- package/dist/workstation/journal.js +257 -0
- package/dist/workstation/names.js +17 -0
- package/dist/workstation/oauth/browser.js +24 -0
- package/dist/workstation/oauth/deviceAuthorization.js +198 -0
- package/dist/workstation/osinfo.js +4 -0
- package/dist/workstation/protocol/agent.js +606 -0
- package/dist/workstation/protocol/common.js +136 -0
- package/dist/workstation/protocol/computer.js +31 -0
- package/dist/workstation/protocol/management.js +116 -0
- package/dist/workstation/register.js +42 -0
- package/dist/workstation/service/installer.js +362 -0
- package/dist/workstation/service/launchd.js +46 -0
- package/dist/workstation/service/loginShell.js +36 -0
- package/dist/workstation/service/systemd.js +48 -0
- package/dist/workstation/service/types.js +73 -0
- package/dist/workstation/supervision.js +52 -0
- package/dist/workstation/systemMetrics.js +271 -0
- package/dist/workstation/update/artifact.js +61 -0
- package/dist/workstation/update/coordinator.js +240 -0
- package/dist/workstation/update/npmInstaller.js +91 -0
- package/dist/workstation/update/redact.js +36 -0
- package/dist/workstation/update/runUpdate.js +215 -0
- package/dist/workstation/update/stateStore.js +87 -0
- package/dist/workstation/update/taskRegistry.js +142 -0
- package/dist/workstation/update/types.js +3 -0
- package/dist/workstation/update/watchdog.js +311 -0
- package/dist/workstation/version.js +12 -0
- package/package.json +39 -0
- package/skills/axom/SKILL.md +83 -0
- package/skills/axom-definition/SKILL.md +74 -0
- package/skills/axom-platform/SKILL.md +78 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One session's memory, and the reason this file is no longer stateless: **what this session has
|
|
3
|
+
* already been told**, for the two branches that cannot answer a frame from the frame alone.
|
|
4
|
+
*
|
|
5
|
+
* It holds two unrelated facts, and they are together only because they have the same lifetime and
|
|
6
|
+
* the same owner (`AcpSession`, one per session, handed to every `mapSessionUpdate` call):
|
|
7
|
+
*
|
|
8
|
+
* 1. `toolCallId → name`, because ACP's `tool_call_update` is a delta (below);
|
|
9
|
+
* 2. the last agent queue depth reported to the server, because `pi-acp` re-states its depth on
|
|
10
|
+
* frames that are not queue events at all (see {@link SessionMapperState.reportsQueueDepth}).
|
|
11
|
+
*
|
|
12
|
+
* ## 1. `toolCallId → name`
|
|
13
|
+
*
|
|
14
|
+
* ACP's `tool_call_update` is a **delta**, keyed by `toolCallId`. Its `name` is optional, and the
|
|
15
|
+
* schema says exactly what leaving it out means (`types.gen.d.ts:164-165`):
|
|
16
|
+
*
|
|
17
|
+
* > "This field is optional. Omitting it or sending `null` both mean that the existing name is
|
|
18
|
+
* > left unchanged."
|
|
19
|
+
*
|
|
20
|
+
* "Unchanged" is a statement about a name the consumer is expected to still be holding — so a
|
|
21
|
+
* consumer that holds nothing has no way to honour it. The previous version of this file wrote the
|
|
22
|
+
* literal `'unknown'` there instead, which does not mean "unchanged": it is a different, false
|
|
23
|
+
* claim, and it went into the durable `item` row for every terminal update a bridged agent ever
|
|
24
|
+
* produced. The settings page's "Ready to launch" defect was the same shape — a value nobody had
|
|
25
|
+
* established, rendered as one that had been.
|
|
26
|
+
*
|
|
27
|
+
* Held per session and passed in, rather than kept in module state, so two concurrent sessions
|
|
28
|
+
* cannot see each other's tool calls. It grows by one short string pair per tool call for the
|
|
29
|
+
* session's lifetime; a session long enough for that to matter has far larger per-call costs
|
|
30
|
+
* already (every call's `rawInput`/`rawOutput` is journaled), so there is no eviction here.
|
|
31
|
+
*/
|
|
32
|
+
export class SessionMapperState {
|
|
33
|
+
byId = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* The agent queue depth this session has already reported to the server.
|
|
36
|
+
*
|
|
37
|
+
* **Starts at 0, and that initial value is load-bearing, not a placeholder**: a session begins with
|
|
38
|
+
* an empty agent queue, so a first frame saying "0 waiting" is not news — it is the state the
|
|
39
|
+
* reader already assumes. Seeding it to `-1` (or `null`) would let the very first frame through,
|
|
40
|
+
* which on `pi-acp` is `startTurn`'s, i.e. the defect this field exists to prevent.
|
|
41
|
+
*/
|
|
42
|
+
lastReportedQueueDepth = 0;
|
|
43
|
+
/**
|
|
44
|
+
* Is `depth` news? Records it if so. **This is the whole fix for a defect that shipped in the
|
|
45
|
+
* previous commit**, so the reasoning is written out rather than summarised.
|
|
46
|
+
*
|
|
47
|
+
* `pi-acp@0.0.33` emits `_meta.piAcp.{queueDepth,running}` from **five** sites (read first-hand
|
|
48
|
+
* from the package pinned on this machine, `pi-acp/dist/index.js`, `package.json` version
|
|
49
|
+
* `0.0.33`), and **the two that fire most are not queue events**:
|
|
50
|
+
*
|
|
51
|
+
* | line | when | payload with nothing queued |
|
|
52
|
+
* |---|---|---|
|
|
53
|
+
* | `:845` | `prompt()` while a turn is running — a real enqueue | `{n≥1, true}` |
|
|
54
|
+
* | `:864` | `cancel()` cleared a non-empty queue — a real drain | `{0, …}` |
|
|
55
|
+
* | **`:925`** | **`startTurn()` — every turn, queue or no queue** | **`{0, true}`** |
|
|
56
|
+
* | `:940` | the turn's prompt rejected | `{depth, false}` |
|
|
57
|
+
* | **`:1228`** | **`agent_settled` with an empty queue — every turn end** | **`{0, false}`** |
|
|
58
|
+
*
|
|
59
|
+
* (`:1267` carries `piAcp.notify` and no depth; `agentQueueNotice`'s shape check drops it.)
|
|
60
|
+
*
|
|
61
|
+
* Because a `step` is a durable item and `foldStep` never merges adjacent ones, emitting on every
|
|
62
|
+
* frame gave **every pi session two permanent "0 messages waiting" cards per turn, from its first
|
|
63
|
+
* turn**. The previous commit's premise — that pi publishes on queue changes — was simply wrong,
|
|
64
|
+
* and nobody had read the package.
|
|
65
|
+
*
|
|
66
|
+
* ## Why "the depth changed", and not the two alternatives
|
|
67
|
+
*
|
|
68
|
+
* **De-duplicating consecutive identical `{depth, running}` pairs does not fix it.** The two
|
|
69
|
+
* every-turn frames are `{0,true}` and `{0,false}`: they differ in `running` and they *alternate*,
|
|
70
|
+
* so they are never consecutive-identical and every single one would still be published. A test
|
|
71
|
+
* below is written to fail under exactly that implementation.
|
|
72
|
+
*
|
|
73
|
+
* **Suppressing `queueDepth === 0` outright eats the news the reader most wants**: the drain. A
|
|
74
|
+
* queue going 2 → 1 → 0 ends on a zero, and that zero is the whole point.
|
|
75
|
+
*
|
|
76
|
+
* Comparing against the last depth *we reported* keeps the drain (`0 ≠ 2`), kills the every-turn
|
|
77
|
+
* noise (`0 === 0`), and self-heals: once a queue has drained, the session goes quiet again.
|
|
78
|
+
*
|
|
79
|
+
* ## Why `running` does not take part in the decision
|
|
80
|
+
*
|
|
81
|
+
* **The decisive reason is the measurement above, not a principle**: `running` is exactly the field
|
|
82
|
+
* that *alternates* across the two every-turn frames (`{0,true}` then `{0,false}`). Any 判据 that
|
|
83
|
+
* reads it therefore publishes on every turn boundary — which is the defect. It is disqualified by
|
|
84
|
+
* what it does, and that is checkable: the pair-dedup test below fails if this ever changes.
|
|
85
|
+
*
|
|
86
|
+
* A secondary reason to be comfortable with that: the subject of this observation is the depth, and
|
|
87
|
+
* "is the agent running" is a fact Axom already owns from its own turn state, which invariant II
|
|
88
|
+
* would rather not have a second independent writer for. That argument is real but it is not what
|
|
89
|
+
* settles it — a field could be redundant with Axom's own state and still be worth reacting to.
|
|
90
|
+
*
|
|
91
|
+
* `running` still rides along inside the sentence, where it is an attribution ("the agent
|
|
92
|
+
* reports…") rather than a second account of the same fact.
|
|
93
|
+
*/
|
|
94
|
+
reportsQueueDepth(depth) {
|
|
95
|
+
if (depth === this.lastReportedQueueDepth)
|
|
96
|
+
return false;
|
|
97
|
+
this.lastReportedQueueDepth = depth;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/** Remember what this call is called. Later updates that carry no `name` fall back to it. */
|
|
101
|
+
record(toolCallId, name) {
|
|
102
|
+
if (name)
|
|
103
|
+
this.byId.set(toolCallId, name);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The name to report for one update, in ACP's own order of authority: an explicit `name` on the
|
|
107
|
+
* update *is* a rename and wins (and is remembered); otherwise the remembered name is what
|
|
108
|
+
* "unchanged" refers to; `title` is the last resort, matching what `toolCallCreatedEvent` would
|
|
109
|
+
* have used had `name` been absent at creation. `'unknown'` survives only for a call this mapper
|
|
110
|
+
* genuinely never saw created — a mid-stream reconnect, or an agent updating an id it never
|
|
111
|
+
* opened.
|
|
112
|
+
*/
|
|
113
|
+
resolve(toolCallId, name, title) {
|
|
114
|
+
if (name) {
|
|
115
|
+
this.byId.set(toolCallId, name);
|
|
116
|
+
return name;
|
|
117
|
+
}
|
|
118
|
+
return this.byId.get(toolCallId) ?? title ?? 'unknown';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Identity, by construction: every ACP `ToolKind` member is a wire value. `delete`, `move` and
|
|
123
|
+
* `switch_mode` used to be flattened to `'other'` here, which destroyed the only signal the card
|
|
124
|
+
* renderer has for telling "removed a file" apart from "did something unclassified" — and the
|
|
125
|
+
* flattening was lossy at the boundary, so no consumer could recover it.
|
|
126
|
+
*
|
|
127
|
+
* The only non-identity case left is ACP reporting no kind at all: `ToolCall.kind` is
|
|
128
|
+
* `ToolKind | null | undefined`, and absent is not a tenth kind — it means "unclassified", which is
|
|
129
|
+
* exactly what `'other'` already spells.
|
|
130
|
+
*/
|
|
131
|
+
function mapToolKind(kind) {
|
|
132
|
+
return kind ?? 'other';
|
|
133
|
+
}
|
|
134
|
+
/** Drop `undefined`-valued keys so optional wire fields are genuinely absent, not `null`/`undefined` literals. */
|
|
135
|
+
function compact(value) {
|
|
136
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `ToolCall.locations` → the wire `tool_call.locations` shape; omitted entirely when there are none.
|
|
140
|
+
*
|
|
141
|
+
* An identity copy of ACP's `ToolCallLocation` (`types.gen.d.ts:581`), which is `{path, line?}` —
|
|
142
|
+
* `line` is `number | null | undefined` (`:586-589`) and used to be dropped here, which cost the
|
|
143
|
+
* "follow along" feature the only signal telling *where* in the file the agent is working. Absent
|
|
144
|
+
* stays absent rather than becoming a `null`: `compact` is applied per location, not just to the
|
|
145
|
+
* enclosing payload.
|
|
146
|
+
*/
|
|
147
|
+
function mapLocations(locations) {
|
|
148
|
+
if (!locations || locations.length === 0)
|
|
149
|
+
return undefined;
|
|
150
|
+
return locations.map((location) => compact({ path: location.path, line: location.line ?? undefined }));
|
|
151
|
+
}
|
|
152
|
+
function toolCallCreatedEvent(update, state) {
|
|
153
|
+
// Recorded here rather than in `mapToolCallEvents` so the one place that decides what a call is
|
|
154
|
+
// called is also the one place that remembers it: the two can never drift apart.
|
|
155
|
+
state.record(update.toolCallId, update.name ?? update.title);
|
|
156
|
+
return {
|
|
157
|
+
eventName: 'tool_call',
|
|
158
|
+
// Every field is now an identity copy of ACP's own `ToolCall` (`types.gen.d.ts:3479`): the wire
|
|
159
|
+
// vocabulary took ACP's `toolCallId`/`name`/`title`/`rawInput`/`kind`/`locations`/`_meta` names
|
|
160
|
+
// verbatim.
|
|
161
|
+
payload: compact({
|
|
162
|
+
// ACP's `ToolCall.name` is the programmatic name and is optional and UNSTABLE (`:3488-3500`);
|
|
163
|
+
// on a *creation* its absence means "no tool name is available" (`:3495-3496`) — not the
|
|
164
|
+
// "left unchanged" that the same-named field on `ToolCallUpdate` means (`:164-165`) — so
|
|
165
|
+
// there is nothing earlier to fall back to and `title` (always present, `:3484-3487`) is the
|
|
166
|
+
// only field guaranteed to give the card a name.
|
|
167
|
+
name: update.name ?? update.title,
|
|
168
|
+
toolCallId: update.toolCallId,
|
|
169
|
+
rawInput: update.rawInput,
|
|
170
|
+
kind: mapToolKind(update.kind),
|
|
171
|
+
title: update.title,
|
|
172
|
+
locations: mapLocations(update.locations),
|
|
173
|
+
// `_meta` is ACP's own extension slot, present on every request/notification shape
|
|
174
|
+
// (`types.gen.d.ts`). This mapper copies it verbatim and reads nothing inside it — Global
|
|
175
|
+
// Constraint 3 ("transport, not translation"): claude-agent-acp puts `parentToolUseId` here to
|
|
176
|
+
// thread a subagent's tool calls back to the `Task` call that spawned it, and other agents put
|
|
177
|
+
// other vendor-private shapes there; this file has no business knowing either.
|
|
178
|
+
_meta: update._meta ?? undefined,
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* ACP `tool_call_update` (`types.gen.d.ts:140`) — a tool call's **entire** remaining life under one
|
|
184
|
+
* event name, increments and answer alike, with ACP's own `status` (`ToolCallStatus`, `:217`:
|
|
185
|
+
* `pending | in_progress | completed | failed`) saying which this one is.
|
|
186
|
+
*
|
|
187
|
+
* There used to be two events here. `tool_call_progress` carried the increments and `tool_call_result`
|
|
188
|
+
* the terminal ones — but `tool_call_progress` *also* carried `status`, so the event name and the field
|
|
189
|
+
* were two spellings of one fact, and ACP has only the field. The two names cost real data on the way
|
|
190
|
+
* out of this file: `extractDiff` kept `content.find(type === 'diff')`, the **first** diff and no other,
|
|
191
|
+
* so a multi-file edit silently lost every diff after the first; `extractOutputChunk`/
|
|
192
|
+
* `flattenContentForResult` flattened content blocks to free text, so a `terminal` reference became a
|
|
193
|
+
* string no consumer could resolve back to a terminal. Carrying ACP's `content` array verbatim ends
|
|
194
|
+
* both losses, and `ToolCallContent` (`:226`, the three-member `content`/`diff`/`terminal` union) is
|
|
195
|
+
* already the wire shape the server decodes (`AcpToolCallContent.fromWire`).
|
|
196
|
+
*
|
|
197
|
+
* `executeStatus` went with the event name it belonged to. It was Axom's `success | failure | running`
|
|
198
|
+
* riding alongside ACP's `status` — same question, two fields, disjoint value domains, and a consumer
|
|
199
|
+
* reading one under the other's meaning would call every failure a success. The server maps
|
|
200
|
+
* `ExecuteResult`'s values into ACP's four rather than keeping a second field.
|
|
201
|
+
*
|
|
202
|
+
* Every key emitted here is a key the server's `decodeToolUpdate` reads (`TimelineEventWire.kt`:
|
|
203
|
+
* name/toolCallId/kind/title/status/content/locations/rawInput/rawOutput/`_meta`): a key this
|
|
204
|
+
* function does not send has no other source, so it simply is not on the timeline. `name` is the one
|
|
205
|
+
* whose absence is fatal rather than lossy — the server's branch is `?: return null`, dropping the
|
|
206
|
+
* whole update silently.
|
|
207
|
+
*
|
|
208
|
+
* `_meta` is forwarded verbatim: it is ACP's own extension slot, and this mapper copies the object
|
|
209
|
+
* without reading anything inside it (Global Constraint 3, "transport, not translation").
|
|
210
|
+
* `decodeToolUpdate` reads it too — also verbatim, straight onto the event payload — so the slot is
|
|
211
|
+
* a live end-to-end channel now, not an additive one waiting for a consumer.
|
|
212
|
+
*
|
|
213
|
+
* **`stream` is deliberately absent, and it is no longer a concept without a home.** `stdout` vs
|
|
214
|
+
* `stderr` has nowhere to live in ACP's typed `content`, and the card renders the two separately.
|
|
215
|
+
* The marker now travels as `_meta.axom.stream` (alongside `_meta.axom.delta`) — `_meta.axom` is the
|
|
216
|
+
* one namespace the platform writes into ACP's extension slot, and `WorkTimelineTools`'
|
|
217
|
+
* `effectiveStream()` reads it first, falling back to the old top-level `stream` only for rows
|
|
218
|
+
* written before that move. What this mapper still does not do is *invent* one: an ACP agent never
|
|
219
|
+
* says which stream a content block came from, and `_meta.axom` is the platform's namespace, not
|
|
220
|
+
* something to synthesize on an agent's behalf. So the key stays omitted — ACP's "unchanged", which
|
|
221
|
+
* is the truthful thing to say when the agent never told us.
|
|
222
|
+
*
|
|
223
|
+
* Everything but the id is optional on `ToolCallUpdate` and absent means "leave it unchanged"
|
|
224
|
+
* (`:140-143`), so `compact` dropping the `undefined`s is load-bearing rather than cosmetic. `kind`
|
|
225
|
+
* in particular does **not** get `mapToolKind`'s `?? 'other'` fallback: that fallback is right on a
|
|
226
|
+
* *creation*, where absent means "unclassified", and wrong here, where it would overwrite the kind
|
|
227
|
+
* the `tool_call` established with a claim the agent never made.
|
|
228
|
+
*/
|
|
229
|
+
function toolCallUpdateEvent(update, state) {
|
|
230
|
+
return {
|
|
231
|
+
eventName: 'tool_call_update',
|
|
232
|
+
payload: compact({
|
|
233
|
+
// Neither `name` nor `title` is guaranteed on an update that only changed `status`, and ACP
|
|
234
|
+
// defines that absence as "unchanged" (`types.gen.d.ts:164-165`) rather than "unknown" — see
|
|
235
|
+
// {@link SessionMapperState}. The emitted key is ACP's own `name` (`ToolCallUpdate.name`, `:169`);
|
|
236
|
+
// `toolName` was a synonym of it.
|
|
237
|
+
name: state.resolve(update.toolCallId, update.name, update.title),
|
|
238
|
+
toolCallId: update.toolCallId,
|
|
239
|
+
title: update.title ?? undefined,
|
|
240
|
+
kind: update.kind ?? undefined,
|
|
241
|
+
status: update.status ?? undefined,
|
|
242
|
+
// An empty array is omitted rather than sent, and the behaviour is deliberate now that the
|
|
243
|
+
// card's fold *does* honour ACP's "replace the content collection": `applyChunks` treats an
|
|
244
|
+
// empty chunk list as "unchanged" at every call site, never as a clear. So an empty array
|
|
245
|
+
// carries nothing either way — and the server drops it on the way back out anyway
|
|
246
|
+
// (`TimelineEvent.ToolCallUpdated.payload` only writes `content` when it is non-empty), which
|
|
247
|
+
// is why sending one could not express a clear even if a consumer wanted to act on it.
|
|
248
|
+
content: update.content && update.content.length > 0 ? update.content : undefined,
|
|
249
|
+
locations: mapLocations(update.locations),
|
|
250
|
+
rawInput: update.rawInput,
|
|
251
|
+
rawOutput: update.rawOutput,
|
|
252
|
+
// See the doc comment above: ACP's extension slot, copied verbatim and read by nothing *here*
|
|
253
|
+
// (the server's `decodeToolUpdate` does read it, also verbatim).
|
|
254
|
+
_meta: update._meta ?? undefined,
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function mapToolCallEvents(update, state) {
|
|
259
|
+
const events = [toolCallCreatedEvent(update, state)];
|
|
260
|
+
// ACP permits a `tool_call` to arrive already finished — the agent reporting a call it both
|
|
261
|
+
// started and completed in one notification, with no separate `tool_call_update` at all. A card
|
|
262
|
+
// only ever leaves "Running" on an update reporting a non-pending status
|
|
263
|
+
// (`WorkTimelineTools.foldToolUpdate`'s outcome branch), and the creation event carries no status,
|
|
264
|
+
// so without this second event an already-terminal call would render as permanently "Running".
|
|
265
|
+
//
|
|
266
|
+
// `ToolCall` is structurally a `ToolCallUpdate` (every field of the latter, with `title` and `kind`
|
|
267
|
+
// merely narrower), so the same function emits both — the two cannot drift into disagreeing about
|
|
268
|
+
// what a terminal update looks like.
|
|
269
|
+
if (update.status === 'completed' || update.status === 'failed') {
|
|
270
|
+
events.push(toolCallUpdateEvent(update, state));
|
|
271
|
+
}
|
|
272
|
+
return events;
|
|
273
|
+
}
|
|
274
|
+
function summarizePlanEntries(entries) {
|
|
275
|
+
if (entries.length === 0)
|
|
276
|
+
return 'No plan entries.';
|
|
277
|
+
return entries.map((entry) => `[${entry.status}] ${entry.content}`).join('\n');
|
|
278
|
+
}
|
|
279
|
+
function summarizePlanUpdateContent(plan) {
|
|
280
|
+
switch (plan.type) {
|
|
281
|
+
case 'items':
|
|
282
|
+
return summarizePlanEntries(plan.entries);
|
|
283
|
+
case 'file':
|
|
284
|
+
return `Plan file: ${plan.uri}`;
|
|
285
|
+
case 'markdown':
|
|
286
|
+
return plan.content;
|
|
287
|
+
default:
|
|
288
|
+
// `PlanUpdateContent` is marked UNSTABLE in the SDK itself, unlike the outer `SessionUpdate`
|
|
289
|
+
// union this file's exhaustiveness check guards — tolerating a shape this switch doesn't
|
|
290
|
+
// recognize (rather than failing the build over an experimental type) matches how the deleted
|
|
291
|
+
// bridges already treat everything the spec marks unstable.
|
|
292
|
+
return 'Plan updated.';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Generic catch-all wire event — the same `{title, message}` shape the deleted `codex/events.ts`
|
|
297
|
+
* used for its own "no dedicated card" `ThreadItem` variants.
|
|
298
|
+
*
|
|
299
|
+
* **Exactly one branch below still calls this, and that is now the whole story of `step` in this
|
|
300
|
+
* file.** `summarizeAvailableCommands` / `summarizeConfigOptions` / `summarizeCompaction` and the
|
|
301
|
+
* `blockText` flattening for `compaction_summary_chunk` are gone with the branches that used them:
|
|
302
|
+
* each of those five variants publishes ACP's own structure under its own event name. What is left
|
|
303
|
+
* is `plan_update`, and its reason is written at that branch.
|
|
304
|
+
*/
|
|
305
|
+
function stepEvent(title, message) {
|
|
306
|
+
return { eventName: 'step', payload: { title, message } };
|
|
307
|
+
}
|
|
308
|
+
/** The `step` line's title, exported so the test asserts the same string the mapper writes. */
|
|
309
|
+
export const AGENT_QUEUE_NOTICE_TITLE = 'Agent queue';
|
|
310
|
+
/**
|
|
311
|
+
* `session_info_update._meta.piAcp.{queueDepth,running}` → a `step` line — ruling N11.
|
|
312
|
+
*
|
|
313
|
+
* `pi-acp@0.0.33` publishes its own queue depth under `_meta.piAcp`, and this branch used to read
|
|
314
|
+
* only `title`/`updatedAt`, so the numbers were dropped on the floor. They are worth showing: on
|
|
315
|
+
* that adapter a mid-turn `session/prompt` waits for `agent_settled`, and until now nothing on the
|
|
316
|
+
* reader's screen said the agent was holding anything at all.
|
|
317
|
+
*
|
|
318
|
+
* **Most of those frames say nothing, and skipping them is not an optimisation — it is the
|
|
319
|
+
* correctness fix for a defect the previous commit shipped.** `pi-acp` re-states the depth when a
|
|
320
|
+
* turn *starts* and when it *settles*, not only when the queue moves; the full site table, the
|
|
321
|
+
* measurement it came from, and why the two obvious alternatives are wrong are at
|
|
322
|
+
* {@link SessionMapperState.reportsQueueDepth}, which owns that decision.
|
|
323
|
+
*
|
|
324
|
+
* **They describe a different set from Axom's queue, and the normalisation is presentational on
|
|
325
|
+
* purpose (ruling N11: "相加即错").** `pi-acp`'s queue holds prompts *that adapter* has accepted and
|
|
326
|
+
* not yet run; Axom's `session_message_queue` holds messages the *server* decided to hold
|
|
327
|
+
* (spec §4.1) — and today Axom never sends a second `session/prompt` into a running turn at all
|
|
328
|
+
* (ruling N1), so the two can never be summed into anything meaningful. That is enforced by shape,
|
|
329
|
+
* not by discipline: `step` is the generic notice event whose payload is four strings
|
|
330
|
+
* (`WorkTimelinePayload.Step`: `message`/`title`/`detail`/`tone`). The depth survives only as a word
|
|
331
|
+
* inside a sentence, so no future reader can add it to a queue length, gate a delivery on it, or
|
|
332
|
+
* render the queue bar from it without first re-parsing prose — which is a thing nobody does by
|
|
333
|
+
* accident. Publishing `{queueDepth, running}` as structured fields under a name of its own would
|
|
334
|
+
* have left exactly that door open.
|
|
335
|
+
*
|
|
336
|
+
* **Reading a vendor key is not vendor branching.** `_meta` is ACP's extension slot, and `piAcp` is
|
|
337
|
+
* where this agent puts the observation; there is no vendor-neutral spelling to read instead, and
|
|
338
|
+
* nothing here changes *behaviour* based on who the agent is (ruling L2's actual prohibition) — a
|
|
339
|
+
* frame without the key simply says nothing. Shape checks are strict for the same reason every
|
|
340
|
+
* other branch here checks: `_meta` is `{[key: string]: unknown}` in the SDK (`types.gen.d.ts:3912`),
|
|
341
|
+
* so anything at all can arrive under it.
|
|
342
|
+
*
|
|
343
|
+
* **`queueDepth` is required, `running` is not.** A frame carrying only `running` would be a second
|
|
344
|
+
* spelling of a fact Axom already owns from its own turn state, which invariant II forbids having
|
|
345
|
+
* two writers for; as part of a sentence *about the agent's own queue* it is an attribution
|
|
346
|
+
* ("the agent reports"), which is a different claim and one only the agent can make.
|
|
347
|
+
*/
|
|
348
|
+
function agentQueueNotice(meta, state) {
|
|
349
|
+
const piAcp = meta?.piAcp;
|
|
350
|
+
if (typeof piAcp !== 'object' || piAcp === null)
|
|
351
|
+
return null;
|
|
352
|
+
const { queueDepth, running } = piAcp;
|
|
353
|
+
if (typeof queueDepth !== 'number' || !Number.isInteger(queueDepth) || queueDepth < 0)
|
|
354
|
+
return null;
|
|
355
|
+
// The shape check comes first on purpose: a malformed frame must not be able to move the
|
|
356
|
+
// watermark this session compares against, or one bad frame silences the next real change.
|
|
357
|
+
if (!state.reportsQueueDepth(queueDepth))
|
|
358
|
+
return null;
|
|
359
|
+
const waiting = queueDepth === 1 ? '1 message waiting' : `${queueDepth} messages waiting`;
|
|
360
|
+
const runState = typeof running === 'boolean'
|
|
361
|
+
? `, and that it is ${running ? 'running' : 'idle'}`
|
|
362
|
+
: '';
|
|
363
|
+
return stepEvent(AGENT_QUEUE_NOTICE_TITLE, `The agent reports ${waiting} in its own queue${runState}. ` +
|
|
364
|
+
`This is the agent's queue, not Axom's.`);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Map one ACP `session/update` payload to zero or more wire events.
|
|
368
|
+
*
|
|
369
|
+
* Carries no state of its own: everything it needs to remember across updates lives in the
|
|
370
|
+
* {@link SessionMapperState} its caller owns and passes in. That parameter is **required**, not
|
|
371
|
+
* optional, on purpose — an optional one would let a future call site forget it and silently
|
|
372
|
+
* reintroduce *both* failures that class exists to prevent (`name: 'unknown'`, and a queue notice on
|
|
373
|
+
* every turn boundary), which is exactly the shape of failure a default value hides. `AcpSession` is
|
|
374
|
+
* the caller with a session's lifetime, and it is where the rest of the cross-update state already
|
|
375
|
+
* lives: `agent_message` synthesis and `user_message_chunk` folding both stay there rather than here
|
|
376
|
+
* (see the file doc comment).
|
|
377
|
+
*/
|
|
378
|
+
export function mapSessionUpdate(update, state) {
|
|
379
|
+
switch (update.sessionUpdate) {
|
|
380
|
+
case 'user_message_chunk':
|
|
381
|
+
// identity mapping, same as the two assistant chunk kinds below: `messageId` is the grouping
|
|
382
|
+
// key ACP defines on `ContentChunk` (`types.gen.d.ts:3456`) and the server folds on it, so it
|
|
383
|
+
// must survive the hop. Dropping it here would make every chunk look like a separate message.
|
|
384
|
+
// `_meta` (ACP's own extension slot, `ContentChunk`, `types.gen.d.ts:3460`) is forwarded
|
|
385
|
+
// verbatim, same as on `tool_call`/`tool_call_update` above and for the same reason (Task 8):
|
|
386
|
+
// claude-agent-acp stamps `_meta.claudeCode.parentToolUseId` on chunks belonging to a
|
|
387
|
+
// subagent (`acp-agent.js:6392-6404`; its own comment at `:1271-1274` says subagent-attributed
|
|
388
|
+
// chunks are recognizable by exactly that stamp), and P2's routing needs it to reach a chunk,
|
|
389
|
+
// not just a tool call. This mapper reads nothing inside it — Global Constraint
|
|
390
|
+
// "transport, not translation".
|
|
391
|
+
return [{ eventName: 'user_message_chunk', payload: compact({ content: update.content, messageId: update.messageId ?? undefined, _meta: update._meta ?? undefined }) }];
|
|
392
|
+
case 'agent_message_chunk':
|
|
393
|
+
// identity mapping since the ACP rename: the server event is now called `agent_message_chunk` too
|
|
394
|
+
return [{ eventName: 'agent_message_chunk', payload: compact({ content: update.content, messageId: update.messageId ?? undefined, _meta: update._meta ?? undefined }) }];
|
|
395
|
+
case 'agent_thought_chunk':
|
|
396
|
+
// identity mapping since the ACP rename: the server event is now called `agent_thought_chunk` too
|
|
397
|
+
return [{ eventName: 'agent_thought_chunk', payload: compact({ content: update.content, messageId: update.messageId ?? undefined, _meta: update._meta ?? undefined }) }];
|
|
398
|
+
case 'tool_call':
|
|
399
|
+
// existing events: tool_call, plus a tool_call_update when ACP reports it already finished — see mapToolCallEvents
|
|
400
|
+
return mapToolCallEvents(update, state);
|
|
401
|
+
case 'tool_call_update':
|
|
402
|
+
// identity mapping since the merge: one wire event per ACP update, whatever its status — see toolCallUpdateEvent
|
|
403
|
+
return [toolCallUpdateEvent(update, state)];
|
|
404
|
+
case 'plan':
|
|
405
|
+
// identity mapping: the server event is now called `plan` too, and it carries ACP's own
|
|
406
|
+
// `entries` (`types.gen.d.ts:3594`) rather than a summary of them.
|
|
407
|
+
//
|
|
408
|
+
// This used to be `stepEvent('Plan', summarizePlanEntries(update.entries))` — a free-text
|
|
409
|
+
// line per entry, joined into one string. Measured on real hardware, claude-agent-acp emits
|
|
410
|
+
// fully structured plans (`dist/acp-agent.js:6489` from Claude Code's TodoWrite, `:6678` from
|
|
411
|
+
// its own Task subagent tools), each entry `{content, status, priority}`. The structure was
|
|
412
|
+
// being destroyed *here*, in Axom's own code, not withheld by the adapter — which is why an
|
|
413
|
+
// external session's plan card was always empty while the built-in agent had one.
|
|
414
|
+
//
|
|
415
|
+
// The entries go over the wire verbatim, `_meta` and all: `TimelineEventWire`'s `plan`
|
|
416
|
+
// decoder rebuilds a typed `AcpPlanEntry` and keeps only ACP's three fields, so nothing
|
|
417
|
+
// unrecognized reaches the database. An empty `entries` is still emitted rather than
|
|
418
|
+
// swallowed — ACP allows it (TodoWrite clearing its list), and the *server* owns the ruling
|
|
419
|
+
// that an empty plan writes no plan; dropping it here would hide that the agent sent one.
|
|
420
|
+
return [{ eventName: 'plan', payload: { entries: update.entries } }];
|
|
421
|
+
case 'plan_update':
|
|
422
|
+
// existing event: step. Deliberately NOT mapped onto `plan`: `PlanUpdate` (`:3738`) is the
|
|
423
|
+
// id-addressed family the SDK marks UNSTABLE/@experimental, its content is a three-way union
|
|
424
|
+
// (`items` | `file` | `markdown`), and it is gated by a client capability Axom does not
|
|
425
|
+
// advertise. `summarizePlanEntries` therefore survives for this branch alone.
|
|
426
|
+
return [stepEvent('Plan updated', summarizePlanUpdateContent(update.plan))];
|
|
427
|
+
case 'plan_removed':
|
|
428
|
+
// identity mapping: the server event is now called `plan_removed` too, carrying ACP's own
|
|
429
|
+
// `planId` (`:3763`). Also UNSTABLE in the SDK, and also id-addressed — but unlike
|
|
430
|
+
// `plan_update` the platform has a real action for it (cancel the session's live plan), and
|
|
431
|
+
// dropping the id into a `step` line left that action impossible to take.
|
|
432
|
+
return [{ eventName: 'plan_removed', payload: { planId: update.planId } }];
|
|
433
|
+
case 'available_commands_update':
|
|
434
|
+
// identity mapping: the server event is now called `available_commands_update` too, and it
|
|
435
|
+
// carries ACP's own `AvailableCommand[]` (`types.gen.d.ts:3782`) rather than a comma-joined
|
|
436
|
+
// list of names. Each command's `description` and `input.hint` (`:3813`) were being destroyed
|
|
437
|
+
// *here* — the adapter sends them; nothing downstream ever saw them.
|
|
438
|
+
//
|
|
439
|
+
// An empty list is still emitted rather than swallowed: "the agent now offers no commands" is
|
|
440
|
+
// a real transition, and dropping it would hide that the agent said anything at all.
|
|
441
|
+
return [{ eventName: 'available_commands_update', payload: { availableCommands: update.availableCommands } }];
|
|
442
|
+
case 'current_mode_update':
|
|
443
|
+
// identity mapping: the server event is now called `current_mode_update` too, carrying ACP's
|
|
444
|
+
// own `currentModeId` (`SessionModeId`, `types.gen.d.ts:3855`) instead of a prose line that
|
|
445
|
+
// spelled the id inside a sentence no consumer could parse back out.
|
|
446
|
+
return [{ eventName: 'current_mode_update', payload: { currentModeId: update.currentModeId } }];
|
|
447
|
+
case 'config_option_update':
|
|
448
|
+
// identity mapping: the server event is now called `config_option_update` too, carrying ACP's
|
|
449
|
+
// own `SessionConfigOption[]` (`:2617`) verbatim — union member, `currentValue` and all. The
|
|
450
|
+
// old `options.map(o => o.name).join(', ')` made "configuration updated" a notice that could
|
|
451
|
+
// not say what changed or what it changed to.
|
|
452
|
+
return [{ eventName: 'config_option_update', payload: { configOptions: update.configOptions } }];
|
|
453
|
+
case 'session_info_update': {
|
|
454
|
+
// identity mapping since the ACP rename: the server event is now called `session_info_update`
|
|
455
|
+
// too (it was `title_suggested`), reusing the bridge-title-sync path the deleted claudeCode
|
|
456
|
+
// bridge used for its own native session titles.
|
|
457
|
+
//
|
|
458
|
+
// **The shape is no longer narrowed.** `SessionInfoUpdate` (`types.gen.d.ts:3896`) is
|
|
459
|
+
// `{title?, updatedAt?}` with both fields `string | null`, and the doc comment says a null
|
|
460
|
+
// clears. That is three states, and this branch used to collapse them to one: anything but a
|
|
461
|
+
// non-empty string emitted nothing, so an explicit clear and a title-less "the session moved"
|
|
462
|
+
// both vanished here. `'title' in update` is what tells absent from null — the server's
|
|
463
|
+
// decoder makes the same distinction with `containsKey`.
|
|
464
|
+
//
|
|
465
|
+
// A wholly empty update is still dropped: the server drops it too, and emitting it would only
|
|
466
|
+
// push the bridge watermark past a frame nobody can act on. "Wholly empty" is about ACP's own
|
|
467
|
+
// two fields; `_meta` is read separately below and can carry an update of its own.
|
|
468
|
+
const payload = {};
|
|
469
|
+
if ('title' in update)
|
|
470
|
+
payload.title = update.title ?? null;
|
|
471
|
+
if (update.updatedAt != null)
|
|
472
|
+
payload.updatedAt = update.updatedAt;
|
|
473
|
+
const events = Object.keys(payload).length > 0
|
|
474
|
+
? [{ eventName: 'session_info_update', payload }]
|
|
475
|
+
: [];
|
|
476
|
+
const queueNotice = agentQueueNotice(update._meta, state);
|
|
477
|
+
if (queueNotice)
|
|
478
|
+
events.push(queueNotice);
|
|
479
|
+
return events;
|
|
480
|
+
}
|
|
481
|
+
case 'usage_update':
|
|
482
|
+
// existing event: usage_update — context occupancy/cost, same wire shape the deleted bridges already emit
|
|
483
|
+
// An identity copy of ACP's `UsageUpdate` (`types.gen.d.ts:3942`): `used` (`:3946`), `size`
|
|
484
|
+
// (`:3950`) and `cost` (`:3954`, a `Cost` of `{amount, currency}` — `:3919-3927`). The wire
|
|
485
|
+
// used to spell these `contextTokens`/`totalCostUsd`, and the latter's baked-in currency is
|
|
486
|
+
// why a non-USD cost had to be dropped rather than mislabeled. Carrying ACP's own `cost`
|
|
487
|
+
// object carries the currency with it, so there is nothing left to drop.
|
|
488
|
+
return [{ eventName: 'usage_update', payload: compact({ used: update.used, size: update.size, cost: update.cost ?? undefined }) }];
|
|
489
|
+
case 'compaction_update':
|
|
490
|
+
// identity mapping: the server event is now called `compaction_update` too, carrying ACP's own
|
|
491
|
+
// `{compactionId, status, summary?, error?}` (`types.gen.d.ts:4003`, status at `:3985`). The
|
|
492
|
+
// server's own compaction audit record moved onto this same name and shape — one concept, one
|
|
493
|
+
// name across both agents, with Axom's engine watermarks riding along as extensions ACP has no
|
|
494
|
+
// field for.
|
|
495
|
+
//
|
|
496
|
+
// `summary` and `error` have **patch semantics** (`:4003`: omission leaves the stored value
|
|
497
|
+
// unchanged, null clears it), so `compact` dropping the undefineds is load-bearing: an omitted
|
|
498
|
+
// key is "unchanged", which is the truthful thing to say when the agent said nothing. A
|
|
499
|
+
// literal null is a different claim and is not one absence is entitled to make.
|
|
500
|
+
return [{
|
|
501
|
+
eventName: 'compaction_update',
|
|
502
|
+
payload: compact({
|
|
503
|
+
compactionId: update.compactionId,
|
|
504
|
+
status: update.status,
|
|
505
|
+
summary: update.summary ?? undefined,
|
|
506
|
+
error: update.error ?? undefined,
|
|
507
|
+
}),
|
|
508
|
+
}];
|
|
509
|
+
case 'compaction_summary_chunk':
|
|
510
|
+
// identity mapping: the server event is now called `compaction_summary_chunk` too, carrying
|
|
511
|
+
// ACP's `{compactionId, content}` (`:4039`). This used to flatten the block to free text and
|
|
512
|
+
// drop the id entirely, so a non-text block became `[image]` and no consumer could attach the
|
|
513
|
+
// chunk back to the compaction it belongs to.
|
|
514
|
+
return [{
|
|
515
|
+
eventName: 'compaction_summary_chunk',
|
|
516
|
+
payload: { compactionId: update.compactionId, content: update.content },
|
|
517
|
+
}];
|
|
518
|
+
// --- draft ACP (RFD #1992) and the AIR async-task extension; shapes in `draftSubagents.ts` ---
|
|
519
|
+
//
|
|
520
|
+
// All five are identity mappings, for the same reason every other branch here is transport
|
|
521
|
+
// rather than translation: the normalisation point is server ingest, one place, so that a
|
|
522
|
+
// workstation running a version behind cannot put a second shape in the database. The names are
|
|
523
|
+
// ACP's (or, for the async-task trio, the ecosystem's), so there is nothing to rename.
|
|
524
|
+
case 'subagent_spawned':
|
|
525
|
+
return [{
|
|
526
|
+
eventName: 'subagent_spawned',
|
|
527
|
+
payload: compact({
|
|
528
|
+
subagentSessionId: update.subagentSessionId,
|
|
529
|
+
name: update.name,
|
|
530
|
+
task: update.task,
|
|
531
|
+
capabilities: update.capabilities,
|
|
532
|
+
_meta: update._meta ?? undefined,
|
|
533
|
+
}),
|
|
534
|
+
}];
|
|
535
|
+
case 'subagent_state_update':
|
|
536
|
+
return [{
|
|
537
|
+
eventName: 'subagent_state_update',
|
|
538
|
+
payload: compact({
|
|
539
|
+
subagentSessionId: update.subagentSessionId,
|
|
540
|
+
state: update.state,
|
|
541
|
+
_meta: update._meta ?? undefined,
|
|
542
|
+
}),
|
|
543
|
+
}];
|
|
544
|
+
case 'async_task_spawned':
|
|
545
|
+
// `toolCallId` is optional here **because it is genuinely absent** on this frame in practice —
|
|
546
|
+
// it arrives on the next `async_task_progress`. `compact` dropping it is what lets the server
|
|
547
|
+
// treat "not said yet" as different from "said to be nothing".
|
|
548
|
+
return [{
|
|
549
|
+
eventName: 'async_task_spawned',
|
|
550
|
+
payload: compact({
|
|
551
|
+
asyncTaskId: update.asyncTaskId,
|
|
552
|
+
name: update.name,
|
|
553
|
+
taskType: update.taskType,
|
|
554
|
+
description: update.description,
|
|
555
|
+
showInTranscript: update.showInTranscript,
|
|
556
|
+
canStop: update.canStop,
|
|
557
|
+
outputFilePath: update.outputFilePath ?? undefined,
|
|
558
|
+
toolCallId: update.toolCallId ?? undefined,
|
|
559
|
+
_meta: update._meta ?? undefined,
|
|
560
|
+
}),
|
|
561
|
+
}];
|
|
562
|
+
case 'async_task_progress':
|
|
563
|
+
return [{
|
|
564
|
+
eventName: 'async_task_progress',
|
|
565
|
+
payload: compact({
|
|
566
|
+
asyncTaskId: update.asyncTaskId,
|
|
567
|
+
description: update.description ?? undefined,
|
|
568
|
+
summary: update.summary ?? undefined,
|
|
569
|
+
lastToolName: update.lastToolName ?? undefined,
|
|
570
|
+
usage: update.usage ?? undefined,
|
|
571
|
+
outputFilePath: update.outputFilePath ?? undefined,
|
|
572
|
+
toolCallId: update.toolCallId ?? undefined,
|
|
573
|
+
_meta: update._meta ?? undefined,
|
|
574
|
+
}),
|
|
575
|
+
}];
|
|
576
|
+
case 'async_task_state_update':
|
|
577
|
+
return [{
|
|
578
|
+
eventName: 'async_task_state_update',
|
|
579
|
+
payload: compact({
|
|
580
|
+
asyncTaskId: update.asyncTaskId,
|
|
581
|
+
state: update.state,
|
|
582
|
+
summary: update.summary ?? undefined,
|
|
583
|
+
outputFilePath: update.outputFilePath ?? undefined,
|
|
584
|
+
toolCallId: update.toolCallId ?? undefined,
|
|
585
|
+
_meta: update._meta ?? undefined,
|
|
586
|
+
}),
|
|
587
|
+
}];
|
|
588
|
+
default: {
|
|
589
|
+
// Exhaustiveness guard (plan requirement): if the SDK ever adds a 16th `SessionUpdate` variant,
|
|
590
|
+
// `update` stops being assignable to `never` here and this file fails to *compile* — not just a
|
|
591
|
+
// test going red. The hardcoded value list in the test file is a second, weaker net.
|
|
592
|
+
//
|
|
593
|
+
// Widening the parameter to `IncomingSessionUpdate` does **not** weaken this: the five draft
|
|
594
|
+
// variants are named cases above, so they are subtracted from the union before it reaches
|
|
595
|
+
// here, and anything genuinely unknown still fails the assignment. Adding a draft variant to
|
|
596
|
+
// `draftSubagents.ts` without a branch here is therefore also a compile error, which is the
|
|
597
|
+
// property that keeps the two files from drifting apart.
|
|
598
|
+
const _exhaustive = update;
|
|
599
|
+
throw new Error(`unmapped sessionUpdate variant: ${JSON.stringify(_exhaustive)}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|