@tanstack/ai 0.59.0 → 0.63.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 (123) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  3. package/dist/esm/activities/chat/adapter.js +1 -0
  4. package/dist/esm/activities/chat/adapter.js.map +1 -1
  5. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +8 -5
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  10. package/dist/esm/activities/chat/index.js +289 -78
  11. package/dist/esm/activities/chat/index.js.map +1 -1
  12. package/dist/esm/activities/chat/messages.js +35 -19
  13. package/dist/esm/activities/chat/messages.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  15. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  17. package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
  18. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  19. package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
  20. package/dist/esm/activities/chat/stream/processor.js +51 -28
  21. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  23. package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
  24. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  25. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  26. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  28. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  29. package/dist/esm/activities/embed/adapter.js +1 -0
  30. package/dist/esm/activities/embed/adapter.js.map +1 -1
  31. package/dist/esm/activities/embed/index.js +2 -0
  32. package/dist/esm/activities/embed/index.js.map +1 -1
  33. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  34. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  35. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  36. package/dist/esm/activities/evaluate/index.js +3 -1
  37. package/dist/esm/activities/evaluate/index.js.map +1 -1
  38. package/dist/esm/activities/files/adapter.d.ts +97 -0
  39. package/dist/esm/activities/files/adapter.js +45 -0
  40. package/dist/esm/activities/files/adapter.js.map +1 -0
  41. package/dist/esm/activities/files/index.d.ts +66 -0
  42. package/dist/esm/activities/files/index.js +78 -0
  43. package/dist/esm/activities/files/index.js.map +1 -0
  44. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  45. package/dist/esm/activities/generateImage/adapter.js +1 -0
  46. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  47. package/dist/esm/activities/generateImage/index.js +2 -0
  48. package/dist/esm/activities/generateImage/index.js.map +1 -1
  49. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  50. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  51. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  52. package/dist/esm/activities/generateVideo/index.js +3 -0
  53. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  54. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  55. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  56. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  57. package/dist/esm/activities/generateWorld/index.js +5 -4
  58. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  59. package/dist/esm/activities/index.d.ts +6 -3
  60. package/dist/esm/activities/index.js +13 -11
  61. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  62. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  63. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  64. package/dist/esm/client.d.ts +3 -1
  65. package/dist/esm/client.js +2 -1
  66. package/dist/esm/client.js.map +1 -1
  67. package/dist/esm/index.d.ts +4 -3
  68. package/dist/esm/index.js +5 -3
  69. package/dist/esm/interrupt-resume.js +29 -4
  70. package/dist/esm/interrupt-resume.js.map +1 -1
  71. package/dist/esm/middlewares/otel.d.ts +5 -2
  72. package/dist/esm/middlewares/otel.js +114 -0
  73. package/dist/esm/middlewares/otel.js.map +1 -1
  74. package/dist/esm/types.d.ts +114 -14
  75. package/dist/esm/utilities/ag-ui-wire.js +39 -16
  76. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  77. package/dist/esm/utilities/content-source.d.ts +60 -0
  78. package/dist/esm/utilities/content-source.js +85 -0
  79. package/dist/esm/utilities/content-source.js.map +1 -0
  80. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  81. package/dist/esm/utilities/provider-executed.js +10 -1
  82. package/dist/esm/utilities/provider-executed.js.map +1 -1
  83. package/dist/esm/utilities/tool-result.d.ts +14 -3
  84. package/dist/esm/utilities/tool-result.js +26 -3
  85. package/dist/esm/utilities/tool-result.js.map +1 -1
  86. package/package.json +4 -4
  87. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  88. package/skills/ai-core/chat-experience/SKILL.md +134 -0
  89. package/skills/ai-core/media-generation/SKILL.md +8 -0
  90. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  91. package/src/activities/chat/adapter.ts +10 -0
  92. package/src/activities/chat/agents/define-agent.ts +20 -3
  93. package/src/activities/chat/agents/spawn.ts +20 -12
  94. package/src/activities/chat/index.ts +458 -99
  95. package/src/activities/chat/messages.ts +52 -6
  96. package/src/activities/chat/middleware/types.ts +1 -0
  97. package/src/activities/chat/stream/message-updaters.ts +27 -2
  98. package/src/activities/chat/stream/processor.ts +81 -49
  99. package/src/activities/chat/tools/tool-calls.ts +104 -9
  100. package/src/activities/chat/tools/tool-definition.ts +8 -0
  101. package/src/activities/embed/adapter.ts +7 -0
  102. package/src/activities/embed/index.ts +5 -0
  103. package/src/activities/evaluate/adapter.ts +4 -0
  104. package/src/activities/evaluate/index.ts +6 -0
  105. package/src/activities/files/adapter.ts +120 -0
  106. package/src/activities/files/index.ts +113 -0
  107. package/src/activities/generateImage/adapter.ts +8 -0
  108. package/src/activities/generateImage/index.ts +4 -0
  109. package/src/activities/generateVideo/adapter.ts +8 -0
  110. package/src/activities/generateVideo/index.ts +7 -0
  111. package/src/activities/generateWorld/adapter.ts +4 -2
  112. package/src/activities/generateWorld/index.ts +7 -6
  113. package/src/activities/index.ts +25 -1
  114. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  115. package/src/client.ts +8 -0
  116. package/src/index.ts +17 -0
  117. package/src/interrupt-resume.ts +55 -4
  118. package/src/middlewares/otel.ts +161 -3
  119. package/src/types.ts +114 -14
  120. package/src/utilities/ag-ui-wire.ts +72 -17
  121. package/src/utilities/content-source.ts +138 -0
  122. package/src/utilities/provider-executed.ts +13 -0
  123. package/src/utilities/tool-result.ts +45 -3
@@ -43,8 +43,13 @@ import { withDurabilityBatchHint } from '../../utilities/durability-batch'
43
43
  import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
44
44
  import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
45
45
  import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
46
- import { normalizeToolResult } from '../../utilities/tool-result'
46
+ import {
47
+ normalizeToolResult,
48
+ parseToolOutput,
49
+ toolResultErrorText,
50
+ } from '../../utilities/tool-result'
47
51
  import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
