@tanstack/ai 0.57.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 (95) 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 +345 -19
  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 +50 -3
  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 +56 -7
  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 +7 -4
  45. package/dist/esm/middlewares/content-guard.js.map +1 -1
  46. package/dist/esm/stream-to-response.js +12 -6
  47. package/dist/esm/stream-to-response.js.map +1 -1
  48. package/dist/esm/strip-to-spec-middleware.js +2 -1
  49. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  50. package/dist/esm/types.d.ts +128 -89
  51. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  52. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  53. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  54. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  55. package/dist/esm/utilities/ag-ui-wire.js +56 -0
  56. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  57. package/dist/esm/utilities/durability-batch.d.ts +8 -0
  58. package/dist/esm/utilities/durability-batch.js +45 -0
  59. package/dist/esm/utilities/durability-batch.js.map +1 -0
  60. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  61. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  62. package/dist/esm/utilities/spec-event-keys.js +13 -8
  63. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  64. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  65. package/dist/esm/utilities/subagent-wire.js +131 -0
  66. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  67. package/package.json +4 -4
  68. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  69. package/skills/ai-core/media-generation/SKILL.md +2 -2
  70. package/skills/ai-core/middleware/SKILL.md +7 -4
  71. package/src/activities/chat/agents/define-agent.ts +121 -0
  72. package/src/activities/chat/agents/route.ts +115 -0
  73. package/src/activities/chat/agents/spawn.ts +806 -0
  74. package/src/activities/chat/agents/turn.ts +151 -0
  75. package/src/activities/chat/index.ts +540 -16
  76. package/src/activities/chat/messages.ts +125 -15
  77. package/src/activities/chat/middleware/run-store.ts +56 -7
  78. package/src/activities/chat/middleware/types.ts +55 -2
  79. package/src/activities/chat/stream/processor.ts +428 -8
  80. package/src/activities/chat/tools/tool-calls.ts +92 -16
  81. package/src/activities/index.ts +15 -0
  82. package/src/client.ts +22 -35
  83. package/src/index.ts +23 -0
  84. package/src/middlewares/content-guard.ts +7 -5
  85. package/src/stream-to-response.ts +16 -6
  86. package/src/strip-to-spec-middleware.ts +2 -1
  87. package/src/types.ts +177 -95
  88. package/src/utilities/adapter-yield-chunk.ts +10 -2
  89. package/src/utilities/ag-ui-usage.test.ts +38 -0
  90. package/src/utilities/ag-ui-usage.ts +98 -11
  91. package/src/utilities/ag-ui-wire.ts +74 -0
  92. package/src/utilities/durability-batch.ts +48 -0
  93. package/src/utilities/normalize-stream-chunk.ts +10 -2
  94. package/src/utilities/spec-event-keys.ts +34 -7
  95. package/src/utilities/subagent-wire.ts +184 -0
@@ -38,6 +38,8 @@ import {
38
38
  tanstackMetadata,
39
39
  withTanstackMetadata,
40
40
  } from '../../utilities/merge-metadata'
41
+ import { subagentHostMessageId } from '../../utilities/subagent-wire'
42
+ import { withDurabilityBatchHint } from '../../utilities/durability-batch'
41
43
  import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
42
44
  import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
43
45
  import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
@@ -45,6 +47,25 @@ import { normalizeToolResult } from '../../utilities/tool-result'
45
47
  import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
46
48
  import { LazyToolManager } from './tools/lazy-tool-manager'
