@tanstack/ai 0.61.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 (65) 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 +8 -5
  6. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  7. package/dist/esm/activities/chat/index.js +156 -57
  8. package/dist/esm/activities/chat/index.js.map +1 -1
  9. package/dist/esm/activities/chat/messages.js +30 -18
  10. package/dist/esm/activities/chat/messages.js.map +1 -1
  11. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  12. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  14. package/dist/esm/activities/chat/stream/message-updaters.js +2 -1
  15. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/processor.d.ts +11 -9
  17. package/dist/esm/activities/chat/stream/processor.js +38 -18
  18. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  19. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  20. package/dist/esm/activities/chat/tools/tool-calls.js +55 -16
  21. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  23. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  24. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  25. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  26. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  27. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  28. package/dist/esm/activities/evaluate/index.js +3 -1
  29. package/dist/esm/activities/evaluate/index.js.map +1 -1
  30. package/dist/esm/client.d.ts +1 -1
  31. package/dist/esm/client.js.map +1 -1
  32. package/dist/esm/index.d.ts +1 -1
  33. package/dist/esm/index.js +2 -2
  34. package/dist/esm/interrupt-resume.js +29 -4
  35. package/dist/esm/interrupt-resume.js.map +1 -1
  36. package/dist/esm/middlewares/otel.d.ts +5 -2
  37. package/dist/esm/middlewares/otel.js +114 -0
  38. package/dist/esm/middlewares/otel.js.map +1 -1
  39. package/dist/esm/types.d.ts +42 -1
  40. package/dist/esm/utilities/ag-ui-wire.js +5 -3
  41. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  42. package/dist/esm/utilities/tool-result.d.ts +2 -1
  43. package/dist/esm/utilities/tool-result.js +4 -1
  44. package/dist/esm/utilities/tool-result.js.map +1 -1
  45. package/package.json +3 -3
  46. package/skills/ai-core/chat-experience/SKILL.md +120 -0
  47. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  48. package/src/activities/chat/agents/define-agent.ts +20 -3
  49. package/src/activities/chat/agents/spawn.ts +20 -12
  50. package/src/activities/chat/index.ts +235 -62
  51. package/src/activities/chat/messages.ts +40 -5
  52. package/src/activities/chat/middleware/types.ts +1 -0
  53. package/src/activities/chat/stream/message-updaters.ts +3 -0
  54. package/src/activities/chat/stream/processor.ts +57 -27
  55. package/src/activities/chat/tools/tool-calls.ts +98 -9
  56. package/src/activities/chat/tools/tool-definition.ts +8 -0
  57. package/src/activities/evaluate/adapter.ts +4 -0
  58. package/src/activities/evaluate/index.ts +6 -0
  59. package/src/client.ts +1 -0
  60. package/src/index.ts +1 -0
  61. package/src/interrupt-resume.ts +55 -4
  62. package/src/middlewares/otel.ts +161 -3
  63. package/src/types.ts +38 -1
  64. package/src/utilities/ag-ui-wire.ts +12 -1
  65. package/src/utilities/tool-result.ts +7 -1
