@tanstack/ai 0.58.0 → 0.61.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 (144) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  2. package/dist/esm/activities/chat/adapter.js +1 -0
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
  5. package/dist/esm/activities/chat/agents/define-agent.js +34 -0
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
  7. package/dist/esm/activities/chat/agents/route.d.ts +53 -0
  8. package/dist/esm/activities/chat/agents/route.js +59 -0
  9. package/dist/esm/activities/chat/agents/route.js.map +1 -0
  10. package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
  11. package/dist/esm/activities/chat/agents/spawn.js +490 -0
  12. package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
  13. package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
  14. package/dist/esm/activities/chat/agents/turn.js +78 -0
  15. package/dist/esm/activities/chat/agents/turn.js.map +1 -0
  16. package/dist/esm/activities/chat/index.d.ts +13 -3
  17. package/dist/esm/activities/chat/index.js +462 -26
  18. package/dist/esm/activities/chat/index.js.map +1 -1
  19. package/dist/esm/activities/chat/messages.d.ts +7 -1
  20. package/dist/esm/activities/chat/messages.js +99 -19
  21. package/dist/esm/activities/chat/messages.js.map +1 -1
  22. package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
  23. package/dist/esm/activities/chat/middleware/run-store.js +8 -1
  24. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
  25. package/dist/esm/activities/chat/middleware/types.d.ts +47 -1
  26. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  27. package/dist/esm/activities/chat/stream/message-updaters.js +9 -2
  28. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  29. package/dist/esm/activities/chat/stream/processor.d.ts +46 -1
  30. package/dist/esm/activities/chat/stream/processor.js +294 -18
  31. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  32. package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
  33. package/dist/esm/activities/chat/tools/tool-calls.js +56 -5
  34. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  35. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  36. package/dist/esm/activities/embed/adapter.js +1 -0
  37. package/dist/esm/activities/embed/adapter.js.map +1 -1
  38. package/dist/esm/activities/embed/index.js +2 -0
  39. package/dist/esm/activities/embed/index.js.map +1 -1
  40. package/dist/esm/activities/files/adapter.d.ts +97 -0
  41. package/dist/esm/activities/files/adapter.js +45 -0
  42. package/dist/esm/activities/files/adapter.js.map +1 -0
  43. package/dist/esm/activities/files/index.d.ts +66 -0
  44. package/dist/esm/activities/files/index.js +78 -0
  45. package/dist/esm/activities/files/index.js.map +1 -0
  46. package/dist/esm/activities/generateAudio/index.js +1 -1
  47. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  48. package/dist/esm/activities/generateImage/adapter.js +1 -0
  49. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  50. package/dist/esm/activities/generateImage/index.js +3 -1
  51. package/dist/esm/activities/generateImage/index.js.map +1 -1
  52. package/dist/esm/activities/generateLiveVideo/index.js +1 -1
  53. package/dist/esm/activities/generateSpeech/index.js +1 -1
  54. package/dist/esm/activities/generateTranscription/index.js +1 -1
  55. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  56. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  57. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  58. package/dist/esm/activities/generateVideo/index.js +3 -0
  59. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  60. package/dist/esm/activities/generateVoice/index.js +1 -1
  61. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  62. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  63. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  64. package/dist/esm/activities/generateWorld/index.js +6 -5
  65. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  66. package/dist/esm/activities/index.d.ts +9 -3
  67. package/dist/esm/activities/index.js +17 -13
  68. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  69. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  70. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  71. package/dist/esm/activities/summarize/index.js +1 -1
  72. package/dist/esm/client.d.ts +7 -36
  73. package/dist/esm/client.js +5 -37
  74. package/dist/esm/client.js.map +1 -1
  75. package/dist/esm/index.d.ts +8 -2
  76. package/dist/esm/index.js +9 -4
  77. package/dist/esm/middlewares/content-guard.js.map +1 -1
  78. package/dist/esm/types.d.ts +179 -98
  79. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  80. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  81. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  82. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  83. package/dist/esm/utilities/ag-ui-wire.js +90 -13
  84. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  85. package/dist/esm/utilities/content-source.d.ts +60 -0
  86. package/dist/esm/utilities/content-source.js +85 -0
  87. package/dist/esm/utilities/content-source.js.map +1 -0
  88. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  89. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  90. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  91. package/dist/esm/utilities/provider-executed.js +10 -1
  92. package/dist/esm/utilities/provider-executed.js.map +1 -1
  93. package/dist/esm/utilities/spec-event-keys.js +13 -8
  94. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  95. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  96. package/dist/esm/utilities/subagent-wire.js +131 -0
  97. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  98. package/dist/esm/utilities/tool-result.d.ts +12 -2
  99. package/dist/esm/utilities/tool-result.js +23 -3
  100. package/dist/esm/utilities/tool-result.js.map +1 -1
  101. package/package.json +3 -3
  102. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  103. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  104. package/skills/ai-core/chat-experience/SKILL.md +14 -0
  105. package/skills/ai-core/media-generation/SKILL.md +10 -2
  106. package/skills/ai-core/middleware/SKILL.md +7 -4
  107. package/src/activities/chat/adapter.ts +10 -0
  108. package/src/activities/chat/agents/define-agent.ts +121 -0
  109. package/src/activities/chat/agents/route.ts +115 -0
  110. package/src/activities/chat/agents/spawn.ts +806 -0
  111. package/src/activities/chat/agents/turn.ts +151 -0
  112. package/src/activities/chat/index.ts +734 -30
  113. package/src/activities/chat/messages.ts +137 -16
  114. package/src/activities/chat/middleware/run-store.ts +56 -7
  115. package/src/activities/chat/middleware/types.ts +47 -0
  116. package/src/activities/chat/stream/message-updaters.ts +24 -2
  117. package/src/activities/chat/stream/processor.ts +452 -30
  118. package/src/activities/chat/tools/tool-calls.ts +83 -11
  119. package/src/activities/embed/adapter.ts +7 -0
  120. package/src/activities/embed/index.ts +5 -0
  121. package/src/activities/files/adapter.ts +120 -0
  122. package/src/activities/files/index.ts +113 -0
  123. package/src/activities/generateImage/adapter.ts +8 -0
  124. package/src/activities/generateImage/index.ts +4 -0
  125. package/src/activities/generateVideo/adapter.ts +8 -0
  126. package/src/activities/generateVideo/index.ts +7 -0
  127. package/src/activities/generateWorld/adapter.ts +4 -2
  128. package/src/activities/generateWorld/index.ts +7 -6
  129. package/src/activities/index.ts +40 -1
  130. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  131. package/src/client.ts +29 -35
  132. package/src/index.ts +39 -0
  133. package/src/middlewares/content-guard.ts +7 -5
  134. package/src/types.ts +226 -103
  135. package/src/utilities/adapter-yield-chunk.ts +10 -2
  136. package/src/utilities/ag-ui-usage.test.ts +38 -0
  137. package/src/utilities/ag-ui-usage.ts +98 -11
  138. package/src/utilities/ag-ui-wire.ts +134 -16
  139. package/src/utilities/content-source.ts +138 -0
  140. package/src/utilities/normalize-stream-chunk.ts +10 -2
  141. package/src/utilities/provider-executed.ts +13 -0
  142. package/src/utilities/spec-event-keys.ts +34 -7
  143. package/src/utilities/subagent-wire.ts +184 -0
  144. package/src/utilities/tool-result.ts +38 -2