52
+ import { assertMessagesFileSourceSupport } from '../../utilities/content-source'
48
53
  import { LazyToolManager } from './tools/lazy-tool-manager'
49
54
  import { assertUniqueToolNames } from './tools/unique-tool-names'
50
55
  import type { DefinedAgent } from './agents/define-agent'
@@ -108,6 +113,7 @@ import type {
108
113
  import type {
109
114
  ApprovalRequest,
110
115
  ClientToolRequest,
116
+ McpInputRequest,
111
117
  ToolResult,
112
118
  } from './tools/tool-calls'
113
119
  import type { ApprovalSchemaConfig } from './tools/tool-definition'
@@ -143,6 +149,7 @@ import type {
143
149
  ToolCallEndEvent,
144
150
  ToolCallResultEvent,
145
151
  ToolCallStartEvent,
152
+ ToolInputResponse,
146
153
  UIMessage,
147
154
  } from '../../types'
148
155
  import type {
@@ -170,6 +177,12 @@ import type {
170
177
  } from './runtime-context-types'
171
178
  import type { ChatMCPOptions } from './mcp/types'
172
179
 
180
+ /** One entry of the per-iteration arrival order (see `turnParts`). */
181
+ type TurnPart =
182
+ | { type: 'thinking'; index: number }
183
+ | { type: 'text'; content: string }
184
+ | { type: 'call'; id: string; providerExecuted: boolean }
185
+
173
186
  // ===========================
174
187
  // Activity Kind
175
188
  // ===========================
@@ -186,25 +199,6 @@ const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
186
199
  /** Resume entries a subagent tool call owns. The parent run skips them. */
187
200
  const CHILD_RESUME_IDS = Symbol('tanstack.ai.childResumeIds')
188
201
 
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
-
208
202
  interface StructuralInterruptFailure {
209
203
  error: Error
210
204
  errors: ReadonlyArray<InterruptSubmissionError>
@@ -861,6 +855,15 @@ class TextEngine<
861
855
  private accumulatedContent = ''
862
856
  private accumulatedThinking: Array<{ content: string; signature?: string }> =
863
857
  []
858
+ /**
859
+ * Arrival order of this iteration's thinking steps, text and tool calls.
860
+ * A ModelMessage keeps `thinking` apart from `content`/`toolCalls`, so a
861
+ * provider turn that thinks between provider-executed tools would otherwise
862
+ * be recorded as "all thinking, then text, then tools" and the provider
863
+ * rejects the replay (signed thinking must keep its position). `null` once
864
+ * the order could no longer be tracked (callers fall back to one message).
865
+ */
866
+ private turnParts: Array<TurnPart> | null = []
864
867
  private currentThinkingContent = ''
865
868
  private currentThinkingSignature = ''
866
869
  private eventOptions?: Record<string, unknown> | undefined
@@ -879,8 +882,10 @@ class TextEngine<
879
882
  private readonly initialClientToolResults: Map<string, any>
880
883
  private readonly resumeApprovals = new Map<string, ToolApprovalResolution>()
881
884
  private readonly resumeClientToolResults = new Map<string, any>()
885
+ private readonly resumeClientToolErrors = new Map<string, string>()
882
886
  private readonly resumeDeniedToolResults = new Map<string, unknown>()
883
887
  private readonly resumeCancelledToolCallIds = new Set<string>()
888
+ private readonly resumeInputResponses = new Map<string, ToolInputResponse>()
884
889
  private readonly resumeGenericInterrupts = new Map<
885
890
  string,
886
891
  ChatResumeGenericResolution
@@ -951,6 +956,8 @@ class TextEngine<
951
956
  message: string
952
957
  code?: string
953
958
  cause?: unknown
959
+ /** Full model text, when a structured-output parse failed (#1485). */
960
+ rawText?: string
954
961
  } | null = null
955
962
  private combinedCompleteEmitted = false
956
963
  private readonly finalStructuredOutput?: {
@@ -1188,10 +1195,54 @@ class TextEngine<
1188
1195
  message: string
1189
1196
  code?: string
1190
1197
  cause?: unknown
1198
+ rawText?: string
1191
1199
  } | null {
1192
1200
  return this.finalizationError
1193
1201
  }
1194
1202
 
1203
+ private async runTerminalHook(): Promise<void> {
1204
+ if (this.terminalHookCalled || this.isCancelled()) return
1205
+
1206
+ this.terminalHookCalled = true
1207
+
1208
+ if (this.finalizationError) {
1209
+ const errForHook = new Error(
1210
+ this.finalizationError.message,
1211
+ this.finalizationError.cause !== undefined
1212
+ ? { cause: this.finalizationError.cause }
1213
+ : undefined,
1214
+ )
1215
+ if (this.finalizationError.code !== undefined) {
1216
+ Object.defineProperty(errForHook, 'code', {
1217
+ value: this.finalizationError.code,
1218
+ enumerable: true,
1219
+ })
1220
+ }
1221
+ if (this.finalizationError.rawText !== undefined) {
1222
+ Object.defineProperty(errForHook, 'rawText', {
1223
+ value: this.finalizationError.rawText,
1224
+ enumerable: true,
1225
+ })
1226
+ }
1227
+ await this.middlewareRunner.runOnError(this.middlewareCtx, {
1228
+ error: errForHook,
1229
+ duration: Date.now() - this.streamStartTime,
1230
+ })
1231
+ return
1232
+ }
1233
+
1234
+ this.addTerminalAssistantMessages()
1235
+ await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
1236
+ finishReason: this.lastFinishReason,
1237
+ duration: Date.now() - this.streamStartTime,
1238
+ content: this.accumulatedContent,
1239
+ usage: rebuildTokenUsage(
1240
+ this.finishedEvent?.usage,
1241
+ tanstackMetadata(this.finishedEvent ?? undefined)?.usage,
1242
+ ),
1243
+ })
1244
+ }
1245
+
1195
1246
  async *run(): AsyncGenerator<StreamChunk> {
1196
1247
  this.beforeRun()
1197
1248
  this.logger.agentLoop('run started', {
@@ -1237,7 +1288,8 @@ class TextEngine<
1237
1288
  }
1238
1289
 
1239
1290
  const pendingPhase = yield* this.checkForPendingToolCalls()
1240
- if (pendingPhase === 'wait') {
1291
+ if (pendingPhase === 'stop' || pendingPhase === 'wait') {
1292
+ await this.runTerminalHook()
1241
1293
  return
1242
1294
  }
1243
1295
 
@@ -1346,46 +1398,11 @@ class TextEngine<
1346
1398
  }
1347
1399
  }
1348
1400
 
1349
- // Call terminal hook (skip when waiting for client — stream is paused, not finished).
1350
- // Priority: finalizationError → onError; otherwise normal onFinish.
1351
- // Skip on cancellation — the finally block routes aborts to onAbort.
1352
- if (
1353
- !this.terminalHookCalled &&
1354
- this.toolPhase !== 'wait' &&
1355
- !this.isCancelled()
1356
- ) {
1357
- if (this.finalizationError) {
1358
- this.terminalHookCalled = true
1359
- const errForHook = new Error(
1360
- this.finalizationError.message,
1361
- this.finalizationError.cause !== undefined
1362
- ? { cause: this.finalizationError.cause }
1363
- : undefined,
1364
- )
1365
- if (this.finalizationError.code !== undefined) {
1366
- Object.defineProperty(errForHook, 'code', {
1367
- value: this.finalizationError.code,
1368
- enumerable: true,
1369
- })
1370
- }
1371
- await this.middlewareRunner.runOnError(this.middlewareCtx, {
1372
- error: errForHook,
1373
- duration: Date.now() - this.streamStartTime,
1374
- })
1375
- } else {
1376
- this.addTerminalAssistantMessages()
1377
- this.terminalHookCalled = true
1378
- await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
1379
- finishReason: this.lastFinishReason,
1380
- duration: Date.now() - this.streamStartTime,
1381
- content: this.accumulatedContent,
1382
- usage: rebuildTokenUsage(
1383
- this.finishedEvent?.usage,
1384
- tanstackMetadata(this.finishedEvent ?? undefined)?.usage,
1385
- ),
1386
- })
1387
- }
1388
- }
1401
+ // A client-tool or approval wait ends this server-side chat invocation.
1402
+ // Structured-output finalization stays paused until the caller submits
1403
+ // the result in a new invocation, but middleware must still observe one
1404
+ // terminal hook for the current invocation.
1405
+ await this.runTerminalHook()
1389
1406
  } catch (error: unknown) {
1390
1407
  if (
1391
1408
  error instanceof Error &&
@@ -1504,6 +1521,7 @@ class TextEngine<
1504
1521
  this.streamIdentityCaptured = false
1505
1522
  this.accumulatedContent = ''
1506
1523
  this.accumulatedThinking = []
1524
+ this.turnParts = []
1507
1525
  this.currentThinkingContent = ''
1508
1526
  this.currentThinkingSignature = ''
1509
1527
 
@@ -1571,7 +1589,12 @@ class TextEngine<
1571
1589
  )
1572
1590
  }
