@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
@@ -38,14 +38,39 @@ 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'
44
45
  import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
45
- import { normalizeToolResult } from '../../utilities/tool-result'
46
+ import {
47
+ normalizeToolResult,
48
+ parseToolOutput,
49
+ toolResultErrorText,
50
+ } from '../../utilities/tool-result'
46
51
  import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
52
+ import { assertMessagesFileSourceSupport } from '../../utilities/content-source'
47
53
  import { LazyToolManager } from './tools/lazy-tool-manager'
48
54
  import { assertUniqueToolNames } from './tools/unique-tool-names'
55
+ import type { DefinedAgent } from './agents/define-agent'
56
+ import {
57
+ collectNamedText,
58
+ createSubagentSink,
59
+ createSyntheticSubagentTools,
60
+ normalizeRouterPick,
61
+ rebindInterrupts,
62
+ spawnNamedAgents,
63
+ subagentCallMessages,
64
+ withChildUsage,
65
+ } from './agents/spawn'
66
+ import type {
67
+ SpawnEntry,
68
+ SubagentSink,
69
+ SubagentStep,
70
+ SubagentStepsPlan,
71
+ SubagentsBag,
72
+ } from './agents/spawn'
73
+ import { SUBAGENT_PLAN_KEY, readSubagentTurn } from './agents/turn'
49
74
  import {
50
75
  MiddlewareAbortError,
51
76
  ToolCallManager,
@@ -150,6 +175,12 @@ import type {
150
175
  } from './runtime-context-types'
151
176
  import type { ChatMCPOptions } from './mcp/types'
152
177
 
178
+ /** One entry of the per-iteration arrival order (see `turnParts`). */
179
+ type TurnPart =
180
+ | { type: 'thinking'; index: number }
181
+ | { type: 'text'; content: string }
182
+ | { type: 'call'; id: string; providerExecuted: boolean }
183
+
153
184
  // ===========================
154
185
  // Activity Kind
155
186
  // ===========================
@@ -163,6 +194,9 @@ type RuntimeToolWithApproval = AnyRuntimeTool & {
163
194
  }
164
195
  const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
165
196
 
197
+ /** Resume entries a subagent tool call owns. The parent run skips them. */
198
+ const CHILD_RESUME_IDS = Symbol('tanstack.ai.childResumeIds')
199
+
166
200
  interface StructuralInterruptFailure {
167
201
  error: Error
168
202
  errors: ReadonlyArray<InterruptSubmissionError>
@@ -395,8 +429,9 @@ type TextActivityOptionsWithContext<
395
429
  [],
396
430
  TContext = unknown,
397
431
  TMiddleware extends Array<unknown> | undefined = undefined,
432
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
398
433
  > = Omit<
399
- TextActivityOptions<TAdapter, TSchema, TStream, any>,
434
+ TextActivityOptions<TAdapter, TSchema, TStream, any, TAgents>,
400
435
  'tools' | 'middleware' | 'context' | 'interrupts'
401
436
  > & {
402
437
  tools?: TTools
@@ -422,6 +457,7 @@ export interface TextActivityOptions<
422
457
  TSchema extends SchemaInput | undefined,
423
458
  TStream extends boolean,
424
459
  TContext = unknown,
460
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
425
461
  > {
426
462
  /** The text adapter to use (created by a provider function like openaiText('gpt-5.5')) */
427
463
  adapter: TAdapter
@@ -499,6 +535,8 @@ export interface TextActivityOptions<
499
535
  runId?: TextOptions['runId']
500
536
  /** Parent run ID for AG-UI protocol nested run correlation. */
501
537
  parentRunId?: TextOptions['parentRunId']
538
+ /** Subagent run id when this chat runs as a child. See `defineAgent`. */
539
+ subagentRunId?: TextOptions['subagentRunId']
502
540
  /** Application state mirrored in a STATE_SNAPSHOT before an interrupt terminal. */
503
541
  state?: TextOptions['state']
504
542
  /**
@@ -506,6 +544,12 @@ export interface TextActivityOptions<
506
544
  * before accepting new input on a thread with pending interrupts.
507
545
  */
508
546
  resume?: TextOptions['resume']
547
+ /**
548
+ * Named child agents. When `router` is set, the library spawns that agent
549
+ * directly. When `router` is omitted, the main model gets one synthetic
550
+ * server tool per agent and picks the child.
551
+ */
552
+ subagents?: SubagentsBag<TAgents>
509
553
  /**
510
554
  * Optional Standard Schema for structured output.
511
555
  * When provided, the activity will:
@@ -809,6 +853,15 @@ class TextEngine<
809
853
  private accumulatedContent = ''
810
854
  private accumulatedThinking: Array<{ content: string; signature?: string }> =
811
855
  []
856
+ /**
857
+ * Arrival order of this iteration's thinking steps, text and tool calls.
858
+ * A ModelMessage keeps `thinking` apart from `content`/`toolCalls`, so a
859
+ * provider turn that thinks between provider-executed tools would otherwise
860
+ * be recorded as "all thinking, then text, then tools" and the provider
861
+ * rejects the replay (signed thinking must keep its position). `null` once
862
+ * the order could no longer be tracked (callers fall back to one message).
863
+ */
864
+ private turnParts: Array<TurnPart> | null = []
812
865
  private currentThinkingContent = ''
813
866
  private currentThinkingSignature = ''
814
867
  private eventOptions?: Record<string, unknown> | undefined
@@ -842,6 +895,7 @@ class TextEngine<
842
895
  private readonly threadId: string
843
896
  private readonly runIdOverride?: string
844
897
  private readonly parentRunIdOverride?: string
898
+ private readonly subagentRunIdOverride?: string
845
899
 
846
900
  // Middleware support
847
901
  private readonly middlewareRunner: MiddlewareRunner<
@@ -972,6 +1026,7 @@ class TextEngine<
972
1026
  this.createId('thread')
973
1027
  this.runIdOverride = config.params.runId
974
1028
  this.parentRunIdOverride = config.params.parentRunId
1029
+ this.subagentRunIdOverride = config.params.subagentRunId
975
1030
 
976
1031
  // Initialize middleware — devtools first. Spec stripping for the AG-UI
977
1032
  // wire happens in toServerSentEventsStream, so in-process chat()
@@ -990,6 +1045,7 @@ class TextEngine<
990
1045
  streamId: this.streamId,
991
1046
  runId: this.runIdOverride ?? this.requestId,
992
1047
  parentRunId: this.parentRunIdOverride,
1048
+ subagentRunId: this.subagentRunIdOverride,
993
1049
  threadId: this.threadId,
994
1050
  // Legacy alias kept on the ctx so middleware that reads
995
1051
  // `ctx.conversationId` keeps working. Always equals `threadId`.
@@ -1449,6 +1505,7 @@ class TextEngine<
1449
1505
  this.streamIdentityCaptured = false
1450
1506
  this.accumulatedContent = ''
1451
1507
  this.accumulatedThinking = []
1508
+ this.turnParts = []
1452
1509
  this.currentThinkingContent = ''
1453
1510
  this.currentThinkingSignature = ''
1454
1511
 
@@ -1516,6 +1573,13 @@ class TextEngine<
1516
1573
  )
1517
1574
  }
1518
1575
 
1576
+ // Fail closed on `{ type: 'file' }` sources for adapters that haven't
1577
+ // declared support — an adapter written before the file arm existed would
1578
+ // otherwise fall through to its URL/data branch and silently mis-map the
1579
+ // reference. Checked per model call so tool results added mid-loop are
1580
+ // covered too.
1581
+ assertMessagesFileSourceSupport(this.adapter, this.messages)
1582
+
1519
1583
  for await (const raw of this.adapter.chatStream({
1520
1584
  model: this.params.model,
1521
1585
  messages: this.providerMessages,
@@ -1748,12 +1812,27 @@ class TextEngine<
1748
1812
  // Adapters still emit leftover cumulative `content` on RAW yields.
1749
1813
  // Alignment suppresses already-delivered deltas, so this snapshot is
1750
1814
  // what a takeover saves as the full assistant text.
1815
+ const before = this.accumulatedContent
1751
1816
  if (typeof extra.content === 'string' && extra.content !== '') {
1752
1817
  this.accumulatedContent = extra.content
1753
1818
  } else {
1754
1819
  this.accumulatedContent += chunk.delta
1755
1820
  }
1756
1821
  this.middlewareCtx.accumulatedContent = this.accumulatedContent
1822
+ if (!this.turnParts) return
1823
+ if (!this.accumulatedContent.startsWith(before)) {
1824
+ // A cumulative `content` snapshot rewrote earlier text; order unknown.
1825
+ this.turnParts = null
1826
+ return
1827
+ }
1828
+ const delta = this.accumulatedContent.slice(before.length)
1829
+ if (delta === '') return
1830
+ const last = this.turnParts[this.turnParts.length - 1]
1831
+ if (last && last.type === 'text') {
1832
+ last.content += delta
1833
+ } else {
1834
+ this.turnParts.push({ type: 'text', content: delta })
1835
+ }
1757
1836
  }
1758
1837
 
1759
1838
  private captureStreamMessageIdentity(messageId: string): void {
@@ -1778,6 +1857,20 @@ class TextEngine<
1778
1857
  this.captureStreamMessageIdentity(chunk.parentMessageId)
1779
1858
  }
1780
1859
  this.toolCallManager.addToolCallStartEvent(chunk)
1860
+ if (
1861
+ this.turnParts &&
1862
+ !this.turnParts.some(
1863
+ (part) => part.type === 'call' && part.id === chunk.toolCallId,
1864
+ )
1865
+ ) {
1866
+ this.turnParts.push({
1867
+ type: 'call',
1868
+ id: chunk.toolCallId,
1869
+ providerExecuted: isProviderExecutedToolCall({
1870
+ metadata: chunk.metadata,
1871
+ }),
1872
+ })
1873
+ }
1781
1874
  const metadata = chunk.metadata
1782
1875
  const thoughtSignature =
1783
1876
  metadata != null &&
@@ -1884,17 +1977,48 @@ class TextEngine<
1884
1977
  signature: this.currentThinkingSignature,
1885
1978
  }),
1886
1979
  })
