@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
@@ -0,0 +1,806 @@
1
+ import { EventType } from '../../../types'
2
+ import {
3
+ addTokenUsage,
4
+ isTanstackUsage,
5
+ rebuildTokenUsage,
6
+ toSpecTokenUsage,
7
+ } from '../../../utilities/ag-ui-usage'
8
+ import {
9
+ tanstackMetadata,
10
+ withTanstackMetadata,
11
+ } from '../../../utilities/merge-metadata'
12
+ import { INTERRUPT_BINDING_METADATA_KEY } from '../../../interrupt-resume'
13
+ import { EMIT_STREAM_CHUNK, SUBAGENT_TOOL } from '../tools/tool-calls'
14
+ import type { SubagentToolOutcome } from '../tools/tool-calls'
15
+ import type { SpecTokenUsage } from '../../../utilities/ag-ui-usage'
16
+ import type {
17
+ Interrupt,
18
+ ModelMessage,
19
+ RunAgentResumeItem,
20
+ StreamChunk,
21
+ SubagentErrorEvent,
22
+ SubagentFinishedEvent,
23
+ SubagentStartedEvent,
24
+ TokenUsage,
25
+ Tool,
26
+ UIMessage,
27
+ } from '../../../types'
28
+ import type { DefinedAgent, SubagentRunContext } from './define-agent'
29
+ import type { ChatMiddleware } from '../middleware/types'
30
+ import type { SubagentTurn } from './turn'
31
+
32
+ export const SUBAGENT_STARTED = EventType.SUBAGENT_STARTED
33
+ export const SUBAGENT_FINISHED = EventType.SUBAGENT_FINISHED
34
+ export const SUBAGENT_ERROR = EventType.SUBAGENT_ERROR
35
+
36
+ export type SubagentOrder = 'parallel' | 'sequence'
37
+
38
+ export interface SubagentRouterPlan {
39
+ names: ReadonlyArray<string>
40
+ /** Overrides `subagents.order` for this turn. */
41
+ order?: SubagentOrder
42
+ }
43
+
44
+ export interface SubagentStep {
45
+ names: ReadonlyArray<string>
46
+ /** Overrides `subagents.order` for this step. */
47
+ order?: SubagentOrder
48
+ }
49
+
50
+ export interface SubagentStepsPlan {
51
+ steps: ReadonlyArray<SubagentStep>
52
+ }
53
+
54
+ export type SubagentRouterPick =
55
+ | 'main'
56
+ | string
57
+ | ReadonlyArray<string>
58
+ | SubagentRouterPlan
59
+ | SubagentStepsPlan
60
+
61
+ export interface SubagentsBag<
62
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
63
+ > {
64
+ agents: TAgents
65
+ router?: (ctx: {
66
+ messages: SubagentRunContext['messages']
67
+ agents: NoInfer<TAgents>
68
+ abortSignal?: AbortSignal
69
+ }) => SubagentRouterPick | Promise<SubagentRouterPick>
70
+ strategy?: 'exclusive' | 'handoff'
71
+ /**
72
+ * How a router list runs. `parallel` starts every name together.
73
+ * `sequence` runs each name after the previous one finishes, and passes
74
+ * that child's text to the next child.
75
+ */
76
+ order?: 'parallel' | 'sequence'
77
+ sandbox?: 'own' | 'inherit'
78
+ }
79
+
80
+ /** What the children of one parent run left behind for the parent terminal. */
81
+ export interface SubagentSink {
82
+ interrupts: Array<Interrupt>
83
+ /** One AG-UI entry per child model call. */
84
+ usage: Array<SpecTokenUsage>
85
+ /** Summed full usage of the children, including cost. */
86
+ total?: TokenUsage
87
+ }
88
+
89
+ export function createSubagentSink(): SubagentSink {
90
+ return { interrupts: [], usage: [] }
91
+ }
92
+
93
+ /** One child to start, or a suspended child to continue. */
94
+ export interface SpawnEntry {
95
+ name: string
96
+ resume?: {
97
+ subagentRunId: string
98
+ /** The child's own messages from the interrupted run. */
99
+ messages: Array<UIMessage | ModelMessage>
100
+ entries: Array<RunAgentResumeItem>
101
+ /** Text the child wrote before it stopped. */
102
+ text: string
103
+ }
104
+ }
105
+
106
+ interface SpawnContext {
107
+ messages: SubagentRunContext['messages']
108
+ abortSignal?: AbortSignal
109
+ threadId: string
110
+ /** The parent chat run. */
111
+ parentRunId: string
112
+ /** The interrupted parent run, on a resume. */
113
+ interruptedRunId?: string
114
+ }
115
+
116
+ export function createSubagentId() {
117
+ return `subagent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
118
+ }
119
+
120
+ function childRunId(parentRunId: string, subagentRunId: string) {
121
+ return `${parentRunId}:${subagentRunId}`
122
+ }
123
+
124
+ /**
125
+ * Bind child interrupts to the parent run. The client resumes the parent run,
126
+ * so each binding must name that run. The resumed child then validates with
127
+ * the parent's interrupted run id.
128
+ */
129
+ export function rebindInterrupts(
130
+ interrupts: ReadonlyArray<Interrupt>,
131
+ runId: string,
132
+ ): Array<Interrupt> {
133
+ return interrupts.map((interrupt) => {
134
+ const binding = interrupt.metadata?.[INTERRUPT_BINDING_METADATA_KEY]
135
+ if (typeof binding !== 'object' || binding === null) return interrupt
136
+ return {
137
+ ...interrupt,
138
+ metadata: {
139
+ ...interrupt.metadata,
140
+ [INTERRUPT_BINDING_METADATA_KEY]: {
141
+ ...binding,
142
+ interruptedRunId: runId,
143
+ generation: 0,
144
+ },
145
+ },
146
+ }
147
+ })
148
+ }
149
+
150
+ function createAbortError() {
151
+ const error = new Error('Aborted')
152
+ error.name = 'AbortError'
153
+ return error
154
+ }
155
+
156
+ function isAbortError(error: unknown, signal?: AbortSignal) {
157
+ if (signal?.aborted) return true
158
+ return (
159
+ error instanceof Error &&
160
+ (error.name === 'AbortError' || error.message === 'Aborted')
161
+ )
162
+ }
163
+
164
+ function stoppedEvent(subagentRunId: string) {
165
+ return {
166
+ type: SUBAGENT_ERROR,
167
+ subagentRunId,
168
+ message: 'Stopped',
169
+ timestamp: Date.now(),
170
+ } satisfies SubagentErrorEvent
171
+ }
172
+
173
+ function childThreadId(
174
+ sandbox: SubagentsBag['sandbox'],
175
+ parentThreadId: string,
176
+ name: string,
177
+ ) {
178
+ return sandbox === 'inherit' ? parentThreadId : `${parentThreadId}:${name}`
179
+ }
180
+
181
+ function linkAbort(parent?: AbortSignal) {
182
+ const controller = new AbortController()
183
+ if (!parent) return { controller, dispose: () => {} }
184
+ if (parent.aborted) {
185
+ controller.abort()
186
+ return { controller, dispose: () => {} }
187
+ }
188
+ const onAbort = () => controller.abort()
189
+ parent.addEventListener('abort', onAbort, { once: true })
190
+ return {
191
+ controller,
192
+ dispose: () => parent.removeEventListener('abort', onAbort),
193
+ }
194
+ }
195
+
196
+ function orAbort<T>(promise: Promise<T>, signal?: AbortSignal) {
197
+ if (!signal) return promise
198
+ if (signal.aborted) return Promise.reject(createAbortError())
199
+ return new Promise<T>((resolve, reject) => {
200
+ const onAbort = () => reject(createAbortError())
201
+ signal.addEventListener('abort', onAbort, { once: true })
202
+ promise.then(
203
+ (value) => {
204
+ signal.removeEventListener('abort', onAbort)
205
+ resolve(value)
206
+ },
207
+ (error: unknown) => {
208
+ signal.removeEventListener('abort', onAbort)
209
+ reject(error)
210
+ },
211
+ )
212
+ })
213
+ }
214
+
215
+ function agentByName(agents: ReadonlyArray<DefinedAgent>, name: string) {
216
+ const agent = agents.find((entry) => entry.name === name)
217
+ if (!agent) {
218
+ throw new Error(`Unknown subagent: ${name}`)
219
+ }
220
+ return agent
221
+ }
222
+
223
+ function openAgentStream(
224
+ entry: SpawnEntry,
225
+ bag: SubagentsBag,
226
+ ctx: SpawnContext,
227
+ sink?: SubagentSink,
228
+ parentToolCallId?: string,
229
+ ) {
230
+ const agent = agentByName(bag.agents, entry.name)
231
+ const resume = entry.resume
232
+ const subagentRunId = resume?.subagentRunId ?? createSubagentId()
233
+ // The parent binds child interrupts to its own run (see rebindInterrupts),
234
+ // so the resumed child continues from the interrupted parent run id.
235
+ if (resume !== undefined && ctx.interruptedRunId === undefined) {
236
+ throw new Error(
237
+ `Subagent "${entry.name}" has interrupt answers, but the run has no parentRunId. Pass the interrupted run id as parentRunId.`,
238
+ )
239
+ }
240
+ const resumed =
241
+ resume !== undefined
242
+ ? {
243
+ messages: [...ctx.messages, ...resume.messages],
244
+ parentRunId: ctx.interruptedRunId,
245
+ resume: resume.entries,
246
+ }
247
+ : undefined
248
+ return spawnAgentStream(
249
+ agent,
250
+ {
251
+ messages: resumed?.messages ?? ctx.messages,
252
+ ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
253
+ threadId: childThreadId(bag.sandbox, ctx.threadId, entry.name),
254
+ runId: childRunId(ctx.parentRunId, subagentRunId),
255
+ parentRunId: resumed?.parentRunId ?? ctx.parentRunId,
256
+ subagentRunId,
257
+ ...(resumed ? { resume: resumed.resume } : {}),
258
+ },
259
+ sink,
260
+ parentToolCallId,
261
+ )
262
+ }
263
+
264
+ const ROUTER_PICK_ERROR =
265
+ 'subagents.router must return main, a name, a list of names, { names, order }, or { steps }.'
266
+
267
+ function assertOrder(order: SubagentOrder | undefined) {
268
+ if (order !== undefined && order !== 'parallel' && order !== 'sequence') {
269
+ throw new Error('subagents.router order must be parallel or sequence.')
270
+ }
271
+ }
272
+
273
+ function normalizeNames(
274
+ names: ReadonlyArray<string>,
275
+ agents: ReadonlyArray<DefinedAgent>,
276
+ ): ReadonlyArray<string> {
277
+ if (names.length === 0) throw new Error(ROUTER_PICK_ERROR)
278
+ const hasMain = names.includes('main')
279
+ if (hasMain && names.length > 1) {
280
+ throw new Error('Do not mix main into a subagent list.')
281
+ }
282
+ if (hasMain) return ['main']
283
+ for (const name of names) agentByName(agents, name)
284
+ return [...names]
285
+ }
286
+
287
+ function isStringList(pick: SubagentRouterPick): pick is ReadonlyArray<string> {
288
+ return Array.isArray(pick)
289
+ }
290
+
291
+ export function normalizeRouterPick(
292
+ pick: SubagentRouterPick,
293
+ agents: ReadonlyArray<DefinedAgent>,
294
+ ): { steps: ReadonlyArray<SubagentStep> } {
295
+ if (pick === 'main' || typeof pick === 'string') {
296
+ return { steps: [{ names: normalizeNames([pick], agents) }] }
297
+ }
298
+ if (isStringList(pick)) {
299
+ return { steps: [{ names: normalizeNames(pick, agents) }] }
300
+ }
301
+ if ('steps' in pick) {
302
+ if (pick.steps.length === 0) throw new Error(ROUTER_PICK_ERROR)
303
+ const steps = pick.steps.map((step) => {
304
+ assertOrder(step.order)
305
+ const names = normalizeNames(step.names, agents)
306
+ return step.order === undefined ? { names } : { names, order: step.order }
307
+ })
308
+ const flat = steps.flatMap((step) => step.names)
309
+ if (flat.includes('main') && flat.length > 1) {
310
+ throw new Error('Do not mix main into a subagent list.')
311
+ }
312
+ return { steps }
313
+ }
314
+ assertOrder(pick.order)
315
+ const names = normalizeNames(pick.names, agents)
316
+ return {
317
+ steps: [
318
+ pick.order === undefined ? { names } : { names, order: pick.order },
319
+ ],
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Tag a child chunk with its subagent. A chunk that a nested child already
325
+ * tagged keeps its own id, and a nested child's start names this child as
326
+ * its parent.
327
+ */
328
+ function attributeChunk(
329
+ chunk: StreamChunk,
330
+ subagentRunId: string,
331
+ ): StreamChunk {
332
+ if (chunk.type === SUBAGENT_STARTED) {
333
+ return chunk.parentSubagentRunId !== undefined
334
+ ? chunk
335
+ : { ...chunk, parentSubagentRunId: subagentRunId }
336
+ }
337
+ if (chunk.type === SUBAGENT_FINISHED || chunk.type === SUBAGENT_ERROR) {
338
+ return chunk
339
+ }
340
+ if ('subagentRunId' in chunk && typeof chunk.subagentRunId === 'string') {
341
+ return chunk
342
+ }
343
+ // RUN_* and MESSAGES_SNAPSHOT never get here (spawnAgentStream drops them).
344
+ // Everything else is tagged.
345
+ return { ...chunk, subagentRunId } as StreamChunk
346
+ }
347
+
348
+ function runUsage(chunk: StreamChunk | undefined): Array<SpecTokenUsage> {
349
+ if (chunk?.type !== EventType.RUN_FINISHED) return []
350
+ if (Array.isArray(chunk.usage)) return chunk.usage
351
+ return isTanstackUsage(chunk.usage) ? toSpecTokenUsage(chunk.usage).usage : []
352
+ }
353
+
354
+ /** The full usage of a run: token counts plus cost and the other fields. */
355
+ function fullUsage(chunk: StreamChunk | undefined): TokenUsage | undefined {
356
+ if (chunk?.type !== EventType.RUN_FINISHED) return undefined
357
+ return rebuildTokenUsage(chunk.usage, tanstackMetadata(chunk)?.usage)
358
+ }
359
+
360
+ /** Add a finished child run's usage to the sink. */
361
+ export function collectUsage(sink: SubagentSink, finished?: StreamChunk) {
362
+ sink.usage.push(...runUsage(finished))
363
+ const full = fullUsage(finished)
364
+ if (full) sink.total = sink.total ? addTokenUsage(sink.total, full) : full
365
+ }
366
+
367
+ /** A parent run's last chunk: it completed, or it failed. */
368
+ type ParentTerminal = Extract<
369
+ StreamChunk,
370
+ { type: 'RUN_FINISHED' | 'RUN_ERROR' }
371
+ >
372
+
373
+ /**
374
+ * Put the children's usage on a parent terminal. `usage[]` keeps one entry per
375
+ * model call. `metadata.tanstack.usage` holds the summed cost and the other
376
+ * TanStack fields, so `fromSpecTokenUsage` reads the full total. Empties the
377
+ * sink, so the next parent terminal does not count it again.
378
+ *
379
+ * `RUN_ERROR` is accepted too: a turn that failed still spent whatever its
380
+ * children spent. Such a chunk carries no usage of its own, so `runUsage` and
381
+ * `fullUsage` return empty for it and the children's total stands alone.
382
+ */
383
+ export function withChildUsage(
384
+ chunk: ParentTerminal,
385
+ sink: SubagentSink,
386
+ ): ParentTerminal {
387
+ if (sink.usage.length === 0 && !sink.total) return chunk
388
+ const own = fullUsage(chunk)
389
+ const total =
390
+ own && sink.total ? addTokenUsage(own, sink.total) : (own ?? sink.total)
391
+ const usage = [...runUsage(chunk), ...sink.usage.splice(0)]
392
+ sink.total = undefined
393
+ const leftover = total ? toSpecTokenUsage(total).leftover : undefined
394
+ const next = { ...chunk, usage }
395
+ if (!leftover) return next
396
+ // `withTanstackMetadata` runs the value through `Omit`, which collapses this
397
+ // union and widens `type` back to `RUN_FINISHED | RUN_ERROR`. It only adds a
398
+ // metadata key, so the runtime shape is the input's: restore that type.
399
+ return withTanstackMetadata(next, { usage: leftover }) as ParentTerminal
400
+ }
401
+
402
+ export async function* spawnAgentStream(
403
+ agent: DefinedAgent,
404
+ ctx: SubagentRunContext,
405
+ sink?: SubagentSink,
406
+ parentToolCallId?: string,
407
+ ): AsyncIterable<StreamChunk> {
408
+ const id = ctx.subagentRunId
409
+ yield {
410
+ type: SUBAGENT_STARTED,
411
+ subagentRunId: id,
412
+ name: agent.name,
413
+ description: agent.description,
414
+ ...(ctx.parentSubagentRunId !== undefined
415
+ ? { parentSubagentRunId: ctx.parentSubagentRunId }
416
+ : {}),
417
+ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
418
+ timestamp: Date.now(),
419
+ } satisfies SubagentStartedEvent
420
+
421
+ let iterator: AsyncIterator<StreamChunk> | undefined
422
+ let finished: StreamChunk | undefined
423
+ try {
424
+ if (ctx.abortSignal?.aborted) {
425
+ yield stoppedEvent(id)
426
+ return
427
+ }
428
+ const stream = await orAbort(
429
+ Promise.resolve(agent.run(ctx)),
430
+ ctx.abortSignal,
431
+ )
432
+ iterator = stream[Symbol.asyncIterator]()
433
+ while (true) {
434
+ if (ctx.abortSignal?.aborted) {
435
+ yield stoppedEvent(id)
436
+ return
437
+ }
438
+ const result = await orAbort(iterator.next(), ctx.abortSignal)
439
+ if (result.done) break
440
+ const chunk = result.value
441
+ // Run-scoped events describe the child run. The SUBAGENT_* events carry
442
+ // that information for the parent stream.
443
+ if (
444
+ chunk.type === EventType.RUN_STARTED ||
445
+ chunk.type === EventType.MESSAGES_SNAPSHOT
446
+ ) {
447
+ continue
448
+ }
449
+ if (chunk.type === EventType.RUN_FINISHED) {
450
+ // The engine yields one RUN_FINISHED per model call. Add each one.
451
+ if (sink) collectUsage(sink, chunk)
452
+ finished = chunk
453
+ continue
454
+ }
455
+ if (chunk.type === EventType.RUN_ERROR) {
456
+ yield {
457
+ type: SUBAGENT_ERROR,
458
+ subagentRunId: id,
459
+ message: chunk.message || 'Subagent failed',
460
+ ...(chunk.code ? { code: chunk.code } : {}),
461
+ timestamp: Date.now(),
462
+ } satisfies SubagentErrorEvent
463
+ return
464
+ }
465
+ yield attributeChunk(chunk, id)
466
+ }
467
+ if (ctx.abortSignal?.aborted) {
468
+ yield stoppedEvent(id)
469
+ return
470
+ }
471
+ const outcome =
472
+ finished?.type === EventType.RUN_FINISHED ? finished.outcome : undefined
473
+ if (outcome?.type === 'cancelled') {
474
+ yield stoppedEvent(id)
475
+ return
476
+ }
477
+ if (outcome?.type === 'interrupt') {
478
+ const interrupts = outcome.interrupts.map((interrupt) =>
479
+ interrupt.subagentRunId
480
+ ? interrupt
481
+ : { ...interrupt, subagentRunId: id },
482
+ )
483
+ sink?.interrupts.push(...interrupts)
484
+ yield {
485
+ type: SUBAGENT_FINISHED,
486
+ subagentRunId: id,
487
+ outcome: {
488
+ type: 'suspended',
489
+ interruptIds: interrupts
490
+ .filter((interrupt) => interrupt.subagentRunId === id)
491
+ .map((interrupt) => interrupt.id),
492
+ },
493
+ timestamp: Date.now(),
494
+ } satisfies SubagentFinishedEvent
495
+ return
496
+ }
497
+ const result =
498
+ finished?.type === EventType.RUN_FINISHED ? finished.result : undefined
499
+ yield {
500
+ type: SUBAGENT_FINISHED,
501
+ subagentRunId: id,
502
+ ...(result !== undefined ? { result } : {}),
503
+ timestamp: Date.now(),
504
+ } satisfies SubagentFinishedEvent
505
+ } catch (error) {
506
+ yield {
507
+ type: SUBAGENT_ERROR,
508
+ subagentRunId: id,
509
+ message: isAbortError(error, ctx.abortSignal)
510
+ ? 'Stopped'
511
+ : error instanceof Error
512
+ ? error.message
513
+ : String(error),
514
+ timestamp: Date.now(),
515
+ } satisfies SubagentErrorEvent
516
+ } finally {
517
+ try {
518
+ await iterator?.return?.()
519
+ } catch {
520
+ // Child stream may already be closed or aborted.
521
+ }
522
+ }
523
+ }
524
+
525
+ async function* mergeAgentStreams(streams: Array<AsyncIterable<StreamChunk>>) {
526
+ const readers = streams.map((stream) => {
527
+ const iterator = stream[Symbol.asyncIterator]()
528
+ return {
529
+ iterator,
530
+ next: iterator.next(),
531
+ }
532
+ })
533
+
534
+ try {
535
+ while (readers.length > 0) {
536
+ const indexed = readers.map((reader, index) =>
537
+ reader.next.then((result) => ({ index, result, reader })),
538
+ )
539
+ const winner = await Promise.race(indexed)
540
+ if (winner.result.done) {
541
+ readers.splice(winner.index, 1)
542
+ continue
543
+ }
544
+ yield winner.result.value
545
+ winner.reader.next = winner.reader.iterator.next()
546
+ }
547
+ } finally {
548
+ // The reader stopped early. Close every child so its finally runs.
549
+ for (const reader of readers) {
550
+ void reader.iterator.return?.().catch(() => {})
551
+ }
552
+ }
553
+ }
554
+
555
+ /** True when a child in these chunks failed or stopped for outside input. */
556
+ function stopsSequence(chunks: ReadonlyArray<StreamChunk>, id: string) {
557
+ return chunks.some(
558
+ (chunk) =>
559
+ (chunk.type === SUBAGENT_ERROR && chunk.subagentRunId === id) ||
560
+ (chunk.type === SUBAGENT_FINISHED &&
561
+ chunk.subagentRunId === id &&
562
+ chunk.outcome?.type === 'suspended'),
563
+ )
564
+ }
565
+
566
+ export async function* spawnNamedAgents(
567
+ entries: ReadonlyArray<SpawnEntry>,
568
+ bag: SubagentsBag,
569
+ ctx: SpawnContext,
570
+ sink?: SubagentSink,
571
+ ) {
572
+ if (bag.sandbox === 'inherit' && entries.length > 1) {
573
+ throw new Error(
574
+ "subagents.sandbox 'inherit' cannot start two children in one turn",
575
+ )
576
+ }
577
+ // One signal for the group. The finally stops every child that still runs
578
+ // when the reader stops early.
579
+ const group = linkAbort(ctx.abortSignal)
580
+ const groupCtx = { ...ctx, abortSignal: group.controller.signal }
581
+ try {
582
+ if (bag.order === 'sequence') {
583
+ let messages = ctx.messages
584
+ for (const entry of entries) {
585
+ const chunks: Array<StreamChunk> = []
586
+ let id: string | undefined
587
+ for await (const chunk of openAgentStream(
588
+ entry,
589
+ bag,
590
+ { ...groupCtx, messages },
591
+ sink,
592
+ )) {
593
+ if (chunk.type === SUBAGENT_STARTED && id === undefined) {
594
+ id = chunk.subagentRunId
595
+ }
596
+ chunks.push(chunk)
597
+ yield chunk
598
+ }
599
+ if (id !== undefined && stopsSequence(chunks, id)) return
600
+ const text = [
601
+ entry.resume?.text,
602
+ collectNamedText(chunks, [entry.name]),
603
+ ]
604
+ .filter((part) => part !== undefined && part !== '')
605
+ .join('')
606
+ if (text) {
607
+ messages = [...messages, { role: 'assistant', content: text }]
608
+ }
609
+ }
610
+ return
611
+ }
612
+ const streams = entries.map((entry) =>
613
+ openAgentStream(entry, bag, groupCtx, sink),
614
+ )
615
+ const onlyStream = streams.length === 1 ? streams[0] : undefined
616
+ if (onlyStream) {
617
+ yield* onlyStream
618
+ return
619
+ }
620
+ yield* mergeAgentStreams(streams)
621
+ } finally {
622
+ group.controller.abort()
623
+ group.dispose()
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Text of the named direct children, in `names` order. Text from nested
629
+ * children stays out: their chunks carry their own id.
630
+ */
631
+ export function collectNamedText(
632
+ chunks: Array<StreamChunk>,
633
+ names: ReadonlyArray<string>,
634
+ ) {
635
+ const nameByRunId = new Map<string, string>()
636
+ const textByName = new Map<string, string>()
637
+ for (const chunk of chunks) {
638
+ if (chunk.type === SUBAGENT_STARTED) {
639
+ if (chunk.parentSubagentRunId === undefined) {
640
+ nameByRunId.set(chunk.subagentRunId, chunk.name)
641
+ }
642
+ continue
643
+ }
644
+ if (chunk.type !== EventType.TEXT_MESSAGE_CONTENT) continue
645
+ if (!('subagentRunId' in chunk) || typeof chunk.subagentRunId !== 'string')
646
+ continue
647
+ const name = nameByRunId.get(chunk.subagentRunId)
648
+ if (!name) continue
649
+ textByName.set(name, `${textByName.get(name) ?? ''}${chunk.delta}`)
650
+ }
651
+ return names
652
+ .map((name) => textByName.get(name)?.trim() ?? '')
653
+ .filter((text) => text.length > 0)
654
+ .join('\n\n')
655
+ }
656
+
657
+ /**
658
+ * The parent conversation up to the message that carries this tool call, with
659
+ * that message's tool calls removed. Its string text stays; array content is
660
+ * dropped.
661
+ */
662
+ function messagesBeforeCall(
663
+ messages: ReadonlyArray<ModelMessage>,
664
+ toolCallId: string,
665
+ ): Array<ModelMessage> {
666
+ const index = messages.findIndex((message) =>
667
+ message.toolCalls?.some((call) => call.id === toolCallId),
668
+ )
669
+ if (index === -1) return [...messages]
670
+ const host = messages[index]
671
+ const kept = messages.slice(0, index)
672
+ if (host && typeof host.content === 'string' && host.content !== '') {
673
+ const { toolCalls: _calls, ...text } = host
674
+ void _calls
675
+ kept.push(text)
676
+ }
677
+ return kept
678
+ }
679
+
680
+ /**
681
+ * Record the parent messages when the model calls a subagent tool, so the
682
+ * child reads the conversation as it is at that call.
683
+ */
684
+ export function subagentCallMessages(names: ReadonlySet<string>) {
685
+ const byCall = new Map<string, Array<ModelMessage>>()
686
+ const middleware: ChatMiddleware = {
687
+ name: 'subagent-call-messages',
688
+ onBeforeToolCall(ctx, hook) {
689
+ if (!names.has(hook.toolName)) return undefined
690
+ byCall.set(
691
+ hook.toolCallId,
692
+ messagesBeforeCall(ctx.messages, hook.toolCallId),
693
+ )
694
+ return undefined
695
+ },
696
+ }
697
+ return {
698
+ middleware,
699
+ messagesFor: (toolCallId: string | undefined) =>
700
+ toolCallId === undefined ? undefined : byCall.get(toolCallId),
701
+ }
702
+ }
703
+
704
+ export function createSyntheticSubagentTools(
705
+ bag: SubagentsBag,
706
+ parent: {
707
+ /** Messages the parent run started with. Used when no call was recorded. */
708
+ messages: SubagentRunContext['messages']
709
+ /** The parent messages at a tool call. See subagentCallMessages. */
710
+ messagesFor?: (
711
+ toolCallId: string | undefined,
712
+ ) => SubagentRunContext['messages'] | undefined
713
+ threadId: string
714
+ runId: string
715
+ interruptedRunId?: string
716
+ abortSignal?: AbortSignal
717
+ turn?: SubagentTurn
718
+ sink: SubagentSink
719
+ },
720
+ ): Array<Tool> {
721
+ return bag.agents.map((agent) => ({
722
+ name: agent.name,
723
+ description: agent.description,
724
+ [SUBAGENT_TOOL]: true,
725
+ execute: async (_input: unknown, context?: unknown) => {
726
+ const toolContext = context as
727
+ | {
728
+ toolCallId?: string
729
+ [EMIT_STREAM_CHUNK]?: (chunk: StreamChunk) => void
730
+ }
731
+ | undefined
732
+ const toolCallId = toolContext?.toolCallId
733
+ const suspended = parent.turn?.children.find(
734
+ (child) =>
735
+ child.status === 'suspended' &&
736
+ child.parentToolCallId !== undefined &&
737
+ child.parentToolCallId === toolCallId,
738
+ )
739
+ const entry: SpawnEntry = suspended
740
+ ? {
741
+ name: agent.name,
742
+ resume: {
743
+ subagentRunId: suspended.subagentRunId,
744
+ messages: suspended.messages,
745
+ entries: suspended.resume,
746
+ text: suspended.text,
747
+ },
748
+ }
749
+ : { name: agent.name }
750
+ const sink = createSubagentSink()
751
+ const link = linkAbort(parent.abortSignal)
752
+ let subagentRunId = suspended?.subagentRunId ?? ''
753
+ let text = suspended?.text ?? ''
754
+ let error: string | undefined
755
+ try {
756
+ for await (const chunk of openAgentStream(
757
+ entry,
758
+ bag,
759
+ {
760
+ messages: parent.messagesFor?.(toolCallId) ?? parent.messages,
761
+ abortSignal: link.controller.signal,
762
+ threadId: parent.threadId,
763
+ parentRunId: parent.runId,
764
+ ...(parent.interruptedRunId !== undefined
765
+ ? { interruptedRunId: parent.interruptedRunId }
766
+ : {}),
767
+ },
768
+ sink,
769
+ toolCallId,
770
+ )) {
771
+ if (chunk.type === SUBAGENT_STARTED && subagentRunId === '') {
772
+ subagentRunId = chunk.subagentRunId
773
+ }
774
+ if (
775
+ chunk.type === EventType.TEXT_MESSAGE_CONTENT &&
776
+ 'subagentRunId' in chunk &&
777
+ chunk.subagentRunId === subagentRunId
778
+ ) {
779
+ text += chunk.delta
780
+ }
781
+ if (
782
+ chunk.type === SUBAGENT_ERROR &&
783
+ chunk.subagentRunId === subagentRunId
784
+ ) {
785
+ error = chunk.message
786
+ }
787
+ toolContext?.[EMIT_STREAM_CHUNK]?.(chunk)
788
+ }
789
+ } finally {
790
+ link.dispose()
791
+ }
792
+ parent.sink.usage.push(...sink.usage)
793
+ if (sink.total) {
794
+ parent.sink.total = parent.sink.total
795
+ ? addTokenUsage(parent.sink.total, sink.total)
796
+ : sink.total
797
+ }
798
+ return {
799
+ subagentRunId,
800
+ text,
801
+ ...(error !== undefined ? { error } : {}),
802
+ ...(sink.interrupts.length > 0 ? { interrupts: sink.interrupts } : {}),
803
+ } satisfies SubagentToolOutcome
804
+ },
805
+ }))
806
+ }