@@ -113,6 +113,7 @@ import type {
113
113
  import type {
114
114
  ApprovalRequest,
115
115
  ClientToolRequest,
116
+ McpInputRequest,
116
117
  ToolResult,
117
118
  } from './tools/tool-calls'
118
119
  import type { ApprovalSchemaConfig } from './tools/tool-definition'
@@ -148,6 +149,7 @@ import type {
148
149
  ToolCallEndEvent,
149
150
  ToolCallResultEvent,
150
151
  ToolCallStartEvent,
152
+ ToolInputResponse,
151
153
  UIMessage,
152
154
  } from '../../types'
153
155
  import type {
@@ -880,8 +882,10 @@ class TextEngine<
880
882
  private readonly initialClientToolResults: Map<string, any>
881
883
  private readonly resumeApprovals = new Map<string, ToolApprovalResolution>()
882
884
  private readonly resumeClientToolResults = new Map<string, any>()
885
+ private readonly resumeClientToolErrors = new Map<string, string>()
883
886
  private readonly resumeDeniedToolResults = new Map<string, unknown>()
884
887
  private readonly resumeCancelledToolCallIds = new Set<string>()
888
+ private readonly resumeInputResponses = new Map<string, ToolInputResponse>()
885
889
  private readonly resumeGenericInterrupts = new Map<
886
890
  string,
887
891
  ChatResumeGenericResolution
@@ -952,6 +956,8 @@ class TextEngine<
952
956
  message: string
953
957
  code?: string
954
958
  cause?: unknown
959
+ /** Full model text, when a structured-output parse failed (#1485). */
960
+ rawText?: string
955
961
  } | null = null
956
962
  private combinedCompleteEmitted = false
957
963
  private readonly finalStructuredOutput?: {
@@ -1189,10 +1195,54 @@ class TextEngine<
1189
1195
  message: string
1190
1196
  code?: string
1191
1197
  cause?: unknown
1198
+ rawText?: string
1192
1199
  } | null {
1193
1200
  return this.finalizationError
1194
1201
  }
1195
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
+
1196
1246
  async *run(): AsyncGenerator<StreamChunk> {
1197
1247
  this.beforeRun()
1198
1248
  this.logger.agentLoop('run started', {
@@ -1238,7 +1288,8 @@ class TextEngine<
1238
1288
  }
1239
1289
 
1240
1290
  const pendingPhase = yield* this.checkForPendingToolCalls()
1241
- if (pendingPhase === 'wait') {
1291
+ if (pendingPhase === 'stop' || pendingPhase === 'wait') {
1292
+ await this.runTerminalHook()
1242
1293
  return
1243
1294
  }
1244
1295
 
@@ -1347,46 +1398,11 @@ class TextEngine<
1347
1398
  }
1348
1399
  }
1349
1400
 
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
- }
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()
1390
1406
  } catch (error: unknown) {
1391
1407
  if (
1392
1408
  error instanceof Error &&
@@ -2181,8 +2197,10 @@ class TextEngine<
2181
2197
  this.middlewareCtx.context,
2182
2198
  this.toolAbortSignal,
2183
2199
  {
2200
+ clientToolErrors: this.resumeClientToolErrors,
2184
2201
  deniedToolResults: this.resumeDeniedToolResults,
2185
2202
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
2203
+ inputResponses: this.resumeInputResponses,
2186
2204
  },
2187
2205
  )
2188
2206
 
@@ -2207,11 +2225,12 @@ class TextEngine<
2207
2225
  }),
2208
2226
  )
2209
2227
 
2210
- if (
2228
+ const pauseForDecision =
2211
2229
  executionResult.needsApproval.length > 0 ||
2212
2230
  executionResult.needsClientExecution.length > 0 ||
2231
+ executionResult.inputRequired.length > 0 ||
2213
2232
  executionResult.subagentInterrupts.length > 0
2214
- ) {
2233
+ if (pauseForDecision) {
2215
2234
  this.discardDeferredToolCallRunFinishedChunks()
2216
2235
 
2217
2236
  if (allResults.length > 0) {
@@ -2228,6 +2247,7 @@ class TextEngine<
2228
2247
  executionResult.needsApproval,
2229
2248
  executionResult.needsClientExecution,
2230
2249
  [],
2250
+ executionResult.inputRequired,
2231
2251
  executionResult.subagentInterrupts,
2232
2252
  )
2233
2253
  this.setToolPhase(emitted ? 'wait' : 'stop')
@@ -2365,8 +2385,10 @@ class TextEngine<
2365
2385
  this.middlewareCtx.context,
2366
2386
  this.toolAbortSignal,
2367
2387
  {
2388
+ clientToolErrors: this.resumeClientToolErrors,
2368
2389
  deniedToolResults: this.resumeDeniedToolResults,
2369
2390
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
2391
+ inputResponses: this.resumeInputResponses,
2370
2392
  },
2371
2393
  )
2372
2394
 
@@ -2414,16 +2436,18 @@ class TextEngine<
2414
2436
  finishEvent,
2415
2437
  toolCalls,
2416
2438
  afterToolRequests,
2439
+ executionResult.inputRequired,
2417
2440
  )
2418
2441
  this.setToolPhase('wait')
2419
2442
  return
2420
2443
  }
2421
2444
 
