@tanstack/ai 0.61.0 → 0.64.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 (104) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  4. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  5. package/dist/esm/activities/chat/agents/spawn.js +10 -33
  6. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  7. package/dist/esm/activities/chat/index.d.ts +2 -0
  8. package/dist/esm/activities/chat/index.js +166 -60
  9. package/dist/esm/activities/chat/index.js.map +1 -1
  10. package/dist/esm/activities/chat/messages.js +43 -22
  11. package/dist/esm/activities/chat/messages.js.map +1 -1
  12. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  13. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  14. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  15. package/dist/esm/activities/chat/stream/message-updaters.js +6 -2
  16. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  17. package/dist/esm/activities/chat/stream/processor.d.ts +11 -9
  18. package/dist/esm/activities/chat/stream/processor.js +38 -18
  19. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  20. package/dist/esm/activities/chat/tools/schema-converter.d.ts +8 -0
  21. package/dist/esm/activities/chat/tools/schema-converter.js +6 -5
  22. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  23. package/dist/esm/activities/chat/tools/tool-calls.d.ts +20 -3
  24. package/dist/esm/activities/chat/tools/tool-calls.js +126 -36
  25. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  26. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  28. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  29. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  30. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  31. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  32. package/dist/esm/activities/evaluate/index.js +3 -1
  33. package/dist/esm/activities/evaluate/index.js.map +1 -1
  34. package/dist/esm/activities/generateSpeech/index.d.ts +1 -1
  35. package/dist/esm/activities/generateSpeech/index.js +1 -1
  36. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  37. package/dist/esm/activities/generateVideo/adapter.d.ts +14 -6
  38. package/dist/esm/activities/generateVideo/adapter.js +6 -3
  39. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  40. package/dist/esm/activities/generateVideo/index.d.ts +5 -4
  41. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  42. package/dist/esm/activities/generateVideo/snap.d.ts +12 -3
  43. package/dist/esm/activities/generateVideo/snap.js +47 -8
  44. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  45. package/dist/esm/activities/generateVoice/index.d.ts +1 -1
  46. package/dist/esm/activities/generateVoice/index.js +1 -1
  47. package/dist/esm/activities/generateVoice/index.js.map +1 -1
  48. package/dist/esm/activities/index.d.ts +2 -2
  49. package/dist/esm/activities/index.js +2 -2
  50. package/dist/esm/adapter-internals.d.ts +1 -0
  51. package/dist/esm/adapter-internals.js +2 -1
  52. package/dist/esm/client.d.ts +1 -1
  53. package/dist/esm/client.js.map +1 -1
  54. package/dist/esm/index.d.ts +1 -1
  55. package/dist/esm/index.js +2 -2
  56. package/dist/esm/interrupt-resume.js +29 -4
  57. package/dist/esm/interrupt-resume.js.map +1 -1
  58. package/dist/esm/middlewares/otel.d.ts +5 -2
  59. package/dist/esm/middlewares/otel.js +114 -0
  60. package/dist/esm/middlewares/otel.js.map +1 -1
  61. package/dist/esm/types.d.ts +70 -4
  62. package/dist/esm/utilities/ag-ui-wire.js +8 -4
  63. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  64. package/dist/esm/utilities/merge-streams.d.ts +6 -0
  65. package/dist/esm/utilities/merge-streams.js +37 -0
  66. package/dist/esm/utilities/merge-streams.js.map +1 -0
  67. package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
  68. package/dist/esm/utilities/reasoning-encrypted-value.js +11 -1
  69. package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -1
  70. package/dist/esm/utilities/tool-result.d.ts +2 -1
  71. package/dist/esm/utilities/tool-result.js +4 -1
  72. package/dist/esm/utilities/tool-result.js.map +1 -1
  73. package/package.json +3 -3
  74. package/skills/ai-core/chat-experience/SKILL.md +120 -0
  75. package/skills/ai-core/media-generation/SKILL.md +3 -3
  76. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  77. package/src/activities/chat/agents/define-agent.ts +20 -3
  78. package/src/activities/chat/agents/spawn.ts +22 -43
  79. package/src/activities/chat/index.ts +248 -65
  80. package/src/activities/chat/messages.ts +54 -7
  81. package/src/activities/chat/middleware/types.ts +1 -0
  82. package/src/activities/chat/stream/message-updaters.ts +8 -0
  83. package/src/activities/chat/stream/processor.ts +57 -27
  84. package/src/activities/chat/tools/schema-converter.ts +17 -5
  85. package/src/activities/chat/tools/tool-calls.ts +215 -68
  86. package/src/activities/chat/tools/tool-definition.ts +8 -0
  87. package/src/activities/evaluate/adapter.ts +4 -0
  88. package/src/activities/evaluate/index.ts +6 -0
  89. package/src/activities/generateSpeech/index.ts +1 -1
  90. package/src/activities/generateVideo/adapter.ts +21 -7
  91. package/src/activities/generateVideo/index.ts +5 -4
  92. package/src/activities/generateVideo/snap.ts +64 -6
  93. package/src/activities/generateVoice/index.ts +1 -1
  94. package/src/activities/index.ts +2 -1
  95. package/src/adapter-internals.ts +1 -0
  96. package/src/client.ts +1 -0
  97. package/src/index.ts +1 -0
  98. package/src/interrupt-resume.ts +55 -4
  99. package/src/middlewares/otel.ts +161 -3
  100. package/src/types.ts +66 -5
  101. package/src/utilities/ag-ui-wire.ts +17 -2
  102. package/src/utilities/merge-streams.ts +34 -0
  103. package/src/utilities/reasoning-encrypted-value.ts +12 -0
  104. package/src/utilities/tool-result.ts +7 -1
