@tanstack/ai 0.58.0 → 0.59.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 (85) hide show
  1. package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
  2. package/dist/esm/activities/chat/agents/define-agent.js +34 -0
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
  4. package/dist/esm/activities/chat/agents/route.d.ts +53 -0
  5. package/dist/esm/activities/chat/agents/route.js +59 -0
  6. package/dist/esm/activities/chat/agents/route.js.map +1 -0
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +490 -0
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
  10. package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
  11. package/dist/esm/activities/chat/agents/turn.js +78 -0
  12. package/dist/esm/activities/chat/agents/turn.js.map +1 -0
  13. package/dist/esm/activities/chat/index.d.ts +13 -3
  14. package/dist/esm/activities/chat/index.js +337 -13
  15. package/dist/esm/activities/chat/index.js.map +1 -1
  16. package/dist/esm/activities/chat/messages.d.ts +7 -1
  17. package/dist/esm/activities/chat/messages.js +94 -18
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
  20. package/dist/esm/activities/chat/middleware/run-store.js +8 -1
  21. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
  22. package/dist/esm/activities/chat/middleware/types.d.ts +47 -1
  23. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  24. package/dist/esm/activities/chat/stream/processor.d.ts +46 -0
  25. package/dist/esm/activities/chat/stream/processor.js +280 -7
  26. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  27. package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
  28. package/dist/esm/activities/chat/tools/tool-calls.js +54 -5
  29. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  30. package/dist/esm/activities/generateAudio/index.js +1 -1
  31. package/dist/esm/activities/generateImage/index.js +1 -1
  32. package/dist/esm/activities/generateLiveVideo/index.js +1 -1
  33. package/dist/esm/activities/generateSpeech/index.js +1 -1
  34. package/dist/esm/activities/generateTranscription/index.js +1 -1
  35. package/dist/esm/activities/generateVoice/index.js +1 -1
  36. package/dist/esm/activities/generateWorld/index.js +1 -1
  37. package/dist/esm/activities/index.d.ts +3 -0
  38. package/dist/esm/activities/index.js +5 -3
  39. package/dist/esm/activities/summarize/index.js +1 -1
  40. package/dist/esm/client.d.ts +5 -36
  41. package/dist/esm/client.js +4 -37
  42. package/dist/esm/client.js.map +1 -1
  43. package/dist/esm/index.d.ts +5 -0
  44. package/dist/esm/index.js +6 -3
  45. package/dist/esm/middlewares/content-guard.js.map +1 -1
  46. package/dist/esm/types.d.ts +110 -88
  47. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  48. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  49. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  50. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  51. package/dist/esm/utilities/ag-ui-wire.js +56 -0
  52. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  53. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  54. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  55. package/dist/esm/utilities/spec-event-keys.js +13 -8
  56. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  57. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  58. package/dist/esm/utilities/subagent-wire.js +131 -0
  59. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  60. package/package.json +2 -2
  61. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  62. package/skills/ai-core/media-generation/SKILL.md +2 -2
  63. package/skills/ai-core/middleware/SKILL.md +7 -4
  64. package/src/activities/chat/agents/define-agent.ts +121 -0
  65. package/src/activities/chat/agents/route.ts +115 -0
  66. package/src/activities/chat/agents/spawn.ts +806 -0
  67. package/src/activities/chat/agents/turn.ts +151 -0
  68. package/src/activities/chat/index.ts +529 -11
  69. package/src/activities/chat/messages.ts +125 -15
  70. package/src/activities/chat/middleware/run-store.ts +56 -7
  71. package/src/activities/chat/middleware/types.ts +47 -0
  72. package/src/activities/chat/stream/processor.ts +428 -8
  73. package/src/activities/chat/tools/tool-calls.ts +77 -11
  74. package/src/activities/index.ts +15 -0
  75. package/src/client.ts +22 -35
  76. package/src/index.ts +23 -0
  77. package/src/middlewares/content-guard.ts +7 -5
  78. package/src/types.ts +154 -94
  79. package/src/utilities/adapter-yield-chunk.ts +10 -2
  80. package/src/utilities/ag-ui-usage.test.ts +38 -0
  81. package/src/utilities/ag-ui-usage.ts +98 -11
  82. package/src/utilities/ag-ui-wire.ts +74 -0
  83. package/src/utilities/normalize-stream-chunk.ts +10 -2
  84. package/src/utilities/spec-event-keys.ts +34 -7
  85. package/src/utilities/subagent-wire.ts +184 -0