2422
- if (
2445
+ const pauseForDecision =
2423
2446
  executionResult.needsApproval.length > 0 ||
2424
2447
  executionResult.needsClientExecution.length > 0 ||
2448
+ executionResult.inputRequired.length > 0 ||
2425
2449
  executionResult.subagentInterrupts.length > 0
2426
- ) {
2450
+ if (pauseForDecision) {
2427
2451
  if (allResults.length > 0) {
2428
2452
  for (const chunk of afterToolBoundaryChunks) {
2429
2453
  yield* this.pipeThroughMiddleware(chunk)
@@ -2435,6 +2459,7 @@ class TextEngine<
2435
2459
  executionResult.needsApproval,
2436
2460
  executionResult.needsClientExecution,
2437
2461
  [],
2462
+ executionResult.inputRequired,
2438
2463
  executionResult.subagentInterrupts,
2439
2464
  )
2440
2465
  this.setToolPhase(emitted ? 'wait' : 'stop')
@@ -2853,6 +2878,7 @@ class TextEngine<
2853
2878
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2854
2879
  > = [],
2855
2880
  genericInterruptIds: ReadonlyArray<string> = [],
2881
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2856
2882
  childInterrupts: ReadonlyArray<Interrupt> = [],
2857
2883
  ): Array<Interrupt> {
2858
2884
  const interrupts: Array<Interrupt> = []
@@ -2971,6 +2997,30 @@ class TextEngine<
2971
2997
  })
2972
2998
  }
2973
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
+ }
2974
3024
  // A subagent tool call raised these. They keep their `subagentRunId`.
2975
3025
  interrupts.push(...childInterrupts)
2976
3026
 
@@ -2995,6 +3045,7 @@ class TextEngine<
2995
3045
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
2996
3046
  > = [],
2997
3047
  genericInterruptIds?: ReadonlyArray<string>,
3048
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
2998
3049
  childInterrupts?: ReadonlyArray<Interrupt>,
2999
3050
  ): StreamChunk {
3000
3051
  return {
@@ -3007,6 +3058,7 @@ class TextEngine<
3007
3058
  clientRequests,
3008
3059
  genericRequests,
3009
3060
  genericInterruptIds,
3061
+ inputRequired,
3010
3062
  childInterrupts,
3011
3063
  ),
3012
3064
  },
@@ -3157,6 +3209,7 @@ class TextEngine<
3157
3209
  genericRequests: ReadonlyArray<
3158
3210
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
3159
3211
  > = [],
3212
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
3160
3213
  childInterrupts: ReadonlyArray<Interrupt> = [],
3161
3214
  ): AsyncGenerator<StreamChunk, boolean, void> {
3162
3215
  yield* this.emitSyntheticRunStarted(finishEvent)
@@ -3171,6 +3224,7 @@ class TextEngine<
3171
3224
  clientRequests,
3172
3225
  genericRequests,
3173
3226
  genericInterruptIds,
3227
+ inputRequired,
3174
3228
  childInterrupts,
3175
3229
  ),
3176
3230
  )
@@ -3207,6 +3261,7 @@ class TextEngine<
3207
3261
  requests?: ReadonlyArray<
3208
3262
  GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
3209
3263
  >,
3264
+ inputRequired: ReadonlyArray<McpInputRequest> = [],
3210
3265
  ): AsyncGenerator<StreamChunk, boolean, void> {
3211
3266
  this.middlewareCtx.phase = phase
3212
3267
  const boundaryRequests =
@@ -3242,6 +3297,7 @@ class TextEngine<
3242
3297
  actionable.approvals,
3243
3298
  actionable.clientRequests,
3244
3299
  boundaryRequests,
3300
+ inputRequired,
3245
3301
  )
3246
3302
  return true
3247
3303
  }
@@ -3289,6 +3345,7 @@ class TextEngine<
3289
3345
  } else if (
3290
3346
  !tool.execute &&
3291
3347
  !clientToolResults.has(toolCall.id) &&
3348
+ !this.resumeClientToolErrors.has(toolCall.id) &&
3292
3349
  !this.resumeCancelledToolCallIds.has(toolCall.id)
3293
3350
  ) {
3294
3351
  clientRequests.push({
@@ -3396,9 +3453,15 @@ class TextEngine<
3396
3453
  content: wireContent,
3397
3454
  role: 'tool' as const,
3398
3455
  }
3456
+ // An outcome always comes with output-error.
3399
3457
  chunks.push(
3400
3458
  (result.state === 'output-error'
3401
- ? withTanstackMetadata(resultChunk, { state: result.state })
3459
+ ? withTanstackMetadata(resultChunk, {
3460
+ state: result.state,
3461
+ ...(result.outcome !== undefined && {
3462
+ toolResultOutcome: result.outcome,
3463
+ }),
3464
+ })
3402
3465
  : resultChunk) as StreamChunk,
3403
3466
  )
3404
3467
 
@@ -3419,22 +3482,63 @@ class TextEngine<
3419
3482
  return false
3420
3483
  }
3421
3484
  })
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
- }),
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)),
3430
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
3431
3525
 