@@ -41,6 +41,7 @@ import {
41
41
  import { subagentHostMessageId } from '../../utilities/subagent-wire'
42
42
  import { withDurabilityBatchHint } from '../../utilities/durability-batch'
43
43
  import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
44
+ import { isRedactedThinkingId } from '../../utilities/reasoning-encrypted-value'
44
45
  import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
45
46
  import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
46
47
  import {
@@ -113,6 +114,7 @@ import type {
113
114
  import type {
114
115
  ApprovalRequest,
115
116
  ClientToolRequest,
117
+ McpInputRequest,
116
118
  ToolResult,
117
119
  } from './tools/tool-calls'
118
120
  import type { ApprovalSchemaConfig } from './tools/tool-definition'
@@ -148,6 +150,7 @@ import type {
148
150
  ToolCallEndEvent,
149
151
  ToolCallResultEvent,
150
152
  ToolCallStartEvent,
153
+ ToolInputResponse,
151
154
  UIMessage,
152
155
  } from '../../types'
153
156
  import type {
@@ -521,6 +524,8 @@ export interface TextActivityOptions<
521
524
  abortController?: TextOptions['abortController']
522
525
  /** Strategy for controlling the agent loop */
523
526
  agentLoopStrategy?: TextOptions['agentLoopStrategy']
527
+ /** How the server tools of one model turn run. Default `'parallel'`. */
528
+ toolExecution?: TextOptions['toolExecution']
524
529
  /**
525
530
  * Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
526
531
  * Tunes how much of each lazy tool's description appears in the discovery
@@ -851,8 +856,7 @@ class TextEngine<
851
856
  private currentMessageCreatedAt: Date | null = null
852
857
  private streamIdentityCaptured = false
853
858
  private accumulatedContent = ''
854
- private accumulatedThinking: Array<{ content: string; signature?: string }> =
855
- []
859
+ private accumulatedThinking: NonNullable<ModelMessage['thinking']> = []
856
860
  /**
857
861
  * Arrival order of this iteration's thinking steps, text and tool calls.
858
862
  * A ModelMessage keeps `thinking` apart from `content`/`toolCalls`, so a
@@ -864,6 +868,7 @@ class TextEngine<
864
868
  private turnParts: Array<TurnPart> | null = []
865
869
  private currentThinkingContent = ''
866
870
  private currentThinkingSignature = ''
871
+ private currentThinkingRedacted = false
867
872
  private eventOptions?: Record<string, unknown> | undefined
868
873
  private eventToolNames?: Array<string>
869
874
  private finishedEvent: RunFinishedEvent | null = null
@@ -880,8 +885,10 @@ class TextEngine<
880
885
  private readonly initialClientToolResults: Map<string, any>
881
886
  private readonly resumeApprovals = new Map<string, ToolApprovalResolution>()
882
887
  private readonly resumeClientToolResults = new Map<string, any>()
888
+ private readonly resumeClientToolErrors = new Map<string, string>()
883
889
  private readonly resumeDeniedToolResults = new Map<string, unknown>()
884
890
  private readonly resumeCancelledToolCallIds = new Set<string>()
891
+ private readonly resumeInputResponses = new Map<string, ToolInputResponse>()
885
892
  private readonly resumeGenericInterrupts = new Map<
886
893
  string,
887
894
  ChatResumeGenericResolution
@@ -952,6 +959,8 @@ class TextEngine<
952
959
  message: string
953
960
  code?: string
954
961
  cause?: unknown
962
+ /** Full model text, when a structured-output parse failed (#1485). */
963
+ rawText?: string
955
964
  } | null = null
956
965
  private combinedCompleteEmitted = false
957
966
  private readonly finalStructuredOutput?: {
@@ -1189,10 +1198,54 @@ class TextEngine<
1189
1198
  message: string
1190
1199
  code?: string
1191
1200
  cause?: unknown
1201
+ rawText?: string
1192
1202
  } | null {
1193
1203
  return this.finalizationError
1194
1204
  }
1195
1205
 
1206
+ private async runTerminalHook(): Promise<void> {
1207
+ if (this.terminalHookCalled || this.isCancelled()) return
1208
+
1209
+ this.terminalHookCalled = true
1210
+
1211
+ if (this.finalizationError) {
1212
+ const errForHook = new Error(
1213
+ this.finalizationError.message,
1214
+ this.finalizationError.cause !== undefined
1215
+ ? { cause: this.finalizationError.cause }
1216
+ : undefined,
1217
+ )
1218
+ if (this.finalizationError.code !== undefined) {
1219
+ Object.defineProperty(errForHook, 'code', {
1220
+ value: this.finalizationError.code,
1221
+ enumerable: true,
1222
+ })
1223
+ }
1224
+ if (this.finalizationError.rawText !== undefined) {
1225
+ Object.defineProperty(errForHook, 'rawText', {
1226
+ value: this.finalizationError.rawText,
1227
+ enumerable: true,
1228
+ })
1229
+ }
1230
+ await this.middlewareRunner.runOnError(this.middlewareCtx, {
1231
+ error: errForHook,
1232
+ duration: Date.now() - this.streamStartTime,
1233
+ })
1234
+ return
1235
+ }
1236
+
1237
+ this.addTerminalAssistantMessages()
1238
+ await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
1239
+ finishReason: this.lastFinishReason,
1240
+ duration: Date.now() - this.streamStartTime,
1241
+ content: this.accumulatedContent,
1242
+ usage: rebuildTokenUsage(
1243
+ this.finishedEvent?.usage,
1244
+ tanstackMetadata(this.finishedEvent ?? undefined)?.usage,
1245
+ ),
1246
+ })
1247
+ }
1248
+
1196
1249
  async *run(): AsyncGenerator<StreamChunk> {
1197
1250
  this.beforeRun()
1198
1251
  this.logger.agentLoop('run started', {
@@ -1238,7 +1291,8 @@ class TextEngine<
1238
1291
  }
1239
1292
 
1240
1293
  const pendingPhase = yield* this.checkForPendingToolCalls()
1241
- if (pendingPhase === 'wait') {
1294
+ if (pendingPhase === 'stop' || pendingPhase === 'wait') {
1295
+ await this.runTerminalHook()
1242
1296
  return
1243
1297
  }
1244
1298
 
@@ -1347,46 +1401,11 @@ class TextEngine<
1347
1401
  }
1348
1402
  }
1349
1403
 
1350
- // Call terminal hook (skip when waiting for client — stream is paused, not finished).
1351
- // Priority: finalizationError → onError; otherwise normal onFinish.
1352
- // Skip on cancellation — the finally block routes aborts to onAbort.
1353
- if (
1354
- !this.terminalHookCalled &&
1355
- this.toolPhase !== 'wait' &&
1356
- !this.isCancelled()
1357
- ) {
1358
- if (this.finalizationError) {
1359
- this.terminalHookCalled = true
1360
- const errForHook = new Error(
1361
- this.finalizationError.message,
1362
- this.finalizationError.cause !== undefined
1363
- ? { cause: this.finalizationError.cause }
1364
- : undefined,
1365
- )
1366
- if (this.finalizationError.code !== undefined) {
1367
- Object.defineProperty(errForHook, 'code', {
1368
- value: this.finalizationError.code,
1369
- enumerable: true,
1370
- })
1371
- }
1372
- await this.middlewareRunner.runOnError(this.middlewareCtx, {
1373
- error: errForHook,
1374
- duration: Date.now() - this.streamStartTime,
1375
- })
1376
- } else {
1377
- this.addTerminalAssistantMessages()
1378
- this.terminalHookCalled = true
1379
- await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
1380
- finishReason: this.lastFinishReason,
1381
- duration: Date.now() - this.streamStartTime,
1382
- content: this.accumulatedContent,
1383
- usage: rebuildTokenUsage(
1384
- this.finishedEvent?.usage,
1385
- tanstackMetadata(this.finishedEvent ?? undefined)?.usage,
1386
- ),
1387
- })
1388
- }
1389
- }
1404
+ // A client-tool or approval wait ends this server-side chat invocation.
1405
+ // Structured-output finalization stays paused until the caller submits
1406
+ // the result in a new invocation, but middleware must still observe one
1407
+ // terminal hook for the current invocation.
1408
+ await this.runTerminalHook()
1390
1409
  } catch (error: unknown) {
1391
1410
  if (
1392
1411
  error instanceof Error &&
@@ -1508,6 +1527,7 @@ class TextEngine<
1508
1527
  this.turnParts = []
1509
1528
  this.currentThinkingContent = ''
1510
1529
  this.currentThinkingSignature = ''
1530
+ this.currentThinkingRedacted = false
1511
1531
 
1512
1532
  this.finishedEvent = null
1513
1533
  this.streamedToolErrorResults.clear()
@@ -1976,6 +1996,7 @@ class TextEngine<
1976
1996
  ...(this.currentThinkingSignature && {
1977
1997
  signature: this.currentThinkingSignature,
1978
1998
  }),
1999
+ ...(this.currentThinkingRedacted && { redacted: true }),
1979
2000
  })