1573
1591
 
1574
- assertNoFileSources(this.adapter.name, this.messages)
1592
+ // Fail closed on `{ type: 'file' }` sources for adapters that haven't
1593
+ // declared support — an adapter written before the file arm existed would
1594
+ // otherwise fall through to its URL/data branch and silently mis-map the
1595
+ // reference. Checked per model call so tool results added mid-loop are
1596
+ // covered too.
1597
+ assertMessagesFileSourceSupport(this.adapter, this.messages)
1575
1598
 
1576
1599
  for await (const raw of this.adapter.chatStream({
1577
1600
  model: this.params.model,
@@ -1805,12 +1828,27 @@ class TextEngine<
1805
1828
  // Adapters still emit leftover cumulative `content` on RAW yields.
1806
1829
  // Alignment suppresses already-delivered deltas, so this snapshot is
1807
1830
  // what a takeover saves as the full assistant text.
1831
+ const before = this.accumulatedContent
1808
1832
  if (typeof extra.content === 'string' && extra.content !== '') {
1809
1833
  this.accumulatedContent = extra.content
1810
1834
  } else {
1811
1835
  this.accumulatedContent += chunk.delta
1812
1836
  }
1813
1837
  this.middlewareCtx.accumulatedContent = this.accumulatedContent
1838
+ if (!this.turnParts) return
1839
+ if (!this.accumulatedContent.startsWith(before)) {
1840
+ // A cumulative `content` snapshot rewrote earlier text; order unknown.
1841
+ this.turnParts = null
1842
+ return
1843
+ }
1844
+ const delta = this.accumulatedContent.slice(before.length)
1845
+ if (delta === '') return
1846
+ const last = this.turnParts[this.turnParts.length - 1]
1847
+ if (last && last.type === 'text') {
1848
+ last.content += delta
1849
+ } else {
1850
+ this.turnParts.push({ type: 'text', content: delta })
1851
+ }
1814
1852
  }
1815
1853
 
1816
1854
  private captureStreamMessageIdentity(messageId: string): void {
@@ -1835,6 +1873,20 @@ class TextEngine<
1835
1873
  this.captureStreamMessageIdentity(chunk.parentMessageId)
1836
1874
  }
1837
1875
  this.toolCallManager.addToolCallStartEvent(chunk)
1876
+ if (
1877
+ this.turnParts &&
1878
+ !this.turnParts.some(
1879
+ (part) => part.type === 'call' && part.id === chunk.toolCallId,
1880
+ )
1881
+ ) {
1882
+ this.turnParts.push({
1883
+ type: 'call',
1884
+ id: chunk.toolCallId,
1885
+ providerExecuted: isProviderExecutedToolCall({
1886
+ metadata: chunk.metadata,
1887
+ }),
1888
+ })
1889
+ }
1838
1890
  const metadata = chunk.metadata
1839
1891
  const thoughtSignature =
1840
1892
  metadata != null &&
@@ -1941,17 +1993,48 @@ class TextEngine<
1941
1993
  signature: this.currentThinkingSignature,
1942
1994
  }),
1943
1995
  })
