@assistant-ui/react-ag-ui 0.0.56 → 0.0.58

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 (42) hide show
  1. package/README.md +16 -0
  2. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +5 -17
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadRuntimeCore.js +133 -234
  5. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  7. package/dist/runtime/adapter/conversions.js +54 -25
  8. package/dist/runtime/adapter/conversions.js.map +1 -1
  9. package/dist/runtime/adapter/run-aggregator.d.ts +31 -4
  10. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.js +419 -123
  12. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  13. package/dist/runtime/adapter/subscriber.d.ts +9 -0
  14. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  15. package/dist/runtime/adapter/subscriber.js +4 -1
  16. package/dist/runtime/adapter/subscriber.js.map +1 -1
  17. package/dist/runtime/adapter/tool-approval.d.ts +3 -1
  18. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  19. package/dist/runtime/adapter/tool-approval.js +11 -12
  20. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  21. package/dist/runtime/event-parser.d.ts.map +1 -1
  22. package/dist/runtime/event-parser.js +176 -45
  23. package/dist/runtime/event-parser.js.map +1 -1
  24. package/dist/runtime/types.d.ts +42 -1
  25. package/dist/runtime/types.d.ts.map +1 -1
  26. package/dist/useAgUiRuntime.js +12 -9
  27. package/dist/useAgUiRuntime.js.map +1 -1
  28. package/package.json +10 -10
  29. package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
  30. package/src/runtime/AgUiThreadRuntimeCore.ts +190 -319
  31. package/src/runtime/adapter/conversions.ts +128 -36
  32. package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
  33. package/src/runtime/adapter/run-aggregator.ts +670 -148
  34. package/src/runtime/adapter/subscriber.ts +10 -1
  35. package/src/runtime/adapter/tool-approval.test.ts +75 -0
  36. package/src/runtime/adapter/tool-approval.ts +17 -12
  37. package/src/runtime/event-parser.ts +216 -36
  38. package/src/runtime/types.ts +70 -12
  39. package/src/useAgUiRuntime.approval.test.tsx +34 -24
  40. package/src/useAgUiRuntime.history.test.tsx +76 -0
  41. package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
  42. package/src/useAgUiRuntime.ts +12 -9
@@ -34,6 +34,9 @@ type Subscriber = {
34
34
  onMessagesSnapshotEvent?: (payload: { event: unknown }) => void;
35
35
  onCustomEvent?: (payload: { event: unknown }) => void;
36
36
  onRawEvent?: (payload: { event: unknown }) => void;
37
+ onSubagentStartedEvent?: (payload: { event: unknown }) => void;
38
+ onSubagentFinishedEvent?: (payload: { event: unknown }) => void;
39
+ onSubagentErrorEvent?: (payload: { event: unknown }) => void;
37
40
  onRunFinishedEvent?: (payload: { event: unknown }) => void;
38
41
  onRunErrorEvent?: (payload: { event: unknown }) => void;
39
42
  onRunFinalized?: () => void;
@@ -97,7 +100,7 @@ export const createAgUiSubscriber = (
97
100
  // Typed handlers will receive this via the discriminated callbacks; avoid duplicates.
98
101
  return;
99
102
  }
100
- const parsed = parseAgUiEvent(event);
103
+ const parsed = parseAgUiEvent(event, logger ? { logger } : undefined);
101
104
  if (parsed) dispatch(parsed);
102
105
  },
103
106
  onTextMessageStartEvent: ({ event }) =>
@@ -146,6 +149,12 @@ export const createAgUiSubscriber = (
146
149
  dispatchIfValid(event, "MESSAGES_SNAPSHOT"),
147
150
  onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
148
151
  onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
152
+ onSubagentStartedEvent: ({ event }) =>
153
+ dispatchIfValid(event, "SUBAGENT_STARTED"),
154
+ onSubagentFinishedEvent: ({ event }) =>
155
+ dispatchIfValid(event, "SUBAGENT_FINISHED"),
156
+ onSubagentErrorEvent: ({ event }) =>
157
+ dispatchIfValid(event, "SUBAGENT_ERROR"),
149
158
  onRunFinishedEvent: ({ event }) => {
150
159
  if (runFinishedDispatched) return;
151
160
  const parsed = ensureEvent(event, "RUN_FINISHED", logger);
@@ -36,6 +36,16 @@ describe("projectAgUiToolApprovals", () => {
36
36
  ]);
37
37
  });
