@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
@@ -1,8 +1,14 @@
1
1
  import { normalizeToolResult } from '../../../utilities/tool-result'
2
2
  import { tanstackMetadata } from '../../../utilities/merge-metadata'
3
3
  import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
4
+ import { mergeStreams } from '../../../utilities/merge-streams'
4
5
  import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
5
- import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
6
+ import {
7
+ StandardSchemaValidationError,
8
+ isStandardSchema,
9
+ parseWithStandardSchema,
10
+ validateWithStandardSchema,
11
+ } from './schema-converter'
6
12
  import type { ToolApprovalResolution } from '../../../interrupts'
7
13
  import type {
8
14
  AnyTool,
@@ -13,12 +19,15 @@ import type {
13
19
  ModelMessage,
14
20
  RunFinishedEvent,
15
21
  StreamChunk,
22
+ TextOptions,
16
23
  Tool,
17
24
  ToolCall,
18
25
  ToolCallArgsEvent,
19
26
  ToolCallEndEvent,
20
27
  ToolCallStartEvent,
21
28
  ToolExecutionContext,
29
+ ToolInputResponse,
30
+ ToolResultOutcome,
22
31
  ToolOutputState,
23
32
  } from '../../../types'
24
33
  import type {
@@ -462,6 +471,8 @@ export interface ToolResult {
462
471
  toolName: string
463
472
  result: any
464
473
  state?: 'output-available' | 'output-error'
474
+ /** Set when the user or middleware cancelled or denied the tool call; state is output-error. */
475
+ outcome?: ToolResultOutcome
465
476
  /** Duration of tool execution in milliseconds (only for server-executed tools) */
466
477
  duration?: number
467
478
  /**
@@ -490,9 +501,38 @@ export interface ClientToolRequest {
490
501
  input: any
491
502
  }
492
503
 
504
+ /** Form or sampling input that paused a server tool. */
505
+ export interface McpInputRequest {
506
+ toolCallId: string
507
+ toolName: string
508
+ kind: 'form' | 'sampling'
509
+ request: unknown
510
+ }
511
+
512
+ interface McpInputRequiredThrow {
513
+ name: 'MCPInputRequiredError'
514
+ kind: 'form' | 'sampling'
515
+ request: unknown
516
+ }
517
+
518
+ function isMcpInputRequired(value: unknown): value is McpInputRequiredThrow {
519
+ if (typeof value !== 'object' || value === null) return false
520
+ if (!('name' in value) || value.name !== 'MCPInputRequiredError') {
521
+ return false
522
+ }
523
+ if (!('kind' in value)) return false
524
+ const kindIsFormOrSampling =
525
+ value.kind === 'form' || value.kind === 'sampling'
526
+ if (!kindIsFormOrSampling) return false
527
+ return 'request' in value
528
+ }
529
+
493
530
  export interface ToolResumeExecutionState {
531
+ clientToolErrors?: ReadonlyMap<string, string>
494
532
  deniedToolResults?: ReadonlyMap<string, unknown>
495
533
  cancelledToolCallIds?: ReadonlySet<string>
534
+ /** Answers to `mcp_input` interrupts, by tool call id. */
535
+ inputResponses?: ReadonlyMap<string, ToolInputResponse>
496
536
  }
497
537
 
498
538
  function approvalResolution(
@@ -527,6 +567,8 @@ interface ExecuteToolCallsResult {
527
567
  needsApproval: Array<ApprovalRequest>
528
568
  /** Tools that need client-side execution */
529
569
  needsClientExecution: Array<ClientToolRequest>
570
+ /** Server tools that paused for MCP form or sampling input */
571
+ inputRequired: Array<McpInputRequest>
530
572
  /** Interrupts raised by subagents that run as tools */
531
573
  subagentInterrupts: Array<Interrupt>
532
574
  }
@@ -639,6 +681,7 @@ export async function* executeServerTool<TContext = unknown>(
639
681
  pendingEvents: Array<CustomEvent | StreamChunk>,
640
682
  results: Array<ToolResult>,
641
683
  middlewareHooks?: ToolExecutionMiddlewareHooks,
684
+ inputRequired?: Array<McpInputRequest>,
642
685
  subagentInterrupts?: Array<Interrupt>,
643
686
  ): AsyncGenerator<CustomEvent | StreamChunk, void, void> {
644
687
  const startTime = Date.now()
@@ -744,6 +787,18 @@ export async function* executeServerTool<TContext = unknown>(
744
787
  throw error
745
788
  }
746
789
 
790
+ // Same shape as MCPInputRequiredError. Pause instead of a tool error.
791
+ if (isMcpInputRequired(error)) {
792
+ if (!inputRequired) throw error
793
+ inputRequired.push({
794
+ toolCallId: toolCall.id,
795
+ toolName,
796
+ kind: error.kind,
797
+ request: error.request,
798
+ })
799
+ return
800
+ }
801
+
747
802
  const message = error instanceof Error ? error.message : 'Unknown error'
748
803
  results.push({
749
804
  toolCallId: toolCall.id,
@@ -768,17 +823,35 @@ export async function* executeServerTool<TContext = unknown>(
768
823
  }
769
824
  }
770
825
 
771
- function buildClientToolResult(
826
+ async function buildClientToolResult(
772
827
  toolCallId: string,
773
828
  toolName: string,
774
829
  tool: AnyTool,
775
830
  rawResult: unknown,
776
831
  input?: unknown,
777
- ): ToolResult {
832
+ errorText?: string,
833
+ ): Promise<ToolResult> {
834
+ if (errorText !== undefined) {
835
+ return {
836
+ toolCallId,
837
+ toolName,
838
+ result: { error: errorText },
839
+ input,
840
+ state: 'output-error',
841
+ }
842
+ }
843
+
778
844
  try {
779
845
  let result = rawResult
780
846
  if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
781
- result = parseWithStandardSchema(tool.outputSchema, result)
847
+ const validation = await validateWithStandardSchema<unknown>(
848
+ tool.outputSchema,
849
+ result,
850
+ )
851
+ if (!validation.success) {
852
+ throw new StandardSchemaValidationError(validation.issues)
853
+ }
854
+ result = validation.data
782
855
  }
783
856
 
784
857
  const parsed =
@@ -816,6 +889,9 @@ function buildClientToolResult(
816
889
  * @param approvals - Map keyed by toolCallId (or `approval_${toolCallId}`) → ToolApprovalResolution
817
890
  * @param clientResults - Map of client-side execution results (toolCallId -> result)
818
891
  * @param createCustomEventChunk - Factory to create CustomEvent chunks (optional)
892
+ * @param toolExecution - `'parallel'` (default) prepares every call in call
893
+ * order, then starts the server tools together. `'sequential'` runs one
894
+ * call at a time. Results come back in call order either way.
819
895
  */
820
896
  export async function* executeToolCalls<TContext = unknown>(
821
897
  toolCalls: Array<ToolCall>,
@@ -831,11 +907,12 @@ export async function* executeToolCalls<TContext = unknown>(
831
907
  userContext?: TContext,
832
908
  abortSignal?: AbortSignal,
833
909
  resumeState?: ToolResumeExecutionState,
910
+ toolExecution: NonNullable<TextOptions['toolExecution']> = 'parallel',
834
911
  ): AsyncGenerator<CustomEvent | StreamChunk, ExecuteToolCallsResult, void> {
835
912
  const results: Array<ToolResult> = []
836
913
  const needsApproval: Array<ApprovalRequest> = []
837
914
  const needsClientExecution: Array<ClientToolRequest> = []
838
- const subagentInterrupts: Array<Interrupt> = []
915
+ const inputRequired: Array<McpInputRequest> = []
839
916
 
840
917
  // Create tool lookup map
841
918
  const toolMap = new Map<string, AnyTool>()
@@ -843,6 +920,66 @@ export async function* executeToolCalls<TContext = unknown>(
843
920
  toolMap.set(tool.name, tool)
844
921
  }
845
922
 
923
+ const runsInOrder = toolExecution === 'sequential'
924
+ // Parallel mode collects the server runs here. `mergeStreams` starts them
925
+ // together after the loop.
926
+ const runs: Array<AsyncGenerator<CustomEvent | StreamChunk, void, void>> = []
927
+ // Errors thrown by a run or a before-hook. They are rethrown only after every
928
+ // started tool has finished, so no tool outlives the batch.
929
+ const failures: Array<unknown> = []
930
+ // Each call's subagent interrupts, so they come back in call order.
931
+ const interruptsByCall = new Map<string, Array<Interrupt>>()
932
+
933
+ // A tool that has not started when the run aborts never starts. It gets an
934
+ // error result, so every call of the batch still has a result.
935
+ async function* runServerTool(
936
+ toolCall: ToolCall,
937
+ tool: AnyTool,
938
+ toolName: string,
939
+ input: unknown,
940
+ context: ToolExecutionContext<TContext>,
941
+ pendingEvents: Array<CustomEvent | StreamChunk>,
942
+ ): AsyncGenerator<CustomEvent | StreamChunk, void, void> {
943
+ try {
944
+ if (abortSignal?.aborted) {
945
+ results.push({
946
+ toolCallId: toolCall.id,
947
+ toolName,
948
+ result: { error: 'Operation aborted' },
949
+ input,
950
+ state: 'output-error',
951
+ duration: 0,
952
+ })
953
+ await middlewareHooks?.onAfterToolCall?.({
954
+ toolCall,
955
+ tool,
956
+ toolName,
957
+ toolCallId: toolCall.id,
958
+ ok: false,
959
+ duration: 0,
960
+ error: new Error('Operation aborted'),
961
+ })
962
+ return
963
+ }
964
+ const interrupts: Array<Interrupt> = []
965
+ interruptsByCall.set(toolCall.id, interrupts)
966
+ yield* executeServerTool(
967
+ toolCall,
968
+ tool,
969
+ toolName,
970
+ input,
971
+ context,
972
+ pendingEvents,
973
+ results,
974
+ middlewareHooks,
975
+ inputRequired,
976
+ interrupts,
977
+ )
978
+ } catch (error) {
979
+ failures.push(error)
980
+ }
981
+ }
982
+
846
983
  // Batch gating: when any tool in the batch still needs an approval decision,
847
984
  // defer all execution so side effects don't happen before the user decides.
848
985
  const hasPendingApprovals = toolCalls.some((tc) => {
@@ -891,6 +1028,7 @@ export async function* executeToolCalls<TContext = unknown>(
891
1028
  toolName,
892
1029
  result: { error: 'Tool execution cancelled' },
893
1030
  state: 'output-error',
1031
+ outcome: 'cancelled',
894
1032
  })
895
1033
  continue
896
1034
  }
@@ -942,10 +1080,12 @@ export async function* executeToolCalls<TContext = unknown>(
942
1080
 
943
1081
  // Create a ToolExecutionContext for this tool call with event emission
944
1082
  const pendingEvents: Array<CustomEvent | StreamChunk> = []
1083
+ const inputResponse = resumeState?.inputResponses?.get(toolCall.id)
945
1084
  const context = {
946
1085
  toolCallId: toolCall.id,
947
1086
  context: userContext,
948
1087
  abortSignal,
1088
+ ...(inputResponse !== undefined ? { inputResponse } : {}),
949
1089
  emitCustomEvent: (
950
1090
  eventName: string,
951
1091
  value: Record<string, any>,
@@ -980,14 +1120,16 @@ export async function* executeToolCalls<TContext = unknown>(
980
1120
  if (approved) {
981
1121
  input = editedApprovalArgs(resolution) ?? input
982
1122
  // Approved - check if client has executed
983
- if (clientResults.has(toolCall.id)) {
1123
+ const clientError = resumeState?.clientToolErrors?.get(toolCall.id)
1124
+ if (clientResults.has(toolCall.id) || clientError !== undefined) {
984
1125
  results.push(
985
- buildClientToolResult(
1126
+ await buildClientToolResult(
986
1127
  toolCall.id,
987
1128
  toolName,
988
1129
  tool,
989
1130
  clientResults.get(toolCall.id),
990
1131
  input,
1132
+ clientError,
991
1133
  ),
992
1134
  )
993
1135
  } else {
@@ -1008,6 +1150,7 @@ export async function* executeToolCalls<TContext = unknown>(
1008
1150
  deniedApprovalResult(resolution),
1009
1151
  input,
1010
1152
  state: 'output-error',
1153
+ outcome: 'denied',
1011
1154
  })
1012
1155
  }
1013
1156
  } else {
@@ -1021,14 +1164,16 @@ export async function* executeToolCalls<TContext = unknown>(
1021
1164
  }
1022
1165
  } else {
1023
1166
  // No approval needed - check if client has executed
1024
- if (clientResults.has(toolCall.id)) {
1167
+ const clientError = resumeState?.clientToolErrors?.get(toolCall.id)
1168
+ if (clientResults.has(toolCall.id) || clientError !== undefined) {
1025
1169
  results.push(
1026
- buildClientToolResult(
1170
+ await buildClientToolResult(
1027
1171
  toolCall.id,
1028
1172
  toolName,
1029
1173
  tool,
1030
1174
  clientResults.get(toolCall.id),
1031
1175
  input,
1176
+ clientError,
1032
1177
  ),
1033
1178
  )
1034
1179
  } else {
@@ -1048,50 +1193,7 @@ export async function* executeToolCalls<TContext = unknown>(
1048
1193
  const approvalId = `approval_${toolCall.id}`
1049
1194
  const resolution = approvalResolution(approvals, toolCall.id)
1050
1195
 
1051
- // Check if approval decision exists
1052
- if (resolution !== undefined) {
1053
- const approved = isApproved(resolution)
1054
-
1055
- if (approved) {
1056
- input = editedApprovalArgs(resolution) ?? input
1057
- // Apply middleware before-hook for approved tools
1058
- if (middlewareHooks) {
1059
- const decision = await applyBeforeToolCallDecision(
1060
- toolCall,
1061
- tool,
1062
- input,
1063
- toolName,
1064
- middlewareHooks,
1065
- results,
1066
- )
1067
- if (!decision.proceed) continue
1068
- input = decision.input
1069
- }
1070
-
1071
- yield* executeServerTool(
1072
- toolCall,
1073
- tool,
1074
- toolName,
1075
- input,
1076
- context,
1077
- pendingEvents,
1078
- results,
1079
- middlewareHooks,
1080
- subagentInterrupts,
1081
- )
1082
- } else {
1083
- // User declined
1084
- results.push({
1085
- toolCallId: toolCall.id,
1086
- toolName,
1087
- result:
1088
- resumeState?.deniedToolResults?.get(toolCall.id) ??
1089
- deniedApprovalResult(resolution),
1090
- input,
1091
- state: 'output-error',
1092
- })
1093
- }
1094
- } else {
1196
+ if (resolution === undefined) {
1095
1197
  // Need approval
1096
1198
  needsApproval.push({
1097
1199
  toolCallId: toolCall.id,
@@ -1099,36 +1201,81 @@ export async function* executeToolCalls<TContext = unknown>(
1099
1201
  input,
1100
1202
  approvalId,
1101
1203
  })
1204
+ continue
1102
1205
  }
1103
- continue
1206
+ if (!isApproved(resolution)) {
1207
+ // User declined
1208
+ results.push({
1209
+ toolCallId: toolCall.id,
1210
+ toolName,
1211
+ result:
1212
+ resumeState?.deniedToolResults?.get(toolCall.id) ??
1213
+ deniedApprovalResult(resolution),
1214
+ input,
1215
+ state: 'output-error',
1216
+ outcome: 'denied',
1217
+ })
1218
+ continue
1219
+ }
1220
+ // Approved: run it like any other server tool below.
1221
+ input = editedApprovalArgs(resolution) ?? input
1104
1222
  }
1105
1223
 
1106
- // CASE 3: Normal server tool - execute immediately
1224
+ // CASE 3: Server tool, approved or with no approval
1107
1225
  if (middlewareHooks) {
1108
- const decision = await applyBeforeToolCallDecision(
1109
- toolCall,
1110
- tool,
1111
- input,
1112
- toolName,
1113
- middlewareHooks,
1114
- results,
1115
- )
1226
+ let decision: Awaited<ReturnType<typeof applyBeforeToolCallDecision>>
1227
+ try {
1228
+ decision = await applyBeforeToolCallDecision(
1229
+ toolCall,
1230
+ tool,
1231
+ input,
1232
+ toolName,
1233
+ middlewareHooks,
1234
+ results,
1235
+ )
1236
+ } catch (error) {
1237
+ // The calls prepared so far still run, as they would one at a time.
1238
+ failures.push(error)
1239
+ break
1240
+ }
1116
1241
  if (!decision.proceed) continue
1117
1242
  input = decision.input
1118
1243
  }
1119
1244
 
1120
- yield* executeServerTool(
1245
+ const run = runServerTool(
1121
1246
  toolCall,
1122
1247
  tool,
1123
1248
  toolName,
1124
1249
  input,
1125
1250
  context,
1126
1251
  pendingEvents,
1127
- results,
1128
- middlewareHooks,
1129
- subagentInterrupts,
1130
1252
  )
1253
+ if (!runsInOrder) {
1254
+ runs.push(run)
1255
+ continue
1256
+ }
1257
+ yield* run
1258
+ if (failures.length > 0) break
1131
1259
  }
1132
1260
 
1133
- return { results, needsApproval, needsClientExecution, subagentInterrupts }
1261
+ yield* mergeStreams(runs)
1262
+ if (failures.length > 0) throw failures[0]
1263
+
1264
+ // Parallel tools finish in any order. The model gets the results, input
1265
+ // requests, and interrupts in the order it made the calls.
1266
+ const callOrder = new Map(toolCalls.map((tc, index) => [tc.id, index]))
1267
+ const byCallOrder = (a: { toolCallId: string }, b: { toolCallId: string }) =>
1268
+ (callOrder.get(a.toolCallId) ?? 0) - (callOrder.get(b.toolCallId) ?? 0)
1269
+ results.sort(byCallOrder)
1270
+ inputRequired.sort(byCallOrder)
1271
+
1272
+ return {
1273
+ results,
1274
+ needsApproval,
1275
+ needsClientExecution,
1276
+ inputRequired,
1277
+ subagentInterrupts: toolCalls.flatMap(
1278
+ (tc) => interruptsByCall.get(tc.id) ?? [],
1279
+ ),
1280
+ }
1134
1281
  }
@@ -99,6 +99,7 @@ export interface ServerTool<
99
99
  outputSchema?: TOutput
100
100
  needsApproval?: TNeedsApproval
101
101
  approvalSchema?: TApprovalSchema
102
+ execution?: 'task'
102
103
  }
103
104
 
104
105
  /**
@@ -127,6 +128,7 @@ export interface ClientTool<
127
128
  outputSchema?: TOutput
128
129
  needsApproval?: TNeedsApproval
129
130
  approvalSchema?: TApprovalSchema
131
+ execution?: 'task'
130
132
  lazy?: boolean
131
133
  metadata?: Record<string, unknown>
132
134
  execute?: ToolExecuteFunction<TInput, TOutput, TContext>
@@ -158,6 +160,7 @@ export interface ToolDefinitionInstance<
158
160
  outputSchema: TOutput
159
161
  needsApproval?: TNeedsApproval
160
162
  approvalSchema: TApprovalSchema
163
+ execution?: 'task'
161
164
  readonly [toolApprovalCapability]?: {
162
165
  needsApproval: TNeedsApproval
163
166
  approvalSchema: TApprovalSchema
@@ -221,6 +224,7 @@ export type ToolDefinitionConfig<
221
224
  outputSchema?: TOutput
222
225
  lazy?: boolean
223
226
  metadata?: Record<string, unknown>
227
+ execution?: 'task'
224
228
  } & ApprovalConfig<TNeedsApproval, TApprovalSchema>
225
229
 
226
230
  /**
@@ -353,6 +357,7 @@ export function toolDefinition<
353
357
  const outputSchema = config.outputSchema as TOutput
354
358
  const approvalSchema = config.approvalSchema as TApprovalSchema
355
359
  const needsApproval = config.needsApproval as TNeedsApproval | undefined
360
+ const execution = config.execution
356
361
 
357
362
  const definition: ToolDefinition<
358
363
  TInput,
@@ -367,6 +372,7 @@ export function toolDefinition<
367
372
  outputSchema,
368
373
  approvalSchema,
369
374
  needsApproval,
375
+ execution,
370
376
  server<TContext = unknown>(
371
377
  execute: ToolExecuteFunction<TInput, TOutput, TContext>,
372
378
  ): ServerTool<
@@ -385,6 +391,7 @@ export function toolDefinition<
385
391
  outputSchema,
386
392
  approvalSchema,
387
393
  needsApproval,
394
+ execution,
388
395
  execute,
389
396
  }
390
397
  },
@@ -407,6 +414,7 @@ export function toolDefinition<
407
414
  outputSchema,
408
415
  approvalSchema,
409
416
  needsApproval,
417
+ execution,
410
418
  ...(execute !== undefined && { execute }),
411
419
  }
412
420
  },
@@ -131,6 +131,10 @@ export interface EvaluateAdapterResult {
131
131
  model: string
132
132
  answers: Record<string, WireAnswer>
133
133
  usage: TokenUsage
134
+ /** Provider response id, for example to look the request up later. */
135
+ id?: string
136
+ /** Upstream provider that served the request, when a router reports it. */
137
+ provider?: string
134
138
  }
135
139
 
136
140
  /**
@@ -100,6 +100,10 @@ export interface EvaluateResultMeta {
100
100
  /** Resolved model id from the provider. */
101
101
  model: string
102
102
  usage: TokenUsage
103
+ /** Provider response id, when the adapter returns one. */
104
+ id?: string
105
+ /** Upstream provider that served the request, when the adapter returns one. */
106
+ provider?: string
103
107
  }
104
108
 
105
109
  /**
@@ -576,6 +580,8 @@ export async function decide<
576
580
  return withMeta(answers, {
577
581
  model: result.model,
578
582
  usage: result.usage,
583
+ ...(result.id !== undefined && { id: result.id }),
584
+ ...(result.provider !== undefined && { provider: result.provider }),
579
585
  })
580
586
  } catch (error) {
581
587
  const duration = Date.now() - startTime
@@ -445,7 +445,7 @@ export interface ListVoicesActivityOptions<
445
445
  * import { elevenlabsSpeech } from '@tanstack/ai-elevenlabs'
446
446
  *
447
447
  * const { voices } = await listVoices({
448
- * adapter: elevenlabsSpeech('eleven_v3'),
448
+ * adapter: elevenlabsSpeech('eleven_v4'),
449
449
  * origins: ['generated', 'cloned'],
450
450
  * })
451
451
  * ```
@@ -1,3 +1,4 @@
1
+ import { snapToDurationOption } from './snap'
1
2
  import type {
2
3
  ModelInputModalitiesByName,
3
4
  VideoGenerationOptions,
@@ -25,6 +26,12 @@ export type DurationOptions<T extends string | number | undefined> =
25
26
  }
26
27
  | { kind: 'none' }
27
28
 
29
+ /**
30
+ * Spellings of one clip length: the number `6`, the string `"6"`, or the
31
+ * template `"6s"`.
32
+ */
33
+ export type VideoDurationSpell<N extends number> = N | `${N}` | `${N}s`
34
+
28
35
  /**
29
36
  * Configuration for video adapter instances
30
37
  *
@@ -126,10 +133,15 @@ export interface VideoAdapter<
126
133
  availableDurations: () => DurationOptions<TModelDurationByName[TModel]>
127
134
 
128
135
  /**
129
- * Coerce a raw seconds value to the closest valid duration for this model.
130
- * Returns `undefined` for models with no duration field.
136
+ * Coerce `input` to the closest duration this model accepts.
137
+ * `input` may be seconds (`7`), a numeric string (`"7"`), a template
138
+ * (`"6s"`), or a keyword the model lists (`"auto"`).
139
+ * Returns `undefined` when the model has no duration field, or when
140
+ * `input` is a keyword that model does not list.
131
141
  */
132
- snapDuration: (seconds: number) => TModelDurationByName[TModel] | undefined
142
+ snapDuration: (
143
+ input: number | string,
144
+ ) => TModelDurationByName[TModel] | undefined
133
145
  }
134
146
 
135
147
  /**
@@ -208,11 +220,13 @@ export abstract class BaseVideoAdapter<
208
220
  }
209
221
 
210
222
  /**
211
- * Default implementation returns `undefined`. Adapters that have declared
212
- * their per-model duration map should override.
223
+ * Uses `availableDurations()`. Adapters that declare a duration map only
224
+ * need to override that method.
213
225
  */
214
- snapDuration(_seconds: number): TModelDurationByName[TModel] | undefined {
215
- return undefined
226
+ snapDuration(
227
+ input: number | string,
228
+ ): TModelDurationByName[TModel] | undefined {
229
+ return snapToDurationOption(input, this.availableDurations())
216
230
  }
217
231
 
218
232
  protected generateId(): string {
@@ -173,10 +173,11 @@ export type VideoCreateOptions<
173
173
  /** Video size — format depends on the provider (e.g., "16:9", "1280x720") */
174
174
  size?: VideoSizeForAdapter<TAdapter>
175
175
  /**
176
- * Video duration in seconds. Adapters that declare a per-model duration
177
- * map narrow this to the model's valid union (e.g. `4 | 6 | 8` for Veo 3).
178
- * Pass `adapter.snapDuration(seconds)` to coerce raw seconds to a valid
179
- * value.
176
+ * Video duration. Adapters that declare a per-model duration map narrow
177
+ * this to that model's union (for example `4 | 6 | 8` for Veo 3, or
178
+ * `4 | "4" | "4s"` for Sora). Pass `adapter.snapDuration(input)` to coerce
179
+ * a raw value. `input` may be seconds, a `"6s"` template, or `"auto"` when
180
+ * the model lists it.
180
181
  */
181
182
  duration?: VideoDurationForAdapter<TAdapter>
182
183
  /**