1996
+ if (this.turnParts) {
1997
+ const placeholder = [...this.turnParts]
1998
+ .reverse()
1999
+ .find(
2000
+ (part): part is Extract<TurnPart, { type: 'thinking' }> =>
2001
+ part.type === 'thinking' && part.index === -1,
2002
+ )
2003
+ const index = this.accumulatedThinking.length - 1
2004
+ if (placeholder) {
2005
+ placeholder.index = index
2006
+ } else {
2007
+ this.turnParts.push({ type: 'thinking', index })
2008
+ }
2009
+ }
1944
2010
  this.currentThinkingContent = ''
1945
2011
  this.currentThinkingSignature = ''
1946
2012
  }
1947
2013
  }
1948
2014
 
2015
+ /**
2016
+ * Record where the current thinking step sits among this turn's parts. A
2017
+ * step is finalized only when the next step starts (or the turn ends), by
2018
+ * which time later tool calls have already arrived, so the position has to
2019
+ * be noted when the step's first content or signature shows up.
2020
+ */
2021
+ private noteThinkingStepPosition(): void {
2022
+ if (
2023
+ this.turnParts &&
2024
+ this.currentThinkingContent === '' &&
2025
+ this.currentThinkingSignature === ''
2026
+ ) {
2027
+ this.turnParts.push({ type: 'thinking', index: -1 })
2028
+ }
2029
+ }
2030
+
1949
2031
  private handleStepStartedEvent(): void {
1950
2032
  this.finalizeCurrentThinkingStep()
1951
2033
  }
1952
2034
 
1953
2035
  private handleStepFinishedEvent(chunk: AdapterYieldChunk): void {
1954
2036
  if (typeof chunk.signature === 'string' && chunk.signature !== '') {
2037
+ this.noteThinkingStepPosition()
1955
2038
  this.currentThinkingSignature = chunk.signature
1956
2039
  }
1957
2040
  }
@@ -1959,6 +2042,7 @@ class TextEngine<
1959
2042
  private handleReasoningMessageContentEvent(
1960
2043
  chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
1961
2044
  ): void {
2045
+ this.noteThinkingStepPosition()
1962
2046
  this.currentThinkingContent += chunk.delta
1963
2047
  }
1964
2048
 
@@ -1979,6 +2063,7 @@ class TextEngine<
1979
2063
  }
1980
2064
  return
1981
2065
  }
2066
+ this.noteThinkingStepPosition()
1982
2067
  this.currentThinkingSignature = chunk.encryptedValue
1983
2068
  }
1984
2069
 
@@ -2112,8 +2197,10 @@ class TextEngine<
2112
2197
  this.middlewareCtx.context,
2113
2198
  this.toolAbortSignal,
2114
2199
  {
2200
+ clientToolErrors: this.resumeClientToolErrors,
2115
2201
  deniedToolResults: this.resumeDeniedToolResults,
2116
2202
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
2203
+ inputResponses: this.resumeInputResponses,
2117
2204
  },
2118
2205
  )
2119
2206
 
@@ -2138,11 +2225,12 @@ class TextEngine<
2138
2225
  }),
2139
2226
  )
2140
2227
 
2141
- if (
2228
+ const pauseForDecision =
2142
2229
  executionResult.needsApproval.length > 0 ||
2143
2230
  executionResult.needsClientExecution.length > 0 ||
2231
+ executionResult.inputRequired.length > 0 ||
2144
2232
  executionResult.subagentInterrupts.length > 0
2145
- ) {
2233
+ if (pauseForDecision) {
2146
2234
  this.discardDeferredToolCallRunFinishedChunks()
2147
2235
 
2148
2236
  if (allResults.length > 0) {
@@ -2159,6 +2247,7 @@ class TextEngine<
2159
2247
  executionResult.needsApproval,
2160
2248
  executionResult.needsClientExecution,
2161
2249
  [],
2250
+ executionResult.inputRequired,
2162
2251
  executionResult.subagentInterrupts,
2163
2252
  )
2164
2253
  this.setToolPhase(emitted ? 'wait' : 'stop')
@@ -2296,8 +2385,10 @@ class TextEngine<
2296
2385
  this.middlewareCtx.context,
2297
2386
  this.toolAbortSignal,
2298
2387
  {
2388
+ clientToolErrors: this.resumeClientToolErrors,
2299
2389
  deniedToolResults: this.resumeDeniedToolResults,
2300
2390
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
2391
+ inputResponses: this.resumeInputResponses,
2301
2392
  },
2302
2393
  )
2303
2394
 
@@ -2345,16 +2436,18 @@ class TextEngine<
2345
2436
  finishEvent,
2346
2437
  toolCalls,
2347
2438
  afterToolRequests,
2439
+ executionResult.inputRequired,
2348
2440
  )
2349
2441
  this.setToolPhase('wait')
2350
2442
  return
2351
2443
  }
2352
2444
 
