@artooi/ag-ui-web-component 0.29.0 → 0.30.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/README.md +196 -33
  3. package/dist/ag-ui-web-component.bundle.js +211 -28
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +60 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +25 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +18 -1
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +578 -35
  14. package/dist/index.js.map +4 -4
  15. package/dist/ui/message_actions.d.ts +14 -4
  16. package/dist/ui/message_actions.d.ts.map +1 -1
  17. package/dist/ui/styles.d.ts +1 -1
  18. package/dist/ui/styles.d.ts.map +1 -1
  19. package/dist/ui/subagent_panel.d.ts +92 -0
  20. package/dist/ui/subagent_panel.d.ts.map +1 -0
  21. package/dist/ui/subagent_update.d.ts +19 -0
  22. package/dist/ui/subagent_update.d.ts.map +1 -0
  23. package/dist/ui/tool_call_card.d.ts +73 -1
  24. package/dist/ui/tool_call_card.d.ts.map +1 -1
  25. package/dist/ui/ui_strings.d.ts +10 -0
  26. package/dist/ui/ui_strings.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/constants.ts +63 -1
  29. package/src/core/ag_ui_chat.ts +180 -26
  30. package/src/core/agui_client.ts +26 -2
  31. package/src/index.ts +4 -0
  32. package/src/ui/message_actions.ts +20 -8
  33. package/src/ui/styles.ts +183 -0
  34. package/src/ui/subagent_panel.ts +213 -0
  35. package/src/ui/subagent_update.ts +80 -0
  36. package/src/ui/tool_call_card.ts +129 -3
  37. package/src/ui/ui_strings.ts +15 -0
  38. package/src/version.ts +1 -1