1980
+ if (this.turnParts) {
1981
+ const placeholder = [...this.turnParts]
1982
+ .reverse()
1983
+ .find(
1984
+ (part): part is Extract<TurnPart, { type: 'thinking' }> =>
1985
+ part.type === 'thinking' && part.index === -1,
1986
+ )
1987
+ const index = this.accumulatedThinking.length - 1
1988
+ if (placeholder) {
1989
+ placeholder.index = index
1990
+ } else {
1991
+ this.turnParts.push({ type: 'thinking', index })
1992
+ }
1993
+ }
1887
1994
  this.currentThinkingContent = ''
1888
1995
  this.currentThinkingSignature = ''
1889
1996
  }
1890
1997
  }
1891
1998
 
1999
+ /**
2000
+ * Record where the current thinking step sits among this turn's parts. A
2001
+ * step is finalized only when the next step starts (or the turn ends), by
2002
+ * which time later tool calls have already arrived, so the position has to
2003
+ * be noted when the step's first content or signature shows up.
2004
+ */
2005
+ private noteThinkingStepPosition(): void {
2006
+ if (
2007
+ this.turnParts &&
2008
+ this.currentThinkingContent === '' &&
2009
+ this.currentThinkingSignature === ''
2010
+ ) {
2011
+ this.turnParts.push({ type: 'thinking', index: -1 })
2012
+ }
2013
+ }
2014
+
1892
2015
  private handleStepStartedEvent(): void {
1893
2016
  this.finalizeCurrentThinkingStep()
1894
2017
  }
1895
2018
 
1896
2019
  private handleStepFinishedEvent(chunk: AdapterYieldChunk): void {
1897
2020
  if (typeof chunk.signature === 'string' && chunk.signature !== '') {
2021
+ this.noteThinkingStepPosition()
1898
2022
  this.currentThinkingSignature = chunk.signature
1899
2023
  }
1900
2024
  }