3432
- if (placeholderIdx >= 0) {
3433
- this.messages = [
3434
- ...this.messages.slice(0, placeholderIdx),
3526
+ if (resultMessageIdx >= 0) {
3527
+ const replacedMessages = [
3528
+ ...this.messages.slice(0, resultMessageIdx),
3435
3529
  newToolMessage,
3436
- ...this.messages.slice(placeholderIdx + 1),
3530
+ ...this.messages.slice(resultMessageIdx + 1),
3437
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
3438
3542
  } else {
3439
3543
  this.messages = [...this.messages, newToolMessage]
3440
3544
  }
@@ -3465,7 +3569,10 @@ class TextEngine<
3465
3569
  }
3466
3570
 
3467
3571
  // Only mark as complete if NOT pending execution
3468
- if (!hasPendingExecution) {
3572
+ if (
3573
+ !hasPendingExecution &&
3574
+ !this.resumeDeniedToolResults.has(message.toolCallId)
3575
+ ) {
3469
3576
  completedToolIds.add(message.toolCallId)
3470
3577
  }
3471
3578
  }
@@ -3853,6 +3960,9 @@ class TextEngine<
3853
3960
  // Track whether a RUN_ERROR has been yielded to streaming consumers so
3854
3961
  // we don't emit a duplicate synthetic one at the end.
3855
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 = ''
3856
3966
 
3857
3967
  // Pipe chunks through middleware; yield to consumer only when yieldChunks=true
3858
3968
  for await (const raw of providerStream) {
@@ -3964,14 +4074,20 @@ class TextEngine<
3964
4074
  await this.runOnUsageFromChunk(chunk)
3965
4075
  }
3966
4076
 
4077
+ if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
4078
+ rawText += chunk.delta
4079
+ }
4080
+
3967
4081
  if (chunk.type === EventType.RUN_ERROR) {
3968
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
3969
4086
  this.finalizationError = {
3970
4087
  message: chunk.message,
3971
4088
  ...(chunk.code ? { code: chunk.code } : {}),
3972
- ...(fallbackAdapterError !== undefined
3973
- ? { cause: fallbackAdapterError }
3974
- : {}),
4089
+ ...(cause !== undefined ? { cause } : {}),
4090
+ ...(rawText ? { rawText } : {}),
3975
4091
  }
3976
4092
  }
3977
4093
 
@@ -4155,6 +4271,7 @@ class TextEngine<
4155
4271
  message: `Failed to parse structured output as JSON. Content: ${detail}`,
4156
4272
  code: 'structured-output-parse-failed',
4157
4273
  cause: err,
4274
+ rawText,
4158
4275
  }
4159
4276
  }
4160
4277
  }
@@ -4265,6 +4382,7 @@ class TextEngine<
4265
4382
  resumeToolState: {
4266
4383
  approvals: this.resumeApprovals,
4267
4384
  clientToolResults: this.resumeClientToolResults,
4385
+ clientToolErrors: this.resumeClientToolErrors,
4268
4386
  deniedToolResults: this.resumeDeniedToolResults,
4269
4387
  cancelledToolCallIds: this.resumeCancelledToolCallIds,
4270
4388
  },
@@ -4407,6 +4525,26 @@ class TextEngine<
4407
4525
  : []
4408
4526
  })
4409
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
+ }
4410
4548
  const validated = await validateInterruptResumeBatch({
4411
4549
  threadId: this.threadId,
4412
4550
  interruptedRunId,
@@ -4436,6 +4574,16 @@ class TextEngine<
4436
4574
  })
4437
4575
 
4438
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
+ }
4439
4587
  if (genericPending.length > 0 && genericResolutions) {
4440
4588
  const resolutions = genericPending
4441
4589
  .sort((left, right) => {
@@ -4485,6 +4633,7 @@ class TextEngine<
4485
4633
  this.earlyTermination = true
4486
4634
  } else if (policy.toolResume === 'cancel') {
4487
4635
  for (const request of pendingToolCalls) {
4636
+ if (this.resumeDeniedToolResults.has(request.id)) continue
4488
4637
  this.resumeCancelledToolCallIds.add(request.id)
4489
4638
  }
4490
4639
  }
@@ -4626,6 +4775,11 @@ class TextEngine<
4626
4775
  this.resumeClientToolResults.set(toolCallId, result)
4627
4776
  }
4628
4777
  }