@@ -18,6 +18,7 @@
18
18
  * adapter contract, single-shot flows, and expected UIMessage output.
19
19
  */
20
20
  import {
21
+ aguiContentToContentParts,
21
22
  aguiSnapshotMessageToUIMessage,
22
23
  coerceCreatedAt,
23
24
  generateMessageId,
@@ -47,6 +48,10 @@ import {
47
48
  } from './message-updaters'
48
49
  import { ImmediateStrategy } from './strategies'
49
50
  import { INTERRUPT_BINDING_METADATA_KEY } from '../../../interrupt-resume'
51
+ import { EventType } from '../../../types'
52
+ import { splitSubagentWire } from '../../../utilities/subagent-wire'
53
+ import type { SubagentWireGroup } from '../../../utilities/subagent-wire'
54
+ import type { Message as AGUIMessage } from '@ag-ui/core'
50
55
  import type {
51
56
  ChunkRecording,
52
57
  ChunkStrategy,
@@ -67,6 +72,7 @@ import type {
67
72
  ToolCall,
68
73
  ToolCallPart,
69
74
  ToolResultPart,
75
+ SubagentPart,
70
76
  UIMessage,
71
77
  UIResourceEvent,
72
78
  UIResourcePart,
@@ -144,6 +150,11 @@ export interface StreamProcessorOptions {
144
150
  recording?: boolean
145
151
  /** Initial messages to populate the processor */
146
152
  initialMessages?: Array<UIMessage>
153
+ /**
154
+ * Set on the processor of a subagent card. Chunks tagged with this id are
155
+ * the card's own; chunks for an id no card holds are dropped.
156
+ */
157
+ subagentRunId?: string
147
158
  }
148
159
 
149
160
  const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12
@@ -207,6 +218,10 @@ export class StreamProcessor {
207
218
 
208
219
  // Run tracking (for concurrent run safety)
209
220
  private readonly activeRuns = new Set<string>()
221
+ // Direct children by subagentRunId. See childProcessor().
222
+ private readonly childProcessors = new Map<string, StreamProcessor>()
223
+ // Child tool calls whose later events may omit subagentRunId.
224
+ private readonly childToolCalls = new Map<string, string>()
210
225
 
211
226
  // Shared stream state
212
227
  private finishReason: string | null = null
@@ -218,7 +233,10 @@ export class StreamProcessor {
218
233
  private recording: ChunkRecording | null = null
219
234
  private recordingStartTime = 0
220
235
 
236
+ private readonly subagentRunId?: string
237
+
221
238
  constructor(options: StreamProcessorOptions = {}) {
239
+ this.subagentRunId = options.subagentRunId
222
240
  this.chunkStrategy = options.chunkStrategy || new ImmediateStrategy()
223
241
  this.events = options.events || {}
224
242
  this.jsonParser = options.jsonParser || defaultJSONParser
@@ -378,6 +396,13 @@ export class StreamProcessor {
378
396
  )
379
397
 
380
398
  if (!messageWithToolCall) {
399
+ const owner = this.childOwningToolCall((part) => part.id === toolCallId)
400
+ if (owner !== undefined) {
401
+ this.updateChild(owner, (child) =>
402
+ child.addToolResult(toolCallId, output, error),
403
+ )
404
+ return
405
+ }
381
406
  console.warn(
382
407
  `[StreamProcessor] Could not find message with tool call ${toolCallId}`,
383
408
  )
@@ -414,6 +439,15 @@ export class StreamProcessor {
414
439
  * Add an approval response (called by client after handling onApprovalRequest)
415
440
  */
416
441
  addToolApprovalResponse(approvalId: string, approved: boolean): void {
442
+ const owner = this.childOwningToolCall(
443
+ (part) => part.approval?.id === approvalId,
444
+ )
445
+ if (owner !== undefined) {
446
+ this.updateChild(owner, (child) =>
447
+ child.addToolApprovalResponse(approvalId, approved),
448
+ )
449
+ return
450
+ }
417
451
  this.messages = updateToolCallApprovalResponse(
418
452
  this.messages,
419
453
  approvalId,
@@ -524,6 +558,8 @@ export class StreamProcessor {
524
558
  this.structuredMessageIds.clear()
525
559
  this.structuredOutputUpdateBatches.clear()
526
560
  this.pendingManualMessageId = null
561
+ this.childProcessors.clear()
562
+ this.childToolCalls.clear()
527
563
  this.emitMessagesChange()
528
564
  }
529
565
 
@@ -578,12 +614,24 @@ export class StreamProcessor {
578
614
  })
579
615
  }
580
616
 
581
- // Cast needed: @ag-ui/core Zod passthrough types add `& { [k: string]: unknown }`
582
- // which prevents TypeScript from narrowing the `type` discriminant in switch.
617
+ if (this.routeToChild(chunk)) return
618
+
583
619
  const c = chunk
584
620
  // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
585
621
  switch (c.type) {
586
622
  // AG-UI Events
623
+ case 'SUBAGENT_STARTED':
624
+ this.handleSubagentStartedEvent(chunk)
625
+ break
626
+
627
+ case 'SUBAGENT_FINISHED':
628
+ this.handleSubagentFinishedEvent(chunk)
629
+ break
630
+
631
+ case 'SUBAGENT_ERROR':
632
+ this.handleSubagentErrorEvent(chunk)
633
+ break
634
+
587
635
  case 'TEXT_MESSAGE_START':
588
636
  this.handleTextMessageStartEvent(
589
637
  chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_START' }>,
@@ -907,6 +955,241 @@ export class StreamProcessor {
907
955
  this.emitMessagesChange()
908
956
  }
909
957
 
958
+ private findSubagentPart(subagentRunId: string) {
959
+ for (const message of this.messages) {
960
+ for (const part of message.parts) {
961
+ if (part.type === 'subagent' && part.subagent.id === subagentRunId) {
962
+ return { message, part }
963
+ }
964
+ }
965
+ }
966
+ return undefined
967
+ }
968
+
969
+ /**
970
+ * The direct child whose card holds `id`: the child itself, or a nested
971
+ * child inside its messages.
972
+ */
973
+ private childOwning(id: string): string | undefined {
974
+ const holds = (messages: ReadonlyArray<UIMessage>): boolean =>
975
+ messages.some((message) =>
976
+ message.parts.some(
977
+ (part) =>
978
+ part.type === 'subagent' &&
979
+ (part.subagent.id === id || holds(part.subagent.messages)),
980
+ ),
981
+ )
982
+ for (const message of this.messages) {
983
+ for (const part of message.parts) {
984
+ if (part.type !== 'subagent') continue
985
+ if (part.subagent.id === id || holds(part.subagent.messages)) {
986
+ return part.subagent.id
987
+ }
988
+ }
989
+ }
990
+ return undefined
991
+ }
992
+
993
+ /** The direct child whose messages hold this tool call. */
994
+ private childOwningToolCall(
995
+ matches: (part: ToolCallPart) => boolean,
996
+ ): string | undefined {
997
+ const holds = (messages: ReadonlyArray<UIMessage>): boolean =>
998
+ messages.some((message) =>
999
+ message.parts.some(
1000
+ (part) =>
1001
+ (part.type === 'tool-call' && matches(part)) ||
1002
+ (part.type === 'subagent' && holds(part.subagent.messages)),
1003
+ ),
1004
+ )
1005
+ for (const message of this.messages) {
1006
+ for (const part of message.parts) {
1007
+ if (part.type === 'subagent' && holds(part.subagent.messages)) {
1008
+ return part.subagent.id
1009
+ }
1010
+ }
1011
+ }
1012
+ return undefined
1013
+ }
1014
+
1015
+ /**
1016
+ * The processor for a direct child. The card's messages are the source of
1017
+ * truth. The processor re-reads them when something else replaced them.
1018
+ */
1019
+ private childProcessor(subagentRunId: string): StreamProcessor | undefined {
1020
+ const found = this.findSubagentPart(subagentRunId)
1021
+ if (!found) return undefined
1022
+ const messages = found.part.subagent.messages
1023
+ const existing = this.childProcessors.get(subagentRunId)
1024
+ if (existing) {
1025
+ if (existing.getMessages() !== messages) existing.messages = messages
1026
+ return existing
1027
+ }
1028
+ const child = new StreamProcessor({
1029
+ subagentRunId,
1030
+ initialMessages: messages,
1031
+ events: {
1032
+ onToolCall: (args) => this.events.onToolCall?.(args),
1033
+ onApprovalRequest: (args) => this.events.onApprovalRequest?.(args),
1034
+ onCustomEvent: (name, value, context) =>
1035
+ this.events.onCustomEvent?.(name, value, context),
1036
+ },
1037
+ })
1038
+ this.childProcessors.set(subagentRunId, child)
1039
+ return child
1040
+ }
1041
+
1042
+ /** Run `update` on a direct child, then copy its messages onto the card. */
1043
+ private updateChild(
1044
+ subagentRunId: string,
1045
+ update: (child: StreamProcessor) => void,
1046
+ ): void {
1047
+ const child = this.childProcessor(subagentRunId)
1048
+ if (!child) return
1049
+ update(child)
1050
+ const messages = child.getMessages()
1051
+ this.patchSubagent(subagentRunId, (subagent) => {
1052
+ subagent.messages = messages
1053
+ })
1054
+ }
1055
+
1056
+ private handleSubagentStartedEvent(chunk: StreamChunk): void {
1057
+ if (chunk.type !== 'SUBAGENT_STARTED') return
1058
+ const existing = this.findSubagentPart(chunk.subagentRunId)
1059
+ if (existing) {
1060
+ // A resume continues the same child. Start a fresh stream on its card.
1061
+ this.childProcessors.delete(chunk.subagentRunId)
1062
+ this.patchSubagent(chunk.subagentRunId, (subagent) => {
1063
+ subagent.status = 'running'
1064
+ delete subagent.interruptIds
1065
+ delete subagent.error
1066
+ if (chunk.metadata !== undefined) subagent.metadata = chunk.metadata
1067
+ })
1068
+ return
1069
+ }
1070
+ const { messageId } = this.ensureAssistantMessage()
1071
+ const part: SubagentPart = {
1072
+ type: 'subagent',
1073
+ subagent: {
1074
+ id: chunk.subagentRunId,
1075
+ name: chunk.name,
1076
+ ...(chunk.description !== undefined && {
1077
+ description: chunk.description,
1078
+ }),
1079
+ status: 'running',
1080
+ ...(chunk.parentSubagentRunId !== undefined && {
1081
+ parentSubagentRunId: chunk.parentSubagentRunId,
1082
+ }),
1083
+ ...(chunk.parentToolCallId !== undefined && {
1084
+ parentToolCallId: chunk.parentToolCallId,
1085
+ }),
1086
+ ...(chunk.metadata !== undefined && { metadata: chunk.metadata }),
1087
+ messages: [],
1088
+ },
1089
+ }
1090
+ this.messages = this.messages.map((message) =>
1091
+ message.id === messageId
1092
+ ? { ...message, parts: [...message.parts, part] }
1093
+ : message,
1094
+ )
1095
+ this.emitMessagesChange()
1096
+ }
1097
+
1098
+ private handleSubagentFinishedEvent(chunk: StreamChunk): void {
1099
+ if (chunk.type !== 'SUBAGENT_FINISHED') return
1100
+ this.updateChild(chunk.subagentRunId, (child) => child.finalizeStream())
1101
+ this.patchSubagent(chunk.subagentRunId, (subagent) => {
1102
+ if (subagent.status === 'error') return
1103
+ if (chunk.outcome?.type === 'suspended') {
1104
+ subagent.status = 'suspended'
1105
+ subagent.interruptIds = chunk.outcome.interruptIds ?? []
1106
+ return
1107
+ }
1108
+ subagent.status = 'finished'
1109
+ })
1110
+ }
1111
+
1112
+ private handleSubagentErrorEvent(chunk: StreamChunk): void {
1113
+ if (chunk.type !== 'SUBAGENT_ERROR') return
1114
+ this.updateChild(chunk.subagentRunId, (child) => child.finalizeStream())
1115
+ this.patchSubagent(chunk.subagentRunId, (subagent) => {
1116
+ subagent.status = 'error'
1117
+ subagent.error = {
1118
+ message: chunk.message,
1119
+ ...(chunk.code !== undefined && { code: chunk.code }),
1120
+ }
1121
+ })
1122
+ }
1123
+
1124
+ private patchSubagent(
1125
+ subagentRunId: string,
1126
+ patch: (subagent: SubagentPart['subagent']) => void,
1127
+ ): void {
1128
+ this.messages = this.messages.map((message) => {
1129
+ const index = message.parts.findIndex(
1130
+ (part) =>
1131
+ part.type === 'subagent' && part.subagent.id === subagentRunId,
1132
+ )
1133
+ if (index === -1) return message
1134
+ const part = message.parts[index]
1135
+ if (!part || part.type !== 'subagent') return message
1136
+ const next: SubagentPart = {
1137
+ type: 'subagent',
1138
+ subagent: { ...part.subagent },
1139
+ }
1140
+ patch(next.subagent)
1141
+ const parts = [...message.parts]
1142
+ parts[index] = next
1143
+ return { ...message, parts }
1144
+ })
1145
+ this.emitMessagesChange()
1146
+ }
1147
+
1148
+ /**
1149
+ * Send a child's chunk to that child's processor, so the card keeps text,
1150
+ * reasoning, tool calls, results, and nested children. A tool event without
1151
+ * `subagentRunId` follows its `TOOL_CALL_START`.
1152
+ */
1153
+ private routeToChild(chunk: StreamChunk): boolean {
1154
+ let id: string | undefined
1155
+ if (chunk.type === 'SUBAGENT_STARTED') {
1156
+ id = chunk.parentSubagentRunId
1157
+ } else if (
1158
+ chunk.type === 'SUBAGENT_FINISHED' ||
1159
+ chunk.type === 'SUBAGENT_ERROR'
1160
+ ) {
1161
+ // A direct child's own lifecycle goes to processChunk's switch, not to a
1162
+ // child.
1163
+ if (this.findSubagentPart(chunk.subagentRunId)) return false
1164
+ id = chunk.subagentRunId
1165
+ } else if ('subagentRunId' in chunk && chunk.subagentRunId) {
1166
+ id = chunk.subagentRunId
1167
+ } else if ('toolCallId' in chunk && typeof chunk.toolCallId === 'string') {
1168
+ id = this.childToolCalls.get(chunk.toolCallId)
1169
+ }
1170
+ if (id === undefined) return false
1171
+ const owner = this.childOwning(id)
1172
+ if (owner === undefined) {
1173
+ // A card's own chunks reach its processor tagged with its id.
1174
+ if (id === this.subagentRunId) return false
1175
+ // No card holds this id. Keep the chunk off this message list.
1176
+ console.warn(
1177
+ `[StreamProcessor] Dropped a chunk for unknown subagent ${id}`,
1178
+ )
1179
+ return true
1180
+ }
1181
+ if (this.findSubagentPart(owner)?.part.subagent.status === 'error') {
1182
+ // An errored child, and its nested children, ignore late events.
1183
+ // Finished and suspended children still accept them, for a resume.
1184
+ return true
1185
+ }
1186
+ if (chunk.type === 'TOOL_CALL_START') {
1187
+ this.childToolCalls.set(chunk.toolCallId, owner)
1188
+ }
1189
+ this.updateChild(owner, (child) => child.processChunk(chunk))
1190
+ return true
1191
+ }
1192
+
910
1193
  /**
911
1194
  * Handle TEXT_MESSAGE_START event
912
1195
  */
@@ -1061,10 +1344,11 @@ export class StreamProcessor {
1061
1344
  // the pre-snapshot state; see `reconcileSnapshotToolCalls`.
1062
1345
  const prevMessages = this.messages
1063
1346
  const prevById = new Map(prevMessages.map((msg) => [msg.id, msg]))
1347
+ const { top, groups } = splitSubagentWire(chunk.messages)
1064
1348
  const normalized = this.mergeReasoningFanOut(
1065
- chunk.messages.map(aguiSnapshotMessageToUIMessage),
1349
+ top.map(aguiSnapshotMessageToUIMessage),
1066
1350
  )
1067
- this.messages = this.reconcileSnapshotToolCalls(
1351
+ const reconciled = this.reconcileSnapshotToolCalls(
1068
1352
  normalized,
1069
1353
  prevMessages,
1070
1354
  ).map((msg) => {
@@ -1073,9 +1357,113 @@ export class StreamProcessor {
1073
1357
  if (prev?.metadata == null) return msg
1074
1358
  return { ...msg, metadata: prev.metadata }
1075
1359
  })
1360
+ this.messages = this.attachSnapshotSubagents(
1361
+ reconciled,
1362
+ top,
1363
+ groups,
1364
+ prevMessages,
1365
+ )
1076
1366
  this.emitMessagesChange()
1077
1367
  }
1078
1368
 
1369
+ /**
1370
+ * Put subagent cards back on a snapshot. Child wire messages (tagged with
1371
+ * `subagentRunId`) become a card on the nearest assistant message before
1372
+ * them, or a new assistant message when there is none. A card that the
1373
+ * snapshot does not carry stays on its message, because a server snapshot
1374
+ * can hold the parent history only.
1375
+ */
1376
+ private attachSnapshotSubagents(
1377
+ messages: Array<UIMessage>,
1378
+ top: ReadonlyArray<AGUIMessage>,
1379
+ groups: ReadonlyArray<SubagentWireGroup<AGUIMessage>>,
1380
+ prevMessages: ReadonlyArray<UIMessage>,
1381
+ ): Array<UIMessage> {
1382
+ const cards: Array<{
1383
+ hostId?: string
1384
+ toolCallId?: string
1385
+ part: SubagentPart
1386
+ carried: boolean
1387
+ }> = []
1388
+ const children = new Map<string, StreamProcessor>()
1389
+ for (const group of groups) {
1390
+ const child = new StreamProcessor({ subagentRunId: group.id })
1391
+ child.processChunk({
1392
+ type: EventType.MESSAGES_SNAPSHOT,
1393
+ messages: group.messages,
1394
+ timestamp: Date.now(),
1395
+ })
1396
+ children.set(group.id, child)
1397
+ const { placeholder: _placeholder, ...info } = group.info
1398
+ void _placeholder
1399
+ cards.push({
1400
+ hostId: top
1401
+ .slice(0, group.hostIndex + 1)
1402
+ .findLast((message) => message.role === 'assistant')?.id,
1403
+ part: {
1404
+ type: 'subagent',
1405
+ subagent: { ...info, id: group.id, messages: child.getMessages() },
1406
+ },
1407
+ carried: false,
1408
+ })
1409
+ }
1410
+ const inSnapshot = new Set(groups.map((group) => group.id))
1411
+ for (const message of prevMessages) {
1412
+ for (const part of message.parts) {
1413
+ if (part.type !== 'subagent' || inSnapshot.has(part.subagent.id)) {
1414
+ continue
1415
+ }
1416
+ cards.push({
1417
+ hostId: message.id,
1418
+ ...(part.subagent.parentToolCallId !== undefined && {
1419
+ toolCallId: part.subagent.parentToolCallId,
1420
+ }),
1421
+ part,
1422
+ carried: true,
1423
+ })
1424
+ const kept = this.childProcessors.get(part.subagent.id)
1425
+ if (kept) children.set(part.subagent.id, kept)
1426
+ }
1427
+ }
1428
+ this.childProcessors.clear()
1429
+ for (const [id, child] of children) this.childProcessors.set(id, child)
1430
+
1431
+ let out = messages
1432
+ for (const card of cards) {
1433
+ let index = out.findIndex((message) => message.id === card.hostId)
1434
+ if (index === -1 && card.toolCallId !== undefined) {
1435
+ index = out.findIndex((message) =>
1436
+ message.parts.some(
1437
+ (part) => part.type === 'tool-call' && part.id === card.toolCallId,
1438
+ ),
1439
+ )
1440
+ }
1441
+ if (index === -1 && !card.carried) {
1442
+ index = out.findLastIndex((message) => message.role === 'assistant')
1443
+ }
1444
+ if (index === -1) {
1445
+ if (!card.carried) {
1446
+ out = [
1447
+ ...out,
1448
+ { id: generateMessageId(), role: 'assistant', parts: [card.part] },
1449
+ ]
1450
+ }
1451
+ continue
1452
+ }
1453
+ out = out.map((message, position) =>
1454
+ position !== index ||
1455
+ message.parts.some(
1456
+ (part) =>
1457
+ part.type === 'subagent' &&
1458
+ part.subagent.id === card.part.subagent.id,
1459
+ )
1460
+ ? message
1461
+ : { ...message, parts: [...message.parts, card.part] },
1462
+ )
1463
+ }
1464
+ return out
1465
+ }
1466
+
1079
1467
  /**
1080
1468
  * Reconcile a freshly normalized snapshot with the pre-snapshot message
1081
1469
  * state so unreconstructable tool-call metadata is preserved.
@@ -1659,7 +2047,10 @@ export class StreamProcessor {
1659
2047
  // Step 1: Update the tool-call part's output field
1660
2048
  let output: unknown
1661
2049
  try {
1662
- output = JSON.parse(chunk.content)
2050
+ output =
2051
+ typeof chunk.content === 'string'
2052
+ ? JSON.parse(chunk.content)
2053
+ : chunk.content
1663
2054
  } catch {
1664
2055
  output = chunk.content
1665
2056
  }
@@ -1676,7 +2067,7 @@ export class StreamProcessor {
1676
2067
  this.messages,
1677
2068
  messageId,
1678
2069
  chunk.toolCallId,
1679
- chunk.content,
2070
+ aguiContentToContentParts(chunk.content),
1680
2071
  resultState,
1681
2072
  resultState === 'error' ? this.extractToolResultError(output) : undefined,
1682
2073
  )
@@ -1732,7 +2123,12 @@ export class StreamProcessor {
1732
2123
  }
1733
2124
  }
1734
2125
 
1735
- private handleInterrupts(interrupts: Array<Interrupt>): void {
2126
+ /**
2127
+ * Apply interrupt state to tool-call parts, then fire the client events.
2128
+ * A child's interrupt updates the child's card. `emit` is false there, so
2129
+ * the events fire once, from the top processor.
2130
+ */
2131
+ private handleInterrupts(interrupts: Array<Interrupt>, emit = true): void {
1736
2132
  const hasGeneric = interruptBatchHasGeneric(interrupts)
1737
2133
  for (const interrupt of interrupts) {
1738
2134
  const metadata =
@@ -1743,6 +2139,16 @@ export class StreamProcessor {
1743
2139
  const toolCallId = interrupt.toolCallId
1744
2140
  if (!toolCallId) continue
1745
2141
 
2142
+ const owner =
2143
+ interrupt.subagentRunId !== undefined
2144
+ ? this.childOwning(interrupt.subagentRunId)
2145
+ : this.childOwningToolCall((part) => part.id === toolCallId)
2146
+ if (owner !== undefined) {
2147
+ this.updateChild(owner, (child) =>
2148
+ child.handleInterrupts([interrupt], false),
2149
+ )
2150
+ }
2151
+
1746
2152
  const toolName =
1747
2153
  typeof metadata.toolName === 'string'
1748
2154
  ? metadata.toolName
@@ -1750,6 +2156,17 @@ export class StreamProcessor {
1750
2156
  const input = Object.hasOwn(metadata, 'input') ? metadata.input : {}
1751
2157
 
1752
2158
  if (kind === 'approval' || interrupt.reason === 'approval_required') {
2159
+ if (owner !== undefined) {
2160
+ if (emit) {
2161
+ this.events.onApprovalRequest?.({
2162
+ toolCallId,
2163
+ toolName,
2164
+ input,
2165
+ approvalId: interrupt.id,
2166
+ })
2167
+ }
2168
+ continue
2169
+ }
1753
2170
  // An interrupt terminal arrives after MESSAGES_SNAPSHOT, which may have
1754
2171
  // rebuilt the message list without the live active-message/tool-call
1755
2172
  // maps. Fall back to locating the assistant message that actually owns
@@ -1774,6 +2191,7 @@ export class StreamProcessor {
1774
2191
  this.emitMessagesChange()
1775
2192
  }
1776
2193
 
2194
+ if (!emit) continue
1777
2195
  this.events.onApprovalRequest?.({
1778
2196
  toolCallId,
1779
2197
  toolName,
@@ -1786,7 +2204,7 @@ export class StreamProcessor {
1786
2204
  if (kind === 'client_tool' || interrupt.reason === 'client_tool_input') {
1787
2205
  // Generic interrupts in the same batch decide `toolResume`. Do not
1788
2206
  // run client tools until that policy is `continue`.
1789
- if (hasGeneric) continue
2207
+ if (hasGeneric || !emit) continue
1790
2208
  this.events.onToolCall?.({
1791
2209
  toolCallId,
1792
2210
  toolName,
@@ -2617,6 +3035,8 @@ export class StreamProcessor {
2617
3035
  reset(): void {
2618
3036
  this.resetStreamState()
2619
3037
  this.messages = []
3038
+ this.childProcessors.clear()
3039
+ this.childToolCalls.clear()
2620
3040
  }
2621
3041
 
2622
3042
  /**