@@ -1902,6 +2026,7 @@ class TextEngine<
1902
2026
  private handleReasoningMessageContentEvent(
1903
2027
  chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
1904
2028
  ): void {
2029
+ this.noteThinkingStepPosition()
1905
2030
  this.currentThinkingContent += chunk.delta
1906
2031
  }
1907
2032
 
@@ -1922,6 +2047,7 @@ class TextEngine<
1922
2047
  }
1923
2048
  return
1924
2049
  }
2050
+ this.noteThinkingStepPosition()
1925
2051
  this.currentThinkingSignature = chunk.encryptedValue
1926
2052
  }
1927
2053
 
@@ -2083,7 +2209,8 @@ class TextEngine<
2083
2209
 
2084
2210
  if (
2085
2211
  executionResult.needsApproval.length > 0 ||
2086
- executionResult.needsClientExecution.length > 0
2212
+ executionResult.needsClientExecution.length > 0 ||
2213
+ executionResult.subagentInterrupts.length > 0
2087
2214
  ) {
2088
2215
  this.discardDeferredToolCallRunFinishedChunks()
2089
2216
 
@@ -2100,6 +2227,8 @@ class TextEngine<
2100
2227
  finishEvent,
2101
2228
  executionResult.needsApproval,
2102
2229
  executionResult.needsClientExecution,
2230
+ [],
2231
+ executionResult.subagentInterrupts,
2103
2232
  )
2104
2233
  this.setToolPhase(emitted ? 'wait' : 'stop')
2105
2234
  return emitted ? 'wait' : 'stop'
@@ -2292,7 +2421,8 @@ class TextEngine<
2292
2421
 
2293
2422
  if (
2294
2423
  executionResult.needsApproval.length > 0 ||
2295
- executionResult.needsClientExecution.length > 0
2424
+ executionResult.needsClientExecution.length > 0 ||
2425
+ executionResult.subagentInterrupts.length > 0
2296
2426
  ) {
2297
2427
  if (allResults.length > 0) {
2298
2428
  for (const chunk of afterToolBoundaryChunks) {
@@ -2304,6 +2434,8 @@ class TextEngine<
2304
2434
  finishEvent,
2305
2435
  executionResult.needsApproval,
2306
2436
  executionResult.needsClientExecution,
2437
+ [],
2438
+ executionResult.subagentInterrupts,
2307
2439
  )
2308
2440
  this.setToolPhase(emitted ? 'wait' : 'stop')
2309
2441
  return
@@ -2401,21 +2533,110 @@ class TextEngine<
2401
2533
  )
2402
2534
  }
2403
2535
 
2536
+ /**
2537
+ * Split this iteration into assistant ModelMessages that keep the provider's
2538
+ * block order: a new segment starts at every thinking step that follows a
2539
+ * provider-executed tool call (the rule buildAssistantMessages applies to
2540
+ * UIMessages). Segments after the first get `${id}-segment-${n}` ids.
2541
+ * Returns null when no split is needed or the order could not be tracked,
2542
+ * so callers fall back to the single-message shape.
2543
+ */
2544
+ private buildOrderedAssistantSegments(
2545
+ toolCalls: ReadonlyArray<ToolCall>,
2546
+ id: string | undefined,
2547
+ createdAt: Date | undefined,
2548
+ ): Array<ModelMessage> | null {
2549
+ const parts = this.turnParts
2550
+ if (!parts) return null
2551
+ const providerCallIds = new Set(
2552
+ parts.flatMap((part) =>
2553
+ part.type === 'call' && part.providerExecuted ? [part.id] : [],
2554
+ ),
2555
+ )
2556
+ type Segment = {
2557
+ thinking: Array<{ content: string; signature?: string }>
2558
+ text: string
2559
+ callIds: Array<string>
2560
+ }
2561
+ let current: Segment = { thinking: [], text: '', callIds: [] }
2562
+ const segments: Array<Segment> = [current]
2563
+ let split = false
2564
+ for (const part of parts) {
2565
+ if (part.type === 'thinking') {
2566
+ const thinking = this.accumulatedThinking[part.index]
2567
+ if (!thinking) return null
2568
+ if (current.callIds.some((callId) => providerCallIds.has(callId))) {
2569
+ current = { thinking: [thinking], text: '', callIds: [] }
2570
+ segments.push(current)
2571
+ split = true
2572
+ } else {
2573
+ current.thinking.push(thinking)
2574
+ }
2575
+ } else if (part.type === 'text') {
2576
+ current.text += part.content
2577
+ } else {
2578
+ current.callIds.push(part.id)
2579
+ }
2580
+ }
2581
+ if (!split) return null
2582
+ if (
2583
+ segments.map((segment) => segment.text).join('') !==
2584
+ this.accumulatedContent
2585
+ ) {
2586
+ return null
2587
+ }
2588
+ if (
2589
+ segments.reduce((n, segment) => n + segment.thinking.length, 0) !==
2590
+ this.accumulatedThinking.length
2591
+ ) {
2592
+ return null
2593
+ }
2594
+ const placed = new Set(segments.flatMap((segment) => segment.callIds))
2595
+ for (const toolCall of toolCalls) {
2596
+ if (!placed.has(toolCall.id)) current.callIds.push(toolCall.id)
2597
+ }
2598
+ return segments.map((segment, index) => {
2599
+ const segmentCalls = toolCalls.filter((toolCall) =>
2600
+ segment.callIds.includes(toolCall.id),
2601
+ )
2602
+ return {
2603
+ role: 'assistant',
2604
+ content: segment.text || null,
2605
+ ...(segmentCalls.length > 0 && { toolCalls: segmentCalls }),
2606
+ id:
2607
+ id === undefined
2608
+ ? undefined
2609
+ : index === 0
2610
+ ? id
2611
+ : `${id}-segment-${index}`,
2612
+ createdAt,
2613
+ ...(segment.thinking.length > 0 && { thinking: segment.thinking }),
2614
+ }
2615
+ })
2616
+ }
2617
+
2404
2618
  private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
2405
2619
  this.finalizeCurrentThinkingStep()
2406
2620
 