1980
2001
  if (this.turnParts) {
1981
2002
  const placeholder = [...this.turnParts]
@@ -1993,6 +2014,7 @@ class TextEngine<
1993
2014
  }
1994
2015
  this.currentThinkingContent = ''
1995
2016
  this.currentThinkingSignature = ''
2017
+ this.currentThinkingRedacted = false
1996
2018
  }
1997
2019
  }
1998
2020
 
@@ -2020,6 +2042,7 @@ class TextEngine<
2020
2042
  if (typeof chunk.signature === 'string' && chunk.signature !== '') {
2021
2043
  this.noteThinkingStepPosition()
2022
2044
  this.currentThinkingSignature = chunk.signature
2045
+ this.currentThinkingRedacted = isRedactedThinkingId(chunk.stepId)
2023
2046
  }
2024
2047
  }
2025
2048
 
@@ -2049,6 +2072,7 @@ class TextEngine<
2049
2072
  }
2050
2073
  this.noteThinkingStepPosition()
2051
2074
  this.currentThinkingSignature = chunk.encryptedValue
2075
+ this.currentThinkingRedacted = isRedactedThinkingId(chunk.entityId)
2052
2076
  }
2053
2077
 
2054
2078
  /**
@@ -2181,9 +2205,12 @@ class TextEngine<
2181
2205
  this.middlewareCtx.context,
2182
2206
  this.toolAbortSignal,
2183
2207
  {
2208
+ clientToolErrors: this.resumeClientToolErrors,
2184
2209
  deniedToolResults: this.resumeDeniedToolResults,
2185
2210
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
2211
+ inputResponses: this.resumeInputResponses,
2186
2212
  },
2213
+ this.params.toolExecution,
2187
2214
  )
2188
2215
 
2189
2216
  // Consume the async generator, yielding custom events and collecting the return value
@@ -2207,11 +2234,12 @@ class TextEngine<
2207
2234
  }),
2208
2235
  )
2209
2236
 
2210
- if (
2237
+ const pauseForDecision =
2211
2238
  executionResult.needsApproval.length > 0 ||
2212
2239
  executionResult.needsClientExecution.length > 0 ||
2240
+ executionResult.inputRequired.length > 0 ||
2213
2241
  executionResult.subagentInterrupts.length > 0
2214
- ) {
2242
+ if (pauseForDecision) {
2215
2243
  this.discardDeferredToolCallRunFinishedChunks()
2216
2244
 
2217
2245
  if (allResults.length > 0) {
@@ -2228,6 +2256,7 @@ class TextEngine<
2228
2256
  executionResult.needsApproval,
2229
2257
  executionResult.needsClientExecution,
2230
2258
  [],
2259
+ executionResult.inputRequired,
2231
2260
  executionResult.subagentInterrupts,
2232
2261
  )
2233
2262
  this.setToolPhase(emitted ? 'wait' : 'stop')
@@ -2365,9 +2394,12 @@ class TextEngine<
2365
2394
  this.middlewareCtx.context,
2366
2395
  this.toolAbortSignal,
2367
2396
  {
2397
+ clientToolErrors: this.resumeClientToolErrors,
2368
2398
  deniedToolResults: this.resumeDeniedToolResults,
2369
2399
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
2400
+ inputResponses: this.resumeInputResponses,
2370
2401
  },
2402
+ this.params.toolExecution,
2371
2403
  )
2372
2404
 
2373
2405
  // Consume the async generator, yielding custom events and collecting the return value
@@ -2414,16 +2446,18 @@ class TextEngine<
2414
2446
  finishEvent,
2415
2447
  toolCalls,
2416
2448
  afterToolRequests,
2449
+ executionResult.inputRequired,
2417
2450
  )
2418
2451
  this.setToolPhase('wait')
2419
2452
  return
2420
2453
  }
2421
2454
 
2422
- if (
2455
+ const pauseForDecision =
2423
2456
  executionResult.needsApproval.length > 0 ||
2424
2457
  executionResult.needsClientExecution.length > 0 ||
2458
+ executionResult.inputRequired.length > 0 ||
2425
2459
  executionResult.subagentInterrupts.length > 0
2426
- ) {
2460
+ if (pauseForDecision) {
2427
2461
  if (allResults.length > 0) {
2428
2462
  for (const chunk of afterToolBoundaryChunks) {
2429
2463
  yield* this.pipeThroughMiddleware(chunk)
@@ -2435,6 +2469,7 @@ class TextEngine<
2435
2469
  executionResult.needsApproval,
2436
2470
  executionResult.needsClientExecution,
2437
2471
  [],
2472
+ executionResult.inputRequired,
2438
2473
  executionResult.subagentInterrupts,
2439
2474
  )
2440
2475
  this.setToolPhase(emitted ? 'wait' : 'stop')
@@ -2554,7 +2589,7 @@ class TextEngine<
2554
2589
  ),
2555
2590
  )
2556
2591
  type Segment = {
2557
- thinking: Array<{ content: string; signature?: string }>
2592
+ thinking: NonNullable<ModelMessage['thinking']>
2558
2593
  text: string
2559
2594
  callIds: Array<string>
2560
2595
  }
@@ -2853,6 +2888,7 @@ class TextEngine<
2853
2888
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2854
2889
  > = [],
2855
2890
  genericInterruptIds: ReadonlyArray<string> = [],
2891
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2856
2892
  childInterrupts: ReadonlyArray<Interrupt> = [],
2857
2893
  ): Array<Interrupt> {
2858
2894
  const interrupts: Array<Interrupt> = []
@@ -2971,6 +3007,30 @@ class TextEngine<
2971
3007
  })
2972
3008
  }
2973
3009
 
3010
+ for (const pendingInput of inputRequired) {
3011
+ const id = `mcp_input_${pendingInput.toolCallId}`
3012
+ interrupts.push({
3013
+ id,
3014
+ reason: 'mcp_input',
3015
+ message: `Input required to run ${pendingInput.toolName}`,
3016
+ toolCallId: pendingInput.toolCallId,
3017
+ metadata: {
3018
+ toolName: pendingInput.toolName,
3019
+ // A generic binding with no definition id. `useChat` shows it as a
3020
+ // generic interrupt with `resolveInterrupt` and `cancel`. The answer
3021
+ // is not validated here. The MCP server checks it.
3022
+ [interruptBindingMetadataKey]: {
3023
+ v: INTERRUPT_BINDING_VERSION,
3024
+ kind: 'generic',
3025
+ interruptId: id,
3026
+ },
3027
+ [INTERRUPT_PAYLOAD_METADATA_KEY]: {
3028
+ kind: pendingInput.kind,
3029
+ request: pendingInput.request,
3030
+ },
3031
+ },
3032
+ })
3033
+ }
2974
3034
  // A subagent tool call raised these. They keep their `subagentRunId`.
2975
3035
  interrupts.push(...childInterrupts)
2976
3036
 
@@ -2995,6 +3055,7 @@ class TextEngine<
2995
3055
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2996
3056
  > = [],
2997
3057
  genericInterruptIds?: ReadonlyArray<string>,
3058
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2998
3059
  childInterrupts?: ReadonlyArray<Interrupt>,
2999
3060
  ): StreamChunk {
3000
3061
  return {
@@ -3007,6 +3068,7 @@ class TextEngine<
3007
3068
  clientRequests,
3008
3069
  genericRequests,
3009
3070
  genericInterruptIds,
3071
+ inputRequired,
3010
3072
  childInterrupts,
3011
3073
  ),
3012
3074
  },
@@ -3157,6 +3219,7 @@ class TextEngine<
3157
3219
  genericRequests: ReadonlyArray<
3158
3220
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
3159
3221
  > = [],
3222
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
3160
3223
  childInterrupts: ReadonlyArray<Interrupt> = [],
3161
3224
  ): AsyncGenerator<StreamChunk, boolean, void> {
3162
3225
  yield* this.emitSyntheticRunStarted(finishEvent)
@@ -3171,6 +3234,7 @@ class TextEngine<
3171
3234
  clientRequests,
3172
3235
  genericRequests,
3173
3236
  genericInterruptIds,
3237
+ inputRequired,
3174
3238
  childInterrupts,
3175
3239
  ),
3176
3240
  )
@@ -3207,6 +3271,7 @@ class TextEngine<
3207
3271
  requests?: ReadonlyArray<
3208
3272
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
3209
3273
  >,
3274
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
3210
3275
  ): AsyncGenerator<StreamChunk, boolean, void> {
3211
3276
  this.middlewareCtx.phase = phase
3212
3277
  const boundaryRequests =
@@ -3242,6 +3307,7 @@ class TextEngine<
3242
3307
  actionable.approvals,
3243
3308
  actionable.clientRequests,
3244
3309
  boundaryRequests,
3310
+ inputRequired,
3245
3311
  )
3246
3312
  return true
3247
3313
  }
@@ -3289,6 +3355,7 @@ class TextEngine<
3289
3355
  } else if (
3290
3356
  !tool.execute &&
3291
3357
  !clientToolResults.has(toolCall.id) &&
3358
+ !this.resumeClientToolErrors.has(toolCall.id) &&
3292
3359
  !this.resumeCancelledToolCallIds.has(toolCall.id)
3293
3360
  ) {
3294
3361
  clientRequests.push({
@@ -3396,9 +3463,15 @@ class TextEngine<
3396
3463
  content: wireContent,
3397
3464
  role: 'tool' as const,
3398
3465
  }
3466
+ // An outcome always comes with output-error.
3399
3467
  chunks.push(
3400
3468
  (result.state === 'output-error'
3401
- ? withTanstackMetadata(resultChunk, { state: result.state })
3469
+ ? withTanstackMetadata(resultChunk, {
3470
+ state: result.state,
3471
+ ...(result.outcome !== undefined && {
3472
+ toolResultOutcome: result.outcome,
3473
+ }),
3474
+ })
3402
3475
  : resultChunk) as StreamChunk,
3403
3476
  )
3404
3477
 
@@ -3419,22 +3492,63 @@ class TextEngine<
3419
3492
  return false
3420
3493
  }
3421
3494
  })
3422
-
3423
- const newToolMessage: ModelMessage = {
3424
- role: 'tool',
3425
- content,
3426
- toolCallId: result.toolCallId,
3427
- ...(result.state === 'output-error' && {
3428
- error: toolResultErrorText(parseToolOutput(wireContent)),
3429
- }),
3495
+ const isDeniedResult = result.outcome === 'denied'
3496
+ const existingToolResultIdx = isDeniedResult
3497
+ ? this.messages.findIndex(
3498
+ (message) =>
3499
+ message.role === 'tool' &&
3500
+ message.toolCallId === result.toolCallId,
3501
+ )
3502
+ : -1
3503
+ const resultMessageIdx =
3504
+ existingToolResultIdx >= 0 ? existingToolResultIdx : placeholderIdx
3505
+
3506
+ // Only a denied result keeps the old message's fields. A replaced
3507
+ // pendingExecution placeholder must not leak into the real result.
3508
+ const existingToolMessage =
3509
+ existingToolResultIdx >= 0
3510
+ ? this.messages[existingToolResultIdx]
3511
+ : undefined
3512
+ const errorField = result.state === 'output-error' && {
3513
+ error: toolResultErrorText(parseToolOutput(wireContent)),
3430
3514
  }
3515
+ const replacementMessage: ModelMessage =
3516
+ existingToolMessage?.role === 'tool'
3517
+ ? {
3518
+ ...existingToolMessage,
3519
+ content,
3520
+ toolCallId: result.toolCallId,
3521
+ ...errorField,
3522
+ }
3523
+ : {
3524
+ role: 'tool',
3525
+ content,
3526
+ toolCallId: result.toolCallId,
3527
+ ...errorField,
3528
+ }
3529
+ const newToolMessage: ModelMessage =
3530
+ result.outcome !== undefined
3531
+ ? withTanstackMetadata(replacementMessage, {
3532
+ toolResultOutcome: result.outcome,
3533
+ })
3534
+ : replacementMessage
3431
3535
 
3432
- if (placeholderIdx >= 0) {
3433
- this.messages = [
3434
- ...this.messages.slice(0, placeholderIdx),
3536
+ if (resultMessageIdx >= 0) {
3537
+ const replacedMessages = [
3538
+ ...this.messages.slice(0, resultMessageIdx),
3435
3539
  newToolMessage,
3436
- ...this.messages.slice(placeholderIdx + 1),
3540
+ ...this.messages.slice(resultMessageIdx + 1),
3437
3541
  ]
3542
+ this.messages = isDeniedResult
3543
+ ? replacedMessages.filter(
3544
+ (message, index) =>
3545
+ index === resultMessageIdx ||
3546
+ !(
3547
+ message.role === 'tool' &&
3548
+ message.toolCallId === result.toolCallId
3549
+ ),
3550
+ )
3551
+ : replacedMessages
3438
3552
  } else {
3439
3553
  this.messages = [...this.messages, newToolMessage]
3440
3554
  }
@@ -3465,7 +3579,10 @@ class TextEngine<
3465
3579
  }
3466
3580
 
3467
3581
  // Only mark as complete if NOT pending execution
3468
- if (!hasPendingExecution) {
3582
+ if (
3583
+ !hasPendingExecution &&
3584
+ !this.resumeDeniedToolResults.has(message.toolCallId)
3585
+ ) {
3469
3586
  completedToolIds.add(message.toolCallId)
3470
3587
  }
3471
3588
  }
@@ -3853,6 +3970,9 @@ class TextEngine<
3853
3970
  // Track whether a RUN_ERROR has been yielded to streaming consumers so
3854
3971
  // we don't emit a duplicate synthetic one at the end.
3855
3972
  let runErrorYielded = false
3973
+ // The full model text. Adapter errors carry only a 200-character preview,
3974
+ // so a Promise caller could not recover the answer (#1485).
3975
+ let rawText = ''
3856
3976
 
3857
3977
  // Pipe chunks through middleware; yield to consumer only when yieldChunks=true
3858
3978
  for await (const raw of providerStream) {
@@ -3964,14 +4084,20 @@ class TextEngine<
3964
4084
  await this.runOnUsageFromChunk(chunk)
3965
4085
  }
3966
4086
 
4087
+ if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
4088
+ rawText += chunk.delta
4089
+ }
4090
+
3967
4091
  if (chunk.type === EventType.RUN_ERROR) {
3968
4092
  // RunErrorEvent already exposes `message` and `code` after narrowing.
4093
+ // A native stream has no adapter error to capture, but its RUN_ERROR
4094
+ // can carry the provider's error body as `rawEvent` (#1005).
4095
+ const cause = fallbackAdapterError ?? chunk.rawEvent
3969
4096
  this.finalizationError = {
3970
4097
  message: chunk.message,
3971
4098
  ...(chunk.code ? { code: chunk.code } : {}),
3972
- ...(fallbackAdapterError !== undefined
3973
- ? { cause: fallbackAdapterError }
3974
- : {}),
4099
+ ...(cause !== undefined ? { cause } : {}),
4100
+ ...(rawText ? { rawText } : {}),
3975
4101
  }
3976
4102
  }
3977
4103
 
@@ -4155,6 +4281,7 @@ class TextEngine<
4155
4281
  message: `Failed to parse structured output as JSON. Content: ${detail}`,
4156
4282
  code: 'structured-output-parse-failed',
4157
4283
  cause: err,
4284
+ rawText,
4158
4285
  }
4159
4286
  }
4160
4287
  }
@@ -4265,6 +4392,7 @@ class TextEngine<
4265
4392
  resumeToolState: {
4266
4393
  approvals: this.resumeApprovals,
4267
4394
  clientToolResults: this.resumeClientToolResults,
4395
+ clientToolErrors: this.resumeClientToolErrors,
4268
4396
  deniedToolResults: this.resumeDeniedToolResults,
4269
4397
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
4270
4398
  },
@@ -4407,6 +4535,26 @@ class TextEngine<
4407
4535
  : []
4408
4536
  })
4409
4537
  pending.push(...genericPending)
4538
+ // An `mcp_input_*` answer is valid only for a server tool call that is
4539
+ // still pending in history.
4540
+ const mcpInputCallIds = new Map<string, string>()
4541
+ for (const toolCall of pendingToolCalls) {
4542
+ const interruptId = `mcp_input_${toolCall.id}`
4543
+ if (!resumeInterruptIds.has(interruptId)) continue
4544
+ if (!toolsByCallId.get(toolCall.id)?.execute) continue
4545
+ mcpInputCallIds.set(interruptId, toolCall.id)
4546
+ pending.push({
4547
+ interruptId,
4548
+ payload: { id: interruptId },
4549
+ binding: {
4550
+ v: INTERRUPT_BINDING_VERSION,
4551
+ kind: 'generic',
4552
+ interruptId,
4553
+ interruptedRunId,
4554
+ generation: 0,
4555
+ },
4556
+ })
4557
+ }
4410
4558
  const validated = await validateInterruptResumeBatch({
4411
4559
  threadId: this.threadId,
4412
4560
  interruptedRunId,
@@ -4436,6 +4584,16 @@ class TextEngine<
4436
4584
  })
4437
4585
 
4438
4586
  const genericResolutions = validated.resumeToolState.genericInterrupts
4587
+ for (const [interruptId, toolCallId] of mcpInputCallIds) {
4588
+ const resolution = genericResolutions?.get(interruptId)
4589
+ if (!resolution) continue
4590
+ this.resumeInputResponses.set(
4591
+ toolCallId,
4592
+ resolution.status === 'resolved'
4593
+ ? { status: 'resolved', payload: resolution.payload }
4594
+ : { status: 'cancelled' },
4595
+ )
4596
+ }
4439
4597
  if (genericPending.length > 0 && genericResolutions) {
4440
4598
  const resolutions = genericPending
4441
4599
  .sort((left, right) => {
@@ -4485,6 +4643,7 @@ class TextEngine<
4485
4643
  this.earlyTermination = true
4486
4644
  } else if (policy.toolResume === 'cancel') {
4487
4645
  for (const request of pendingToolCalls) {
4646
+ if (this.resumeDeniedToolResults.has(request.id)) continue
4488
4647
  this.resumeCancelledToolCallIds.add(request.id)
4489
4648
  }
4490
4649
  }
@@ -4626,6 +4785,11 @@ class TextEngine<
4626
4785
  this.resumeClientToolResults.set(toolCallId, result)
4627
4786
  }
4628
4787
  }
4788
+ if (state?.clientToolErrors) {
4789
+ for (const [toolCallId, errorText] of state.clientToolErrors) {
4790
+ this.resumeClientToolErrors.set(toolCallId, errorText)
4791
+ }
4792
+ }
4629
4793
  if (state?.deniedToolResults) {
4630
4794
  for (const [toolCallId, result] of state.deniedToolResults) {
4631
4795
  this.resumeDeniedToolResults.set(toolCallId, result)
@@ -4685,6 +4849,7 @@ class TextEngine<
4685
4849
  this.earlyTermination = true
4686
4850
  } else if (policy.toolResume === 'cancel') {
4687
4851
  for (const toolCall of this.getPendingToolCallsFromMessages()) {
4852
+ if (this.resumeDeniedToolResults.has(toolCall.id)) continue
4688
4853
  this.resumeCancelledToolCallIds.add(toolCall.id)
4689
4854
  }
4690
4855
  }
@@ -4839,6 +5004,7 @@ class TextEngine<
4839
5004
  results: Array<ToolResult>
4840
5005
  needsApproval: Array<ApprovalRequest>
4841
5006
  needsClientExecution: Array<ClientToolRequest>
5007
+ inputRequired: Array<McpInputRequest>
4842
5008
  subagentInterrupts: Array<Interrupt>
4843
5009
  },
4844
5010
  void
@@ -4849,6 +5015,7 @@ class TextEngine<
4849
5015
  results: Array<ToolResult>
4850
5016
  needsApproval: Array<ApprovalRequest>
4851
5017
  needsClientExecution: Array<ClientToolRequest>
5018
+ inputRequired: Array<McpInputRequest>
4852
5019
  subagentInterrupts: Array<Interrupt>
4853
5020
  },
4854
5021
  void
@@ -5287,6 +5454,16 @@ async function* runRoutedSubagents(
5287
5454
  yield* runChatEngine(options, engineRef)
5288
5455
  return
5289
5456
  }
5457
+ // ponytail: a router cannot write input yet. Add it to the router pick if
5458
+ // someone needs it.
5459
+ const withInput = bag.agents.find(
5460
+ (agent: DefinedAgent) => agent.inputSchema !== undefined,
5461
+ )
5462
+ if (withInput) {
5463
+ throw new Error(
5464
+ `Subagent "${withInput.name}" has an inputSchema. inputSchema needs tool mode: remove subagents.router so the model writes the input.`,
5465
+ )
5466
+ }
5290
5467
  const threadId = options.threadId ?? `thread-${Date.now()}`
5291
5468
  const runId = options.runId ?? `run-${Date.now()}`
5292
5469
  const messages = options.messages ?? []
@@ -5742,6 +5919,12 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
5742
5919
  enumerable: true,
5743
5920
  })
5744
5921
  }
5922
+ if (finalizationError.rawText !== undefined) {
5923
+ Object.defineProperty(err, 'rawText', {
5924
+ value: finalizationError.rawText,
5925
+ enumerable: true,
5926
+ })
5927
+ }
5745
5928
  throw err
5746
5929
  }
5747
5930