2353
- if (
2445
+ const pauseForDecision =
2354
2446
  executionResult.needsApproval.length > 0 ||
2355
2447
  executionResult.needsClientExecution.length > 0 ||
2448
+ executionResult.inputRequired.length > 0 ||
2356
2449
  executionResult.subagentInterrupts.length > 0
2357
- ) {
2450
+ if (pauseForDecision) {
2358
2451
  if (allResults.length > 0) {
2359
2452
  for (const chunk of afterToolBoundaryChunks) {
2360
2453
  yield* this.pipeThroughMiddleware(chunk)
@@ -2366,6 +2459,7 @@ class TextEngine<
2366
2459
  executionResult.needsApproval,
2367
2460
  executionResult.needsClientExecution,
2368
2461
  [],
2462
+ executionResult.inputRequired,
2369
2463
  executionResult.subagentInterrupts,
2370
2464
  )
2371
2465
  this.setToolPhase(emitted ? 'wait' : 'stop')
@@ -2464,21 +2558,110 @@ class TextEngine<
2464
2558
  )
2465
2559
  }
2466
2560
 
2561
+ /**
2562
+ * Split this iteration into assistant ModelMessages that keep the provider's
2563
+ * block order: a new segment starts at every thinking step that follows a
2564
+ * provider-executed tool call (the rule buildAssistantMessages applies to
2565
+ * UIMessages). Segments after the first get `${id}-segment-${n}` ids.
2566
+ * Returns null when no split is needed or the order could not be tracked,
2567
+ * so callers fall back to the single-message shape.
2568
+ */
2569
+ private buildOrderedAssistantSegments(
2570
+ toolCalls: ReadonlyArray<ToolCall>,
2571
+ id: string | undefined,
2572
+ createdAt: Date | undefined,
2573
+ ): Array<ModelMessage> | null {
2574
+ const parts = this.turnParts
2575
+ if (!parts) return null
2576
+ const providerCallIds = new Set(
2577
+ parts.flatMap((part) =>
2578
+ part.type === 'call' && part.providerExecuted ? [part.id] : [],
2579
+ ),
2580
+ )
2581
+ type Segment = {
2582
+ thinking: Array<{ content: string; signature?: string }>
2583
+ text: string
2584
+ callIds: Array<string>
2585
+ }
2586
+ let current: Segment = { thinking: [], text: '', callIds: [] }
2587
+ const segments: Array<Segment> = [current]
2588
+ let split = false
2589
+ for (const part of parts) {
2590
+ if (part.type === 'thinking') {
2591
+ const thinking = this.accumulatedThinking[part.index]
2592
+ if (!thinking) return null
2593
+ if (current.callIds.some((callId) => providerCallIds.has(callId))) {
2594
+ current = { thinking: [thinking], text: '', callIds: [] }
2595
+ segments.push(current)
2596
+ split = true
2597
+ } else {
2598
+ current.thinking.push(thinking)
2599
+ }
2600
+ } else if (part.type === 'text') {
2601
+ current.text += part.content
2602
+ } else {
2603
+ current.callIds.push(part.id)
2604
+ }
2605
+ }
2606
+ if (!split) return null
2607
+ if (
2608
+ segments.map((segment) => segment.text).join('') !==
2609
+ this.accumulatedContent
2610
+ ) {
2611
+ return null
2612
+ }
2613
+ if (
2614
+ segments.reduce((n, segment) => n + segment.thinking.length, 0) !==
2615
+ this.accumulatedThinking.length
2616
+ ) {
2617
+ return null
2618
+ }
2619
+ const placed = new Set(segments.flatMap((segment) => segment.callIds))
2620
+ for (const toolCall of toolCalls) {
2621
+ if (!placed.has(toolCall.id)) current.callIds.push(toolCall.id)
2622
+ }
2623
+ return segments.map((segment, index) => {
2624
+ const segmentCalls = toolCalls.filter((toolCall) =>
2625
+ segment.callIds.includes(toolCall.id),
2626
+ )
2627
+ return {
2628
+ role: 'assistant',
2629
+ content: segment.text || null,
2630
+ ...(segmentCalls.length > 0 && { toolCalls: segmentCalls }),
2631
+ id:
2632
+ id === undefined
2633
+ ? undefined
2634
+ : index === 0
2635
+ ? id
2636
+ : `${id}-segment-${index}`,
2637
+ createdAt,
2638
+ ...(segment.thinking.length > 0 && { thinking: segment.thinking }),
2639
+ }
2640
+ })
2641
+ }
2642
+
2467
2643
  private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
2468
2644
  this.finalizeCurrentThinkingStep()
2469
2645
 
2646
+ const segments = this.buildOrderedAssistantSegments(
2647
+ toolCalls,
2648
+ this.currentMessageId ?? undefined,
2649
+ this.currentMessageCreatedAt ?? undefined,
2650
+ )
2470
2651
  this.messages = [
2471
2652
  ...this.messages,
2472
- {
2473
- role: 'assistant',
2474
- content: this.accumulatedContent || null,
2475
- toolCalls,
2476
- id: this.currentMessageId ?? undefined,
2477
- createdAt: this.currentMessageCreatedAt ?? undefined,
2478
- ...(this.accumulatedThinking.length > 0 && {
2479
- thinking: this.accumulatedThinking,
2480
- }),
2481
- },
2653
+ ...(segments ?? [
2654
+ {
2655
+ role: 'assistant' as const,
2656
+ content: this.accumulatedContent || null,
2657
+ toolCalls,
2658
+ id: this.currentMessageId ?? undefined,
2659
+ createdAt: this.currentMessageCreatedAt ?? undefined,
2660
+ ...(this.accumulatedThinking.length > 0 && {
2661
+ thinking: this.accumulatedThinking,
2662
+ }),
2663
+ },
2664
+ ]),
2482
2665
  ]
2483
2666
  this.middlewareCtx.messages = this.messages
2484
2667
  }
@@ -2557,13 +2740,23 @@ class TextEngine<
2557
2740
  !currentTurnAlreadyRecorded &&
2558
2741
  (this.accumulatedContent !== '' || thinking)
2559
2742
  ) {
2560
- messages.push({
2561
- role: 'assistant',
2562
- content: this.accumulatedContent || null,
2563
- id: this.currentMessageId ?? this.createId('msg'),
2564
- createdAt: this.currentMessageCreatedAt ?? new Date(),
2565
- ...(thinking ? { thinking } : {}),
2566
- })
2743
+ const id = this.currentMessageId ?? this.createId('msg')
2744
+ const createdAt = this.currentMessageCreatedAt ?? new Date()
2745
+ messages.push(
2746
+ ...(this.buildOrderedAssistantSegments(
2747
+ this.toolCallManager.getToolCalls(),
2748
+ id,
2749
+ createdAt,
2750
+ ) ?? [
2751
+ {
2752
+ role: 'assistant',
2753
+ content: this.accumulatedContent || null,
2754
+ id,
2755
+ createdAt,
2756
+ ...(thinking ? { thinking } : {}),
2757
+ },
2758
+ ]),
2759
+ )
2567
2760
  }
2568
2761
  if (structuredOutput) {
2569
2762
  messages.push({
@@ -2685,6 +2878,7 @@ class TextEngine<
2685
2878
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2686
2879
  > = [],
2687
2880
  genericInterruptIds: ReadonlyArray<string> = [],
2881
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2688
2882
  childInterrupts: ReadonlyArray<Interrupt> = [],
2689
2883
  ): Array<Interrupt> {
2690
2884
  const interrupts: Array<Interrupt> = []
@@ -2803,6 +2997,30 @@ class TextEngine<
2803
2997
  })
2804
2998
  }
