@tanstack/ai 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
  2. package/dist/esm/activities/chat/agents/define-agent.js +34 -0
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
  4. package/dist/esm/activities/chat/agents/route.d.ts +53 -0
  5. package/dist/esm/activities/chat/agents/route.js +59 -0
  6. package/dist/esm/activities/chat/agents/route.js.map +1 -0
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +490 -0
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
  10. package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
  11. package/dist/esm/activities/chat/agents/turn.js +78 -0
  12. package/dist/esm/activities/chat/agents/turn.js.map +1 -0
  13. package/dist/esm/activities/chat/index.d.ts +13 -3
  14. package/dist/esm/activities/chat/index.js +337 -13
  15. package/dist/esm/activities/chat/index.js.map +1 -1
  16. package/dist/esm/activities/chat/messages.d.ts +7 -1
  17. package/dist/esm/activities/chat/messages.js +94 -18
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
  20. package/dist/esm/activities/chat/middleware/run-store.js +8 -1
  21. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
  22. package/dist/esm/activities/chat/middleware/types.d.ts +47 -1
  23. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  24. package/dist/esm/activities/chat/stream/processor.d.ts +46 -0
  25. package/dist/esm/activities/chat/stream/processor.js +280 -7
  26. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  27. package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
  28. package/dist/esm/activities/chat/tools/tool-calls.js +54 -5
  29. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  30. package/dist/esm/activities/generateAudio/index.js +1 -1
  31. package/dist/esm/activities/generateImage/index.js +1 -1
  32. package/dist/esm/activities/generateLiveVideo/index.js +1 -1
  33. package/dist/esm/activities/generateSpeech/index.js +1 -1
  34. package/dist/esm/activities/generateTranscription/index.js +1 -1
  35. package/dist/esm/activities/generateVoice/index.js +1 -1
  36. package/dist/esm/activities/generateWorld/index.js +1 -1
  37. package/dist/esm/activities/index.d.ts +3 -0
  38. package/dist/esm/activities/index.js +5 -3
  39. package/dist/esm/activities/summarize/index.js +1 -1
  40. package/dist/esm/client.d.ts +5 -36
  41. package/dist/esm/client.js +4 -37
  42. package/dist/esm/client.js.map +1 -1
  43. package/dist/esm/index.d.ts +5 -0
  44. package/dist/esm/index.js +6 -3
  45. package/dist/esm/middlewares/content-guard.js.map +1 -1
  46. package/dist/esm/types.d.ts +110 -88
  47. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  48. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  49. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  50. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  51. package/dist/esm/utilities/ag-ui-wire.js +56 -0
  52. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  53. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  54. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  55. package/dist/esm/utilities/spec-event-keys.js +13 -8
  56. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  57. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  58. package/dist/esm/utilities/subagent-wire.js +131 -0
  59. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  60. package/package.json +2 -2
  61. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  62. package/skills/ai-core/media-generation/SKILL.md +2 -2
  63. package/skills/ai-core/middleware/SKILL.md +7 -4
  64. package/src/activities/chat/agents/define-agent.ts +121 -0
  65. package/src/activities/chat/agents/route.ts +115 -0
  66. package/src/activities/chat/agents/spawn.ts +806 -0
  67. package/src/activities/chat/agents/turn.ts +151 -0
  68. package/src/activities/chat/index.ts +529 -11
  69. package/src/activities/chat/messages.ts +125 -15
  70. package/src/activities/chat/middleware/run-store.ts +56 -7
  71. package/src/activities/chat/middleware/types.ts +47 -0
  72. package/src/activities/chat/stream/processor.ts +428 -8
  73. package/src/activities/chat/tools/tool-calls.ts +77 -11
  74. package/src/activities/index.ts +15 -0
  75. package/src/client.ts +22 -35
  76. package/src/index.ts +23 -0
  77. package/src/middlewares/content-guard.ts +7 -5
  78. package/src/types.ts +154 -94
  79. package/src/utilities/adapter-yield-chunk.ts +10 -2
  80. package/src/utilities/ag-ui-usage.test.ts +38 -0
  81. package/src/utilities/ag-ui-usage.ts +98 -11
  82. package/src/utilities/ag-ui-wire.ts +74 -0
  83. package/src/utilities/normalize-stream-chunk.ts +10 -2
  84. package/src/utilities/spec-event-keys.ts +34 -7
  85. package/src/utilities/subagent-wire.ts +184 -0
@@ -38,6 +38,7 @@ import {
38
38
  tanstackMetadata,
39
39
  withTanstackMetadata,
40
40
  } from '../../utilities/merge-metadata'
41
+ import { subagentHostMessageId } from '../../utilities/subagent-wire'
41
42
  import { withDurabilityBatchHint } from '../../utilities/durability-batch'
42
43
  import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
43
44
  import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