2621
+ const segments = this.buildOrderedAssistantSegments(
2622
+ toolCalls,
2623
+ this.currentMessageId ?? undefined,
2624
+ this.currentMessageCreatedAt ?? undefined,
2625
+ )
2407
2626
  this.messages = [
2408
2627
  ...this.messages,
2409
- {
2410
- role: 'assistant',
2411
- content: this.accumulatedContent || null,
2412
- toolCalls,
2413
- id: this.currentMessageId ?? undefined,
2414
- createdAt: this.currentMessageCreatedAt ?? undefined,
2415
- ...(this.accumulatedThinking.length > 0 && {
2416
- thinking: this.accumulatedThinking,
2417
- }),
2418
- },
2628
+ ...(segments ?? [
2629
+ {
2630
+ role: 'assistant' as const,
2631
+ content: this.accumulatedContent || null,
2632
+ toolCalls,
2633
+ id: this.currentMessageId ?? undefined,
2634
+ createdAt: this.currentMessageCreatedAt ?? undefined,
2635
+ ...(this.accumulatedThinking.length > 0 && {
2636
+ thinking: this.accumulatedThinking,
2637
+ }),
2638
+ },
2639
+ ]),
2419
2640
  ]
2420
2641
  this.middlewareCtx.messages = this.messages
2421
2642
  }
@@ -2494,13 +2715,23 @@ class TextEngine<
2494
2715
  !currentTurnAlreadyRecorded &&
2495
2716
  (this.accumulatedContent !== '' || thinking)
2496
2717
  ) {
2497
- messages.push({
2498
- role: 'assistant',
2499
- content: this.accumulatedContent || null,
2500
- id: this.currentMessageId ?? this.createId('msg'),
2501
- createdAt: this.currentMessageCreatedAt ?? new Date(),
2502
- ...(thinking ? { thinking } : {}),
2503
- })
2718
+ const id = this.currentMessageId ?? this.createId('msg')
2719
+ const createdAt = this.currentMessageCreatedAt ?? new Date()
2720
+ messages.push(
2721
+ ...(this.buildOrderedAssistantSegments(
2722
+ this.toolCallManager.getToolCalls(),
2723
+ id,
2724
+ createdAt,
2725
+ ) ?? [
2726
+ {
2727
+ role: 'assistant',
2728
+ content: this.accumulatedContent || null,
2729
+ id,
2730
+ createdAt,
2731
+ ...(thinking ? { thinking } : {}),
2732
+ },
2733
+ ]),
2734
+ )
2504
2735
  }
2505
2736
  if (structuredOutput) {
2506
2737
  messages.push({
@@ -2622,6 +2853,7 @@ class TextEngine<
2622
2853
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2623
2854
  > = [],
2624
2855
  genericInterruptIds: ReadonlyArray<string> = [],
2856
+ childInterrupts: ReadonlyArray<Interrupt> = [],
2625
2857
  ): Array<Interrupt> {
2626
2858
  const interrupts: Array<Interrupt> = []
2627
2859
 
@@ -2739,6 +2971,9 @@ class TextEngine<
2739
2971
  })
2740
2972
  }
2741
2973
 
2974
+ // A subagent tool call raised these. They keep their `subagentRunId`.
2975
+ interrupts.push(...childInterrupts)
2976
+
2742
2977
  const ids = new Set<string>()
2743
2978
  for (const interrupt of interrupts) {
2744
2979
  if (ids.has(interrupt.id)) {
@@ -2760,6 +2995,7 @@ class TextEngine<
2760
2995
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2761
2996
  > = [],
2762
2997
  genericInterruptIds?: ReadonlyArray<string>,
2998
+ childInterrupts?: ReadonlyArray<Interrupt>,
2763
2999
  ): StreamChunk {
2764
3000
  return {
2765
3001
  ...finishEvent,
@@ -2771,6 +3007,7 @@ class TextEngine<
2771
3007
  clientRequests,
2772
3008
  genericRequests,
2773
3009
  genericInterruptIds,
3010
+ childInterrupts,
2774
3011
  ),
2775
3012
  },
2776
3013
  }
@@ -2920,11 +3157,13 @@ class TextEngine<
2920
3157
  genericRequests: ReadonlyArray<
2921
3158
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2922
3159
  > = [],