@@ -8,11 +8,16 @@ export interface MessageActionsOptions {
8
8
  /** Localized strings. */
9
9
  strings: UiStrings;
10
10
  /**
11
- * The text Copy puts on the clipboard. A function rather than a string
12
- * because a bubble's content is rewritten while it streams, and the bar is
13
- * attached to the element rather than to a snapshot of it.
11
+ * The text Copy puts on the clipboard. Absent means no copy button.
12
+ *
13
+ * A function rather than a string because a bubble's content is rewritten
14
+ * while it streams, and the bar is attached to the element rather than to a
15
+ * snapshot of it. Optional for the same reason `onFeedback` is: what a button
16
+ * needs to do its job is also the statement that the button belongs here, so
17
+ * there is no second flag saying the same thing and no way for the two to
18
+ * disagree.
14
19
  */
15
- text: () => string;
20
+ text?: () => string;
16
21
  /**
17
22
  * Report a rating for this message. Absent means no feedback buttons.
18
23
  *
@@ -35,13 +40,21 @@ export interface MessageActionsOptions {
35
40
  *
36
41
  * Idempotent -- a bubble already given a bar is skipped, so a re-render or a
37
42
  * second call cannot stack rows.
43
+ *
44
+ * Each button is present because the option it needs was passed: `text` for
45
+ * Copy, `onFeedback` for the rating pair. Passing neither builds an empty row,
46
+ * which is a caller's mistake rather than a state to guard against -- the
47
+ * element skips the call entirely when a host has turned both off.
38
48
  */
39
49
  export function attachMessageActions(bubble: HTMLElement, options: MessageActionsOptions): void {
40
50
  if (existingBar(bubble) !== null) {
41
51
  return;
42
52
  }
43
53
  const bar = messageActionBar(bubble, options.strings);
44
- bar.appendChild(copyButton(options));
54
+ const text = options.text;
55
+ if (text !== undefined) {
56
+ bar.appendChild(copyButton(options.strings, text));
57
+ }
45
58
  if (options.onFeedback !== undefined) {
46
59
  bar.append(
47
60
  feedbackButton("up", options.strings.feedbackUp, options.onFeedback),
@@ -104,11 +117,10 @@ export function messageActionButton(
104
117
  return button;
105
118
  }
106
119
 
107
- function copyButton(options: MessageActionsOptions): HTMLButtonElement {
108
- const { strings } = options;
120
+ function copyButton(strings: UiStrings, text: () => string): HTMLButtonElement {
109
121
  const button = messageActionButton("copy", strings.copyMessage, "⎘");
110
122
  button.addEventListener("click", () => {
111
- void navigator.clipboard.writeText(options.text()).then(
123
+ void navigator.clipboard.writeText(text()).then(
112
124
  () => flash(button, strings.copied, strings.copyMessage),
113
125
  // A denied clipboard permission is the common case, not an exception:
114
126
  // say so on the button rather than throwing into an unhandled rejection.
package/src/ui/styles.ts CHANGED
@@ -1195,6 +1195,21 @@ export const STYLES = `
1195
1195
  color: var(--_fg);
1196
1196
  }
1197
1197
 
1198
+ /* A region a host formatter took over, marked by the card. Preformatted
1199
+ whitespace is what makes the built-in block read as written, and it is the one
1200
+ thing a host cannot want: a table inherits it as mangled cell spacing, and a
1201
+ sentence as line breaks nobody typed.
1202
+
1203
+ Whitespace only. The card's own face, frame, padding and scroll cap stay,
1204
+ because the card is one visual object -- the head row and the status pill are
1205
+ monospaced too -- and a region that dropped the family would be the only part
1206
+ of it wearing a different one. A host that wants that restyles the
1207
+ tool-card-result part, which does not need the formatter at all. */
1208
+ .tool-call-args[data-formatted],
1209
+ .tool-call-result[data-formatted] {
1210
+ white-space: normal;
1211
+ }
1212
+
1198
1213
  /* Display modes are pure visibility over one DOM shape, selected from the host
1199
1214
  attribute rather than a value stamped on the card at build time, so flipping
1200
1215
  data-tool-display re-styles cards already on screen. See ToolCallCard.
@@ -1259,6 +1274,174 @@ export const STYLES = `
1259
1274
  display: flex;
1260
1275
  }
1261
1276
 
1277
+ /* A delegated sub-agent's progress, inside the card that delegated. Empty on
1278
+ every card that delegated nothing, so it collapses rather than adding a gap
1279
+ to each one -- the same shape the approval slot uses. */
1280
+ .tool-call-subagent:empty {
1281
+ display: none;
1282
+ }
1283
+
1284
+ .tool-call-subagent {
1285
+ display: flex;
1286
+ flex-direction: column;
1287
+ gap: 4px;
1288
+ }
1289
+
1290
+ .subagent {
1291
+ display: flex;
1292
+ flex-direction: column;
1293
+ gap: 4px;
1294
+ min-width: 0;
1295
+ }
1296
+
1297
+ /* The collapsed row is the status and the expander at once, which is what keeps
1298
+ a ten-step child one row until somebody opens it. Full width and left-aligned,
1299
+ because it is a line of the card rather than a button on it. */
1300
+ .subagent-row {
1301
+ display: flex;
1302
+ align-items: center;
1303
+ gap: 6px;
1304
+ width: 100%;
1305
+ box-sizing: border-box;
1306
+ padding: 2px 0;
1307
+ border: none;
1308
+ background: none;
1309
+ font: inherit;
1310
+ text-align: left;
1311
+ color: var(--_muted);
1312
+ cursor: pointer;
1313
+ }
1314
+
1315
+ /* Nothing behind the row yet -- a delegation that failed before calling
1316
+ anything. Drop the affordances rather than offer a control that expands onto
1317
+ an empty region, which is the refusal the card's own toggle already makes. */
1318
+ .subagent-row:disabled {
1319
+ cursor: default;
1320
+ }
1321
+
1322
+ .subagent-row::after {
1323
+ content: "▸";
1324
+ flex: none;
1325
+ margin-left: auto;
1326
+ color: var(--_accent);
1327
+ }
1328
+
1329
+ .subagent-row[aria-expanded="true"]::after {
1330
+ content: "▾";
1331
+ }
1332
+
1333
+ .subagent-row:disabled::after {
1334
+ display: none;
1335
+ }
1336
+
1337
+ /* Empty in the DOM; the glyph is drawn here from the panel's data-phase, so a
1338
+ host re-themes it through the same tool-icon custom properties the card uses. */
1339
+ .subagent-icon {
1340
+ flex: none;
1341
+ box-sizing: border-box;
1342
+ display: inline-flex;
1343
+ align-items: center;
1344
+ justify-content: center;
1345
+ width: 10px;
1346
+ height: 10px;
1347
+ font-size: 10px;
1348
+ line-height: 1;
1349
+ }
1350
+
1351
+ /* Anything that is not a terminal phase is the child still working. Selected by
1352
+ what it is not, so a phase this client has not heard of still spins rather
1353
+ than rendering as a blank. */
1354
+ .subagent[data-phase]:not([data-phase="finished"]):not([data-phase="failed"]) .subagent-icon {
1355
+ border: 2px solid var(--_muted);
1356
+ border-top-color: transparent;
1357
+ border-radius: 50%;
1358
+ animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
1359
+ }
1360
+
1361
+ .subagent[data-phase="finished"] .subagent-icon::before {
1362
+ content: var(--_tool-icon-done);
1363
+ color: var(--_success);
1364
+ }
1365
+
1366
+ .subagent[data-phase="failed"] .subagent-icon::before {
1367
+ content: var(--_tool-icon-error);
1368
+ color: var(--_danger);
1369
+ }
1370
+
1371
+ @media (prefers-reduced-motion: reduce) {
1372
+ .subagent .subagent-icon {
1373
+ animation: none;
1374
+ }
1375
+ }
1376
+
1377
+ /* The server's own pre-rendered line. Shrinks and wraps rather than pushing the
1378
+ chevron out of the card, which is what a fixed-width sibling in a flex row
1379
+ does to a panel at sidebar width. */
1380
+ .subagent-status {
1381
+ flex: 1 1 auto;
1382
+ min-width: 0;
1383
+ overflow-wrap: anywhere;
1384
+ }
1385
+
1386
+ /* The child's own calls. Indented and ruled, so the nesting is visible without
1387
+ a second card frame around it. */
1388
+ .subagent-steps {
1389
+ display: flex;
1390
+ flex-direction: column;
1391
+ gap: 2px;
1392
+ margin-left: 4px;
1393
+ padding-left: 10px;
1394
+ border-left: 1px solid var(--_border);
1395
+ }
1396
+
1397
+ .subagent-steps[hidden] {
1398
+ display: none;
1399
+ }
1400
+
1401
+ .subagent-step {
1402
+ display: flex;
1403
+ align-items: center;
1404
+ gap: 6px;
1405
+ min-width: 0;
1406
+ color: var(--_muted);
1407
+ }
1408
+
1409
+ .subagent-step-icon {
1410
+ flex: none;
1411
+ box-sizing: border-box;
1412
+ display: inline-flex;
1413
+ align-items: center;
1414
+ justify-content: center;
1415
+ width: 8px;
1416
+ height: 8px;
1417
+ font-size: 9px;
1418
+ line-height: 1;
1419
+ }
1420
+
1421
+ /* No outcome yet: the wire says null while the call is in flight, and the
1422
+ absence of the attribute is how that arrives here. A hollow ring, not a
1423
+ spinner -- several can be on screen at once and the row above already spins. */
1424
+ .subagent-step:not([data-ok]) .subagent-step-icon {
1425
+ border: 1px solid var(--_muted);
1426
+ border-radius: 50%;
1427
+ }
1428
+
1429
+ .subagent-step[data-ok="true"] .subagent-step-icon::before {
1430
+ content: var(--_tool-icon-done);
1431
+ color: var(--_success);
1432
+ }
1433
+
1434
+ .subagent-step[data-ok="false"] .subagent-step-icon::before {
1435
+ content: var(--_tool-icon-error);
1436
+ color: var(--_danger);
1437
+ }
1438
+
1439
+ .subagent-step-name {
1440
+ flex: 1 1 auto;
1441
+ min-width: 0;
1442
+ overflow-wrap: anywhere;
1443
+ }
1444
+
1262
1445
  .tool-call-toggle {
1263
1446
  align-self: flex-start;
1264
1447
  border: none;
@@ -0,0 +1,213 @@
1
+ import type { SUBAGENT_PHASE } from "../constants.js";
2
+ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
3
+
4
+ /** One phase of a delegation's life, as the server spells it. */
5
+ export type SubAgentPhase = (typeof SUBAGENT_PHASE)[keyof typeof SUBAGENT_PHASE];
6
+
7
+ /**
8
+ * One tool call the child made, as the two tool phases report it.
9
+ *
10
+ * `ok` is a tri-state and stays one here rather than collapsing to a boolean:
11
+ * `null` is the call in flight, `true` a result the child accepted, `false` a
12
+ * result that came back to it. Flattening `null` into `false` would draw a
13
+ * running call as a failed one for as long as it runs.
14
+ */
15
+ export interface SubAgentTool {
16
+ readonly toolCallId: string;
17
+ readonly name: string;
18
+ readonly ok: boolean | null;
19
+ }
20
+
21
+ /**
22
+ * One narrowed progress announcement about a delegation.
23
+ *
24
+ * Every field but `phase` and `delegationId` is nullable because the wire's
25
+ * `value` is `unknown` and a malformed announcement must not take the run down
26
+ * with it. `null` means "said nothing about this", never "said nothing was
27
+ * there" — the panel leaves what it already shows alone.
28
+ */
29
+ export interface SubAgentUpdate {
30
+ /** The **parent's** `delegate_task` tool-call id, not the child's run id. */
31
+ readonly delegationId: string;
32
+ /** The child agent's name, for a host that wants to style or select by it. */
33
+ readonly agent: string | null;
34
+ readonly phase: SubAgentPhase;
35
+ /** The server's pre-rendered line. The collapsed row needs nothing else. */
36
+ readonly status: string | null;
37
+ /** Present on the two tool phases only. */
38
+ readonly tool: SubAgentTool | null;
39
+ }
40
+
41
+ /**
42
+ * The nested surface for one delegation: a collapsed status row that expands
43
+ * onto the child agent's own tool calls.
44
+ *
45
+ * ## Where it goes and why
46
+ *
47
+ * Into {@link ToolCallCard.subagentSlot} — the card the parent's own
48
+ * `delegate_task` call already drew. The wire keys progress on the *parent's*
49
+ * tool-call id, so the thing being narrated is already on screen; a floating
50
+ * element would have duplicated its identity and then had to explain the
51
+ * relationship. Attaching instead means the delegation reuses how tool cards
52
+ * already behave, and there is no second visual language to learn.
53
+ *
54
+ * ## The shape
55
+ *
56
+ * One row per delegation, live, carrying nothing but the server's own `status`
57
+ * line — which is what makes a ten-step child cost one row until somebody opens
58
+ * it. Two alternatives were rejected on the way here and both are worth naming:
59
+ * a bare status line is cheaper and gives up the detail entirely, and inline
60
+ * child cards in the transcript interleave parent and child with nothing marking
61
+ * whose is whose, in an order the persisted transcript will not reproduce.
62
+ *
63
+ * A child's steps are keyed by the child's own `toolCallId`, so the `tool_call`
64
+ * that opens one and the `tool_result` that settles it are the same row updated
65
+ * in place rather than two rows stacked.
66
+ *
67
+ * ## What it never does
68
+ *
69
+ * It never renders failure text. A `failed` phase carries none, on purpose; the
70
+ * detail arrives on the ordinary `TOOL_CALL_RESULT` and lands in the same card's
71
+ * result region, a few pixels below. Anything invented here would be this
72
+ * component guessing at words the server declined to send.
73
+ *
74
+ * Nothing here is persisted: the events ride the imperative carrier, so a thread
75
+ * restore rebuilds the tool card and not the delegation under it.
76
+ *
77
+ * Pure DOM, like the other widgets: the host appends {@link element}, and all
78
+ * chrome text comes from {@link UiStrings}. The status line is server text and
79
+ * is set with `textContent`, never parsed as markup.
80
+ */
81
+ export class SubAgentPanel {
82
+ /** The panel's root; append this into the delegating card's slot. */
83
+ readonly element: HTMLDivElement;
84
+
85
+ /**
86
+ * The collapsed row, which is the expander as well as the status.
87
+ *
88
+ * Disabled while the child has called nothing, so a delegation that failed
89
+ * before it started offers no control that expands onto an empty region —
90
+ * the same refusal the card's own Details toggle already makes.
91
+ */
92
+ readonly #row: HTMLButtonElement;
93
+ readonly #status: HTMLSpanElement;
94
+ readonly #steps: HTMLDivElement;
95
+ /** The child's tool calls, keyed by the child's own call id. */
96
+ readonly #stepRows = new Map<string, HTMLDivElement>();
97
+
98
+ constructor(strings: UiStrings = DEFAULT_UI_STRINGS) {
99
+ this.element = document.createElement("div");
100
+ this.element.className = "subagent";
101
+ this.element.setAttribute("part", "subagent");
102
+
103
+ this.#row = document.createElement("button");
104
+ this.#row.type = "button";
105
+ this.#row.className = "subagent-row";
106
+ this.#row.setAttribute("part", "subagent-row");
107
+ this.#row.setAttribute("aria-expanded", "false");
108
+ this.#row.disabled = true;
109
+
110
+ // Left empty in the DOM, like the tool card's: the shadow CSS draws a
111
+ // spinner or a settled mark from the panel's data-phase, so a host themes
112
+ // the glyph without either side reaching into the other's stylesheet.
113
+ const icon = document.createElement("span");
114
+ icon.className = "subagent-icon";
115
+ icon.setAttribute("part", "subagent-icon");
116
+ icon.setAttribute("aria-hidden", "true");
117
+
118
+ this.#status = document.createElement("span");
119
+ this.#status.className = "subagent-status";
120
+ this.#status.setAttribute("part", "subagent-status");
121
+ // Seeded rather than left blank: an announcement whose status field is
122
+ // unusable must still leave a readable row, since the row is the control.
123
+ this.#status.textContent = strings.subAgentWorking;
124
+
125
+ this.#row.append(icon, this.#status);
126
+
127
+ this.#steps = document.createElement("div");
128
+ this.#steps.className = "subagent-steps";
129
+ this.#steps.setAttribute("part", "subagent-steps");
130
+ this.#steps.setAttribute("role", "list");
131
+ this.#steps.setAttribute("aria-label", strings.subAgentSteps);
132
+ this.#steps.hidden = true;
133
+
134
+ // The attribute is the state, as it is on the tool card: one place holds
135
+ // whether the region is open, and it is the one a screen reader reads.
136
+ this.#row.addEventListener("click", () => {
137
+ this.#setExpanded(this.#row.getAttribute("aria-expanded") !== "true");
138
+ });
139
+
140
+ this.element.append(this.#row, this.#steps);
141
+ }
142
+
143
+ /**
144
+ * Fold one announcement in.
145
+ *
146
+ * Every field is applied only when the update actually carried it, so a phase
147
+ * that says nothing about the agent or the status leaves both as they stand.
148
+ * That is what lets `finished` be two keys wide on the wire without blanking
149
+ * the row it closes.
150
+ */
151
+ report(update: SubAgentUpdate): void {
152
+ this.element.setAttribute("data-phase", update.phase);
153
+ if (update.agent !== null) {
154
+ this.element.setAttribute("data-agent", update.agent);
155
+ }
156
+ if (update.status !== null) {
157
+ this.#status.textContent = update.status;
158
+ }
159
+ if (update.tool !== null) {
160
+ this.#recordStep(update.tool);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Open or settle one of the child's calls, keyed by its own id.
166
+ *
167
+ * The absence of `data-ok` is what "still running" looks like, mirroring the
168
+ * wire's `null` rather than inventing a third value for it — so the attribute
169
+ * is removed on the way in and written on the way out.
170
+ */
171
+ #recordStep(tool: SubAgentTool): void {
172
+ const row = this.#stepRows.get(tool.toolCallId) ?? this.#createStep(tool);
173
+ if (tool.ok === null) {
174
+ row.removeAttribute("data-ok");
175
+ return;
176
+ }
177
+ row.setAttribute("data-ok", String(tool.ok));
178
+ }
179
+
180
+ #createStep(tool: SubAgentTool): HTMLDivElement {
181
+ const row = document.createElement("div");
182
+ row.className = "subagent-step";
183
+ row.setAttribute("part", "subagent-step");
184
+ row.setAttribute("role", "listitem");
185
+ row.setAttribute("data-tool-call-id", tool.toolCallId);
186
+
187
+ const icon = document.createElement("span");
188
+ icon.className = "subagent-step-icon";
189
+ icon.setAttribute("part", "subagent-step-icon");
190
+ icon.setAttribute("aria-hidden", "true");
191
+
192
+ const name = document.createElement("span");
193
+ name.className = "subagent-step-name";
194
+ name.setAttribute("part", "subagent-step-name");
195
+ // The child's raw tool name. Not prettified and not looked up in the tool
196
+ // catalog: a sub-agent's tools are its own and never reached the browser's
197
+ // schema, and the status line above quotes the same raw name, so relabelling
198
+ // here would make the two lines disagree about one call.
199
+ name.textContent = tool.name;
200
+
201
+ row.append(icon, name);
202
+ this.#steps.appendChild(row);
203
+ this.#stepRows.set(tool.toolCallId, row);
204
+ // There is something behind the row now, so it becomes a control.
205
+ this.#row.disabled = false;
206
+ return row;
207
+ }
208
+
209
+ #setExpanded(expanded: boolean): void {
210
+ this.#steps.hidden = !expanded;
211
+ this.#row.setAttribute("aria-expanded", String(expanded));
212
+ }
213
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Narrow one `ag_ui.subagent` payload into a {@link SubAgentUpdate}.
3
+ *
4
+ * Kept out of the panel for the reason `chartSpecFrom` is kept out of the
5
+ * renderer: the panel's job is drawing, and a value that reaches it has already
6
+ * been vouched for.
7
+ *
8
+ * Defensive about the payload, not about the name. A `CUSTOM` event's `value` is
9
+ * `unknown` by the protocol, so a server can put anything there, and a malformed
10
+ * announcement must not take a run down with it — the same rule the invalidation
11
+ * channel applies to the same field. What is refused here is only what cannot be
12
+ * rendered at all: without a `delegationId` there is no card to attach to, and
13
+ * without a known `phase` there is no state to be in. Everything else degrades to
14
+ * `null`, which the panel reads as "said nothing about this".
15
+ */
16
+
17
+ import { SUBAGENT_PHASE } from "../constants.js";
18
+ import type { SubAgentPhase, SubAgentTool, SubAgentUpdate } from "./subagent_panel.js";
19
+
20
+ const PHASES: readonly string[] = Object.values(SUBAGENT_PHASE);
21
+
22
+ /** A record view of `value`, or `null` for anything that is not an object. */
23
+ function asRecord(value: unknown): Record<string, unknown> | null {
24
+ // `typeof null` is "object", and an array is one too — neither carries the
25
+ // keys below, and both arrive from a JSON decoder without any warning.
26
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
27
+ return null;
28
+ }
29
+ return value as Record<string, unknown>;
30
+ }
31
+
32
+ /** A non-empty string, or `null`. */
33
+ function asText(value: unknown): string | null {
34
+ return typeof value === "string" && value !== "" ? value : null;
35
+ }
36
+
37
+ /**
38
+ * The `tool` record the two tool phases carry.
39
+ *
40
+ * All-or-nothing: the contract states all three keys on every tool phase, so a
41
+ * partial record is a payload this client does not understand rather than a step
42
+ * to draw half of. A step row keyed by an empty id would also collide with the
43
+ * next one, silently merging two of the child's calls into one row.
44
+ */
45
+ function asTool(value: unknown): SubAgentTool | null {
46
+ const record = asRecord(value);
47
+ if (record === null) {
48
+ return null;
49
+ }
50
+ const toolCallId = asText(record["toolCallId"]);
51
+ const name = asText(record["name"]);
52
+ const ok = record["ok"];
53
+ if (toolCallId === null || name === null) {
54
+ return null;
55
+ }
56
+ if (ok !== null && typeof ok !== "boolean") {
57
+ return null;
58
+ }
59
+ return { toolCallId, name, ok };
60
+ }
61
+
62
+ /** Narrow an `ag_ui.subagent` `CUSTOM` value, or `null` if it cannot be drawn. */
63
+ export function subAgentUpdate(value: unknown): SubAgentUpdate | null {
64
+ const record = asRecord(value);
65
+ if (record === null) {
66
+ return null;
67
+ }
68
+ const delegationId = asText(record["delegationId"]);
69
+ const phase = record["phase"];
70
+ if (delegationId === null || typeof phase !== "string" || !PHASES.includes(phase)) {
71
+ return null;
72
+ }
73
+ return {
74
+ delegationId,
75
+ phase: phase as SubAgentPhase,
76
+ agent: asText(record["agent"]),
77
+ status: asText(record["status"]),
78
+ tool: asTool(record["tool"]),
79
+ };
80
+ }