4778
+ if (state?.clientToolErrors) {
4779
+ for (const [toolCallId, errorText] of state.clientToolErrors) {
4780
+ this.resumeClientToolErrors.set(toolCallId, errorText)
4781
+ }
4782
+ }
4629
4783
  if (state?.deniedToolResults) {
4630
4784
  for (const [toolCallId, result] of state.deniedToolResults) {
4631
4785
  this.resumeDeniedToolResults.set(toolCallId, result)
@@ -4685,6 +4839,7 @@ class TextEngine<
4685
4839
  this.earlyTermination = true
4686
4840
  } else if (policy.toolResume === 'cancel') {
4687
4841
  for (const toolCall of this.getPendingToolCallsFromMessages()) {
4842
+ if (this.resumeDeniedToolResults.has(toolCall.id)) continue
4688
4843
  this.resumeCancelledToolCallIds.add(toolCall.id)
4689
4844
  }
4690
4845
  }
@@ -4839,6 +4994,7 @@ class TextEngine<
4839
4994
  results: Array<ToolResult>
4840
4995
  needsApproval: Array<ApprovalRequest>
4841
4996
  needsClientExecution: Array<ClientToolRequest>
4997
+ inputRequired: Array<McpInputRequest>
4842
4998
  subagentInterrupts: Array<Interrupt>
4843
4999
  },
4844
5000
  void
@@ -4849,6 +5005,7 @@ class TextEngine<
4849
5005
  results: Array<ToolResult>
4850
5006
  needsApproval: Array<ApprovalRequest>
4851
5007
  needsClientExecution: Array<ClientToolRequest>
5008
+ inputRequired: Array<McpInputRequest>
4852
5009
  subagentInterrupts: Array<Interrupt>
4853
5010
  },
4854
5011
  void
@@ -5287,6 +5444,16 @@ async function* runRoutedSubagents(
5287
5444
  yield* runChatEngine(options, engineRef)
5288
5445
  return
5289
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
+ }
5290
5457
  const threadId = options.threadId ?? `thread-${Date.now()}`
5291
5458
  const runId = options.runId ?? `run-${Date.now()}`
5292
5459
  const messages = options.messages ?? []
@@ -5742,6 +5909,12 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
5742
5909
  enumerable: true,
5743
5910
  })
5744
5911
  }
5912
+ if (finalizationError.rawText !== undefined) {
5913
+ Object.defineProperty(err, 'rawText', {
5914
+ value: finalizationError.rawText,
5915
+ enumerable: true,
5916
+ })
5917
+ }
5745
5918
  throw err
5746
5919
  }
5747
5920
 
@@ -4,9 +4,13 @@ import {
4
4
  } from '../../utilities/provider-executed'
5
5
  import {
6
6
  isContentPartArray,
7
+ isToolResultOutcome,
7
8
  normalizeToolResult,
8
9
  } from '../../utilities/tool-result'
9
- import { tanstackMetadata } from '../../utilities/merge-metadata'
10
+ import {
11
+ tanstackMetadata,
12
+ withTanstackMetadata,
13
+ } from '../../utilities/merge-metadata'
10
14
  import {
11
15
  splitSubagentWire,
12
16
  subagentWireText,
@@ -29,6 +33,7 @@ import type {
29
33
  function isRecord(value: unknown): value is Record<string, unknown> {
30
34
  return typeof value === 'object' && value !== null && !Array.isArray(value)
31
35
  }
36
+
32
37
  // ===========================
33
38
  // Message Converters
34
39
  // ===========================
@@ -546,6 +551,7 @@ function assistantMetadata(
546
551
  const tanstack: TanStackMessageMetadata = {}
547
552
  if (previous?.model !== undefined) tanstack.model = previous.model
548
553
  if (previous?.runId !== undefined) tanstack.runId = previous.runId
554
+ if (previous?.run?.id !== undefined) tanstack.run = { id: previous.run.id }
549
555
  if (previous?.signature !== undefined) tanstack.signature = previous.signature
550
556
  if (fromParts.length > 0) tanstack.uiResources = fromParts
551
557
  const result = { ...current }
@@ -729,6 +735,11 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
729
735
  (part.state === 'complete' || part.state === 'error') &&
730
736
  !emittedToolResultIds.has(part.toolCallId)
731
737
  ) {
738
+ const metadata =
739
+ part.outcome === undefined
740
+ ? part.metadata
741
+ : withTanstackMetadata(part, { toolResultOutcome: part.outcome })
742
+ .metadata
732
743
  messageList.push({
733
744
  ...(part.id !== undefined && { id: part.id }),
734
745
  ...optionalCreatedAt(part),
@@ -736,7 +747,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
736
747
  content: part.content,
737
748
  toolCallId: part.toolCallId,
738
749
  ...(part.name !== undefined && { name: part.name }),
739
- ...(part.metadata !== undefined && { metadata: part.metadata }),
750
+ ...(metadata !== undefined && { metadata }),
740
751
  ...(part.error !== undefined && { error: part.error }),
741
752
  })
742
753
  emittedToolResultIds.add(part.toolCallId)
@@ -841,6 +852,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
841
852
  : 'User denied this action',
842
853
  }),
843
854
  toolCallId: part.id,
855
+ ...(approved === false && {
856
+ metadata: { tanstack: { toolResultOutcome: 'denied' } },
857
+ }),
844
858
  })