2805
2999
 
3000
+ for (const pendingInput of inputRequired) {
3001
+ const id = `mcp_input_${pendingInput.toolCallId}`
3002
+ interrupts.push({
3003
+ id,
3004
+ reason: 'mcp_input',
3005
+ message: `Input required to run ${pendingInput.toolName}`,
3006
+ toolCallId: pendingInput.toolCallId,
3007
+ metadata: {
3008
+ toolName: pendingInput.toolName,
3009
+ // A generic binding with no definition id. `useChat` shows it as a
3010
+ // generic interrupt with `resolveInterrupt` and `cancel`. The answer
3011
+ // is not validated here. The MCP server checks it.
3012
+ [interruptBindingMetadataKey]: {
3013
+ v: INTERRUPT_BINDING_VERSION,
3014
+ kind: 'generic',
3015
+ interruptId: id,
3016
+ },
3017
+ [INTERRUPT_PAYLOAD_METADATA_KEY]: {
3018
+ kind: pendingInput.kind,
3019
+ request: pendingInput.request,
3020
+ },
3021
+ },
3022
+ })
3023
+ }
2806
3024
  // A subagent tool call raised these. They keep their `subagentRunId`.
2807
3025
  interrupts.push(...childInterrupts)
2808
3026
 
@@ -2827,6 +3045,7 @@ class TextEngine<
2827
3045
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2828
3046
  > = [],
2829
3047
  genericInterruptIds?: ReadonlyArray<string>,
3048
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2830
3049
  childInterrupts?: ReadonlyArray<Interrupt>,
2831
3050
  ): StreamChunk {
2832
3051
  return {
@@ -2839,6 +3058,7 @@ class TextEngine<
2839
3058
  clientRequests,
2840
3059
  genericRequests,
2841
3060
  genericInterruptIds,
3061
+ inputRequired,
2842
3062
  childInterrupts,
2843
3063
  ),
2844
3064
  },
@@ -2989,6 +3209,7 @@ class TextEngine<
2989
3209
  genericRequests: ReadonlyArray<
2990
3210
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2991
3211
  > = [],
3212
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2992
3213
  childInterrupts: ReadonlyArray<Interrupt> = [],
2993
3214
  ): AsyncGenerator<StreamChunk, boolean, void> {
2994
3215
  yield* this.emitSyntheticRunStarted(finishEvent)
@@ -3003,6 +3224,7 @@ class TextEngine<
3003
3224
  clientRequests,
3004
3225
  genericRequests,
3005
3226
  genericInterruptIds,
3227
+ inputRequired,
3006
3228
  childInterrupts,
3007
3229
  ),
3008
3230
  )
@@ -3039,6 +3261,7 @@ class TextEngine<
3039
3261
  requests?: ReadonlyArray<
3040
3262
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
3041
3263
  >,
3264
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
3042
3265
  ): AsyncGenerator<StreamChunk, boolean, void> {
3043
3266
  this.middlewareCtx.phase = phase
3044
3267
  const boundaryRequests =
@@ -3074,6 +3297,7 @@ class TextEngine<
3074
3297
  actionable.approvals,
3075
3298
  actionable.clientRequests,
3076
3299
  boundaryRequests,
3300
+ inputRequired,
3077
3301
  )
3078
3302
  return true
3079
3303
  }
@@ -3097,6 +3321,8 @@ class TextEngine<
3097
3321
  const approvalRequests: Array<ApprovalRequest> = []
3098
3322
  const clientRequests: Array<ClientToolRequest> = []
3099
3323
  for (const toolCall of toolCalls) {
3324
+ // Provider-executed calls are complete; never surface them as client work.
3325
+ if (isProviderExecutedToolCall(toolCall)) continue
3100
3326
  const tool = this.resolveExecutableTools([toolCall]).find(
3101
3327
  (candidate) => candidate.name === toolCall.function.name,
3102
3328
  ) as RuntimeToolWithApproval | undefined
@@ -3119,6 +3345,7 @@ class TextEngine<
3119
3345
  } else if (
3120
3346
  !tool.execute &&
3121
3347
  !clientToolResults.has(toolCall.id) &&
3348
+ !this.resumeClientToolErrors.has(toolCall.id) &&
3122
3349
  !this.resumeCancelledToolCallIds.has(toolCall.id)
3123
3350
  ) {
3124
3351
  clientRequests.push({
@@ -3226,9 +3453,15 @@ class TextEngine<
3226
3453
  content: wireContent,
3227
3454
  role: 'tool' as const,
3228
3455
  }
3456
+ // An outcome always comes with output-error.
3229
3457
  chunks.push(
3230
3458
  (result.state === 'output-error'
3231
- ? withTanstackMetadata(resultChunk, { state: result.state })
3459
+ ? withTanstackMetadata(resultChunk, {
3460
+ state: result.state,
3461
+ ...(result.outcome !== undefined && {
3462
+ toolResultOutcome: result.outcome,
3463
+ }),
3464
+ })
3232
3465
  : resultChunk) as StreamChunk,
3233
3466
  )
3234
3467
 
@@ -3249,19 +3482,63 @@ class TextEngine<
3249
3482
  return false
3250
3483
  }
3251
3484
  })