47
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'
48
69
  import {
49
70
  MiddlewareAbortError,
50
71
  ToolCallManager,
@@ -101,6 +122,7 @@ import type {
101
122
  ChatStream,
102
123
  ConstrainedModelMessage,
103
124
  CustomEvent,
125
+ EmitCustomEventOptions,
104
126
  InferSchemaType,
105
127
  Interrupt,
106
128
  JSONSchema,
@@ -161,6 +183,28 @@ type RuntimeToolWithApproval = AnyRuntimeTool & {
161
183
  }
162
184
  const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
163
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
+
164
208
  interface StructuralInterruptFailure {
165
209
  error: Error
166
210
  errors: ReadonlyArray<InterruptSubmissionError>
@@ -393,8 +437,9 @@ type TextActivityOptionsWithContext<
393
437
  [],
394
438
  TContext = unknown,
395
439
  TMiddleware extends Array<unknown> | undefined = undefined,
440
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
396
441
  > = Omit<
397
- TextActivityOptions<TAdapter, TSchema, TStream, any>,
442
+ TextActivityOptions<TAdapter, TSchema, TStream, any, TAgents>,
398
443
  'tools' | 'middleware' | 'context' | 'interrupts'
399
444
  > & {
400
445
  tools?: TTools
@@ -420,6 +465,7 @@ export interface TextActivityOptions<
420
465
  TSchema extends SchemaInput | undefined,
421
466
  TStream extends boolean,
422
467
  TContext = unknown,
468
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
423
469
  > {
424
470
  /** The text adapter to use (created by a provider function like openaiText('gpt-5.5')) */
425
471
  adapter: TAdapter
@@ -497,6 +543,8 @@ export interface TextActivityOptions<
497
543
  runId?: TextOptions['runId']
498
544
  /** Parent run ID for AG-UI protocol nested run correlation. */
499
545
  parentRunId?: TextOptions['parentRunId']
546
+ /** Subagent run id when this chat runs as a child. See `defineAgent`. */
547
+ subagentRunId?: TextOptions['subagentRunId']
500
548
  /** Application state mirrored in a STATE_SNAPSHOT before an interrupt terminal. */
501
549
  state?: TextOptions['state']
502
550
  /**
@@ -504,6 +552,12 @@ export interface TextActivityOptions<
504
552
  * before accepting new input on a thread with pending interrupts.
505
553
  */
506
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>
507
561
  /**
508
562
  * Optional Standard Schema for structured output.
509
563
  * When provided, the activity will:
@@ -840,6 +894,7 @@ class TextEngine<
840
894
  private readonly threadId: string
841
895
  private readonly runIdOverride?: string
842
896
  private readonly parentRunIdOverride?: string
897
+ private readonly subagentRunIdOverride?: string
843
898
 
844
899
  // Middleware support
845
900
  private readonly middlewareRunner: MiddlewareRunner<
@@ -970,6 +1025,7 @@ class TextEngine<
970
1025
  this.createId('thread')
971
1026
  this.runIdOverride = config.params.runId
972
1027
  this.parentRunIdOverride = config.params.parentRunId
1028
+ this.subagentRunIdOverride = config.params.subagentRunId
973
1029
 
974
1030
  // Initialize middleware — devtools first. Spec stripping for the AG-UI
975
1031
  // wire happens in toServerSentEventsStream, so in-process chat()
@@ -988,6 +1044,7 @@ class TextEngine<
988
1044
  streamId: this.streamId,
989
1045
  runId: this.runIdOverride ?? this.requestId,
990
1046
  parentRunId: this.parentRunIdOverride,
1047
+ subagentRunId: this.subagentRunIdOverride,
991
1048
  threadId: this.threadId,
992
1049
  // Legacy alias kept on the ctx so middleware that reads
993
1050
  // `ctx.conversationId` keeps working. Always equals `threadId`.
@@ -1000,9 +1057,9 @@ class TextEngine<
1000
1057
  this.abortReason = reason
1001
1058
  this.middlewareAbortController?.abort(reason)
1002
1059
  },
1003
- emitCustomEvent: (name, value) => {
1060
+ emitCustomEvent: (name, value, options) => {
1004
1061
  this.middlewareCustomQueue.push(
1005
- this.createCustomEventChunk(name, value),
1062
+ this.createCustomEventChunk(name, value, options),
1006
1063
  )
1007
1064
  const waiters = this.middlewareCustomWaiters
1008
1065
  this.middlewareCustomWaiters = []
@@ -1514,6 +1571,8 @@ class TextEngine<
1514
1571
  )
1515
1572
  }
1516
1573
 
1574
+ assertNoFileSources(this.adapter.name, this.messages)
1575
+
1517
1576
  for await (const raw of this.adapter.chatStream({
1518
1577
  model: this.params.model,
1519
1578
  messages: this.providerMessages,
@@ -2019,7 +2078,8 @@ class TextEngine<
2019
2078
  this.resolveExecutableTools(executablePendingCalls),
2020
2079
  approvals,
2021
2080
  clientToolResults,
2022
- (eventName, data) => this.createCustomEventChunk(eventName, data),
2081
+ (eventName, data, options) =>
2082
+ this.createCustomEventChunk(eventName, data, options),
2023
2083
  {
2024
2084
  onBeforeToolCall: async (toolCall, tool, args) => {
2025
2085
  this.logger.tools(`phase=before name=${toolCall.function.name}`, {
@@ -2080,7 +2140,8 @@ class TextEngine<
2080
2140
 
2081
2141
  if (
2082
2142
  executionResult.needsApproval.length > 0 ||
2083
- executionResult.needsClientExecution.length > 0
2143
+ executionResult.needsClientExecution.length > 0 ||
2144
+ executionResult.subagentInterrupts.length > 0
2084
2145
  ) {
2085
2146
  this.discardDeferredToolCallRunFinishedChunks()
2086
2147
 
@@ -2097,6 +2158,8 @@ class TextEngine<
2097
2158
  finishEvent,
2098
2159
  executionResult.needsApproval,
2099
2160
  executionResult.needsClientExecution,
2161
+ [],
2162
+ executionResult.subagentInterrupts,
2100
2163
  )
2101
2164
  this.setToolPhase(emitted ? 'wait' : 'stop')
2102
2165
  return emitted ? 'wait' : 'stop'
@@ -2199,7 +2262,8 @@ class TextEngine<
2199
2262
  this.resolveExecutableTools(executableToolCalls),
2200
2263
  approvals,
2201
2264
  clientToolResults,
2202
- (eventName, data) => this.createCustomEventChunk(eventName, data),
2265
+ (eventName, data, options) =>
2266
+ this.createCustomEventChunk(eventName, data, options),
2203
2267
  {
2204
2268
  onBeforeToolCall: async (toolCall, tool, args) => {
2205
2269
  this.logger.tools(`phase=before name=${toolCall.function.name}`, {
@@ -2288,7 +2352,8 @@ class TextEngine<
2288
2352
 
2289
2353
  if (
2290
2354
  executionResult.needsApproval.length > 0 ||
2291
- executionResult.needsClientExecution.length > 0
2355
+ executionResult.needsClientExecution.length > 0 ||
2356
+ executionResult.subagentInterrupts.length > 0
2292
2357
  ) {
2293
2358
  if (allResults.length > 0) {
2294
2359
  for (const chunk of afterToolBoundaryChunks) {
@@ -2300,6 +2365,8 @@ class TextEngine<
2300
2365
  finishEvent,
2301
2366
  executionResult.needsApproval,
2302
2367
  executionResult.needsClientExecution,
2368
+ [],
2369
+ executionResult.subagentInterrupts,
2303
2370
  )
2304
2371
  this.setToolPhase(emitted ? 'wait' : 'stop')
2305
2372
  return
@@ -2618,6 +2685,7 @@ class TextEngine<
2618
2685
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2619
2686
  > = [],
2620
2687
  genericInterruptIds: ReadonlyArray<string> = [],
2688
+ childInterrupts: ReadonlyArray<Interrupt> = [],
2621
2689
  ): Array<Interrupt> {
2622
2690
  const interrupts: Array<Interrupt> = []
2623
2691
 
@@ -2735,6 +2803,9 @@ class TextEngine<
2735
2803
  })
2736
2804
  }
2737
2805
 
2806
+ // A subagent tool call raised these. They keep their `subagentRunId`.
2807
+ interrupts.push(...childInterrupts)
2808
+
2738
2809
  const ids = new Set<string>()
2739
2810
  for (const interrupt of interrupts) {
2740
2811
  if (ids.has(interrupt.id)) {
@@ -2756,6 +2827,7 @@ class TextEngine<
2756
2827
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2757
2828
  > = [],
2758
2829
  genericInterruptIds?: ReadonlyArray<string>,
2830
+ childInterrupts?: ReadonlyArray<Interrupt>,
2759
2831
  ): StreamChunk {
2760
2832
  return {
2761
2833
  ...finishEvent,
@@ -2767,6 +2839,7 @@ class TextEngine<
2767
2839
  clientRequests,
2768
2840
  genericRequests,
2769
2841
  genericInterruptIds,
2842
+ childInterrupts,
2770
2843
  ),
2771
2844
  },
2772
2845
  }
@@ -2916,11 +2989,13 @@ class TextEngine<
2916
2989
  genericRequests: ReadonlyArray<
2917
2990
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2918
2991
  > = [],
2992
+ childInterrupts: ReadonlyArray<Interrupt> = [],
2919
2993
  ): AsyncGenerator<StreamChunk, boolean, void> {
2920
2994
  yield* this.emitSyntheticRunStarted(finishEvent)
2921
2995
  const genericInterruptIds = genericRequests.map(() =>
2922
2996
  this.genericInterruptId(),
2923
2997
  )
2998
+ // Binding completion also binds child interrupts to this run.
2924
2999
  const terminal = this.completeEphemeralInterruptBindings(
2925
3000
  this.buildInterruptFinishedChunk(
2926
3001
  finishEvent,
@@ -2928,6 +3003,7 @@ class TextEngine<
2928
3003
  clientRequests,
2929
3004
  genericRequests,
2930
3005
  genericInterruptIds,
3006
+ childInterrupts,
2931
3007
  ),
2932
3008
  )
2933
3009
  let terminalOutputs: Array<StreamChunk>
@@ -3513,6 +3589,8 @@ class TextEngine<
3513
3589
  // Apply merged config back to engine state
3514
3590
  this.applyMiddlewareConfig(postOnConfig)
3515
3591
 
3592
+ assertNoFileSources(this.adapter.name, this.messages)
3593
+
3516
3594
  // Build the StructuredOutputOptions the adapter expects.
3517
3595
  // `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
3518
3596
  // class generics — no cast needed.
@@ -4009,9 +4087,25 @@ class TextEngine<
4009
4087
  }
4010
4088
  }
4011
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
+
4012
4102
  private async applyEphemeralInterruptResume(
4013
- config: ChatMiddlewareConfig,
4103
+ middlewareConfig: ChatMiddlewareConfig,
4014
4104
  ): Promise<void> {
4105
+ const config = {
4106
+ ...middlewareConfig,
4107
+ resume: this.ownResume(middlewareConfig.resume),
4108
+ }
4015
4109
  if ((config.resume?.length ?? 0) === 0) {
4016
4110
  return
4017
4111
  }
@@ -4244,7 +4338,7 @@ class TextEngine<
4244
4338
  }> = []
4245
4339
  const ids = new Set<string>()
4246
4340
  const batchIndexes = new Set<number>()
4247
- for (const resumeItem of this.params.resume ?? []) {
4341
+ for (const resumeItem of this.ownResume(this.params.resume) ?? []) {
4248
4342
  const parsed = readGenericInterruptContinuation(resumeItem.metadata)
4249
4343
  if (parsed.status === 'absent') continue
4250
4344
  if (parsed.status === 'invalid') {
@@ -4554,11 +4648,12 @@ class TextEngine<
4554
4648
  */
4555
4649
  private async *drainToolCallGenerator(
4556
4650
  generator: AsyncGenerator<
4557
- CustomEvent,
4651
+ CustomEvent | StreamChunk,
4558
4652
  {
4559
4653
  results: Array<ToolResult>
4560
4654
  needsApproval: Array<ApprovalRequest>
4561
4655
  needsClientExecution: Array<ClientToolRequest>
4656
+ subagentInterrupts: Array<Interrupt>
4562
4657
  },
4563
4658
  void
4564
4659
  >,
@@ -4568,6 +4663,7 @@ class TextEngine<
4568
4663
  results: Array<ToolResult>
4569
4664
  needsApproval: Array<ApprovalRequest>
4570
4665
  needsClientExecution: Array<ClientToolRequest>
4666
+ subagentInterrupts: Array<Interrupt>
4571
4667
  },
4572
4668
  void
4573
4669
  > {
@@ -4583,13 +4679,15 @@ class TextEngine<
4583
4679
  private createCustomEventChunk(
4584
4680
  eventName: string,
4585
4681
  value: Record<string, unknown>,
4682
+ options?: EmitCustomEventOptions,
4586
4683
  ): CustomEvent {
4587
- return {
4684
+ const chunk: CustomEvent = {
4588
4685
  type: EventType.CUSTOM,
4589
4686
  timestamp: Date.now(),
4590
4687
  name: eventName,
4591
4688
  value,
4592
4689
  }
4690
+ return options?.batch ? withDurabilityBatchHint(chunk) : chunk
4593
4691
  }
4594
4692
 
4595
4693
  private createId(prefix: string): string {
@@ -4677,6 +4775,8 @@ export function chat<
4677
4775
  > = [],
4678
4776
  TContext = unknown,
4679
4777
  const TMiddleware extends Array<unknown> | undefined = undefined,
4778
+ const TAgents extends ReadonlyArray<DefinedAgent> =
4779
+ ReadonlyArray<DefinedAgent>,
4680
4780
  >(
4681
4781
  options: TextActivityOptionsWithContext<
4682
4782
  TAdapter,
@@ -4685,7 +4785,8 @@ export function chat<
4685
4785
  TTools,
4686
4786
  TInterrupts,
4687
4787
  TContext,
4688
- TMiddleware
4788
+ TMiddleware,
4789
+ TAgents
4689
4790
  >,
4690
4791
  ): TextActivityResult<TSchema, TStream, TTools> {
4691
4792
  validateInterruptDefinitions(options.interrupts)
@@ -4699,6 +4800,12 @@ export function chat<
4699
4800
 
4700
4801
  const { outputSchema, stream } = options
4701
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
+
4702
4809
  if (outputSchema && stream === true) {
4703
4810
  return runStreamingStructuredOutput(
4704
4811
  toRuntimeTextActivityOptions(options, {
@@ -4738,7 +4845,10 @@ type RuntimeTextActivityOptions<
4738
4845
  TAdapter extends AnyTextAdapter,
4739
4846
  TSchema extends SchemaInput | undefined,
4740
4847
  TStream extends boolean,
4741
- > = Omit<TextActivityOptions<TAdapter, TSchema, TStream, any>, 'middleware'> & {
4848
+ > = Omit<
4849
+ TextActivityOptions<TAdapter, TSchema, TStream, any, any>,
4850
+ 'middleware'
4851
+ > & {
4742
4852
  middleware?: Array<AnyChatMiddleware>
4743
4853
  }
4744
4854
 
@@ -4767,6 +4877,7 @@ function toRuntimeTextActivityOptions<
4767
4877
  TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
4768
4878
  TContext,
4769
4879
  TMiddleware extends Array<unknown> | undefined,
4880
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
4770
4881
  >(
4771
4882
  options: TextActivityOptionsWithContext<
4772
4883
  TAdapter,
@@ -4775,7 +4886,8 @@ function toRuntimeTextActivityOptions<
4775
4886
  TTools,
4776
4887
  TInterrupts,
4777
4888
  TContext,
4778
- TMiddleware
4889
+ TMiddleware,
4890
+ TAgents
4779
4891
  >,
4780
4892
  overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
4781
4893
  ): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
@@ -4862,11 +4974,19 @@ function runStreamingText(
4862
4974
  return stream
4863
4975
  }
4864
4976
 
4865
- async function* streamTextChunks(
4977
+ async function* runChatEngine(
4866
4978
  options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
4867
4979
  engineRef: DeliveryEngineRef,
4868
4980
  ): AsyncIterable<StreamChunk> {
4869
- 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
4870
4990
  const model = adapter.model
4871
4991
  const logger = resolveDebugOption(debug)
4872
4992
 
@@ -4900,6 +5020,410 @@ async function* streamTextChunks(
4900
5020
  }
4901
5021
  }
4902
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
+
4903
5427
  /**
4904
5428
  * Run non-streaming text - collects all content and returns as a string.
4905
5429
  * Runs the full agentic loop (if tools are provided) but returns collected text.