@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
@@ -1,9 +1,16 @@
1
- import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
1
+ import {
2
+ isAssistantSegmentOf,
3
+ isProviderExecutedToolCall,
4
+ } from '../../utilities/provider-executed'
2
5
  import {
3
6
  isContentPartArray,
4
7
  normalizeToolResult,
5
8
  } from '../../utilities/tool-result'
6
9
  import { tanstackMetadata } from '../../utilities/merge-metadata'
10
+ import {
11
+ splitSubagentWire,
12
+ subagentWireText,
13
+ } from '../../utilities/subagent-wire'
7
14
  import type { Message as AGUIMessage } from '@ag-ui/core'
8
15
  import type {
9
16
  ContentPart,
@@ -14,6 +21,7 @@ import type {
14
21
  TextPart,
15
22
  ToolCall,
16
23
  ToolCallPart,
24
+ SubagentPart,
17
25
  UIMessage,
18
26
  UIResourcePart,
19
27
  } from '../../types'
@@ -124,6 +132,37 @@ function getTextContent(
124
132
  .join('')
125
133
  }
126
134
 
135
+ function historyTextFromParts(parts: ReadonlyArray<MessagePart>): string {
136
+ const blocks: Array<string> = []
137
+ for (const part of parts) {
138
+ if (part.type === 'text' && part.content !== '') {
139
+ blocks.push(part.content)
140
+ } else if (
141
+ part.type === 'structured-output' &&
142
+ part.status === 'complete' &&
143
+ part.raw !== ''
144
+ ) {
145
+ blocks.push(part.raw)
146
+ } else if (part.type === 'subagent') {
147
+ const nested = subagentHistoryText(part)
148
+ if (nested !== '') blocks.push(nested)
149
+ }
150
+ }
151
+ return blocks.join('\n\n')
152
+ }
153
+
154
+ /** Child text for a later turn. The name stays so the next agent can tell the notes apart. */
155
+ export function subagentHistoryText(part: SubagentPart): string {
156
+ const blocks: Array<string> = []
157
+ for (const message of part.subagent.messages) {
158
+ if (!('parts' in message)) continue
159
+ const text = historyTextFromParts(message.parts).trim()
160
+ if (text !== '') blocks.push(text)
161
+ }
162
+ if (blocks.length === 0) return ''
163
+ return `${part.subagent.name}:\n${blocks.join('\n\n')}`
164
+ }
165
+
127
166
  function toolResultContent(
128
167
  content: string | null | undefined | Array<ContentPart>,
129
168
  ): string | Array<ContentPart> {
@@ -135,6 +174,57 @@ function toolResultContent(
135
174
  */
136
175
  export function convertMessagesToModelMessages(
137
176
  messages: Array<UIMessage | ModelMessage>,
177
+ ): Array<ModelMessage> {
178
+ const { top, groups } = splitSubagentWire(messages)
179
+ if (groups.length === 0) return convertOwnMessages(messages)
180
+
181
+ // Child wire messages leave the parent history. The parent model reads each
182
+ // child's text on the assistant message before it, the same as for a UI
183
+ // subagent part. A child that a tool call started reports through the tool
184
+ // result instead.
185
+ const blocks = new Map<string | undefined, Array<string>>()
186
+ for (const group of groups) {
187
+ if (group.info.parentToolCallId !== undefined) continue
188
+ const text = subagentWireText(group.messages)
189
+ if (text === '') continue
190
+ const host = top
191
+ .slice(0, group.hostIndex + 1)
192
+ .findLast((message) => message.role === 'assistant')
193
+ const hostId = host && 'id' in host ? host.id : undefined
194
+ blocks.set(hostId, [
195
+ ...(blocks.get(hostId) ?? []),
196
+ `${group.info.name}:\n${text}`,
197
+ ])
198
+ }
199
+ const converted = convertOwnMessages(top)
200
+ for (const [hostId, texts] of blocks) {
201
+ const block = texts.join('\n\n')
202
+ const index =
203
+ hostId === undefined
204
+ ? -1
205
+ : converted.findIndex(
206
+ (message) => message.role === 'assistant' && message.id === hostId,
207
+ )
208
+ const host = converted[index]
209
+ if (!host) {
210
+ converted.push({ role: 'assistant', content: block })
211
+ continue
212
+ }
213
+ converted[index] = {
214
+ ...host,
215
+ content:
216
+ typeof host.content === 'string' && host.content !== ''
217
+ ? `${host.content}\n\n${block}`
218
+ : Array.isArray(host.content)
219
+ ? [...host.content, { type: 'text', content: block }]
220
+ : block,
221
+ }
222
+ }
223
+ return converted
224
+ }
225
+
226
+ function convertOwnMessages(
227
+ messages: Array<UIMessage | ModelMessage>,
138
228
  ): Array<ModelMessage> {
139
229
  // Pre-pass: collect toolCallIds already represented in anchor UIMessage parts.
140
230
  // Fan-out tool messages whose toolCallId matches an anchored ToolResultPart
@@ -455,6 +545,7 @@ function assistantMetadata(
455
545
  const previous = tanstackMetadata(uiMessage)
456
546
  const tanstack: TanStackMessageMetadata = {}
457
547
  if (previous?.model !== undefined) tanstack.model = previous.model
548
+ if (previous?.runId !== undefined) tanstack.runId = previous.runId
458
549
  if (previous?.signature !== undefined) tanstack.signature = previous.signature
459
550
  if (fromParts.length > 0) tanstack.uiResources = fromParts
460
551
  const result = { ...current }
@@ -690,6 +781,22 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
690
781
  // model input, so it is intentionally dropped from the model message.
691
782
  break
692
783
 
784
+ case 'subagent': {
785
+ // A child that a tool call started reports through the tool result.
786
+ const block =
787
+ part.subagent.parentToolCallId === undefined
788
+ ? subagentHistoryText(part)
789
+ : ''
790
+ if (block !== '') {
791
+ const prefix = current.contentParts.length > 0 ? '\n\n' : ''
792
+ current.contentParts.push({
793
+ type: 'text',
794
+ content: `${prefix}${block}`,
795
+ })
796
+ }
797
+ break
798
+ }
799
+
693
800
  default:
694
801
  break
695
802
  }
@@ -947,7 +1054,9 @@ export function aguiSnapshotMessageToUIMessage(
947
1054
  modelMessageToUIMessage(
948
1055
  {
949
1056
  role: 'tool',
950
- content: message.content,
1057
+ content: isContentPartArray(message.content)
1058
+ ? message.content
1059
+ : aguiContentToContentParts(message.content),
951
1060
  toolCallId: message.toolCallId,
952
1061
  ...('name' in message && typeof message.name === 'string'
953
1062
  ? { name: message.name }
@@ -1071,25 +1180,29 @@ function snapshotStructuredOutput(
1071
1180
  * AG-UI user content is either a plain string or a multimodal array whose text
1072
1181
  * entries use `{ type: 'text', text }` (vs. TanStack's `{ type: 'text', content }`).
1073
1182
  * Text entries are rewritten to the TanStack shape; image/audio/video/document
1074
- * entries already match `ContentPart` and pass through. `binary` entries have no
1075
- * TanStack equivalent and are dropped.
1183
+ * entries already match `ContentPart` and pass through.
1076
1184
  */
1077
1185
  function aguiUserContentToParts(
1078
1186
  content: Extract<AGUIMessage, { role: 'user' }>['content'],
1079
1187
  ): Array<MessagePart> {
1080
- if (typeof content === 'string') {
1081
- return content ? [{ type: 'text', content }] : []
1082
- }
1188
+ const converted = aguiContentToContentParts(content)
1189
+ return typeof converted === 'string'
1190
+ ? converted
1191
+ ? [{ type: 'text', content: converted }]
1192
+ : []
1193
+ : converted
1194
+ }
1083
1195
 
1084
- const parts: Array<MessagePart> = []
1085
- for (const part of content) {
1086
- if (part.type === 'text') {
1087
- parts.push({ type: 'text', content: part.text })
1088
- } else if (part.type !== 'binary') {
1089
- parts.push(part)
1090
- }
1091
- }
1092
- return parts
1196
+ /** Convert wire content parts. Data, url, and file sources pass through. */
1197
+ export function aguiContentToContentParts(
1198
+ content: Extract<AGUIMessage, { role: 'user' }>['content'],
1199
+ ): string | Array<ContentPart> {
1200
+ if (typeof content === 'string') return content
1201
+ return content.map((part) => {
1202
+ if (part.type !== 'text') return part
1203
+ const { text, ...rest } = part
1204
+ return { ...rest, content: text }
1205
+ })
1093
1206
  }
1094
1207
 
1095
1208
  /**
@@ -1149,6 +1262,14 @@ export function modelMessagesToUIMessages(
1149
1262
  // Regular message. Preserve a persisted stable id so a hydrated message
1150
1263
  // keeps the same identity as its live stream (enables in-place resume).
1151
1264
  const uiMessage = modelMessageToUIMessage(msg, msg.id)
1265
+ if (
1266
+ msg.role === 'assistant' &&
1267
+ currentAssistantMessage &&
1268
+ isAssistantSegmentOf(msg.id, currentAssistantMessage.id)
1269
+ ) {
1270
+ currentAssistantMessage.parts.push(...uiMessage.parts)
1271
+ continue
1272
+ }
1152
1273
  uiMessages.push(uiMessage)
1153
1274
 
1154
1275
  // Track assistant messages for potential tool result merging
@@ -112,8 +112,25 @@ export interface RunRecord {
112
112
  * reuse this record by faking `threadId = requestId`; they need a separate
113
113
  * job store. `withGenerationPersistence` currently does exactly that and
114
114
  * labels itself a stopgap — do not copy it.
115
+ *
116
+ * A subagent child record stores `subagent:<subagentRunId>` here, the key of
117
+ * its own transcript, so `findActiveRun` and `listByThread` on the
118
+ * conversation never return children. Use `listByParentRun`.
115
119
  */
116
120
  threadId: string
121
+ /**
122
+ * Parent chat run that started this child, when this record is a subagent.
123
+ * Absent on the parent run itself.
124
+ */
125
+ parentRunId?: string
126
+ /**
127
+ * The child's AG-UI subagentRunId, the id on its `SUBAGENT_*` chunks and on
128
+ * every chunk it streams. On a child record this equals `runId`. Absent on
129
+ * the parent run.
130
+ */
131
+ subagentRunId?: string
132
+ /** Agent name (`researcher`, `writer`) when this record is a subagent. */
133
+ name?: string
117
134
  status: RunStatus
118
135
  startedAt: number
119
136
  finishedAt?: number
@@ -172,9 +189,10 @@ export interface RunRecord {
172
189
  * instead of failing at build time. It was optional for exactly one release
173
190
  * cycle and cost precisely that.
174
191
  *
175
- * OPTIONAL: `listByThread`, `listReclaimable`. Each serves one higher-level
176
- * feature (thread history, reclaim reaping) and callers feature-detect them,
177
- * degrading gracefully when a backend omits them.
192
+ * OPTIONAL: `listByThread`, `listByParentRun`, `listReclaimable`. Each serves
193
+ * one higher-level feature (thread history, subagent card reload, reclaim
194
+ * reaping) and callers feature-detect them, degrading when a backend omits
195
+ * them.
178
196
  */
179
197
  export interface RunStore {
180
198
  /**
@@ -182,12 +200,17 @@ export interface RunStore {
182
200
  * already present.
183
201
  *
184
202
  * INVARIANT (idempotency): an existing record is returned **unchanged** and
185
- * the passed `threadId`/`startedAt`/`status` are ignored. This is what makes
186
- * resuming a run safe. `status` defaults to `'running'` on first creation.
203
+ * the passed `threadId`, `startedAt`, `status`, `parentRunId`,
204
+ * `subagentRunId`, and `name` are ignored. This is what makes resuming a
205
+ * run safe. `status` defaults to `'running'` on first creation. The three
206
+ * link fields are copied only on the first insert.
187
207
  */
188
208
  createOrResume: (
189
209
  input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
190
210
  status?: RunStatus
211
+ parentRunId?: string
212
+ subagentRunId?: string
213
+ name?: string
191
214
  },
192
215
  ) => Promise<RunRecord>
193
216
  /**
@@ -215,10 +238,19 @@ export interface RunStore {
215
238
  /** Current record, or null when unknown. */
216
239
  get: (runId: string) => Promise<RunRecord | null>
217
240
  /**
218
- * Every run in a conversation, ascending by `startedAt`. OPTIONAL: only
219
- * needed to render a thread's past agent activity. Consumers feature-detect.
241
+ * Every run in a conversation, ascending by `startedAt`. OPTIONAL.
242
+ * `reconstructChat` calls it to find the parent runs of children that a
243
+ * tool call started. Without it those cards stay absent on reload.
244
+ * Consumers feature-detect.
220
245
  */
221
246
  listByThread?: (threadId: string) => Promise<Array<RunRecord>>
247
+ /**
248
+ * Child runs started by `parentRunId`, ascending by `startedAt`.
249
+ * OPTIONAL. `reconstructChat` uses this to put subagent cards back
250
+ * on the parent assistant message. A store that omits it reloads the
251
+ * text and not the cards.
252
+ */
253
+ listByParentRun?: (parentRunId: string) => Promise<Array<RunRecord>>
222
254
  /**
223
255
  * Runs that may be reclaimed: ALL THREE of `status === 'running'`,
224
256
  * `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is
@@ -341,6 +373,9 @@ export class InMemoryRunStore implements RunStore {
341
373
  createOrResume(
342
374
  input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
343
375
  status?: RunStatus
376
+ parentRunId?: string
377
+ subagentRunId?: string
378
+ name?: string
344
379
  },
345
380
  ): Promise<RunRecord> {
346
381
  const existing = this.runs.get(input.runId)
@@ -350,6 +385,13 @@ export class InMemoryRunStore implements RunStore {
350
385
  threadId: input.threadId,
351
386
  status: input.status ?? 'running',
352
387
  startedAt: input.startedAt,
388
+ ...(input.parentRunId !== undefined
389
+ ? { parentRunId: input.parentRunId }
390
+ : {}),
391
+ ...(input.subagentRunId !== undefined
392
+ ? { subagentRunId: input.subagentRunId }
393
+ : {}),
394
+ ...(input.name !== undefined ? { name: input.name } : {}),
353
395
  }
354
396
  this.runs.set(record.runId, record)
355
397
  return Promise.resolve(record)
@@ -387,6 +429,13 @@ export class InMemoryRunStore implements RunStore {
387
429
  return Promise.resolve(matching)
388
430
  }
389
431
 
432
+ listByParentRun(parentRunId: string): Promise<Array<RunRecord>> {
433
+ const matching = [...this.runs.values()]
434
+ .filter((run) => run.parentRunId === parentRunId)
435
+ .sort((a, b) => a.startedAt - b.startedAt)
436
+ return Promise.resolve(matching)
437
+ }
438
+
390
439
  listReclaimable(opts: {
391
440
  now: number
392
441
  ttlMs: number
@@ -5,8 +5,10 @@ import type {
5
5
  import type {
6
6
  AgentLoopState,
7
7
  EmitCustomEventOptions,
8
+ Interrupt,
8
9
  JSONSchema,
9
10
  ModelMessage,
11
+ UIMessage,
10
12
  RunAgentResumeItem,
11
13
  StreamChunk,
12
14
  TokenUsage,
@@ -191,6 +193,11 @@ export interface ChatMiddlewareContext<TContext = unknown> {
191
193
  runId: string
192
194
  /** Interrupted or parent run correlated with this continuation. */
193
195
  parentRunId?: string
196
+ /**
197
+ * Set when this run is a subagent. The id on the child's `SUBAGENT_STARTED`
198
+ * and on every chunk it streams. Absent on a top-level run.
199
+ */
200
+ subagentRunId?: string
194
201
  /**
195
202
  * AG-UI thread identifier — a stable per-conversation ID used to
196
203
  * correlate client and server devtools events. Resolves to the
@@ -544,6 +551,40 @@ export interface ErrorInfo {
544
551
  duration: number
545
552
  }
546
553
 
554
+ /**
555
+ * Saves subagent runs while a router owns the turn.
556
+ * `withPersistence` sets this. `chat()` calls it. Apps do not.
557
+ */
558
+ export interface RoutedSubagentPersistence {
559
+ start: (input: {
560
+ threadId: string
561
+ runId: string
562
+ messages: ReadonlyArray<UIMessage | ModelMessage>
563
+ /**
564
+ * The run's resume entries: answers to earlier child interrupts, plus any
565
+ * the parent answers itself.
566
+ */
567
+ resume?: ReadonlyArray<RunAgentResumeItem>
568
+ }) => Promise<void>
569
+ chunk: (input: {
570
+ threadId: string
571
+ runId: string
572
+ chunk: StreamChunk
573
+ }) => Promise<void>
574
+ finish: (input: { threadId: string; runId: string }) => Promise<void>
575
+ /** The run stopped because a child waits for outside input. */
576
+ suspend?: (input: {
577
+ threadId: string
578
+ runId: string
579
+ interrupts: ReadonlyArray<Interrupt>
580
+ }) => Promise<void>
581
+ abort: (input: {
582
+ threadId: string
583
+ runId: string
584
+ error?: unknown
585
+ }) => Promise<void>
586
+ }
587
+
547
588
  // ===========================
548
589
  // Middleware Interface
549
590
  // ===========================
@@ -584,6 +625,12 @@ export interface ChatMiddleware<
584
625
  /** Optional name for debugging and identification */
585
626
  name?: string
586
627
 
628
+ /**
629
+ * Present when this middleware stores subagent runs.
630
+ * The router calls it. An app does not set it.
631
+ */
632
+ routedSubagentPersistence?: RoutedSubagentPersistence
633
+
587
634
  /**
588
635
  * Called at a lifecycle boundary. Return interrupt requests to pause the run.
589
636
  * Requests from every middleware in the same boundary form one batch.
@@ -455,15 +455,37 @@ export function updateThinkingPart(
455
455
  }
456
456
 
457
457
  const parts = [...msg.parts]
458
- const thinkingPartIndex = parts.findIndex(
458
+ let thinkingPartIndex = parts.findIndex(
459
459
  (p) => p.type === 'thinking' && p.stepId === stepId,
460
460
  )
461
461
 
462
+ // A hydrated message carries its thinking without a stepId: the stored form
463
+ // has no field for one, so `modelMessageToUIMessage` cannot put it back.
464
+ // When a run is rejoined mid-stream the replayed reasoning is keyed by
465
+ // stepId, matches nothing, and gets appended -- leaving a second thinking
466
+ // part sitting after the answer text. Adopt the first stepId-less thinking
467
+ // part instead, so the replay lands on the part it belongs to. Live
468
+ // streaming always writes a stepId, so the only parts this can match are
469
+ // hydrated ones.
470
+ let adopted: ThinkingPart | undefined
471
+ if (thinkingPartIndex < 0) {
472
+ thinkingPartIndex = parts.findIndex(
473
+ (p) => p.type === 'thinking' && p.stepId === undefined,
474
+ )
475
+ const candidate = parts[thinkingPartIndex]
476
+ if (candidate?.type === 'thinking') adopted = candidate
477
+ }
478
+
479
+ // Keep the signature the hydrated part already had when this update does
480
+ // not carry one; losing it would strip the provider's encrypted reasoning
481
+ // from a message that is about to be sent back.
482
+ const nextSignature = signature ?? adopted?.signature
483
+
462
484
  const thinkingPart: ThinkingPart = {
463
485
  type: 'thinking',
464
486
  content,
465
487
  stepId,
466
- ...(signature && { signature }),
488
+ ...(nextSignature && { signature: nextSignature }),
467
489
  }
468
490
 
469
491
  if (thinkingPartIndex >= 0) {