3252
-
3253
- const newToolMessage: ModelMessage = {
3254
- role: 'tool',
3255
- content,
3256
- toolCallId: result.toolCallId,
3485
+ const isDeniedResult = result.outcome === 'denied'
3486
+ const existingToolResultIdx = isDeniedResult
3487
+ ? this.messages.findIndex(
3488
+ (message) =>
3489
+ message.role === 'tool' &&
3490
+ message.toolCallId === result.toolCallId,
3491
+ )
3492
+ : -1
3493
+ const resultMessageIdx =
3494
+ existingToolResultIdx >= 0 ? existingToolResultIdx : placeholderIdx
3495
+
3496
+ // Only a denied result keeps the old message's fields. A replaced
3497
+ // pendingExecution placeholder must not leak into the real result.
3498
+ const existingToolMessage =
3499
+ existingToolResultIdx >= 0
3500
+ ? this.messages[existingToolResultIdx]
3501
+ : undefined
3502
+ const errorField = result.state === 'output-error' && {
3503
+ error: toolResultErrorText(parseToolOutput(wireContent)),
3257
3504
  }
3505
+ const replacementMessage: ModelMessage =
3506
+ existingToolMessage?.role === 'tool'
3507
+ ? {
3508
+ ...existingToolMessage,
3509
+ content,
3510
+ toolCallId: result.toolCallId,
3511
+ ...errorField,
3512
+ }
3513
+ : {
3514
+ role: 'tool',
3515
+ content,
3516
+ toolCallId: result.toolCallId,
3517
+ ...errorField,
3518
+ }
3519
+ const newToolMessage: ModelMessage =
3520
+ result.outcome !== undefined
3521
+ ? withTanstackMetadata(replacementMessage, {
3522
+ toolResultOutcome: result.outcome,
3523
+ })
3524
+ : replacementMessage
3258
3525
 
3259
- if (placeholderIdx >= 0) {
3260
- this.messages = [
3261
- ...this.messages.slice(0, placeholderIdx),
3526
+ if (resultMessageIdx >= 0) {
3527
+ const replacedMessages = [
3528
+ ...this.messages.slice(0, resultMessageIdx),
3262
3529
  newToolMessage,
3263
- ...this.messages.slice(placeholderIdx + 1),
3530
+ ...this.messages.slice(resultMessageIdx + 1),
3264
3531
  ]
3532
+ this.messages = isDeniedResult
3533
+ ? replacedMessages.filter(
3534
+ (message, index) =>
3535
+ index === resultMessageIdx ||
3536
+ !(
3537
+ message.role === 'tool' &&
3538
+ message.toolCallId === result.toolCallId
3539
+ ),
3540
+ )
3541
+ : replacedMessages
3265
3542
  } else {
3266
3543
  this.messages = [...this.messages, newToolMessage]
3267
3544
  }
@@ -3292,7 +3569,10 @@ class TextEngine<
3292
3569
  }
3293
3570
 
3294
3571
  // Only mark as complete if NOT pending execution
3295
- if (!hasPendingExecution) {
3572
+ if (
3573
+ !hasPendingExecution &&
3574
+ !this.resumeDeniedToolResults.has(message.toolCallId)
3575
+ ) {
3296
3576
  completedToolIds.add(message.toolCallId)
3297
3577
  }
3298
3578
  }
@@ -3589,7 +3869,11 @@ class TextEngine<
3589
3869
  // Apply merged config back to engine state
3590
3870
  this.applyMiddlewareConfig(postOnConfig)
3591
3871
 
3592
- assertNoFileSources(this.adapter.name, this.messages)
3872
+ // Schema-only structured output with no tools skips the agent loop, so
3873
+ // `streamModelResponse` never runs this check. Middleware can also
3874
+ // replace `this.messages` above. Fail closed here before the
3875
+ // structured-output adapter call.
3876
+ assertMessagesFileSourceSupport(this.adapter, this.messages)
3593
3877
 
3594
3878
  // Build the StructuredOutputOptions the adapter expects.
3595
3879
  // `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
@@ -3676,6 +3960,9 @@ class TextEngine<
3676
3960
  // Track whether a RUN_ERROR has been yielded to streaming consumers so
3677
3961
  // we don't emit a duplicate synthetic one at the end.
3678
3962
  let runErrorYielded = false
3963
+ // The full model text. Adapter errors carry only a 200-character preview,
3964
+ // so a Promise caller could not recover the answer (#1485).
3965
+ let rawText = ''
3679
3966
 
3680
3967
  // Pipe chunks through middleware; yield to consumer only when yieldChunks=true
3681
3968
  for await (const raw of providerStream) {
@@ -3787,14 +4074,20 @@ class TextEngine<
3787
4074
  await this.runOnUsageFromChunk(chunk)
3788
4075
  }
3789
4076
 
4077
+ if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
4078
+ rawText += chunk.delta
4079
+ }
4080
+
3790
4081
  if (chunk.type === EventType.RUN_ERROR) {
3791
4082
  // RunErrorEvent already exposes `message` and `code` after narrowing.
4083
+ // A native stream has no adapter error to capture, but its RUN_ERROR
4084
+ // can carry the provider's error body as `rawEvent` (#1005).
4085
+ const cause = fallbackAdapterError ?? chunk.rawEvent
3792
4086
  this.finalizationError = {
3793
4087
  message: chunk.message,
3794
4088
  ...(chunk.code ? { code: chunk.code } : {}),
3795
- ...(fallbackAdapterError !== undefined
3796
- ? { cause: fallbackAdapterError }
3797
- : {}),
4089
+ ...(cause !== undefined ? { cause } : {}),
4090
+ ...(rawText ? { rawText } : {}),
3798
4091
  }
3799
4092
  }
3800
4093
 
@@ -3932,7 +4225,16 @@ class TextEngine<
3932
4225
  const yieldChunks = this.finalStructuredOutput.yieldChunks
3933
4226
  const source = this.finalStructuredOutput.source ?? 'text'
3934
4227
 
3935
- if (source === 'event') {
4228
+ // A final turn cut off at the output cap holds truncated JSON, or none
4229
+ // for a reasoning model that spent the budget. Report the token limit
4230
+ // instead of a parse or missing-result error (#1426).
4231
+ if (this.lastFinishReason === 'length') {
4232
+ this.finalizationError = {
4233
+ message:
4234
+ 'The response was cut off because the maximum token limit was reached (finish_reason=length); raise the output token limit.',
4235
+ code: 'max_tokens',
4236
+ }
4237
+ } else if (source === 'event') {
3936
4238
  if (!this.structuredOutputResult) {
3937
4239
  this.finalizationError = {
3938
4240
  message: 'missing structured result',
@@ -3969,6 +4271,7 @@ class TextEngine<
3969
4271
  message: `Failed to parse structured output as JSON. Content: ${detail}`,
3970
4272
  code: 'structured-output-parse-failed',
3971
4273
  cause: err,
4274
+ rawText,
3972
4275
  }
3973
4276
  }
3974
4277
  }
@@ -4079,6 +4382,7 @@ class TextEngine<
4079
4382
  resumeToolState: {
4080
4383
  approvals: this.resumeApprovals,
4081
4384
  clientToolResults: this.resumeClientToolResults,
4385
+ clientToolErrors: this.resumeClientToolErrors,
4082
4386
  deniedToolResults: this.resumeDeniedToolResults,
4083
4387
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
4084
4388
  },
@@ -4221,6 +4525,26 @@ class TextEngine<
4221
4525
  : []
4222
4526
  })