845
859
  emittedToolResultIds.add(part.id)
846
860
  }
@@ -908,11 +922,21 @@ export function modelMessageToUIMessage(
908
922
  }
909
923
  parts.push(structuredOutput)
910
924
  } else if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
925
+ const rawToolResultOutcome =
926
+ tanstackMetadata(modelMessage)?.toolResultOutcome
927
+ const toolResultOutcome = isToolResultOutcome(rawToolResultOutcome)
928
+ ? rawToolResultOutcome
929
+ : undefined
930
+ const resultState =
931
+ modelMessage.error === undefined && toolResultOutcome === undefined
932
+ ? 'complete'
933
+ : 'error'
911
934
  parts.push({
912
935
  type: 'tool-result',
913
936
  toolCallId: modelMessage.toolCallId,
914
937
  content: toolResultContent(modelMessage.content),
915
- state: modelMessage.error === undefined ? 'complete' : 'error',
938
+ state: resultState,
939
+ ...(toolResultOutcome !== undefined && { outcome: toolResultOutcome }),
916
940
  ...(modelMessage.id !== undefined && { id: modelMessage.id }),
917
941
  ...(modelMessage.name !== undefined && { name: modelMessage.name }),
918
942
  ...(modelMessage.metadata !== undefined && {
@@ -1228,6 +1252,14 @@ export function modelMessagesToUIMessages(
1228
1252
  currentAssistantMessage.role === 'assistant'
1229
1253
  ) {
1230
1254
  const content = toolResultContent(msg.content)
1255
+ const rawToolResultOutcome = tanstackMetadata(msg)?.toolResultOutcome
1256
+ const toolResultOutcome = isToolResultOutcome(rawToolResultOutcome)
1257
+ ? rawToolResultOutcome
1258
+ : undefined
1259
+ const resultState =
1260
+ msg.error === undefined && toolResultOutcome === undefined
1261
+ ? 'complete'
1262
+ : 'error'
1231
1263
  const toolCallPart = currentAssistantMessage.parts.find(
1232
1264
  (part): part is ToolCallPart =>
1233
1265
  part.type === 'tool-call' && part.id === msg.toolCallId,
@@ -1238,14 +1270,17 @@ export function modelMessagesToUIMessages(
1238
1270
  typeof content === 'string'
1239
1271
  ? parseToolResultContent(content)
1240
1272
  : content
1241
- toolCallPart.state = msg.error === undefined ? 'complete' : 'error'
1273
+ toolCallPart.state = resultState
1242
1274
  }
1243
1275
 
1244
1276
  currentAssistantMessage.parts.push({
1245
1277
  type: 'tool-result',
1246
1278
  toolCallId: msg.toolCallId,
1247
1279
  content,
1248
- state: msg.error === undefined ? 'complete' : 'error',
1280
+ state: resultState,
1281
+ ...(toolResultOutcome !== undefined && {
1282
+ outcome: toolResultOutcome,
1283
+ }),
1249
1284
  ...(msg.id !== undefined &&
1250
1285
  msg.id !== currentAssistantMessage.id && { id: msg.id }),
1251
1286
  ...(msg.name !== undefined && { name: msg.name }),