@@ -46,6 +47,25 @@ import { normalizeToolResult } from '../../utilities/tool-result'
46
47
  import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
47
48
  import { LazyToolManager } from './tools/lazy-tool-manager'
48
49
  import { assertUniqueToolNames } from './tools/unique-tool-names'
50
+ import type { DefinedAgent } from './agents/define-agent'
51
+ import {
52
+ collectNamedText,
53
+ createSubagentSink,
54
+ createSyntheticSubagentTools,
55
+ normalizeRouterPick,
56
+ rebindInterrupts,
57
+ spawnNamedAgents,
58
+ subagentCallMessages,
59
+ withChildUsage,
60
+ } from './agents/spawn'
61
+ import type {
62
+ SpawnEntry,
63
+ SubagentSink,
64
+ SubagentStep,
65
+ SubagentStepsPlan,
66
+ SubagentsBag,
67
+ } from './agents/spawn'
68
+ import { SUBAGENT_PLAN_KEY, readSubagentTurn } from './agents/turn'
49
69
  import {
50
70
  MiddlewareAbortError,
51
71
  ToolCallManager,
@@ -163,6 +183,28 @@ type RuntimeToolWithApproval = AnyRuntimeTool & {
163
183
  }
164
184
  const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
165
185
 
186
+ /** Resume entries a subagent tool call owns. The parent run skips them. */
187
+ const CHILD_RESUME_IDS = Symbol('tanstack.ai.childResumeIds')
188
+
189
+ // ponytail: no adapter maps `{ type: 'file' }` yet, so every one fails closed
190
+ // instead of reading the handle as a URL or base64. The Files API work
191
+ // replaces this with a per-adapter capability check.
192
+ function assertNoFileSources(
193
+ adapterName: string,
194
+ messages: ReadonlyArray<ModelMessage>,
195
+ ): void {
196
+ for (const message of messages) {
197
+ if (!Array.isArray(message.content)) continue
198
+ for (const part of message.content) {
199
+ if ('source' in part && part.source.type === 'file') {
200
+ throw new Error(
201
+ `${adapterName} does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.`,
202
+ )
203
+ }
204
+ }
205
+ }
206
+ }
207
+
166
208
  interface StructuralInterruptFailure {
167
209
  error: Error
168
210
  errors: ReadonlyArray<InterruptSubmissionError>
@@ -395,8 +437,9 @@ type TextActivityOptionsWithContext<
395
437
  [],
396
438
  TContext = unknown,
397
439
  TMiddleware extends Array<unknown> | undefined = undefined,
440
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
398
441
  > = Omit<
399
- TextActivityOptions<TAdapter, TSchema, TStream, any>,
442
+ TextActivityOptions<TAdapter, TSchema, TStream, any, TAgents>,
400
443
  'tools' | 'middleware' | 'context' | 'interrupts'
401
444
  > & {
402
445
  tools?: TTools
@@ -422,6 +465,7 @@ export interface TextActivityOptions<
422
465
  TSchema extends SchemaInput | undefined,
423
466
  TStream extends boolean,
424
467
  TContext = unknown,
468
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
425
469
  > {
426
470
  /** The text adapter to use (created by a provider function like openaiText('gpt-5.5')) */
427
471
  adapter: TAdapter
@@ -499,6 +543,8 @@ export interface TextActivityOptions<
499
543
  runId?: TextOptions['runId']
500
544
  /** Parent run ID for AG-UI protocol nested run correlation. */
501
545
  parentRunId?: TextOptions['parentRunId']
546
+ /** Subagent run id when this chat runs as a child. See `defineAgent`. */
547
+ subagentRunId?: TextOptions['subagentRunId']
502
548
  /** Application state mirrored in a STATE_SNAPSHOT before an interrupt terminal. */
503
549
  state?: TextOptions['state']
504
550
  /**
@@ -506,6 +552,12 @@ export interface TextActivityOptions<
506
552
  * before accepting new input on a thread with pending interrupts.
507
553
  */
508
554
  resume?: TextOptions['resume']
555
+ /**
556
+ * Named child agents. When `router` is set, the library spawns that agent
557
+ * directly. When `router` is omitted, the main model gets one synthetic
558
+ * server tool per agent and picks the child.
559
+ */
560
+ subagents?: SubagentsBag<TAgents>
509
561
  /**
510
562
  * Optional Standard Schema for structured output.
511
563
  * When provided, the activity will:
@@ -842,6 +894,7 @@ class TextEngine<
842
894
  private readonly threadId: string
843
895
  private readonly runIdOverride?: string
844
896
  private readonly parentRunIdOverride?: string
897
+ private readonly subagentRunIdOverride?: string
845
898
 
846
899
  // Middleware support
847
900
  private readonly middlewareRunner: MiddlewareRunner<
@@ -972,6 +1025,7 @@ class TextEngine<
972
1025
  this.createId('thread')
973
1026
  this.runIdOverride = config.params.runId
974
1027
  this.parentRunIdOverride = config.params.parentRunId
1028
+ this.subagentRunIdOverride = config.params.subagentRunId
975
1029
 
976
1030
  // Initialize middleware — devtools first. Spec stripping for the AG-UI
977
1031
  // wire happens in toServerSentEventsStream, so in-process chat()
@@ -990,6 +1044,7 @@ class TextEngine<
990
1044
  streamId: this.streamId,
991
1045
  runId: this.runIdOverride ?? this.requestId,
992
1046
  parentRunId: this.parentRunIdOverride,
1047
+ subagentRunId: this.subagentRunIdOverride,
993
1048
  threadId: this.threadId,
994
1049
  // Legacy alias kept on the ctx so middleware that reads
995
1050
  // `ctx.conversationId` keeps working. Always equals `threadId`.
@@ -1516,6 +1571,8 @@ class TextEngine<
1516
1571
  )
1517
1572
  }
1518
1573
 
1574
+ assertNoFileSources(this.adapter.name, this.messages)
1575
+
1519
1576
  for await (const raw of this.adapter.chatStream({
1520
1577
  model: this.params.model,
1521
1578
  messages: this.providerMessages,
@@ -2083,7 +2140,8 @@ class TextEngine<
2083
2140
 
2084
2141
  if (
2085
2142
  executionResult.needsApproval.length > 0 ||
2086
- executionResult.needsClientExecution.length > 0
2143
+ executionResult.needsClientExecution.length > 0 ||
2144
+ executionResult.subagentInterrupts.length > 0
2087
2145
  ) {
2088
2146
  this.discardDeferredToolCallRunFinishedChunks()
2089
2147
 
@@ -2100,6 +2158,8 @@ class TextEngine<
2100
2158
  finishEvent,
2101
2159
  executionResult.needsApproval,
2102
2160
  executionResult.needsClientExecution,
2161
+ [],
2162
+ executionResult.subagentInterrupts,
2103
2163
  )
2104
2164
  this.setToolPhase(emitted ? 'wait' : 'stop')
2105
2165
  return emitted ? 'wait' : 'stop'
@@ -2292,7 +2352,8 @@ class TextEngine<
2292
2352
 
2293
2353
  if (
2294
2354
  executionResult.needsApproval.length > 0 ||
2295
- executionResult.needsClientExecution.length > 0
2355
+ executionResult.needsClientExecution.length > 0 ||
2356
+ executionResult.subagentInterrupts.length > 0
2296
2357
  ) {
2297
2358
  if (allResults.length > 0) {
2298
2359
  for (const chunk of afterToolBoundaryChunks) {
@@ -2304,6 +2365,8 @@ class TextEngine<
2304
2365
  finishEvent,
2305
2366
  executionResult.needsApproval,
2306
2367
  executionResult.needsClientExecution,
2368
+ [],
2369
+ executionResult.subagentInterrupts,
2307
2370
  )
2308
2371
  this.setToolPhase(emitted ? 'wait' : 'stop')
2309
2372
  return
@@ -2622,6 +2685,7 @@ class TextEngine<
2622
2685
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2623
2686
  > = [],
2624
2687
  genericInterruptIds: ReadonlyArray<string> = [],
2688
+ childInterrupts: ReadonlyArray<Interrupt> = [],
2625
2689
  ): Array<Interrupt> {
2626
2690
  const interrupts: Array<Interrupt> = []
2627
2691
 
@@ -2739,6 +2803,9 @@ class TextEngine<
2739
2803
  })
2740
2804
  }
2741
2805
 
2806
+ // A subagent tool call raised these. They keep their `subagentRunId`.
2807
+ interrupts.push(...childInterrupts)
2808
+
2742
2809
  const ids = new Set<string>()
2743
2810
  for (const interrupt of interrupts) {
2744
2811
  if (ids.has(interrupt.id)) {
@@ -2760,6 +2827,7 @@ class TextEngine<
2760
2827
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2761
2828
  > = [],
2762
2829
  genericInterruptIds?: ReadonlyArray<string>,
2830
+ childInterrupts?: ReadonlyArray<Interrupt>,
2763
2831
  ): StreamChunk {
2764
2832
  return {
2765
2833
  ...finishEvent,
@@ -2771,6 +2839,7 @@ class TextEngine<
2771
2839
  clientRequests,
2772
2840
  genericRequests,
2773
2841
  genericInterruptIds,
2842
+ childInterrupts,
2774
2843
  ),
2775
2844
  },
2776
2845
  }
@@ -2920,11 +2989,13 @@ class TextEngine<
2920
2989
  genericRequests: ReadonlyArray<
2921
2990
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2922
2991
  > = [],
2992
+ childInterrupts: ReadonlyArray<Interrupt> = [],
2923
2993
  ): AsyncGenerator<StreamChunk, boolean, void> {
2924
2994
  yield* this.emitSyntheticRunStarted(finishEvent)
2925
2995
  const genericInterruptIds = genericRequests.map(() =>
2926
2996
  this.genericInterruptId(),
2927
2997
  )
2998
+ // Binding completion also binds child interrupts to this run.
2928
2999
  const terminal = this.completeEphemeralInterruptBindings(
2929
3000
  this.buildInterruptFinishedChunk(
2930
3001
  finishEvent,
@@ -2932,6 +3003,7 @@ class TextEngine<
2932
3003
  clientRequests,
2933
3004
  genericRequests,
2934
3005
  genericInterruptIds,
3006
+ childInterrupts,
2935
3007
  ),
2936
3008
  )
2937
3009
  let terminalOutputs: Array<StreamChunk>
@@ -3517,6 +3589,8 @@ class TextEngine<
3517
3589
  // Apply merged config back to engine state
3518
3590
  this.applyMiddlewareConfig(postOnConfig)
3519
3591
 
3592
+ assertNoFileSources(this.adapter.name, this.messages)
3593
+
3520
3594
  // Build the StructuredOutputOptions the adapter expects.
3521
3595
  // `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
3522
3596
  // class generics — no cast needed.
@@ -4013,9 +4087,25 @@ class TextEngine<
4013
4087
  }
4014
4088
  }
4015
4089
 
4090
+ /** Resume entries this run answers itself. A subagent tool owns the rest. */
4091
+ private ownResume(
4092
+ resume: ChatMiddlewareConfig['resume'],
4093
+ ): ChatMiddlewareConfig['resume'] {
4094
+ const childIds = (
4095
+ this.params as { [CHILD_RESUME_IDS]?: ReadonlySet<string> }
4096
+ )[CHILD_RESUME_IDS]
4097
+ return childIds
4098
+ ? resume?.filter((entry) => !childIds.has(entry.interruptId))
4099
+ : resume
4100
+ }
4101
+
4016
4102
  private async applyEphemeralInterruptResume(
4017
- config: ChatMiddlewareConfig,
4103
+ middlewareConfig: ChatMiddlewareConfig,
4018
4104
  ): Promise<void> {
4105
+ const config = {
4106
+ ...middlewareConfig,
4107
+ resume: this.ownResume(middlewareConfig.resume),
4108
+ }
4019
4109
  if ((config.resume?.length ?? 0) === 0) {
4020
4110
  return
4021
4111
  }
@@ -4248,7 +4338,7 @@ class TextEngine<
4248
4338
  }> = []
4249
4339
  const ids = new Set<string>()
4250
4340
  const batchIndexes = new Set<number>()
4251
- for (const resumeItem of this.params.resume ?? []) {
4341
+ for (const resumeItem of this.ownResume(this.params.resume) ?? []) {
4252
4342
  const parsed = readGenericInterruptContinuation(resumeItem.metadata)
4253
4343
  if (parsed.status === 'absent') continue
4254
4344
  if (parsed.status === 'invalid') {
@@ -4558,11 +4648,12 @@ class TextEngine<
4558
4648
  */
4559
4649
  private async *drainToolCallGenerator(
4560
4650
  generator: AsyncGenerator<
4561
- CustomEvent,
4651
+ CustomEvent | StreamChunk,
4562
4652
  {
4563
4653
  results: Array<ToolResult>
4564
4654
  needsApproval: Array<ApprovalRequest>
4565
4655
  needsClientExecution: Array<ClientToolRequest>
4656
+ subagentInterrupts: Array<Interrupt>
4566
4657
  },
4567
4658
  void
4568
4659
  >,
@@ -4572,6 +4663,7 @@ class TextEngine<
4572
4663
  results: Array<ToolResult>
4573
4664
  needsApproval: Array<ApprovalRequest>
4574
4665
  needsClientExecution: Array<ClientToolRequest>
4666
+ subagentInterrupts: Array<Interrupt>
4575
4667
  },
4576
4668
  void
4577
4669
  > {
@@ -4683,6 +4775,8 @@ export function chat<
4683
4775
  > = [],
4684
4776
  TContext = unknown,
4685
4777
  const TMiddleware extends Array<unknown> | undefined = undefined,
4778
+ const TAgents extends ReadonlyArray<DefinedAgent> =
4779
+ ReadonlyArray<DefinedAgent>,
4686
4780
  >(
4687
4781
  options: TextActivityOptionsWithContext<
4688
4782
  TAdapter,
@@ -4691,7 +4785,8 @@ export function chat<
4691
4785
  TTools,
4692
4786
  TInterrupts,
4693
4787
  TContext,
4694
- TMiddleware
4788
+ TMiddleware,
4789
+ TAgents
4695
4790
  >,
4696
4791
  ): TextActivityResult<TSchema, TStream, TTools> {
4697
4792
  validateInterruptDefinitions(options.interrupts)
@@ -4705,6 +4800,12 @@ export function chat<
4705
4800
 
4706
4801
  const { outputSchema, stream } = options
4707
4802
 
4803
+ if (outputSchema && (options.subagents?.agents.length ?? 0) > 0) {
4804
+ throw new Error(
4805
+ 'chat() does not support subagents together with outputSchema. Put outputSchema on a child chat() instead.',
4806
+ )
4807
+ }
4808
+
4708
4809
  if (outputSchema && stream === true) {
4709
4810
  return runStreamingStructuredOutput(
4710
4811
  toRuntimeTextActivityOptions(options, {
@@ -4744,7 +4845,10 @@ type RuntimeTextActivityOptions<
4744
4845
  TAdapter extends AnyTextAdapter,
4745
4846
  TSchema extends SchemaInput | undefined,
4746
4847
  TStream extends boolean,
4747
- > = Omit<TextActivityOptions<TAdapter, TSchema, TStream, any>, 'middleware'> & {
4848
+ > = Omit<
4849
+ TextActivityOptions<TAdapter, TSchema, TStream, any, any>,
4850
+ 'middleware'
4851
+ > & {
4748
4852
  middleware?: Array<AnyChatMiddleware>
4749
4853
  }
4750
4854
 
@@ -4773,6 +4877,7 @@ function toRuntimeTextActivityOptions<
4773
4877
  TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
4774
4878
  TContext,
4775
4879
  TMiddleware extends Array<unknown> | undefined,
4880
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
4776
4881
  >(
4777
4882
  options: TextActivityOptionsWithContext<
4778
4883
  TAdapter,
@@ -4781,7 +4886,8 @@ function toRuntimeTextActivityOptions<
4781
4886
  TTools,
4782
4887
  TInterrupts,
4783
4888
  TContext,
4784
- TMiddleware
4889
+ TMiddleware,
4890
+ TAgents
4785
4891
  >,
4786
4892
  overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
4787
4893
  ): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
@@ -4868,11 +4974,19 @@ function runStreamingText(
4868
4974
  return stream
4869
4975
  }
4870
4976
 
4871
- async function* streamTextChunks(
4977
+ async function* runChatEngine(
4872
4978
  options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
4873
4979
  engineRef: DeliveryEngineRef,
4874
4980
  ): AsyncIterable<StreamChunk> {
4875
- const { adapter, middleware, context, debug, mcp, ...textOptions } = options
4981
+ const {
4982
+ adapter,
4983
+ middleware,
4984
+ context,
4985
+ debug,
4986
+ mcp,
4987
+ subagents: _subagents,
4988
+ ...textOptions
4989
+ } = options
4876
4990
  const model = adapter.model
4877
4991
  const logger = resolveDebugOption(debug)
4878
4992
 
@@ -4906,6 +5020,410 @@ async function* streamTextChunks(
4906
5020
  }
4907
5021
  }
4908
5022
 
5023
+ async function* streamTextChunks(
5024
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
5025
+ engineRef: DeliveryEngineRef,
5026
+ ): AsyncIterable<StreamChunk> {
5027
+ const bag = options.subagents
5028
+ const agents = bag?.agents ?? []
5029
+ if (bag && agents.length > 0 && bag.router) {
5030
+ yield* runRoutedSubagents(options, engineRef)
5031
+ return
5032
+ }
5033
+ if (bag && agents.length > 0) {
5034
+ const threadId = options.threadId ?? `thread-${Date.now()}`
5035
+ const runId = options.runId ?? `run-${Date.now()}`
5036
+ const messages = options.messages ?? []
5037
+ // A resume can answer a child's interrupts. Those entries go to the child
5038
+ // tool call. The parent run validates only its own entries.
5039
+ const turn = readSubagentTurn(messages, options.resume)
5040
+ const sink = createSubagentSink()
5041
+ const calls = subagentCallMessages(
5042
+ new Set(bag.agents.map((agent: DefinedAgent) => agent.name)),
5043
+ )
5044
+ const synthetic = createSyntheticSubagentTools(bag, {
5045
+ messages: turn?.before ?? messages,
5046
+ messagesFor: calls.messagesFor,
5047
+ threadId,
5048
+ runId,
5049
+ ...(options.parentRunId !== undefined && {
5050
+ interruptedRunId: options.parentRunId,
5051
+ }),
5052
+ ...(options.abortController && {
5053
+ abortSignal: options.abortController.signal,
5054
+ }),
5055
+ ...(turn && { turn }),
5056
+ sink,
5057
+ })
5058
+ yield* streamWithChildUsage(
5059
+ runChatEngine(
5060
+ {
5061
+ ...options,
5062
+ threadId,
5063
+ runId,
5064
+ ...(turn && {
5065
+ [CHILD_RESUME_IDS]: new Set(
5066
+ turn.children.flatMap((child) =>
5067
+ child.resume.map((entry) => entry.interruptId),
5068
+ ),
5069
+ ),
5070
+ }),
5071
+ tools: [...(options.tools ?? []), ...synthetic],
5072
+ middleware: [...(options.middleware ?? []), calls.middleware],
5073
+ },
5074
+ engineRef,
5075
+ ),
5076
+ sink,
5077
+ )
5078
+ return
5079
+ }
5080
+ yield* runChatEngine(options, engineRef)
5081
+ }
5082
+
5083
+ /** Add child usage to the next RUN_FINISHED of the parent. */
5084
+ async function* streamWithChildUsage(
5085
+ stream: AsyncIterable<StreamChunk>,
5086
+ sink: SubagentSink,
5087
+ ): AsyncIterable<StreamChunk> {
5088
+ for await (const chunk of stream) {
5089
+ yield chunk.type === EventType.RUN_FINISHED
5090
+ ? withChildUsage(chunk, sink)
5091
+ : chunk
5092
+ }
5093
+ }
5094
+
5095
+ async function* runRoutedSubagents(
5096
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
5097
+ engineRef: DeliveryEngineRef,
5098
+ ): AsyncIterable<StreamChunk> {
5099
+ const bag = options.subagents
5100
+ if (!bag?.router) {
5101
+ yield* runChatEngine(options, engineRef)
5102
+ return
5103
+ }
5104
+ const threadId = options.threadId ?? `thread-${Date.now()}`
5105
+ const runId = options.runId ?? `run-${Date.now()}`
5106
+ const messages = options.messages ?? []
5107
+ const turn = readSubagentTurn(messages, options.resume)
5108
+ if (!turn && (options.resume?.length ?? 0) > 0) {
5109
+ // No child owns these answers. Main raised them, after a main pick or a
5110
+ // handoff. Run main alone.
5111
+ yield* runChatEngine(
5112
+ { ...options, threadId, runId, subagents: undefined },
5113
+ engineRef,
5114
+ )
5115
+ return
5116
+ }
5117
+ const abortSignal = options.abortController?.signal
5118
+ const turnMessages = turn?.before ?? messages
5119
+ const subagentPersistence = readRoutedSubagentPersistence(options.middleware)
5120
+ await subagentPersistence?.start({
5121
+ threadId,
5122
+ runId,
5123
+ messages,
5124
+ ...(options.resume && { resume: options.resume }),
5125
+ })
5126
+ try {
5127
+ // A resume reuses the plan that the first run put on each child's
5128
+ // SUBAGENT_STARTED metadata. Children that finished keep their result.
5129
+ // Suspended children continue.
5130
+ const plan =
5131
+ savedPlan(turn?.plan, bag.agents, {
5132
+ threadId,
5133
+ interruptedRunId: options.parentRunId ?? runId,
5134
+ interruptIds: options.resume?.map((entry) => entry.interruptId) ?? [],
5135
+ }) ??
5136
+ normalizeRouterPick(
5137
+ await bag.router({
5138
+ messages: turnMessages,
5139
+ agents: bag.agents,
5140
+ ...(abortSignal && { abortSignal }),
5141
+ }),
5142
+ bag.agents,
5143
+ )
5144
+ // The run was stopped while the router decided. Start nothing. No
5145
+ // RUN_STARTED went out, so the stream ends without a terminal, but the
5146
+ // record start() opened still has to settle. Otherwise it stays `running`
5147
+ // and reconstructChat hands the client a run to tail that never emits.
5148
+ if (abortSignal?.aborted) {
5149
+ const error = new Error('Aborted')
5150
+ error.name = 'AbortError'
5151
+ await subagentPersistence?.abort({ threadId, runId, error })
5152
+ return
5153
+ }
5154
+ const onlyStep = plan.steps.length === 1 ? plan.steps[0] : undefined
5155
+ if (onlyStep?.names.length === 1 && onlyStep.names[0] === 'main') {
5156
+ // Earlier children own answers in this resume. Commit them first.
5157
+ if (turn) await subagentPersistence?.finish({ threadId, runId })
5158
+ yield* runChatEngine(
5159
+ {
5160
+ ...options,
5161
+ threadId,
5162
+ runId,
5163
+ subagents: undefined,
5164
+ ...(turn && { resume: turn.rest }),
5165
+ },
5166
+ engineRef,
5167
+ )
5168
+ return
5169
+ }
5170
+
5171
+ yield {
5172
+ type: EventType.RUN_STARTED,
5173
+ threadId,
5174
+ runId,
5175
+ timestamp: Date.now(),
5176
+ }
5177
+
5178
+ const earlier = [...(turn?.children ?? [])]
5179
+ const takeEarlier = (name: string) => {
5180
+ const index = earlier.findIndex((child) => child.name === name)
5181
+ return index === -1 ? undefined : earlier.splice(index, 1)[0]
5182
+ }
5183
+ const sink = createSubagentSink()
5184
+ const stepTexts: Array<string> = []
5185
+ let stepMessages = turnMessages
5186
+ let failure:
5187
+ | Extract<StreamChunk, { type: EventType.SUBAGENT_ERROR }>
5188
+ | undefined
5189
+ for (const step of plan.steps) {
5190
+ const entries: Array<SpawnEntry> = []
5191
+ const textByName = new Map<string, string>()
5192
+ for (const name of step.names) {
5193
+ const prior = takeEarlier(name)
5194
+ if (prior?.status === 'finished') {
5195
+ textByName.set(name, prior.text)
5196
+ continue
5197
+ }
5198
+ entries.push(
5199
+ prior?.status === 'suspended'
5200
+ ? {
5201
+ name,
5202
+ resume: {
5203
+ subagentRunId: prior.subagentRunId,
5204
+ messages: prior.messages,
5205
+ entries: prior.resume,
5206
+ text: prior.text,
5207
+ },
5208
+ }
5209
+ : { name },
5210
+ )
5211
+ }
5212
+ const stepChunks: Array<StreamChunk> = []
5213
+ if (entries.length > 0) {
5214
+ for await (const chunk of spawnNamedAgents(
5215
+ entries,
5216
+ { ...bag, order: step.order ?? bag.order },
5217
+ {
5218
+ messages: stepMessages,
5219
+ ...(abortSignal && { abortSignal }),
5220
+ threadId,
5221
+ parentRunId: runId,
5222
+ ...(options.parentRunId !== undefined && {
5223
+ interruptedRunId: options.parentRunId,
5224
+ }),
5225
+ },
5226
+ sink,
5227
+ )) {
5228
+ const tagged = withPlan(chunk, plan)
5229
+ stepChunks.push(tagged)
5230
+ await subagentPersistence?.chunk({ threadId, runId, chunk: tagged })
5231
+ yield tagged
5232
+ }
5233
+ }
5234
+ failure = stepChunks.find(
5235
+ (chunk) => chunk.type === EventType.SUBAGENT_ERROR,
5236
+ )
5237
+ if (failure) break
5238
+ if (sink.interrupts.length > 0) break
5239
+ for (const entry of entries) {
5240
+ const text = collectNamedText(stepChunks, [entry.name])
5241
+ textByName.set(
5242
+ entry.name,
5243
+ [entry.resume?.text, text].filter(Boolean).join('\n\n'),
5244
+ )
5245
+ }
5246
+ const text = step.names
5247
+ .map((name) => textByName.get(name)?.trim() ?? '')
5248
+ .filter((block) => block !== '')
5249
+ .join('\n\n')
5250
+ if (text) {
5251
+ stepTexts.push(text)
5252
+ stepMessages = [...stepMessages, { role: 'assistant', content: text }]
5253
+ }
5254
+ }
5255
+
5256
+ if (abortSignal?.aborted) {
5257
+ const error = new Error('Aborted')
5258
+ error.name = 'AbortError'
5259
+ await subagentPersistence?.abort({ threadId, runId, error })
5260
+ yield withChildUsage(
5261
+ {
5262
+ type: EventType.RUN_FINISHED,
5263
+ threadId,
5264
+ runId,
5265
+ outcome: { type: 'cancelled' },
5266
+ timestamp: Date.now(),
5267
+ },
5268
+ sink,
5269
+ )
5270
+ return
5271
+ }
5272
+
5273
+ if (failure) {
5274
+ // Carry the child's own cause, not a generic label, and keep the usage
5275
+ // the children already spent: the run failed, but the tokens were real.
5276
+ const message = failure.message || 'A subagent failed'
5277
+ await subagentPersistence?.abort({
5278
+ threadId,
5279
+ runId,
5280
+ error: new Error(message),
5281
+ })
5282
+ yield withChildUsage(
5283
+ {
5284
+ type: EventType.RUN_ERROR,
5285
+ threadId,
5286
+ runId,
5287
+ message,
5288
+ ...(failure.code !== undefined ? { code: failure.code } : {}),
5289
+ timestamp: Date.now(),
5290
+ },
5291
+ sink,
5292
+ )
5293
+ return
5294
+ }
5295
+
5296
+ if (sink.interrupts.length > 0) {
5297
+ const interrupts = rebindInterrupts(sink.interrupts, runId)
5298
+ await subagentPersistence?.suspend?.({ threadId, runId, interrupts })
5299
+ yield withChildUsage(
5300
+ {
5301
+ type: EventType.RUN_FINISHED,
5302
+ threadId,
5303
+ runId,
5304
+ outcome: { type: 'interrupt', interrupts },
5305
+ timestamp: Date.now(),
5306
+ },
5307
+ sink,
5308
+ )
5309
+ return
5310
+ }
5311
+
5312
+ const strategy = bag.strategy ?? 'exclusive'
5313
+ if (strategy === 'handoff') {
5314
+ const childText = stepTexts.join('\n\n')
5315
+ // The children are done. Commit their answers and settle their records
5316
+ // before main runs. Main's own persistence takes over from here.
5317
+ await subagentPersistence?.finish({ threadId, runId })
5318
+ yield* streamWithChildUsage(
5319
+ runChatEngine(
5320
+ {
5321
+ ...options,
5322
+ threadId,
5323
+ runId,
5324
+ subagents: undefined,
5325
+ ...(turn && { resume: turn.rest }),
5326
+ messages: [
5327
+ ...turnMessages,
5328
+ {
5329
+ // Same id as the recorder's parent row, so the stored thread
5330
+ // keeps one host message for the cards.
5331
+ id: subagentHostMessageId(runId),
5332
+ role: 'assistant',
5333
+ content: childText || 'Subagent finished.',
5334
+ metadata: { tanstack: { runId } },
5335
+ },
5336
+ ],
5337
+ },
5338
+ engineRef,
5339
+ ),
5340
+ sink,
5341
+ )
5342
+ return
5343
+ }
5344
+
5345
+ await subagentPersistence?.finish({ threadId, runId })
5346
+ yield withChildUsage(
5347
+ {
5348
+ type: EventType.RUN_FINISHED,
5349
+ threadId,
5350
+ runId,
5351
+ timestamp: Date.now(),
5352
+ },
5353
+ sink,
5354
+ )
5355
+ } catch (error) {
5356
+ await subagentPersistence?.abort({ threadId, runId, error })
5357
+ throw error
5358
+ }
5359
+ }
5360
+
5361
+ /**
5362
+ * The plan from a resumed turn, or undefined when it is absent. A plan that
5363
+ * names agents this chat does not have is stale: re-routing would strand the
5364
+ * suspended child that owns the answers, so fail the resume instead.
5365
+ */
5366
+ function savedPlan(
5367
+ plan: unknown,
5368
+ agents: ReadonlyArray<DefinedAgent>,
5369
+ run: {
5370
+ threadId: string
5371
+ interruptedRunId: string
5372
+ interruptIds: ReadonlyArray<string>
5373
+ },
5374
+ ): { steps: ReadonlyArray<SubagentStep> } | undefined {
5375
+ if (typeof plan !== 'object' || plan === null || !('steps' in plan)) {
5376
+ return undefined
5377
+ }
5378
+ try {
5379
+ // The plan comes back from the client. Check it like a router pick.
5380
+ return normalizeRouterPick(plan as SubagentStepsPlan, agents)
5381
+ } catch (error) {
5382
+ throw new InterruptResumeValidationError([
5383
+ {
5384
+ scope: 'batch',
5385
+ threadId: run.threadId,
5386
+ interruptedRunId: run.interruptedRunId,
5387
+ generation: 0,
5388
+ interruptIds: run.interruptIds,
5389
+ code: 'stale',
5390
+ message: `The saved subagent plan does not match the agents of this chat. ${
5391
+ error instanceof Error ? error.message : String(error)
5392
+ }`,
5393
+ source: 'server',
5394
+ retryable: false,
5395
+ },
5396
+ ])
5397
+ }
5398
+ }
5399
+
5400
+ /** Put the router plan on a direct child's SUBAGENT_STARTED metadata. */
5401
+ function withPlan(
5402
+ chunk: StreamChunk,
5403
+ plan: { steps: ReadonlyArray<SubagentStep> },
5404
+ ): StreamChunk {
5405
+ if (
5406
+ chunk.type !== EventType.SUBAGENT_STARTED ||
5407
+ chunk.parentSubagentRunId !== undefined
5408
+ ) {
5409
+ return chunk
5410
+ }
5411
+ return withTanstackMetadata(chunk, { [SUBAGENT_PLAN_KEY]: plan })
5412
+ }
5413
+
5414
+ function readRoutedSubagentPersistence(
5415
+ middleware:
5416
+ | ReadonlyArray<{
5417
+ routedSubagentPersistence?: ChatMiddleware['routedSubagentPersistence']
5418
+ }>
5419
+ | undefined,
5420
+ ) {
5421
+ for (const item of middleware ?? []) {
5422
+ if (item.routedSubagentPersistence) return item.routedSubagentPersistence
5423
+ }
5424
+ return undefined
5425
+ }
5426
+
4909
5427
  /**
4910
5428
  * Run non-streaming text - collects all content and returns as a string.
4911
5429
  * Runs the full agentic loop (if tools are provided) but returns collected text.