4223
4527
  pending.push(...genericPending)
4528
+ // An `mcp_input_*` answer is valid only for a server tool call that is
4529
+ // still pending in history.
4530
+ const mcpInputCallIds = new Map<string, string>()
4531
+ for (const toolCall of pendingToolCalls) {
4532
+ const interruptId = `mcp_input_${toolCall.id}`
4533
+ if (!resumeInterruptIds.has(interruptId)) continue
4534
+ if (!toolsByCallId.get(toolCall.id)?.execute) continue
4535
+ mcpInputCallIds.set(interruptId, toolCall.id)
4536
+ pending.push({
4537
+ interruptId,
4538
+ payload: { id: interruptId },
4539
+ binding: {
4540
+ v: INTERRUPT_BINDING_VERSION,
4541
+ kind: 'generic',
4542
+ interruptId,
4543
+ interruptedRunId,
4544
+ generation: 0,
4545
+ },
4546
+ })
4547
+ }
4224
4548
  const validated = await validateInterruptResumeBatch({
4225
4549
  threadId: this.threadId,
4226
4550
  interruptedRunId,
@@ -4250,6 +4574,16 @@ class TextEngine<
4250
4574
  })
4251
4575
 
4252
4576
  const genericResolutions = validated.resumeToolState.genericInterrupts
4577
+ for (const [interruptId, toolCallId] of mcpInputCallIds) {
4578
+ const resolution = genericResolutions?.get(interruptId)
4579
+ if (!resolution) continue
4580
+ this.resumeInputResponses.set(
4581
+ toolCallId,
4582
+ resolution.status === 'resolved'
4583
+ ? { status: 'resolved', payload: resolution.payload }
4584
+ : { status: 'cancelled' },
4585
+ )
4586
+ }
4253
4587
  if (genericPending.length > 0 && genericResolutions) {
4254
4588
  const resolutions = genericPending
4255
4589
  .sort((left, right) => {
@@ -4299,6 +4633,7 @@ class TextEngine<
4299
4633
  this.earlyTermination = true
4300
4634
  } else if (policy.toolResume === 'cancel') {
4301
4635
  for (const request of pendingToolCalls) {
4636
+ if (this.resumeDeniedToolResults.has(request.id)) continue
4302
4637
  this.resumeCancelledToolCallIds.add(request.id)
4303
4638
  }
4304
4639
  }
@@ -4440,6 +4775,11 @@ class TextEngine<
4440
4775
  this.resumeClientToolResults.set(toolCallId, result)
4441
4776
  }
4442
4777
  }
4778
+ if (state?.clientToolErrors) {
4779
+ for (const [toolCallId, errorText] of state.clientToolErrors) {
4780
+ this.resumeClientToolErrors.set(toolCallId, errorText)
4781
+ }
4782
+ }
4443
4783
  if (state?.deniedToolResults) {
4444
4784
  for (const [toolCallId, result] of state.deniedToolResults) {
4445
4785
  this.resumeDeniedToolResults.set(toolCallId, result)
@@ -4499,6 +4839,7 @@ class TextEngine<
4499
4839
  this.earlyTermination = true
4500
4840
  } else if (policy.toolResume === 'cancel') {
4501
4841
  for (const toolCall of this.getPendingToolCallsFromMessages()) {
4842
+ if (this.resumeDeniedToolResults.has(toolCall.id)) continue
4502
4843
  this.resumeCancelledToolCallIds.add(toolCall.id)
4503
4844
  }
4504
4845
  }
@@ -4653,6 +4994,7 @@ class TextEngine<
4653
4994
  results: Array<ToolResult>
4654
4995
  needsApproval: Array<ApprovalRequest>
4655
4996
  needsClientExecution: Array<ClientToolRequest>
4997
+ inputRequired: Array<McpInputRequest>
4656
4998
  subagentInterrupts: Array<Interrupt>
4657
4999
  },
4658
5000
  void
@@ -4663,6 +5005,7 @@ class TextEngine<
4663
5005
  results: Array<ToolResult>
4664
5006
  needsApproval: Array<ApprovalRequest>
4665
5007
  needsClientExecution: Array<ClientToolRequest>
5008
+ inputRequired: Array<McpInputRequest>
4666
5009
  subagentInterrupts: Array<Interrupt>
4667
5010
  },
4668
5011
  void
@@ -5101,6 +5444,16 @@ async function* runRoutedSubagents(
5101
5444
  yield* runChatEngine(options, engineRef)
5102
5445
  return
5103
5446
  }
5447
+ // ponytail: a router cannot write input yet. Add it to the router pick if
5448
+ // someone needs it.
5449
+ const withInput = bag.agents.find(
5450
+ (agent: DefinedAgent) => agent.inputSchema !== undefined,
5451
+ )
5452
+ if (withInput) {
5453
+ throw new Error(
5454
+ `Subagent "${withInput.name}" has an inputSchema. inputSchema needs tool mode: remove subagents.router so the model writes the input.`,
5455
+ )
5456
+ }
5104
5457
  const threadId = options.threadId ?? `thread-${Date.now()}`
5105
5458
  const runId = options.runId ?? `run-${Date.now()}`
5106
5459
  const messages = options.messages ?? []
@@ -5556,6 +5909,12 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
5556
5909
  enumerable: true,
5557
5910
  })
5558
5911
  }
5912
+ if (finalizationError.rawText !== undefined) {
5913
+ Object.defineProperty(err, 'rawText', {
5914
+ value: finalizationError.rawText,
5915
+ enumerable: true,
5916
+ })
5917
+ }
5559
5918
  throw err
5560
5919
  }
5561
5920