@@ -18,20 +18,27 @@
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,
24
25
  uiMessageToModelMessages,
25
26
  } from '../messages.js'
26
27
  import { runErrorEventToError } from '../../../utilities/errors'
27
- import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
28
+ import {
29
+ isAssistantSegmentOf,
30
+ isProviderExecutedToolCall,
31
+ } from '../../../utilities/provider-executed'
28
32
  import {
29
33
  mergeMetadata,
30
34
  tanstackMetadata,
31
35
  } from '../../../utilities/merge-metadata'
32
36
  import { getChunkRunId } from '../../../utilities/chunk-ids'
33
37
  import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
34
- import { normalizeToolResult } from '../../../utilities/tool-result'
38
+ import {
39
+ normalizeToolResult,
40
+ toolResultErrorText,
41
+ } from '../../../utilities/tool-result'
35
42
  import { defaultJSONParser } from './json-parser'
36
43
  import {
37
44
  appendStructuredOutputDelta,
@@ -47,6 +54,10 @@ import {
47
54
  } from './message-updaters'
48
55
  import { ImmediateStrategy } from './strategies'
49
56
  import { INTERRUPT_BINDING_METADATA_KEY } from '../../../interrupt-resume'
57
+ import { EventType } from '../../../types'
58
+ import { splitSubagentWire } from '../../../utilities/subagent-wire'
59
+ import type { SubagentWireGroup } from '../../../utilities/subagent-wire'
60
+ import type { Message as AGUIMessage } from '@ag-ui/core'
50
61
  import type {
51
62
  ChunkRecording,
52
63
  ChunkStrategy,
@@ -67,6 +78,7 @@ import type {
67
78
  ToolCall,
68
79
  ToolCallPart,
69
80
  ToolResultPart,
81
+ SubagentPart,
70
82
  UIMessage,
71
83
  UIResourceEvent,
72
84
  UIResourcePart,
@@ -144,6 +156,11 @@ export interface StreamProcessorOptions {
144
156
  recording?: boolean
145
157
  /** Initial messages to populate the processor */
146
158
  initialMessages?: Array<UIMessage>
159
+ /**
160
+ * Set on the processor of a subagent card. Chunks tagged with this id are
161
+ * the card's own; chunks for an id no card holds are dropped.
162
+ */
163
+ subagentRunId?: string
147
164
  }
148
165
 
149
166
  const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12
@@ -207,6 +224,10 @@ export class StreamProcessor {
207
224
 
208
225
  // Run tracking (for concurrent run safety)
209
226
  private readonly activeRuns = new Set<string>()
227
+ // Direct children by subagentRunId. See childProcessor().
228
+ private readonly childProcessors = new Map<string, StreamProcessor>()
229
+ // Child tool calls whose later events may omit subagentRunId.
230
+ private readonly childToolCalls = new Map<string, string>()
210
231
 
211
232
  // Shared stream state
212
233
  private finishReason: string | null = null
@@ -218,7 +239,10 @@ export class StreamProcessor {
218
239
  private recording: ChunkRecording | null = null
219
240
  private recordingStartTime = 0
220
241
 
242
+ private readonly subagentRunId?: string
243
+
221
244
  constructor(options: StreamProcessorOptions = {}) {
245
+ this.subagentRunId = options.subagentRunId
222
246
  this.chunkStrategy = options.chunkStrategy || new ImmediateStrategy()
223
247
  this.events = options.events || {}
224
248
  this.jsonParser = options.jsonParser || defaultJSONParser
@@ -378,6 +402,13 @@ export class StreamProcessor {
378
402
  )
379
403
 
380
404
  if (!messageWithToolCall) {
405
+ const owner = this.childOwningToolCall((part) => part.id === toolCallId)
406
+ if (owner !== undefined) {
407
+ this.updateChild(owner, (child) =>
408
+ child.addToolResult(toolCallId, output, error),
409
+ )
410
+ return
411
+ }
381
412
  console.warn(
382
413
  `[StreamProcessor] Could not find message with tool call ${toolCallId}`,
383
414
  )
@@ -414,6 +445,15 @@ export class StreamProcessor {
414
445
  * Add an approval response (called by client after handling onApprovalRequest)
415
446
  */
416
447
  addToolApprovalResponse(approvalId: string, approved: boolean): void {
448
+ const owner = this.childOwningToolCall(
449
+ (part) => part.approval?.id === approvalId,
450
+ )
451
+ if (owner !== undefined) {
452
+ this.updateChild(owner, (child) =>
453
+ child.addToolApprovalResponse(approvalId, approved),
454
+ )
455
+ return
456
+ }
417
457
  this.messages = updateToolCallApprovalResponse(
418
458
  this.messages,
419
459
  approvalId,
@@ -524,6 +564,8 @@ export class StreamProcessor {
524
564
  this.structuredMessageIds.clear()
525
565
  this.structuredOutputUpdateBatches.clear()
526
566
  this.pendingManualMessageId = null
567
+ this.childProcessors.clear()
568
+ this.childToolCalls.clear()
527
569
  this.emitMessagesChange()
528
570
  }
529
571
 
@@ -578,12 +620,24 @@ export class StreamProcessor {
578
620
  })
579
621
  }
580
622
 
581
- // Cast needed: @ag-ui/core Zod passthrough types add `& { [k: string]: unknown }`
582
- // which prevents TypeScript from narrowing the `type` discriminant in switch.
623
+ if (this.routeToChild(chunk)) return
624
+
583
625
  const c = chunk
584
626
  // 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
627
  switch (c.type) {
586
628
  // AG-UI Events
629
+ case 'SUBAGENT_STARTED':
630
+ this.handleSubagentStartedEvent(chunk)
631
+ break
632
+
633
+ case 'SUBAGENT_FINISHED':
634
+ this.handleSubagentFinishedEvent(chunk)
635
+ break
636
+
637
+ case 'SUBAGENT_ERROR':
638
+ this.handleSubagentErrorEvent(chunk)
639
+ break
640
+
587
641
  case 'TEXT_MESSAGE_START':
588
642
  this.handleTextMessageStartEvent(
589
643
  chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_START' }>,
@@ -907,6 +961,241 @@ export class StreamProcessor {
907
961
  this.emitMessagesChange()
908
962
  }
909
963
 
964
+ private findSubagentPart(subagentRunId: string) {
965
+ for (const message of this.messages) {
966
+ for (const part of message.parts) {
967
+ if (part.type === 'subagent' && part.subagent.id === subagentRunId) {
968
+ return { message, part }
969
+ }
970
+ }
971
+ }
972
+ return undefined
973
+ }
974
+
975
+ /**
976
+ * The direct child whose card holds `id`: the child itself, or a nested
977
+ * child inside its messages.
978
+ */
979
+ private childOwning(id: string): string | undefined {
980
+ const holds = (messages: ReadonlyArray<UIMessage>): boolean =>
981
+ messages.some((message) =>
982
+ message.parts.some(
983
+ (part) =>
984
+ part.type === 'subagent' &&
985
+ (part.subagent.id === id || holds(part.subagent.messages)),
986
+ ),
987
+ )
988
+ for (const message of this.messages) {
989
+ for (const part of message.parts) {
990
+ if (part.type !== 'subagent') continue
991
+ if (part.subagent.id === id || holds(part.subagent.messages)) {
992
+ return part.subagent.id
993
+ }
994
+ }
995
+ }
996
+ return undefined
997
+ }
998
+
999
+ /** The direct child whose messages hold this tool call. */
1000
+ private childOwningToolCall(
1001
+ matches: (part: ToolCallPart) => boolean,
1002
+ ): string | undefined {
1003
+ const holds = (messages: ReadonlyArray<UIMessage>): boolean =>
1004
+ messages.some((message) =>
1005
+ message.parts.some(
1006
+ (part) =>
1007
+ (part.type === 'tool-call' && matches(part)) ||
1008
+ (part.type === 'subagent' && holds(part.subagent.messages)),
1009
+ ),
1010
+ )
1011
+ for (const message of this.messages) {
1012
+ for (const part of message.parts) {
1013
+ if (part.type === 'subagent' && holds(part.subagent.messages)) {
1014
+ return part.subagent.id
1015
+ }
1016
+ }
1017
+ }
1018
+ return undefined
1019
+ }
1020
+
1021
+ /**
1022
+ * The processor for a direct child. The card's messages are the source of
1023
+ * truth. The processor re-reads them when something else replaced them.
1024
+ */
1025
+ private childProcessor(subagentRunId: string): StreamProcessor | undefined {
1026
+ const found = this.findSubagentPart(subagentRunId)
1027
+ if (!found) return undefined
1028
+ const messages = found.part.subagent.messages
1029
+ const existing = this.childProcessors.get(subagentRunId)
1030
+ if (existing) {
1031
+ if (existing.getMessages() !== messages) existing.messages = messages
1032
+ return existing
1033
+ }
1034
+ const child = new StreamProcessor({
1035
+ subagentRunId,
1036
+ initialMessages: messages,
1037
+ events: {
1038
+ onToolCall: (args) => this.events.onToolCall?.(args),
1039
+ onApprovalRequest: (args) => this.events.onApprovalRequest?.(args),
1040
+ onCustomEvent: (name, value, context) =>
1041
+ this.events.onCustomEvent?.(name, value, context),
1042
+ },
1043
+ })
1044
+ this.childProcessors.set(subagentRunId, child)
1045
+ return child
1046
+ }
1047
+
1048
+ /** Run `update` on a direct child, then copy its messages onto the card. */
1049
+ private updateChild(
1050
+ subagentRunId: string,
1051
+ update: (child: StreamProcessor) => void,
1052
+ ): void {
1053
+ const child = this.childProcessor(subagentRunId)
1054
+ if (!child) return
1055
+ update(child)
1056
+ const messages = child.getMessages()
1057
+ this.patchSubagent(subagentRunId, (subagent) => {
1058
+ subagent.messages = messages
1059
+ })
1060
+ }
1061
+
1062
+ private handleSubagentStartedEvent(chunk: StreamChunk): void {
1063
+ if (chunk.type !== 'SUBAGENT_STARTED') return
1064
+ const existing = this.findSubagentPart(chunk.subagentRunId)
1065
+ if (existing) {
1066
+ // A resume continues the same child. Start a fresh stream on its card.
1067
+ this.childProcessors.delete(chunk.subagentRunId)
1068
+ this.patchSubagent(chunk.subagentRunId, (subagent) => {
1069
+ subagent.status = 'running'
1070
+ delete subagent.interruptIds
1071
+ delete subagent.error
1072
+ if (chunk.metadata !== undefined) subagent.metadata = chunk.metadata
1073
+ })
1074
+ return
1075
+ }
1076
+ const { messageId } = this.ensureAssistantMessage()
1077
+ const part: SubagentPart = {
1078
+ type: 'subagent',
1079
+ subagent: {
1080
+ id: chunk.subagentRunId,
1081
+ name: chunk.name,
1082
+ ...(chunk.description !== undefined && {
1083
+ description: chunk.description,
1084
+ }),
1085
+ status: 'running',
1086
+ ...(chunk.parentSubagentRunId !== undefined && {
1087
+ parentSubagentRunId: chunk.parentSubagentRunId,
1088
+ }),
1089
+ ...(chunk.parentToolCallId !== undefined && {
1090
+ parentToolCallId: chunk.parentToolCallId,
1091
+ }),
1092
+ ...(chunk.metadata !== undefined && { metadata: chunk.metadata }),
1093
+ messages: [],
1094
+ },
1095
+ }
1096
+ this.messages = this.messages.map((message) =>
1097
+ message.id === messageId
1098
+ ? { ...message, parts: [...message.parts, part] }
1099
+ : message,
1100
+ )
1101
+ this.emitMessagesChange()
1102
+ }
1103
+
1104
+ private handleSubagentFinishedEvent(chunk: StreamChunk): void {
1105
+ if (chunk.type !== 'SUBAGENT_FINISHED') return
1106
+ this.updateChild(chunk.subagentRunId, (child) => child.finalizeStream())
1107
+ this.patchSubagent(chunk.subagentRunId, (subagent) => {
1108
+ if (subagent.status === 'error') return
1109
+ if (chunk.outcome?.type === 'suspended') {
1110
+ subagent.status = 'suspended'
1111
+ subagent.interruptIds = chunk.outcome.interruptIds ?? []
1112
+ return
1113
+ }
1114
+ subagent.status = 'finished'
1115
+ })
1116
+ }
1117
+
1118
+ private handleSubagentErrorEvent(chunk: StreamChunk): void {
1119
+ if (chunk.type !== 'SUBAGENT_ERROR') return
1120
+ this.updateChild(chunk.subagentRunId, (child) => child.finalizeStream())
1121
+ this.patchSubagent(chunk.subagentRunId, (subagent) => {
1122
+ subagent.status = 'error'
1123
+ subagent.error = {
1124
+ message: chunk.message,
1125
+ ...(chunk.code !== undefined && { code: chunk.code }),
1126
+ }
1127
+ })
1128
+ }
1129
+
1130
+ private patchSubagent(
1131
+ subagentRunId: string,
1132
+ patch: (subagent: SubagentPart['subagent']) => void,
1133
+ ): void {
1134
+ this.messages = this.messages.map((message) => {
1135
+ const index = message.parts.findIndex(
1136
+ (part) =>
1137
+ part.type === 'subagent' && part.subagent.id === subagentRunId,
1138
+ )
1139
+ if (index === -1) return message
1140
+ const part = message.parts[index]
1141
+ if (!part || part.type !== 'subagent') return message
1142
+ const next: SubagentPart = {
1143
+ type: 'subagent',
1144
+ subagent: { ...part.subagent },
1145
+ }
1146
+ patch(next.subagent)
1147
+ const parts = [...message.parts]
1148
+ parts[index] = next
1149
+ return { ...message, parts }
1150
+ })
1151
+ this.emitMessagesChange()
1152
+ }
1153
+
1154
+ /**
1155
+ * Send a child's chunk to that child's processor, so the card keeps text,
1156
+ * reasoning, tool calls, results, and nested children. A tool event without
1157
+ * `subagentRunId` follows its `TOOL_CALL_START`.
1158
+ */
1159
+ private routeToChild(chunk: StreamChunk): boolean {
1160
+ let id: string | undefined
1161
+ if (chunk.type === 'SUBAGENT_STARTED') {
1162
+ id = chunk.parentSubagentRunId
1163
+ } else if (
1164
+ chunk.type === 'SUBAGENT_FINISHED' ||
1165
+ chunk.type === 'SUBAGENT_ERROR'
1166
+ ) {
1167
+ // A direct child's own lifecycle goes to processChunk's switch, not to a
1168
+ // child.
1169
+ if (this.findSubagentPart(chunk.subagentRunId)) return false
1170
+ id = chunk.subagentRunId
1171
+ } else if ('subagentRunId' in chunk && chunk.subagentRunId) {
1172
+ id = chunk.subagentRunId
1173
+ } else if ('toolCallId' in chunk && typeof chunk.toolCallId === 'string') {
1174
+ id = this.childToolCalls.get(chunk.toolCallId)
1175
+ }
1176
+ if (id === undefined) return false
1177
+ const owner = this.childOwning(id)
1178
+ if (owner === undefined) {
1179
+ // A card's own chunks reach its processor tagged with its id.
1180
+ if (id === this.subagentRunId) return false
1181
+ // No card holds this id. Keep the chunk off this message list.
1182
+ console.warn(
1183
+ `[StreamProcessor] Dropped a chunk for unknown subagent ${id}`,
1184
+ )
1185
+ return true
1186
+ }
1187
+ if (this.findSubagentPart(owner)?.part.subagent.status === 'error') {
1188
+ // An errored child, and its nested children, ignore late events.
1189
+ // Finished and suspended children still accept them, for a resume.
1190
+ return true
1191
+ }
1192
+ if (chunk.type === 'TOOL_CALL_START') {
1193
+ this.childToolCalls.set(chunk.toolCallId, owner)
1194
+ }
1195
+ this.updateChild(owner, (child) => child.processChunk(chunk))
1196
+ return true
1197
+ }
1198
+
910
1199
  /**
911
1200
  * Handle TEXT_MESSAGE_START event
912
1201
  */
@@ -1061,10 +1350,11 @@ export class StreamProcessor {
1061
1350
  // the pre-snapshot state; see `reconcileSnapshotToolCalls`.
1062
1351
  const prevMessages = this.messages
1063
1352
  const prevById = new Map(prevMessages.map((msg) => [msg.id, msg]))
1353
+ const { top, groups } = splitSubagentWire(chunk.messages)
1064
1354
  const normalized = this.mergeReasoningFanOut(
1065
- chunk.messages.map(aguiSnapshotMessageToUIMessage),
1355
+ top.map(aguiSnapshotMessageToUIMessage),
1066
1356
  )
1067
- this.messages = this.reconcileSnapshotToolCalls(
1357
+ const reconciled = this.reconcileSnapshotToolCalls(
1068
1358
  normalized,
1069
1359
  prevMessages,
1070
1360
  ).map((msg) => {
@@ -1073,9 +1363,113 @@ export class StreamProcessor {
1073
1363
  if (prev?.metadata == null) return msg
1074
1364
  return { ...msg, metadata: prev.metadata }
1075
1365
  })
1366
+ this.messages = this.attachSnapshotSubagents(
1367
+ reconciled,
1368
+ top,
1369
+ groups,
1370
+ prevMessages,
1371
+ )
1076
1372
  this.emitMessagesChange()
1077
1373
  }
1078
1374
 
1375
+ /**
1376
+ * Put subagent cards back on a snapshot. Child wire messages (tagged with
1377
+ * `subagentRunId`) become a card on the nearest assistant message before
1378
+ * them, or a new assistant message when there is none. A card that the
1379
+ * snapshot does not carry stays on its message, because a server snapshot
1380
+ * can hold the parent history only.
1381
+ */
1382
+ private attachSnapshotSubagents(
1383
+ messages: Array<UIMessage>,
1384
+ top: ReadonlyArray<AGUIMessage>,
1385
+ groups: ReadonlyArray<SubagentWireGroup<AGUIMessage>>,
1386
+ prevMessages: ReadonlyArray<UIMessage>,
1387
+ ): Array<UIMessage> {
1388
+ const cards: Array<{
1389
+ hostId?: string
1390
+ toolCallId?: string
1391
+ part: SubagentPart
1392
+ carried: boolean
1393
+ }> = []
1394
+ const children = new Map<string, StreamProcessor>()
1395
+ for (const group of groups) {
1396
+ const child = new StreamProcessor({ subagentRunId: group.id })
1397
+ child.processChunk({
1398
+ type: EventType.MESSAGES_SNAPSHOT,
1399
+ messages: group.messages,
1400
+ timestamp: Date.now(),
1401
+ })
1402
+ children.set(group.id, child)
1403
+ const { placeholder: _placeholder, ...info } = group.info
1404
+ void _placeholder
1405
+ cards.push({
1406
+ hostId: top
1407
+ .slice(0, group.hostIndex + 1)
1408
+ .findLast((message) => message.role === 'assistant')?.id,
1409
+ part: {
1410
+ type: 'subagent',
1411
+ subagent: { ...info, id: group.id, messages: child.getMessages() },
1412
+ },
1413
+ carried: false,
1414
+ })
1415
+ }
1416
+ const inSnapshot = new Set(groups.map((group) => group.id))
1417
+ for (const message of prevMessages) {
1418
+ for (const part of message.parts) {
1419
+ if (part.type !== 'subagent' || inSnapshot.has(part.subagent.id)) {
1420
+ continue
1421
+ }
1422
+ cards.push({
1423
+ hostId: message.id,
1424
+ ...(part.subagent.parentToolCallId !== undefined && {
1425
+ toolCallId: part.subagent.parentToolCallId,
1426
+ }),
1427
+ part,
1428
+ carried: true,
1429
+ })
1430
+ const kept = this.childProcessors.get(part.subagent.id)
1431
+ if (kept) children.set(part.subagent.id, kept)
1432
+ }
1433
+ }
1434
+ this.childProcessors.clear()
1435
+ for (const [id, child] of children) this.childProcessors.set(id, child)
1436
+
1437
+ let out = messages
1438
+ for (const card of cards) {
1439
+ let index = out.findIndex((message) => message.id === card.hostId)
1440
+ if (index === -1 && card.toolCallId !== undefined) {
1441
+ index = out.findIndex((message) =>
1442
+ message.parts.some(
1443
+ (part) => part.type === 'tool-call' && part.id === card.toolCallId,
1444
+ ),
1445
+ )
1446
+ }
1447
+ if (index === -1 && !card.carried) {
1448
+ index = out.findLastIndex((message) => message.role === 'assistant')
1449
+ }
1450
+ if (index === -1) {
1451
+ if (!card.carried) {
1452
+ out = [
1453
+ ...out,
1454
+ { id: generateMessageId(), role: 'assistant', parts: [card.part] },
1455
+ ]
1456
+ }
1457
+ continue
1458
+ }
1459
+ out = out.map((message, position) =>
1460
+ position !== index ||
1461
+ message.parts.some(
1462
+ (part) =>
1463
+ part.type === 'subagent' &&
1464
+ part.subagent.id === card.part.subagent.id,
1465
+ )
1466
+ ? message
1467
+ : { ...message, parts: [...message.parts, card.part] },
1468
+ )
1469
+ }
1470
+ return out
1471
+ }
1472
+
1079
1473
  /**
1080
1474
  * Reconcile a freshly normalized snapshot with the pre-snapshot message
1081
1475
  * state so unreconstructable tool-call metadata is preserved.
@@ -1130,20 +1524,30 @@ export class StreamProcessor {
1130
1524
  pending.push(msg)
1131
1525
  continue
1132
1526
  }
1527
+ let next = msg
1133
1528
  if (
1134
1529
  msg.role === 'assistant' &&
1135
1530
  pending.length > 0 &&
1136
1531
  !isToolResultOnly(msg)
1137
1532
  ) {
1138
- out.push({
1533
+ next = {
1139
1534
  ...msg,
1140
1535
  parts: [...pending.flatMap(thinkingParts), ...msg.parts],
1141
- })
1536
+ }
1142
1537
  pending = []
1538
+ } else {
1539
+ flushPending()
1540
+ }
1541
+ const prev = out.at(-1)
1542
+ if (
1543
+ prev?.role === 'assistant' &&
1544
+ next.role === 'assistant' &&
1545
+ isAssistantSegmentOf(next.id, prev.id)
1546
+ ) {
1547
+ out[out.length - 1] = { ...prev, parts: [...prev.parts, ...next.parts] }
1143
1548
  continue
1144
1549
  }
1145
- flushPending()
1146
- out.push(msg)
1550
+ out.push(next)
1147
1551
  }
1148
1552
  flushPending()
1149
1553
  return out
@@ -1317,9 +1721,7 @@ export class StreamProcessor {
1317
1721
  }
1318
1722
  }
1319
1723
  const errorText =
1320
- result.state === 'error'
1321
- ? this.extractToolResultError(output)
1322
- : undefined
1724
+ result.state === 'error' ? toolResultErrorText(output) : undefined
1323
1725
  next = {
1324
1726
  ...next,
1325
1727
  output: errorText ? { error: errorText } : output,
@@ -1613,18 +2015,6 @@ export class StreamProcessor {
1613
2015
  }
1614
2016
  }
1615
2017
 
1616
- private extractToolResultError(output: unknown): string {
1617
- if (
1618
- output &&
1619
- typeof output === 'object' &&
1620
- 'error' in output &&
1621
- typeof output.error === 'string'
1622
- ) {
1623
- return output.error
1624
- }
1625
- return typeof output === 'string' ? output : 'Tool execution failed'
1626
- }
1627
-
1628
2018
  /**
1629
2019
  * Handle TOOL_CALL_RESULT event (AG-UI spec).
1630
2020
  *
@@ -1659,7 +2049,10 @@ export class StreamProcessor {
1659
2049
  // Step 1: Update the tool-call part's output field
1660
2050
  let output: unknown
1661
2051
  try {
1662
- output = JSON.parse(chunk.content)
2052
+ output =
2053
+ typeof chunk.content === 'string'
2054
+ ? JSON.parse(chunk.content)
2055
+ : chunk.content
1663
2056
  } catch {
1664
2057
  output = chunk.content
1665
2058
  }
@@ -1676,9 +2069,9 @@ export class StreamProcessor {
1676
2069
  this.messages,
1677
2070
  messageId,
1678
2071
  chunk.toolCallId,
1679
- chunk.content,
2072
+ aguiContentToContentParts(chunk.content),
1680
2073
  resultState,
1681
- resultState === 'error' ? this.extractToolResultError(output) : undefined,
2074
+ resultState === 'error' ? toolResultErrorText(output) : undefined,
1682
2075
  )
1683
2076
  this.emitMessagesChange()
1684
2077
  }
@@ -1732,7 +2125,12 @@ export class StreamProcessor {
1732
2125
  }
1733
2126
  }
1734
2127
 
1735
- private handleInterrupts(interrupts: Array<Interrupt>): void {
2128
+ /**
2129
+ * Apply interrupt state to tool-call parts, then fire the client events.
2130
+ * A child's interrupt updates the child's card. `emit` is false there, so
2131
+ * the events fire once, from the top processor.
2132
+ */
2133
+ private handleInterrupts(interrupts: Array<Interrupt>, emit = true): void {
1736
2134
  const hasGeneric = interruptBatchHasGeneric(interrupts)
1737
2135
  for (const interrupt of interrupts) {
1738
2136
  const metadata =
@@ -1743,6 +2141,16 @@ export class StreamProcessor {
1743
2141
  const toolCallId = interrupt.toolCallId
1744
2142
  if (!toolCallId) continue
1745
2143
 
2144
+ const owner =
2145
+ interrupt.subagentRunId !== undefined
2146
+ ? this.childOwning(interrupt.subagentRunId)
2147
+ : this.childOwningToolCall((part) => part.id === toolCallId)
2148
+ if (owner !== undefined) {
2149
+ this.updateChild(owner, (child) =>
2150
+ child.handleInterrupts([interrupt], false),
2151
+ )
2152
+ }
2153
+
1746
2154
  const toolName =
1747
2155
  typeof metadata.toolName === 'string'
1748
2156
  ? metadata.toolName
@@ -1750,6 +2158,17 @@ export class StreamProcessor {
1750
2158
  const input = Object.hasOwn(metadata, 'input') ? metadata.input : {}
1751
2159
 
1752
2160
  if (kind === 'approval' || interrupt.reason === 'approval_required') {
2161
+ if (owner !== undefined) {
2162
+ if (emit) {
2163
+ this.events.onApprovalRequest?.({
2164
+ toolCallId,
2165
+ toolName,
2166
+ input,
2167
+ approvalId: interrupt.id,
2168
+ })
2169
+ }
2170
+ continue
2171
+ }
1753
2172
  // An interrupt terminal arrives after MESSAGES_SNAPSHOT, which may have
1754
2173
  // rebuilt the message list without the live active-message/tool-call
1755
2174
  // maps. Fall back to locating the assistant message that actually owns
@@ -1774,6 +2193,7 @@ export class StreamProcessor {
1774
2193
  this.emitMessagesChange()
1775
2194
  }
1776
2195
 
2196
+ if (!emit) continue
1777
2197
  this.events.onApprovalRequest?.({
1778
2198
  toolCallId,
1779
2199
  toolName,
@@ -1786,7 +2206,7 @@ export class StreamProcessor {
1786
2206
  if (kind === 'client_tool' || interrupt.reason === 'client_tool_input') {
1787
2207
  // Generic interrupts in the same batch decide `toolResume`. Do not
1788
2208
  // run client tools until that policy is `continue`.
1789
- if (hasGeneric) continue
2209
+ if (hasGeneric || !emit) continue
1790
2210
  this.events.onToolCall?.({
1791
2211
  toolCallId,
1792
2212
  toolName,
@@ -2617,6 +3037,8 @@ export class StreamProcessor {
2617
3037
  reset(): void {
2618
3038
  this.resetStreamState()
2619
3039
  this.messages = []
3040
+ this.childProcessors.clear()
3041
+ this.childToolCalls.clear()
2620
3042
  }
2621
3043
 
2622
3044
  /**