3160
+ childInterrupts: ReadonlyArray<Interrupt> = [],
2923
3161
  ): AsyncGenerator<StreamChunk, boolean, void> {
2924
3162
  yield* this.emitSyntheticRunStarted(finishEvent)
2925
3163
  const genericInterruptIds = genericRequests.map(() =>
2926
3164
  this.genericInterruptId(),
2927
3165
  )
3166
+ // Binding completion also binds child interrupts to this run.
2928
3167
  const terminal = this.completeEphemeralInterruptBindings(
2929
3168
  this.buildInterruptFinishedChunk(
2930
3169
  finishEvent,
@@ -2932,6 +3171,7 @@ class TextEngine<
2932
3171
  clientRequests,
2933
3172
  genericRequests,
2934
3173
  genericInterruptIds,
3174
+ childInterrupts,
2935
3175
  ),
2936
3176
  )
2937
3177
  let terminalOutputs: Array<StreamChunk>
@@ -3025,6 +3265,8 @@ class TextEngine<
3025
3265
  const approvalRequests: Array<ApprovalRequest> = []
3026
3266
  const clientRequests: Array<ClientToolRequest> = []
3027
3267
  for (const toolCall of toolCalls) {
3268
+ // Provider-executed calls are complete; never surface them as client work.
3269
+ if (isProviderExecutedToolCall(toolCall)) continue
3028
3270
  const tool = this.resolveExecutableTools([toolCall]).find(
3029
3271
  (candidate) => candidate.name === toolCall.function.name,
3030
3272
  ) as RuntimeToolWithApproval | undefined
@@ -3182,6 +3424,9 @@ class TextEngine<
3182
3424
  role: 'tool',
3183
3425
  content,
3184
3426
  toolCallId: result.toolCallId,
3427
+ ...(result.state === 'output-error' && {
3428
+ error: toolResultErrorText(parseToolOutput(wireContent)),
3429
+ }),
3185
3430
  }
3186
3431
 
3187
3432
  if (placeholderIdx >= 0) {
@@ -3517,6 +3762,12 @@ class TextEngine<
3517
3762
  // Apply merged config back to engine state
3518
3763
  this.applyMiddlewareConfig(postOnConfig)
3519
3764
 
3765
+ // Schema-only structured output with no tools skips the agent loop, so
3766
+ // `streamModelResponse` never runs this check. Middleware can also
3767
+ // replace `this.messages` above. Fail closed here before the
3768
+ // structured-output adapter call.
3769
+ assertMessagesFileSourceSupport(this.adapter, this.messages)
3770
+
3520
3771
  // Build the StructuredOutputOptions the adapter expects.
3521
3772
  // `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
3522
3773
  // class generics — no cast needed.
@@ -3858,7 +4109,16 @@ class TextEngine<
3858
4109
  const yieldChunks = this.finalStructuredOutput.yieldChunks
3859
4110
  const source = this.finalStructuredOutput.source ?? 'text'
3860
4111
 
3861
- if (source === 'event') {
4112
+ // A final turn cut off at the output cap holds truncated JSON, or none
4113
+ // for a reasoning model that spent the budget. Report the token limit
4114
+ // instead of a parse or missing-result error (#1426).
4115
+ if (this.lastFinishReason === 'length') {
4116
+ this.finalizationError = {
4117
+ message:
4118
+ 'The response was cut off because the maximum token limit was reached (finish_reason=length); raise the output token limit.',
4119
+ code: 'max_tokens',
4120
+ }
4121
+ } else if (source === 'event') {
3862
4122
  if (!this.structuredOutputResult) {
3863
4123
  this.finalizationError = {
3864
4124
  message: 'missing structured result',
@@ -4013,9 +4273,25 @@ class TextEngine<
4013
4273
  }
4014
4274
  }
4015
4275
 
4276
+ /** Resume entries this run answers itself. A subagent tool owns the rest. */
4277
+ private ownResume(
4278
+ resume: ChatMiddlewareConfig['resume'],
4279
+ ): ChatMiddlewareConfig['resume'] {
4280
+ const childIds = (
4281
+ this.params as { [CHILD_RESUME_IDS]?: ReadonlySet<string> }
4282
+ )[CHILD_RESUME_IDS]
4283
+ return childIds
4284
+ ? resume?.filter((entry) => !childIds.has(entry.interruptId))
4285
+ : resume
4286
+ }
4287
+
4016
4288
  private async applyEphemeralInterruptResume(
4017
- config: ChatMiddlewareConfig,
4289
+ middlewareConfig: ChatMiddlewareConfig,
4018
4290
  ): Promise<void> {
4291
+ const config = {
4292
+ ...middlewareConfig,
4293
+ resume: this.ownResume(middlewareConfig.resume),
4294
+ }
4019
4295
  if ((config.resume?.length ?? 0) === 0) {
4020
4296
  return
4021
4297
  }
@@ -4248,7 +4524,7 @@ class TextEngine<
4248
4524
  }> = []
4249
4525
  const ids = new Set<string>()
4250
4526
  const batchIndexes = new Set<number>()
4251
- for (const resumeItem of this.params.resume ?? []) {
4527
+ for (const resumeItem of this.ownResume(this.params.resume) ?? []) {
4252
4528
  const parsed = readGenericInterruptContinuation(resumeItem.metadata)
4253
4529
  if (parsed.status === 'absent') continue
4254
4530
  if (parsed.status === 'invalid') {
@@ -4558,11 +4834,12 @@ class TextEngine<
4558
4834
  */
4559
4835
  private async *drainToolCallGenerator(
4560
4836
  generator: AsyncGenerator<
4561
- CustomEvent,
4837
+ CustomEvent | StreamChunk,
4562
4838
  {
4563
4839
  results: Array<ToolResult>
4564
4840
  needsApproval: Array<ApprovalRequest>
4565
4841
  needsClientExecution: Array<ClientToolRequest>
4842
+ subagentInterrupts: Array<Interrupt>
4566
4843
  },
4567
4844
  void
4568
4845
  >,
@@ -4572,6 +4849,7 @@ class TextEngine<
4572
4849
  results: Array<ToolResult>
4573
4850
  needsApproval: Array<ApprovalRequest>
4574
4851
  needsClientExecution: Array<ClientToolRequest>
4852
+ subagentInterrupts: Array<Interrupt>
4575
4853
  },
4576
4854
  void
4577
4855
  > {
@@ -4683,6 +4961,8 @@ export function chat<
4683
4961
  > = [],
4684
4962
  TContext = unknown,
4685
4963
  const TMiddleware extends Array<unknown> | undefined = undefined,
4964
+ const TAgents extends ReadonlyArray<DefinedAgent> =
4965
+ ReadonlyArray<DefinedAgent>,
4686
4966
  >(
4687
4967
  options: TextActivityOptionsWithContext<
4688
4968
  TAdapter,
@@ -4691,7 +4971,8 @@ export function chat<
4691
4971
  TTools,
4692
4972
  TInterrupts,
4693
4973
  TContext,
4694
- TMiddleware
4974
+ TMiddleware,
4975
+ TAgents
4695
4976
  >,
4696
4977
  ): TextActivityResult<TSchema, TStream, TTools> {
4697
4978
  validateInterruptDefinitions(options.interrupts)
@@ -4705,6 +4986,12 @@ export function chat<
4705
4986
 
4706
4987
  const { outputSchema, stream } = options
4707
4988
 
4989
+ if (outputSchema && (options.subagents?.agents.length ?? 0) > 0) {
4990
+ throw new Error(
4991
+ 'chat() does not support subagents together with outputSchema. Put outputSchema on a child chat() instead.',
4992
+ )
4993
+ }
4994
+
4708
4995
  if (outputSchema && stream === true) {
4709
4996
  return runStreamingStructuredOutput(
4710
4997
  toRuntimeTextActivityOptions(options, {
@@ -4744,7 +5031,10 @@ type RuntimeTextActivityOptions<
4744
5031
  TAdapter extends AnyTextAdapter,
4745
5032
  TSchema extends SchemaInput | undefined,
4746
5033
  TStream extends boolean,
4747
- > = Omit<TextActivityOptions<TAdapter, TSchema, TStream, any>, 'middleware'> & {
5034
+ > = Omit<
5035
+ TextActivityOptions<TAdapter, TSchema, TStream, any, any>,
5036
+ 'middleware'
5037
+ > & {
4748
5038
  middleware?: Array<AnyChatMiddleware>
4749
5039
  }
4750
5040
 
@@ -4773,6 +5063,7 @@ function toRuntimeTextActivityOptions<
4773
5063
  TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
4774
5064
  TContext,
4775
5065
  TMiddleware extends Array<unknown> | undefined,
5066
+ TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
4776
5067
  >(
4777
5068
  options: TextActivityOptionsWithContext<
4778
5069
  TAdapter,
@@ -4781,7 +5072,8 @@ function toRuntimeTextActivityOptions<
4781
5072
  TTools,
4782
5073
  TInterrupts,
4783
5074
  TContext,
4784
- TMiddleware
5075
+ TMiddleware,
5076
+ TAgents
4785
5077
  >,
4786
5078
  overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
4787
5079
  ): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
@@ -4868,11 +5160,19 @@ function runStreamingText(
4868
5160
  return stream
4869
5161
  }
4870
5162
 
4871
- async function* streamTextChunks(
5163
+ async function* runChatEngine(
4872
5164
  options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
4873
5165
  engineRef: DeliveryEngineRef,
4874
5166
  ): AsyncIterable<StreamChunk> {
4875
- const { adapter, middleware, context, debug, mcp, ...textOptions } = options
5167
+ const {
5168
+ adapter,
5169
+ middleware,
5170
+ context,
5171
+ debug,
5172
+ mcp,
5173
+ subagents: _subagents,
5174
+ ...textOptions
5175
+ } = options
4876
5176
  const model = adapter.model
4877
5177
  const logger = resolveDebugOption(debug)
4878
5178
 
@@ -4906,6 +5206,410 @@ async function* streamTextChunks(
4906
5206
  }
4907
5207
  }
4908
5208
 
5209
+ async function* streamTextChunks(
5210
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
5211
+ engineRef: DeliveryEngineRef,
5212
+ ): AsyncIterable<StreamChunk> {
5213
+ const bag = options.subagents
5214
+ const agents = bag?.agents ?? []
5215
+ if (bag && agents.length > 0 && bag.router) {
5216
+ yield* runRoutedSubagents(options, engineRef)
5217
+ return
5218
+ }
5219
+ if (bag && agents.length > 0) {
5220
+ const threadId = options.threadId ?? `thread-${Date.now()}`
5221
+ const runId = options.runId ?? `run-${Date.now()}`
5222
+ const messages = options.messages ?? []
5223
+ // A resume can answer a child's interrupts. Those entries go to the child
5224
+ // tool call. The parent run validates only its own entries.
5225
+ const turn = readSubagentTurn(messages, options.resume)
5226
+ const sink = createSubagentSink()
5227
+ const calls = subagentCallMessages(
5228
+ new Set(bag.agents.map((agent: DefinedAgent) => agent.name)),
5229
+ )
5230
+ const synthetic = createSyntheticSubagentTools(bag, {
5231
+ messages: turn?.before ?? messages,
5232
+ messagesFor: calls.messagesFor,
5233
+ threadId,
5234
+ runId,
5235
+ ...(options.parentRunId !== undefined && {
5236
+ interruptedRunId: options.parentRunId,
5237
+ }),
5238
+ ...(options.abortController && {
5239
+ abortSignal: options.abortController.signal,
5240
+ }),
5241
+ ...(turn && { turn }),
5242
+ sink,
5243
+ })
5244
+ yield* streamWithChildUsage(
5245
+ runChatEngine(
5246
+ {
5247
+ ...options,
5248
+ threadId,
5249
+ runId,
5250
+ ...(turn && {
5251
+ [CHILD_RESUME_IDS]: new Set(
5252
+ turn.children.flatMap((child) =>
5253
+ child.resume.map((entry) => entry.interruptId),
5254
+ ),
5255
+ ),
5256
+ }),
5257
+ tools: [...(options.tools ?? []), ...synthetic],
5258
+ middleware: [...(options.middleware ?? []), calls.middleware],
5259
+ },
5260
+ engineRef,
5261
+ ),
5262
+ sink,
5263
+ )
5264
+ return
5265
+ }
5266
+ yield* runChatEngine(options, engineRef)
5267
+ }
5268
+
5269
+ /** Add child usage to the next RUN_FINISHED of the parent. */
5270
+ async function* streamWithChildUsage(
5271
+ stream: AsyncIterable<StreamChunk>,
5272
+ sink: SubagentSink,
5273
+ ): AsyncIterable<StreamChunk> {
5274
+ for await (const chunk of stream) {
5275
+ yield chunk.type === EventType.RUN_FINISHED
5276
+ ? withChildUsage(chunk, sink)
5277
+ : chunk
5278
+ }
5279
+ }
5280
+
5281
+ async function* runRoutedSubagents(
5282
+ options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
5283
+ engineRef: DeliveryEngineRef,
5284
+ ): AsyncIterable<StreamChunk> {
5285
+ const bag = options.subagents
5286
+ if (!bag?.router) {
5287
+ yield* runChatEngine(options, engineRef)
5288
+ return
5289
+ }
5290
+ const threadId = options.threadId ?? `thread-${Date.now()}`
5291
+ const runId = options.runId ?? `run-${Date.now()}`
5292
+ const messages = options.messages ?? []
5293
+ const turn = readSubagentTurn(messages, options.resume)
5294
+ if (!turn && (options.resume?.length ?? 0) > 0) {
5295
+ // No child owns these answers. Main raised them, after a main pick or a
5296
+ // handoff. Run main alone.
5297
+ yield* runChatEngine(
5298
+ { ...options, threadId, runId, subagents: undefined },
5299
+ engineRef,
5300
+ )
5301
+ return
5302
+ }
5303
+ const abortSignal = options.abortController?.signal
5304
+ const turnMessages = turn?.before ?? messages
5305
+ const subagentPersistence = readRoutedSubagentPersistence(options.middleware)
5306
+ await subagentPersistence?.start({
5307
+ threadId,
5308
+ runId,
5309
+ messages,
5310
+ ...(options.resume && { resume: options.resume }),
5311
+ })
5312
+ try {
5313
+ // A resume reuses the plan that the first run put on each child's
5314
+ // SUBAGENT_STARTED metadata. Children that finished keep their result.
5315
+ // Suspended children continue.
5316
+ const plan =
5317
+ savedPlan(turn?.plan, bag.agents, {
5318
+ threadId,
5319
+ interruptedRunId: options.parentRunId ?? runId,
5320
+ interruptIds: options.resume?.map((entry) => entry.interruptId) ?? [],
5321
+ }) ??
5322
+ normalizeRouterPick(
5323
+ await bag.router({
5324
+ messages: turnMessages,
5325
+ agents: bag.agents,
5326
+ ...(abortSignal && { abortSignal }),
5327
+ }),
5328
+ bag.agents,
5329
+ )
5330
+ // The run was stopped while the router decided. Start nothing. No
5331
+ // RUN_STARTED went out, so the stream ends without a terminal, but the
5332
+ // record start() opened still has to settle. Otherwise it stays `running`
5333
+ // and reconstructChat hands the client a run to tail that never emits.
5334
+ if (abortSignal?.aborted) {
5335
+ const error = new Error('Aborted')
5336
+ error.name = 'AbortError'
5337
+ await subagentPersistence?.abort({ threadId, runId, error })
5338
+ return
5339
+ }
5340
+ const onlyStep = plan.steps.length === 1 ? plan.steps[0] : undefined
5341
+ if (onlyStep?.names.length === 1 && onlyStep.names[0] === 'main') {
5342
+ // Earlier children own answers in this resume. Commit them first.
5343
+ if (turn) await subagentPersistence?.finish({ threadId, runId })
5344
+ yield* runChatEngine(
5345
+ {
5346
+ ...options,
5347
+ threadId,
5348
+ runId,
5349
+ subagents: undefined,
5350
+ ...(turn && { resume: turn.rest }),
5351
+ },
5352
+ engineRef,
5353
+ )
5354
+ return
5355
+ }
5356
+
5357
+ yield {
5358
+ type: EventType.RUN_STARTED,
5359
+ threadId,
5360
+ runId,
5361
+ timestamp: Date.now(),
5362
+ }
5363
+
5364
+ const earlier = [...(turn?.children ?? [])]
5365
+ const takeEarlier = (name: string) => {
5366
+ const index = earlier.findIndex((child) => child.name === name)
5367
+ return index === -1 ? undefined : earlier.splice(index, 1)[0]
5368
+ }
5369
+ const sink = createSubagentSink()
5370
+ const stepTexts: Array<string> = []
5371
+ let stepMessages = turnMessages
5372
+ let failure:
5373
+ | Extract<StreamChunk, { type: EventType.SUBAGENT_ERROR }>
5374
+ | undefined
5375
+ for (const step of plan.steps) {
5376
+ const entries: Array<SpawnEntry> = []
5377
+ const textByName = new Map<string, string>()
5378
+ for (const name of step.names) {
5379
+ const prior = takeEarlier(name)
5380
+ if (prior?.status === 'finished') {
5381
+ textByName.set(name, prior.text)
5382
+ continue
5383
+ }
5384
+ entries.push(
5385
+ prior?.status === 'suspended'
5386
+ ? {
5387
+ name,
5388
+ resume: {
5389
+ subagentRunId: prior.subagentRunId,
5390
+ messages: prior.messages,
5391
+ entries: prior.resume,
5392
+ text: prior.text,
5393
+ },
5394
+ }
5395
+ : { name },
5396
+ )
5397
+ }
5398
+ const stepChunks: Array<StreamChunk> = []
5399
+ if (entries.length > 0) {
5400
+ for await (const chunk of spawnNamedAgents(
5401
+ entries,
5402
+ { ...bag, order: step.order ?? bag.order },
5403
+ {
5404
+ messages: stepMessages,
5405
+ ...(abortSignal && { abortSignal }),
5406
+ threadId,
5407
+ parentRunId: runId,
5408
+ ...(options.parentRunId !== undefined && {
5409
+ interruptedRunId: options.parentRunId,
5410
+ }),
5411
+ },
5412
+ sink,
5413
+ )) {
5414
+ const tagged = withPlan(chunk, plan)
5415
+ stepChunks.push(tagged)
5416
+ await subagentPersistence?.chunk({ threadId, runId, chunk: tagged })
5417
+ yield tagged
5418
+ }
5419
+ }
5420
+ failure = stepChunks.find(
5421
+ (chunk) => chunk.type === EventType.SUBAGENT_ERROR,
5422
+ )
5423
+ if (failure) break
5424
+ if (sink.interrupts.length > 0) break
5425
+ for (const entry of entries) {
5426
+ const text = collectNamedText(stepChunks, [entry.name])
5427
+ textByName.set(
5428
+ entry.name,
5429
+ [entry.resume?.text, text].filter(Boolean).join('\n\n'),
5430
+ )
5431
+ }
5432
+ const text = step.names
5433
+ .map((name) => textByName.get(name)?.trim() ?? '')
5434
+ .filter((block) => block !== '')
5435
+ .join('\n\n')
5436
+ if (text) {
5437
+ stepTexts.push(text)
5438
+ stepMessages = [...stepMessages, { role: 'assistant', content: text }]
5439
+ }
5440
+ }
5441
+
5442
+ if (abortSignal?.aborted) {
5443
+ const error = new Error('Aborted')
5444
+ error.name = 'AbortError'
5445
+ await subagentPersistence?.abort({ threadId, runId, error })
5446
+ yield withChildUsage(
5447
+ {
5448
+ type: EventType.RUN_FINISHED,
5449
+ threadId,
5450
+ runId,
5451
+ outcome: { type: 'cancelled' },
5452
+ timestamp: Date.now(),
5453
+ },
5454
+ sink,
5455
+ )
5456
+ return
5457
+ }
5458
+
5459
+ if (failure) {
5460
+ // Carry the child's own cause, not a generic label, and keep the usage
5461
+ // the children already spent: the run failed, but the tokens were real.
5462
+ const message = failure.message || 'A subagent failed'
5463
+ await subagentPersistence?.abort({
5464
+ threadId,
5465
+ runId,
5466
+ error: new Error(message),
5467
+ })
5468
+ yield withChildUsage(
5469
+ {
5470
+ type: EventType.RUN_ERROR,
5471
+ threadId,
5472
+ runId,
5473
+ message,
5474
+ ...(failure.code !== undefined ? { code: failure.code } : {}),
5475
+ timestamp: Date.now(),
5476
+ },
5477
+ sink,
5478
+ )
5479
+ return
5480
+ }
5481
+
5482
+ if (sink.interrupts.length > 0) {
5483
+ const interrupts = rebindInterrupts(sink.interrupts, runId)
5484
+ await subagentPersistence?.suspend?.({ threadId, runId, interrupts })
5485
+ yield withChildUsage(
5486
+ {
5487
+ type: EventType.RUN_FINISHED,
5488
+ threadId,
5489
+ runId,
5490
+ outcome: { type: 'interrupt', interrupts },
5491
+ timestamp: Date.now(),
5492
+ },
5493
+ sink,
5494
+ )
5495
+ return
5496
+ }
5497
+
5498
+ const strategy = bag.strategy ?? 'exclusive'
5499
+ if (strategy === 'handoff') {
5500
+ const childText = stepTexts.join('\n\n')
5501
+ // The children are done. Commit their answers and settle their records
5502
+ // before main runs. Main's own persistence takes over from here.
5503
+ await subagentPersistence?.finish({ threadId, runId })
5504
+ yield* streamWithChildUsage(
5505
+ runChatEngine(
5506
+ {
5507
+ ...options,
5508
+ threadId,
5509
+ runId,
5510
+ subagents: undefined,
5511
+ ...(turn && { resume: turn.rest }),
5512
+ messages: [
5513
+ ...turnMessages,
5514
+ {
5515
+ // Same id as the recorder's parent row, so the stored thread
5516
+ // keeps one host message for the cards.
5517
+ id: subagentHostMessageId(runId),
5518
+ role: 'assistant',
5519
+ content: childText || 'Subagent finished.',
5520
+ metadata: { tanstack: { runId } },
5521
+ },
5522
+ ],
5523
+ },
5524
+ engineRef,
5525
+ ),
5526
+ sink,
5527
+ )
5528
+ return
5529
+ }
5530
+
5531
+ await subagentPersistence?.finish({ threadId, runId })
5532
+ yield withChildUsage(
5533
+ {
5534
+ type: EventType.RUN_FINISHED,
5535
+ threadId,
5536
+ runId,
5537
+ timestamp: Date.now(),
5538
+ },
5539
+ sink,
5540
+ )
5541
+ } catch (error) {
5542
+ await subagentPersistence?.abort({ threadId, runId, error })
5543
+ throw error
5544
+ }
5545
+ }
5546
+
5547
+ /**
5548
+ * The plan from a resumed turn, or undefined when it is absent. A plan that
5549
+ * names agents this chat does not have is stale: re-routing would strand the
5550
+ * suspended child that owns the answers, so fail the resume instead.
5551
+ */
5552
+ function savedPlan(
5553
+ plan: unknown,
5554
+ agents: ReadonlyArray<DefinedAgent>,
5555
+ run: {
5556
+ threadId: string
5557
+ interruptedRunId: string
5558
+ interruptIds: ReadonlyArray<string>
5559
+ },
5560
+ ): { steps: ReadonlyArray<SubagentStep> } | undefined {
5561
+ if (typeof plan !== 'object' || plan === null || !('steps' in plan)) {
5562
+ return undefined
5563
+ }
5564
+ try {
5565
+ // The plan comes back from the client. Check it like a router pick.
5566
+ return normalizeRouterPick(plan as SubagentStepsPlan, agents)
5567
+ } catch (error) {
5568
+ throw new InterruptResumeValidationError([
5569
+ {
5570
+ scope: 'batch',
5571
+ threadId: run.threadId,
5572
+ interruptedRunId: run.interruptedRunId,
5573
+ generation: 0,
5574
+ interruptIds: run.interruptIds,
5575
+ code: 'stale',
5576
+ message: `The saved subagent plan does not match the agents of this chat. ${
5577
+ error instanceof Error ? error.message : String(error)
5578
+ }`,
5579
+ source: 'server',
5580
+ retryable: false,
5581
+ },
5582
+ ])
5583
+ }
5584
+ }
5585
+
5586
+ /** Put the router plan on a direct child's SUBAGENT_STARTED metadata. */
5587
+ function withPlan(
5588
+ chunk: StreamChunk,
5589
+ plan: { steps: ReadonlyArray<SubagentStep> },
5590
+ ): StreamChunk {
5591
+ if (
5592
+ chunk.type !== EventType.SUBAGENT_STARTED ||
5593
+ chunk.parentSubagentRunId !== undefined
5594
+ ) {
5595
+ return chunk
5596
+ }
5597
+ return withTanstackMetadata(chunk, { [SUBAGENT_PLAN_KEY]: plan })
5598
+ }
5599
+
5600
+ function readRoutedSubagentPersistence(
5601
+ middleware:
5602
+ | ReadonlyArray<{
5603
+ routedSubagentPersistence?: ChatMiddleware['routedSubagentPersistence']
5604
+ }>
5605
+ | undefined,
5606
+ ) {
5607
+ for (const item of middleware ?? []) {
5608
+ if (item.routedSubagentPersistence) return item.routedSubagentPersistence
5609
+ }
5610
+ return undefined
5611
+ }
5612
+
4909
5613
  /**
4910
5614
  * Run non-streaming text - collects all content and returns as a string.
4911
5615
  * Runs the full agentic loop (if tools are provided) but returns collected text.