38
38
 
39
+ it("carries the gate's message as the approval prompt", () => {
40
+ expect([
41
+ ...projectAgUiToolApprovals([
42
+ { ...gate("int-1", "tc-1"), message: "Delete the release branch?" },
43
+ ]),
44
+ ]).toEqual([
45
+ ["tc-1", { id: "int-1", prompt: "Delete the release branch?" }],
46
+ ]);
47
+ });
48
+
39
49
  it("gates a batch whose every tool call is rendered", () => {
40
50
  expect([
41
51
  ...projectAgUiToolApprovals(
@@ -679,3 +689,68 @@ describe("restored snapshots", () => {
679
689
  }
680
690
  });
681
691
  });
692
+
693
+ describe("nested tool-call approval seams", () => {
694
+ const nestedContent = (
695
+ approval?: Record<string, unknown>,
696
+ ): ThreadAssistantMessagePart[] => [
697
+ {
698
+ ...(toolCall("tc-spawn") as any),
699
+ messages: [
700
+ {
701
+ id: "sub-1",
702
+ role: "assistant",
703
+ createdAt: new Date(),
704
+ status: { type: "requires-action", reason: "interrupt" },
705
+ content: [toolCall("tc-nested", approval ?? { id: "int-nested" })],
706
+ metadata: {
707
+ unstable_state: null,
708
+ unstable_annotations: [],
709
+ unstable_data: [],
710
+ steps: [],
711
+ custom: {},
712
+ },
713
+ },
714
+ ],
715
+ } as ThreadAssistantMessagePart,
716
+ ];
717
+
718
+ it("records a decision on a nested gate", () => {
719
+ const next = withToolApprovalDecision(nestedContent(), {
720
+ approvalId: "int-nested",
721
+ approved: true,
722
+ });
723
+ const nested = (next[0] as any).messages[0].content[0];
724
+ expect(nested.approval).toEqual({ id: "int-nested", approved: true });
725
+ });
726
+
727
+ it("reads a nested decision into the resume array", () => {
728
+ const decided = nestedContent({ id: "int-nested", approved: false });
729
+ const resume = buildToolApprovalResume(decided, [
730
+ {
731
+ id: "int-nested",
732
+ reason: "tool_call",
733
+ toolCallId: "tc-nested",
734
+ message: "Delete?",
735
+ },
736
+ ]);
737
+ expect(resume).toEqual([
738
+ expect.objectContaining({
739
+ interruptId: "int-nested",
740
+ status: "resolved",
741
+ payload: { approved: false },
742
+ }),
743
+ ]);
744
+ });
745
+
746
+ it("settles a nested gate that a cancellation resumes", () => {
747
+ const next = withSettledToolApprovals(nestedContent(), [
748
+ { interruptId: "int-nested", status: "cancelled" },
749
+ ]);
750
+ const nested = (next[0] as any).messages[0].content[0];
751
+ expect(nested.approval).toEqual({
752
+ id: "int-nested",
753
+ resolution: "cancelled",
754
+ });
755
+ });
756
+ });
@@ -1,3 +1,7 @@
1
+ import {
2
+ iterateToolCallParts,
3
+ mapToolCallPartsDeep,
4
+ } from "@assistant-ui/core/internal";
1
5
  import { buildResumeArray } from "@ag-ui/client";
2
6
  import type {
3
7
  RespondToToolApprovalOptions,
@@ -193,7 +197,9 @@ const isPending = (approval: ToolCallMessagePart["approval"]) =>
193
197
  * render can never be decided through this seam, so the batch it belongs to
194
198
  * could never be completed either: the decisions taken on its visible siblings
195
199
  * would sit on the message while the run waited for a response that no click
196
- * can produce. `boundToolCallIds` leaves such a batch bespoke instead.
200
+ * can produce. `boundToolCallIds` spans every call the run produced — root or
201
+ * nested on ToolCallMessagePart.messages — so only a gate naming a call the
202
+ * message renders nowhere leaves the batch bespoke.
197
203
  */
198
204
  export const projectAgUiToolApprovals = (
199
205
  interrupts: readonly AgUiInterrupt[] | undefined,
@@ -212,7 +218,13 @@ export const projectAgUiToolApprovals = (
212
218
  const toolCallIds = interrupts.map((interrupt) => interrupt.toolCallId);
213
219
  if (new Set(toolCallIds).size !== toolCallIds.length) return EMPTY_APPROVALS;
214
220
  return new Map(
215
- interrupts.map((interrupt) => [interrupt.toolCallId, { id: interrupt.id }]),
221
+ interrupts.map((interrupt) => [
222
+ interrupt.toolCallId,
223
+ {
224
+ id: interrupt.id,
225
+ ...(interrupt.message !== undefined && { prompt: interrupt.message }),
226
+ },
227
+ ]),
216
228
  );
217
229
  };
218
230
 
@@ -228,12 +240,9 @@ export const withToolApprovalDecision = (
228
240
  content: readonly ThreadAssistantMessagePart[],
229
241
  { approvalId, approved, reason }: RespondToToolApprovalOptions,
230
242
  ): readonly ThreadAssistantMessagePart[] => {
231
- let changed = false;
232
- const next = content.map((part) => {
233
- if (part.type !== "tool-call") return part;
243
+ const { content: next, changed } = mapToolCallPartsDeep(content, (part) => {
234
244
  const approval = part.approval;
235
245
  if (approval?.id !== approvalId || !isPending(approval)) return part;
236
- changed = true;
237
246
  return {
238
247
  ...part,
239
248
  approval: {
@@ -263,8 +272,7 @@ export const buildToolApprovalResume = (
263
272
  // instead of recording a response, and `constructor` or `toString` would
264
273
  // report themselves as answered while undecided.
265
274
  const responses = new Map<string, { status: "resolved"; payload: unknown }>();
266
- for (const part of content) {
267
- if (part.type !== "tool-call") continue;
275
+ for (const part of iterateToolCallParts(content)) {
268
276
  const approval = part.approval;
269
277
  if (!approval || approval.approved === undefined) continue;
270
278
  if (!open.has(approval.id)) continue;
@@ -350,16 +358,13 @@ export const withSettledToolApprovals = (
350
358
  ): readonly ThreadAssistantMessagePart[] => {
351
359
  if (resume.length === 0) return content;
352
360
  const byId = new Map(resume.map((entry) => [entry.interruptId, entry]));
353
- let changed = false;
354
- const next = content.map((part) => {
355
- if (part.type !== "tool-call") return part;
361
+ const { content: next, changed } = mapToolCallPartsDeep(content, (part) => {
356
362
  const approval = part.approval;
357
363
  if (!approval) return part;
358
364
  const entry = byId.get(approval.id);
359
365
  if (!entry) return part;
360
366
  const settled = settle(approval, entry);
361
367
  if (!settled) return part;
362
- changed = true;
363
368
  return { ...part, approval: settled };
364
369
  });
365
370
  return changed ? next : content;
@@ -1,4 +1,9 @@
1
- import type { AgUiEvent, AgUiInterrupt, AgUiRunFinishedOutcome } from "./types";
1
+ import type {
2
+ AgUiEvent,
3
+ AgUiInterrupt,
4
+ AgUiRunFinishedOutcome,
5
+ AgUiSubagentFinishedOutcome,
6
+ } from "./types";
2
7
  import type { Logger } from "./logger";
3
8
  import { parseMcpToolCallResult } from "./mcp-tool-result";
4
9
  import { withRawResponseSchema } from "./interrupt-internals";
@@ -26,6 +31,16 @@ const withOptional = <T extends object>(
26
31
  const isPlainObject = (value: unknown): value is Record<string, unknown> =>
27
32
  !!value && typeof value === "object" && !Array.isArray(value);
28
33
 
34
+ const logRejectedEvent = (
35
+ logger: Logger | undefined,
36
+ type: unknown,
37
+ payload: unknown,
38
+ reason: string,
39
+ ) => {
40
+ const typeLabel = isString(type) ? type : "unknown";
41
+ logger?.debug?.(`[agui] ${typeLabel} ${reason}`, payload);
42
+ };
43
+
29
44
  const parseInterrupt = (raw: unknown): AgUiInterrupt | null => {
30
45
  if (!isPlainObject(raw)) return null;
31
46
  const id = raw.id;
@@ -51,30 +66,61 @@ const parseInterrupt = (raw: unknown): AgUiInterrupt | null => {
51
66
 
52
67
  const parseRunFinishedOutcome = (
53
68
  raw: unknown,
69
+ runId: string,
54
70
  logger: Logger | undefined,
55
71
  ): AgUiRunFinishedOutcome | undefined => {
56
- if (!isPlainObject(raw)) return undefined;
72
+ if (raw == null) return undefined;
73
+ if (!isPlainObject(raw)) {
74
+ logRejectedEvent(
75
+ logger,
76
+ "RUN_FINISHED",
77
+ { runId, outcome: raw },
78
+ "has invalid outcome",
79
+ );
80
+ return undefined;
81
+ }
57
82
  if (raw.type === "success") return { type: "success" };
58
83
  if (raw.type === "interrupt") {
59
84
  if (!Array.isArray(raw.interrupts)) {
60
- logger?.debug?.(
61
- "[agui] RUN_FINISHED interrupt outcome missing interrupts array",
62
- raw,
85
+ logRejectedEvent(
86
+ logger,
87
+ "RUN_FINISHED",
88
+ { runId, outcome: raw },
89
+ "has an interrupt outcome missing interrupts array",
63
90
  );
64
91
  return undefined;
65
92
  }
66
93
  const parsed = raw.interrupts
67
- .map((entry) => parseInterrupt(entry))
94
+ .map((entry) => {
95
+ const interrupt = parseInterrupt(entry);
96
+ if (interrupt === null) {
97
+ logRejectedEvent(
98
+ logger,
99
+ "RUN_FINISHED",
100
+ { runId, interrupt: entry },
101
+ "has malformed interrupt",
102
+ );
103
+ }
104
+ return interrupt;
105
+ })
68
106
  .filter((entry): entry is AgUiInterrupt => entry !== null);
69
107
  if (parsed.length === 0) {
70
- logger?.debug?.(
71
- "[agui] RUN_FINISHED interrupt outcome has no valid interrupts",
72
- raw.interrupts,
108
+ logRejectedEvent(
109
+ logger,
110
+ "RUN_FINISHED",
111
+ { runId, interrupts: raw.interrupts },
112
+ "has an interrupt outcome with no valid interrupts",
73
113
  );
74
114
  return undefined;
75
115
  }
76
116
  return { type: "interrupt", interrupts: parsed };
77
117
  }
118
+ logRejectedEvent(
119
+ logger,
120
+ "RUN_FINISHED",
121
+ { runId, outcome: raw },
122
+ "has unsupported outcome type",
123
+ );
78
124
  return undefined;
79
125
  };
80
126
 
@@ -82,10 +128,21 @@ export const parseAgUiEvent = (
82
128
  event: unknown,
83
129
  options?: ParseAgUiEventOptions,
84
130
  ): AgUiEvent | null => {
85
- if (!event || typeof event !== "object") return null;
131
+ if (!event || typeof event !== "object") {
132
+ logRejectedEvent(options?.logger, undefined, event, "is not an object");
133
+ return null;
134
+ }
86
135
  const payload = event as Record<string, unknown>;
87
136
  const typeValue = payload.type;
88
- if (!isString(typeValue)) return null;
137
+ if (!isString(typeValue)) {
138
+ logRejectedEvent(options?.logger, typeValue, payload, "has no string type");
139
+ return null;
140
+ }
141
+
142
+ const reject = (reason: string): null => {
143
+ logRejectedEvent(options?.logger, typeValue, payload, reason);
144
+ return null;
145
+ };
89
146
 
90
147
  const getString = (key: string) =>
91
148
  isString(payload[key]) ? (payload[key] as string) : undefined;
@@ -93,15 +150,19 @@ export const parseAgUiEvent = (
93
150
  switch (typeValue) {
94
151
  case "RUN_STARTED": {
95
152
  const runId = getString("runId");
96
- return runId ? { type: "RUN_STARTED", runId } : null;
153
+ return runId ? { type: "RUN_STARTED", runId } : reject("missing runId");
97
154
  }
98
155
  case "RUN_FINISHED": {
99
156
  const runId = getString("runId");
100
- if (!runId) return null;
157
+ if (!runId) return reject("missing runId");
101
158
  return withOptional(
102
159
  { type: "RUN_FINISHED" as const, runId },
103
160
  {
104
- outcome: parseRunFinishedOutcome(payload.outcome, options?.logger),
161
+ outcome: parseRunFinishedOutcome(
162
+ payload.outcome,
163
+ runId,
164
+ options?.logger,
165
+ ),
105
166
  },
106
167
  );
107
168
  }
@@ -121,26 +182,38 @@ export const parseAgUiEvent = (
121
182
  case "TEXT_MESSAGE_START":
122
183
  return withOptional(
123
184
  { type: "TEXT_MESSAGE_START" as const },
124
- { messageId: getString("messageId") },
185
+ {
186
+ messageId: getString("messageId"),
187
+ subagentRunId: getString("subagentRunId"),
188
+ },
125
189
  );
126
190
  case "TEXT_MESSAGE_CONTENT": {
127
191
  const delta = getString("delta");
128
- if (!isNonEmptyString(delta)) return null;
192
+ if (!isNonEmptyString(delta)) return reject("missing non-empty delta");
129
193
  return withOptional(
130
194
  { type: "TEXT_MESSAGE_CONTENT" as const, delta },
131
- { messageId: getString("messageId") },
195
+ {
196
+ messageId: getString("messageId"),
197
+ subagentRunId: getString("subagentRunId"),
198
+ },
132
199
  );
133
200
  }
134
201
  case "TEXT_MESSAGE_END":
135
202
  return withOptional(
136
203
  { type: "TEXT_MESSAGE_END" as const },
137
- { messageId: getString("messageId") },
204
+ {
205
+ messageId: getString("messageId"),
206
+ subagentRunId: getString("subagentRunId"),
207
+ },
138
208
  );
139
209
  case "TEXT_MESSAGE_CHUNK": {
140
210
  const delta = getString("delta") ?? "";
141
211
  return withOptional(
142
212
  { type: "TEXT_MESSAGE_CHUNK" as const, delta },
143
- { messageId: getString("messageId") },
213
+ {
214
+ messageId: getString("messageId"),
215
+ subagentRunId: getString("subagentRunId"),
216
+ },
144
217
  );
145
218
  }
146
219
  case "THINKING_START":
@@ -161,63 +234,94 @@ export const parseAgUiEvent = (
161
234
  case "REASONING_START":
162
235
  return withOptional(
163
236
  { type: "REASONING_START" as const },
164
- { messageId: getString("messageId") },
237
+ {
238
+ messageId: getString("messageId"),
239
+ subagentRunId: getString("subagentRunId"),
240
+ },
165
241
  );
166
242
  case "REASONING_MESSAGE_START":
167
243
  return withOptional(
168
244
  { type: "REASONING_MESSAGE_START" as const },
169
- { messageId: getString("messageId") },
245
+ {
246
+ messageId: getString("messageId"),
247
+ subagentRunId: getString("subagentRunId"),
248
+ },
170
249
  );
171
250
  case "REASONING_MESSAGE_CONTENT": {
172
251
  const delta = getString("delta") ?? "";
173
252
  return withOptional(
174
253
  { type: "REASONING_MESSAGE_CONTENT" as const, delta },
175
- { messageId: getString("messageId") },
254
+ {
255
+ messageId: getString("messageId"),
256
+ subagentRunId: getString("subagentRunId"),
257
+ },
176
258
  );
177
259
  }
178
260
  case "REASONING_MESSAGE_END":
179
261
  return withOptional(
180
262
  { type: "REASONING_MESSAGE_END" as const },
181
- { messageId: getString("messageId") },
263
+ {
264
+ messageId: getString("messageId"),
265
+ subagentRunId: getString("subagentRunId"),
266
+ },
182
267
  );
183
268
  case "REASONING_ENCRYPTED_VALUE": {
184
269
  const entityId = getString("entityId");
185
270
  const encryptedValue = getString("encryptedValue");
186
271
  const subtype = getString("subtype");
187
- if (!entityId || !encryptedValue) return null;
188
- if (subtype !== "message" && subtype !== "tool-call") return null;
272
+ if (!entityId || !encryptedValue) {
273
+ return reject("missing entityId or encryptedValue");
274
+ }
275
+ if (subtype !== "message" && subtype !== "tool-call") {
276
+ return reject("has an invalid subtype");
277
+ }
278
+ // Spread rather than withOptional: routing the narrowed `subtype` through
279
+ // a generic helper widens it back to string.
280
+ const subagentRunId = getString("subagentRunId");
189
281
  return {
190
282
  type: "REASONING_ENCRYPTED_VALUE" as const,
191
283
  subtype,
192
284
  entityId,
193
285
  encryptedValue,
286
+ ...(subagentRunId !== undefined ? { subagentRunId } : {}),
194
287
  };
195
288
  }
196
289
  case "REASONING_END":
197
290
  return withOptional(
198
291
  { type: "REASONING_END" as const },
199
- { messageId: getString("messageId") },
292
+ {
293
+ messageId: getString("messageId"),
294
+ subagentRunId: getString("subagentRunId"),
295
+ },
200
296
  );
201
297
  case "TOOL_CALL_START": {
202
298
  const toolCallId = getString("toolCallId");
203
- if (!toolCallId) return null;
299
+ if (!toolCallId) return reject("missing toolCallId");
204
300
  return withOptional(
205
301
  { type: "TOOL_CALL_START" as const, toolCallId },
206
302
  {
207
303
  toolCallName: getString("toolCallName"),
208
304
  parentMessageId: getString("parentMessageId"),
305
+ subagentRunId: getString("subagentRunId"),
209
306
  },
210
307
  );
211
308
  }
212
309
  case "TOOL_CALL_ARGS": {
213
310
  const toolCallId = getString("toolCallId");
214
- if (!toolCallId) return null;
311
+ if (!toolCallId) return reject("missing toolCallId");
215
312
  const delta = getString("delta") ?? "";
216
- return { type: "TOOL_CALL_ARGS", toolCallId, delta };
313
+ return withOptional(
314
+ { type: "TOOL_CALL_ARGS" as const, toolCallId, delta },
315
+ { subagentRunId: getString("subagentRunId") },
316
+ );
217
317
  }
218
318
  case "TOOL_CALL_END": {
219
319
  const toolCallId = getString("toolCallId");
220
- return toolCallId ? { type: "TOOL_CALL_END", toolCallId } : null;
320
+ if (!toolCallId) return reject("missing toolCallId");
321
+ return withOptional(
322
+ { type: "TOOL_CALL_END" as const, toolCallId },
323
+ { subagentRunId: getString("subagentRunId") },
324
+ );
221
325
  }
222
326
  case "TOOL_CALL_CHUNK":
223
327
  return withOptional(
@@ -227,11 +331,12 @@ export const parseAgUiEvent = (
227
331
  toolCallName: getString("toolCallName"),
228
332
  parentMessageId: getString("parentMessageId"),
229
333
  delta: getString("delta"),
334
+ subagentRunId: getString("subagentRunId"),
230
335
  },
231
336
  );
232
337
  case "TOOL_CALL_RESULT": {
233
338
  const toolCallId = getString("toolCallId");
234
- if (!toolCallId) return null;
339
+ if (!toolCallId) return reject("missing toolCallId");
235
340
  const content = getString("content") ?? "";
236
341
  return withOptional(
237
342
  {
@@ -243,10 +348,14 @@ export const parseAgUiEvent = (
243
348
  messageId: getString("messageId"),
244
349
  role: payload.role === "tool" ? "tool" : undefined,
245
350
  mcpResult: parseMcpToolCallResult(payload, content),
351
+ subagentRunId: getString("subagentRunId"),
246
352
  },
247
353
  );
248
354
  }
249
355
  case "STATE_SNAPSHOT":
356
+ if (payload.snapshot === undefined) {
357
+ return reject("missing snapshot");
358
+ }
250
359
  return { type: "STATE_SNAPSHOT", snapshot: payload.snapshot };
251
360
  case "STATE_DELTA":
252
361
  return {
@@ -254,15 +363,18 @@ export const parseAgUiEvent = (
254
363
  delta: Array.isArray(payload.delta) ? (payload.delta as any[]) : [],
255
364
  };
256
365
  case "MESSAGES_SNAPSHOT":
366
+ if (!Array.isArray(payload.messages)) {
367
+ return reject("missing messages array");
368
+ }
257
369
  return {
258
370
  type: "MESSAGES_SNAPSHOT",
259
- messages: Array.isArray(payload.messages)
260
- ? (payload.messages as any[])
261
- : [],
371
+ messages: payload.messages as any[],
262
372
  };
263
373
  case "ACTIVITY_SNAPSHOT": {
264
374
  const activityType = getString("activityType");
265
- if (!activityType || !isPlainObject(payload.content)) return null;
375
+ if (!activityType || !isPlainObject(payload.content)) {
376
+ return reject("missing activityType or object content");
377
+ }
266
378
  return withOptional(
267
379
  {
268
380
  type: "ACTIVITY_SNAPSHOT" as const,
@@ -273,6 +385,7 @@ export const parseAgUiEvent = (
273
385
  messageId: getString("messageId"),
274
386
  replace:
275
387
  typeof payload.replace === "boolean" ? payload.replace : undefined,
388
+ subagentRunId: getString("subagentRunId"),
276
389
  },
277
390
  );
278
391
  }
@@ -283,9 +396,76 @@ export const parseAgUiEvent = (
283
396
  );
284
397
  case "CUSTOM": {
285
398
  const name = getString("name");
286
- if (!name) return null;
399
+ if (!name) return reject("missing name");
287
400
  return { type: "CUSTOM", name, value: payload.value };
288
401
  }
402
+ case "SUBAGENT_STARTED": {
403
+ const subagentRunId = getString("subagentRunId");
404
+ const name = getString("name");
405
+ if (!subagentRunId || !name) {
406
+ return reject("missing subagentRunId or name");
407
+ }
408
+ return withOptional(
409
+ { type: "SUBAGENT_STARTED" as const, subagentRunId, name },
410
+ {
411
+ description: getString("description"),
412
+ parentSubagentRunId: getString("parentSubagentRunId"),
413
+ parentToolCallId: getString("parentToolCallId"),
414
+ parentMessageId: getString("parentMessageId"),
415
+ },
416
+ );
417
+ }
418
+ case "SUBAGENT_FINISHED": {
419
+ const subagentRunId = getString("subagentRunId");
420
+ if (!subagentRunId) return reject("missing subagentRunId");
421
+ const rawOutcome = payload.outcome;
422
+ let outcome: AgUiSubagentFinishedOutcome | undefined;
423
+ if (rawOutcome == null) {
424
+ outcome = undefined;
425
+ } else if (!isPlainObject(rawOutcome)) {
426
+ logRejectedEvent(
427
+ options?.logger,
428
+ "SUBAGENT_FINISHED",
429
+ { subagentRunId, outcome: rawOutcome },
430
+ "has invalid outcome",
431
+ );
432
+ outcome = undefined;
433
+ } else if (rawOutcome.type === "success") {
434
+ outcome = { type: "success" as const };
435
+ } else if (rawOutcome.type === "suspended") {
436
+ outcome = withOptional(
437
+ { type: "suspended" as const },
438
+ {
439
+ interruptIds: Array.isArray(rawOutcome.interruptIds)
440
+ ? (rawOutcome.interruptIds as unknown[]).filter(isString)
441
+ : undefined,
442
+ },
443
+ );
444
+ } else {
445
+ logRejectedEvent(
446
+ options?.logger,
447
+ "SUBAGENT_FINISHED",
448
+ { subagentRunId, outcome: rawOutcome },
449
+ "has unsupported outcome type",
450
+ );
451
+ outcome = undefined;
452
+ }
453
+ return withOptional(
454
+ { type: "SUBAGENT_FINISHED" as const, subagentRunId },
455
+ { result: payload.result, outcome },
456
+ );
457
+ }
458
+ case "SUBAGENT_ERROR": {
459
+ const subagentRunId = getString("subagentRunId");
460
+ const message = getString("message");
461
+ if (!subagentRunId || !message) {
462
+ return reject("missing subagentRunId or message");
463
+ }
464
+ return withOptional(
465
+ { type: "SUBAGENT_ERROR" as const, subagentRunId, message },
466
+ { code: getString("code") },
467
+ );
468
+ }
289
469
  default:
290
470
  return withOptional(
291
471
  { type: "RAW" as const, event: payload },