@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
@@ -344,6 +344,7 @@ export interface ChatMiddlewareConfig {
344
344
  export interface ChatResumeToolState {
345
345
  approvals?: ReadonlyMap<string, ToolApprovalResolution> | undefined
346
346
  clientToolResults?: ReadonlyMap<string, unknown> | undefined
347
+ clientToolErrors?: ReadonlyMap<string, string> | undefined
347
348
  genericInterrupts?:
348
349
  | ReadonlyMap<string, ChatResumeGenericResolution>
349
350
  | undefined
@@ -11,6 +11,7 @@ import type {
11
11
  StructuredOutputPart,
12
12
  ThinkingPart,
13
13
  ToolCallPart,
14
+ ToolResultOutcome,
14
15
  ToolResultPart,
15
16
  UIMessage,
16
17
  } from '../../../types'
@@ -117,6 +118,7 @@ export function updateToolResultPart(
117
118
  content: string | Array<ContentPart>,
118
119
  state: ToolResultState,
119
120
  error?: string,
121
+ outcome?: ToolResultOutcome,
120
122
  ): Array<UIMessage> {
121
123
  return messages.map((msg) => {
122
124
  if (msg.id !== messageId) {
@@ -134,6 +136,7 @@ export function updateToolResultPart(
134
136
  toolCallId,
135
137
  content,
136
138
  state,
139
+ ...(outcome !== undefined && { outcome }),
137
140
  ...(error && { error }),
138
141
  }
139
142
 
@@ -36,6 +36,8 @@ import {
36
36
  import { getChunkRunId } from '../../../utilities/chunk-ids'
37
37
  import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
38
38
  import {
39
+ isContentPartArray,
40
+ isToolResultOutcome,
39
41
  normalizeToolResult,
40
42
  toolResultErrorText,
41
43
  } from '../../../utilities/tool-result'
@@ -181,6 +183,23 @@ function interruptBatchHasGeneric(interrupts: Array<Interrupt>): boolean {
181
183
  })
182
184
  }
183
185
 
186
+ /**
187
+ * The canonical arguments string for a `TOOL_CALL_END.input`, or `undefined`
188
+ * when JSON cannot carry it: `JSON.stringify` returns `undefined` (despite
189
+ * its declared type) for a top-level function or symbol and throws on BigInt
190
+ * and circular references. `null` is not tool arguments either, so it also
191
+ * keeps the streamed value instead of writing `arguments = "null"`.
192
+ */
193
+ function serializeToolInput(input: unknown): string | undefined {
194
+ if (input === undefined || input === null) return undefined
195
+ try {
196
+ const serialized: unknown = JSON.stringify(input)
197
+ return typeof serialized === 'string' ? serialized : undefined
198
+ } catch {
199
+ return undefined
200
+ }
201
+ }
202
+
184
203
  /**
185
204
  * StreamProcessor - State machine for processing AI response streams
186
205
  *
@@ -1953,11 +1972,13 @@ export class StreamProcessor {
1953
1972
  * Handle TOOL_CALL_END event — arguments are finalized (input-complete).
1954
1973
  * Tool output arrives on TOOL_CALL_RESULT, not on this event.
1955
1974
  *
1956
- * If TOOL_CALL_END carries parsed `input`, use it as the canonical arguments:
1957
- * back-fill the accumulated string when no TOOL_CALL_ARGS deltas were seen
1958
- * (adapters that deliver the whole input on END — e.g. Anthropic
1959
- * server_tool_use / web_search — issue #839) and override the rendered part's
1960
- * `input` with the canonical value.
1975
+ * If TOOL_CALL_END carries parsed `input`, it is the canonical arguments:
1976
+ * write it into the accumulated string and override the rendered part's
1977
+ * `input` with it. That covers adapters that deliver the whole input on END
1978
+ * (e.g. Anthropic server_tool_use / web_search — issue #839) and adapters
1979
+ * that stream the wire arguments and then normalize them (OpenAI strict-mode
1980
+ * null widening, undone by the adapter after #939), so `arguments` and
1981
+ * `input` never disagree on the persisted part.
1961
1982
  *
1962
1983
  * @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
1963
1984
  */
@@ -1980,16 +2001,15 @@ export class StreamProcessor {
1980
2001
  // Transition the tool call to input-complete (the authoritative completion signal)
1981
2002
  const existingToolCall = msgState.toolCalls.get(chunk.toolCallId)
1982
2003
  if (existingToolCall && existingToolCall.state !== 'input-complete') {
1983
- // Back-fill the arguments string from the parsed input when no
1984
- // TOOL_CALL_ARGS deltas were received, so completeToolCall's strict parse
1985
- // surfaces the correct value on the ToolCallPart.
1986
- if (input !== undefined && !existingToolCall.arguments) {
1987
- try {
1988
- existingToolCall.arguments = JSON.stringify(input)
1989
- } catch {
1990
- // circular refs, BigInt, etc. — leave arguments empty rather than
1991
- // aborting stream processing
1992
- }
2004
+ // The parsed input replaces the accumulated arguments string, so
2005
+ // completeToolCall's strict parse surfaces the canonical value on the
2006
+ // ToolCallPart even when the streamed deltas carried a provider-side
2007
+ // reshaping of it. An input JSON cannot carry is not canonical: both
2008
+ // `arguments` and `input` then stay with the streamed value, so the
2009
+ // two never disagree.
2010
+ const serializedInput = serializeToolInput(input)
2011
+ if (serializedInput !== undefined) {
2012
+ existingToolCall.arguments = serializedInput
1993
2013
  }
1994
2014
 
1995
2015
  const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
@@ -1998,7 +2018,7 @@ export class StreamProcessor {
1998
2018
  // Canonicalize on the parsed input: overrides the accumulated-args parse
1999
2019
  // that completeToolCall wrote (adapters may coerce values differently
2000
2020
  // between streamed args and the final structured input).
2001
- if (input !== undefined) {
2021
+ if (serializedInput !== undefined) {
2002
2022
  existingToolCall.parsedArguments = input
2003
2023
  this.messages = updateToolCallPart(this.messages, messageId, {
2004
2024
  id: existingToolCall.id,
@@ -2042,9 +2062,14 @@ export class StreamProcessor {
2042
2062
  if (!messageId) return
2043
2063
 
2044
2064
  const extra = chunk as AdapterYieldChunk
2065
+ const rawToolResultOutcome = tanstackMetadata(chunk)?.toolResultOutcome
2066
+ const toolResultOutcome = isToolResultOutcome(rawToolResultOutcome)
2067
+ ? rawToolResultOutcome
2068
+ : undefined
2045
2069
  const isOutputError =
2046
2070
  extra.state === 'output-error' ||
2047
- tanstackMetadata(chunk)?.state === 'output-error'
2071
+ tanstackMetadata(chunk)?.state === 'output-error' ||
2072
+ toolResultOutcome !== undefined
2048
2073
 
2049
2074
  // Step 1: Update the tool-call part's output field
2050
2075
  let output: unknown
@@ -2069,9 +2094,14 @@ export class StreamProcessor {
2069
2094
  this.messages,
2070
2095
  messageId,
2071
2096
  chunk.toolCallId,
2072
- aguiContentToContentParts(chunk.content),
2097
+ // The server sends a ContentPart[] result as a JSON string. Keep the
2098
+ // parsed array so uiMessagesToWire can carry it in metadata.
2099
+ isContentPartArray(output)
2100
+ ? output
2101
+ : aguiContentToContentParts(chunk.content),
2073
2102
  resultState,
2074
2103
  resultState === 'error' ? toolResultErrorText(output) : undefined,
2104
+ toolResultOutcome,
2075
2105
  )
2076
2106
  this.emitMessagesChange()
2077
2107
  }
@@ -2678,11 +2708,11 @@ export class StreamProcessor {
2678
2708
  toolCall.parsedArguments = undefined
2679
2709
  }
2680
2710
 
2681
- // Don't downgrade the rendered part of a call that already reached the
2682
- // terminal 'error' state (e.g. an output-error TOOL_CALL_RESULT arrived
2711
+ // Don't downgrade the rendered part of a call that already reached a
2712
+ // terminal 'error' or 'complete' state (e.g. a tool result arrived
2683
2713
  // without a preceding TOOL_CALL_END). The RUN_FINISHED / finalizeStream
2684
- // safety net must not clobber a failed call back to 'input-complete'.
2685
- if (this.isToolCallPartErrored(toolCall.id)) {
2714
+ // safety net must not clobber a finished call back to 'input-complete'.
2715
+ if (this.isToolCallPartTerminal(toolCall.id)) {
2686
2716
  return
2687
2717
  }
2688
2718
 
@@ -2728,11 +2758,11 @@ export class StreamProcessor {
2728
2758
  }
2729
2759
 
2730
2760
  /**
2731
- * Whether the rendered tool-call part for the given id has reached the
2732
- * terminal 'error' state. Used to prevent the completion safety net from
2733
- * downgrading a failed call back to 'input-complete'.
2761
+ * Whether the rendered tool-call part for the given id has reached a
2762
+ * terminal 'error' or 'complete' state. Used to prevent the completion
2763
+ * safety net from downgrading a finished call back to 'input-complete'.
2734
2764
  */
2735
- private isToolCallPartErrored(toolCallId: string): boolean {
2765
+ private isToolCallPartTerminal(toolCallId: string): boolean {
2736
2766
  // `initialMessages` may be ModelMessage-shaped (no `parts`) — e.g. the
2737
2767
  // common pattern of seeding a processor with the same messages passed to
2738
2768
  // `chat()`. Guard the access so iterating them never throws.
@@ -2742,7 +2772,7 @@ export class StreamProcessor {
2742
2772
  (part) =>
2743
2773
  part.type === 'tool-call' &&
2744
2774
  part.id === toolCallId &&
2745
- part.state === 'error',
2775
+ (part.state === 'error' || part.state === 'complete'),
2746
2776
  ),
2747
2777
  )
2748
2778
  }
@@ -2,7 +2,12 @@ import { normalizeToolResult } from '../../../utilities/tool-result'
2
2
  import { tanstackMetadata } from '../../../utilities/merge-metadata'
3
3
  import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
4
4
  import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
5
- import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
5
+ import {
6
+ StandardSchemaValidationError,
7
+ isStandardSchema,
8
+ parseWithStandardSchema,
9
+ validateWithStandardSchema,
10
+ } from './schema-converter'
6
11
  import type { ToolApprovalResolution } from '../../../interrupts'
7
12
  import type {
8
13
  AnyTool,
@@ -19,6 +24,8 @@ import type {
19
24
  ToolCallEndEvent,
20
25
  ToolCallStartEvent,
21
26
  ToolExecutionContext,
27
+ ToolInputResponse,
28
+ ToolResultOutcome,
22
29
  ToolOutputState,
23
30
  } from '../../../types'
24
31
  import type {
@@ -462,6 +469,8 @@ export interface ToolResult {
462
469
  toolName: string
463
470
  result: any
464
471
  state?: 'output-available' | 'output-error'
472
+ /** Set when the user or middleware cancelled or denied the tool call; state is output-error. */
473
+ outcome?: ToolResultOutcome
465
474
  /** Duration of tool execution in milliseconds (only for server-executed tools) */
466
475
  duration?: number
467
476
  /**
@@ -490,9 +499,38 @@ export interface ClientToolRequest {
490
499
  input: any
491
500
  }
492
501
 
502
+ /** Form or sampling input that paused a server tool. */
503
+ export interface McpInputRequest {
504
+ toolCallId: string
505
+ toolName: string
506
+ kind: 'form' | 'sampling'
507
+ request: unknown
508
+ }
509
+
510
+ interface McpInputRequiredThrow {
511
+ name: 'MCPInputRequiredError'
512
+ kind: 'form' | 'sampling'
513
+ request: unknown
514
+ }
515
+
516
+ function isMcpInputRequired(value: unknown): value is McpInputRequiredThrow {
517
+ if (typeof value !== 'object' || value === null) return false
518
+ if (!('name' in value) || value.name !== 'MCPInputRequiredError') {
519
+ return false
520
+ }
521
+ if (!('kind' in value)) return false
522
+ const kindIsFormOrSampling =
523
+ value.kind === 'form' || value.kind === 'sampling'
524
+ if (!kindIsFormOrSampling) return false
525
+ return 'request' in value
526
+ }
527
+
493
528
  export interface ToolResumeExecutionState {
529
+ clientToolErrors?: ReadonlyMap<string, string>
494
530
  deniedToolResults?: ReadonlyMap<string, unknown>
495
531
  cancelledToolCallIds?: ReadonlySet<string>
532
+ /** Answers to `mcp_input` interrupts, by tool call id. */
533
+ inputResponses?: ReadonlyMap<string, ToolInputResponse>
496
534
  }
497
535
 
498
536
  function approvalResolution(
@@ -527,6 +565,8 @@ interface ExecuteToolCallsResult {
527
565
  needsApproval: Array<ApprovalRequest>
528
566
  /** Tools that need client-side execution */
529
567
  needsClientExecution: Array<ClientToolRequest>
568
+ /** Server tools that paused for MCP form or sampling input */
569
+ inputRequired: Array<McpInputRequest>
530
570
  /** Interrupts raised by subagents that run as tools */
531
571
  subagentInterrupts: Array<Interrupt>
532
572
  }
@@ -639,6 +679,7 @@ export async function* executeServerTool<TContext = unknown>(
639
679
  pendingEvents: Array<CustomEvent | StreamChunk>,
640
680
  results: Array<ToolResult>,
641
681
  middlewareHooks?: ToolExecutionMiddlewareHooks,
682
+ inputRequired?: Array<McpInputRequest>,
642
683
  subagentInterrupts?: Array<Interrupt>,
643
684
  ): AsyncGenerator<CustomEvent | StreamChunk, void, void> {
644
685
  const startTime = Date.now()
@@ -744,6 +785,18 @@ export async function* executeServerTool<TContext = unknown>(
744
785
  throw error
745
786
  }
746
787
 
788
+ // Same shape as MCPInputRequiredError. Pause instead of a tool error.
789
+ if (isMcpInputRequired(error)) {
790
+ if (!inputRequired) throw error
791
+ inputRequired.push({
792
+ toolCallId: toolCall.id,
793
+ toolName,
794
+ kind: error.kind,
795
+ request: error.request,
796
+ })
797
+ return
798
+ }
799
+
747
800
  const message = error instanceof Error ? error.message : 'Unknown error'
748
801
  results.push({
749
802
  toolCallId: toolCall.id,
@@ -768,17 +821,35 @@ export async function* executeServerTool<TContext = unknown>(
768
821
  }
769
822
  }
770
823
 
771
- function buildClientToolResult(
824
+ async function buildClientToolResult(
772
825
  toolCallId: string,
773
826
  toolName: string,
774
827
  tool: AnyTool,
775
828
  rawResult: unknown,
776
829
  input?: unknown,
777
- ): ToolResult {
830
+ errorText?: string,
831
+ ): Promise<ToolResult> {
832
+ if (errorText !== undefined) {
833
+ return {
834
+ toolCallId,
835
+ toolName,
836
+ result: { error: errorText },
837
+ input,
838
+ state: 'output-error',
839
+ }
840
+ }
841
+
778
842
  try {
779
843
  let result = rawResult
780
844
  if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
781
- result = parseWithStandardSchema(tool.outputSchema, result)
845
+ const validation = await validateWithStandardSchema<unknown>(
846
+ tool.outputSchema,
847
+ result,
848
+ )
849
+ if (!validation.success) {
850
+ throw new StandardSchemaValidationError(validation.issues)
851
+ }
852
+ result = validation.data
782
853
  }
783
854
 
784
855
  const parsed =
@@ -835,6 +906,7 @@ export async function* executeToolCalls<TContext = unknown>(
835
906
  const results: Array<ToolResult> = []
836
907
  const needsApproval: Array<ApprovalRequest> = []
837
908
  const needsClientExecution: Array<ClientToolRequest> = []
909
+ const inputRequired: Array<McpInputRequest> = []
838
910
  const subagentInterrupts: Array<Interrupt> = []
839
911
 
840
912
  // Create tool lookup map
@@ -891,6 +963,7 @@ export async function* executeToolCalls<TContext = unknown>(
891
963
  toolName,
892
964
  result: { error: 'Tool execution cancelled' },
893
965
  state: 'output-error',
966
+ outcome: 'cancelled',
894
967
  })
895
968
  continue
896
969
  }
@@ -942,10 +1015,12 @@ export async function* executeToolCalls<TContext = unknown>(
942
1015
 
943
1016
  // Create a ToolExecutionContext for this tool call with event emission
944
1017
  const pendingEvents: Array<CustomEvent | StreamChunk> = []
1018
+ const inputResponse = resumeState?.inputResponses?.get(toolCall.id)
945
1019
  const context = {
946
1020
  toolCallId: toolCall.id,
947
1021
  context: userContext,
948
1022
  abortSignal,
1023
+ ...(inputResponse !== undefined ? { inputResponse } : {}),
949
1024
  emitCustomEvent: (
950
1025
  eventName: string,
951
1026
  value: Record<string, any>,
@@ -980,14 +1055,16 @@ export async function* executeToolCalls<TContext = unknown>(
980
1055
  if (approved) {
981
1056
  input = editedApprovalArgs(resolution) ?? input
982
1057
  // Approved - check if client has executed
983
- if (clientResults.has(toolCall.id)) {
1058
+ const clientError = resumeState?.clientToolErrors?.get(toolCall.id)
1059
+ if (clientResults.has(toolCall.id) || clientError !== undefined) {
984
1060
  results.push(
985
- buildClientToolResult(
1061
+ await buildClientToolResult(
986
1062
  toolCall.id,
987
1063
  toolName,
988
1064
  tool,
989
1065
  clientResults.get(toolCall.id),
990
1066
  input,
1067
+ clientError,
991
1068
  ),
992
1069
  )
993
1070
  } else {
@@ -1008,6 +1085,7 @@ export async function* executeToolCalls<TContext = unknown>(
1008
1085
  deniedApprovalResult(resolution),
1009
1086
  input,
1010
1087
  state: 'output-error',
1088
+ outcome: 'denied',
1011
1089
  })
1012
1090
  }
1013
1091
  } else {
@@ -1021,14 +1099,16 @@ export async function* executeToolCalls<TContext = unknown>(
1021
1099
  }
1022
1100
  } else {
1023
1101
  // No approval needed - check if client has executed
1024
- if (clientResults.has(toolCall.id)) {
1102
+ const clientError = resumeState?.clientToolErrors?.get(toolCall.id)
1103
+ if (clientResults.has(toolCall.id) || clientError !== undefined) {
1025
1104
  results.push(
1026
- buildClientToolResult(
1105
+ await buildClientToolResult(
1027
1106
  toolCall.id,
1028
1107
  toolName,
1029
1108
  tool,
1030
1109
  clientResults.get(toolCall.id),
1031
1110
  input,
1111
+ clientError,
1032
1112
  ),
1033
1113
  )
1034
1114
  } else {
@@ -1077,6 +1157,7 @@ export async function* executeToolCalls<TContext = unknown>(
1077
1157
  pendingEvents,
1078
1158
  results,
1079
1159
  middlewareHooks,
1160
+ inputRequired,
1080
1161
  subagentInterrupts,
1081
1162
  )
1082
1163
  } else {
@@ -1089,6 +1170,7 @@ export async function* executeToolCalls<TContext = unknown>(
1089
1170
  deniedApprovalResult(resolution),
1090
1171
  input,
1091
1172
  state: 'output-error',
1173
+ outcome: 'denied',
1092
1174
  })
1093
1175
  }
1094
1176
  } else {
@@ -1126,9 +1208,16 @@ export async function* executeToolCalls<TContext = unknown>(
1126
1208
  pendingEvents,
1127
1209
  results,
1128
1210
  middlewareHooks,
1211
+ inputRequired,
1129
1212
  subagentInterrupts,
1130
1213
  )
1131
1214
  }
1132
1215
 
1133
- return { results, needsApproval, needsClientExecution, subagentInterrupts }
1216
+ return {
1217
+ results,
1218
+ needsApproval,
1219
+ needsClientExecution,
1220
+ inputRequired,
1221
+ subagentInterrupts,
1222
+ }
1134
1223
  }
@@ -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
package/src/client.ts CHANGED
@@ -371,6 +371,7 @@ export type {
371
371
  ThinkingPart,
372
372
  ToolCall,
373
373
  ToolCallPart,
374
+ ToolResultOutcome,
374
375
  ToolResultPart,
375
376
  UIMessage,
376
377
  UIResourcePart,
package/src/index.ts CHANGED
@@ -535,6 +535,7 @@ export type { WireMessage } from './utilities/ag-ui-wire'
535
535
  export {
536
536
  isContentPart,
537
537
  isContentPartArray,
538
+ isToolResultOutcome,
538
539
  normalizeToolResult,
539
540
  } from './utilities/tool-result'
540
541
  export {
@@ -14,6 +14,7 @@ import {
14
14
  isStandardSchema,
15
15
  validateWithStandardSchema,
16
16
  } from './activities/chat/tools/schema-converter'
17
+ import { tanstackMetadata } from './utilities/merge-metadata'
17
18
  import type {
18
19
  InterruptBinding,
19
20
  InterruptSubmissionError,
@@ -95,6 +96,33 @@ function stringField(
95
96
  return typeof value[key] === 'string' ? value[key] : undefined
96
97
  }
97
98
 
99
+ type ClientToolResumeResult =
100
+ | { state: 'output-available'; output: unknown }
101
+ | { state: 'output-error'; errorText: string }
102
+
103
+ function clientToolResult(
104
+ entry: RunAgentResumeItem,
105
+ ): ClientToolResumeResult | null {
106
+ if (tanstackMetadata(entry)?.state === 'output-error') {
107
+ const result = objectValue(entry.payload)
108
+ if (
109
+ !result ||
110
+ Object.keys(result).length !== 1 ||
111
+ typeof result.error !== 'string'
112
+ ) {
113
+ return null
114
+ }
115
+ return {
116
+ state: 'output-error',
117
+ errorText: result.error,
118
+ }
119
+ }
120
+ return {
121
+ state: 'output-available',
122
+ output: entry.payload,
123
+ }
124
+ }
125
+
98
126
  function normalizeIssuePath(
99
127
  path: ReadonlyArray<unknown> | undefined,
100
128
  ): ReadonlyArray<string | number> | undefined {
@@ -528,7 +556,19 @@ export async function validateInterruptResumeBatch(
528
556
  if (schemaDrifted) continue
529
557
 
530
558
  if (binding.kind === 'client-tool-execution') {
531
- if (responseSchema !== undefined) {
559
+ const result = clientToolResult(entry)
560
+ if (!result) {
561
+ errors.push(
562
+ interruptItemError(
563
+ input,
564
+ record.interruptId,
565
+ 'invalid-tool-output',
566
+ `Tool ${binding.toolName} result is invalid.`,
567
+ ),
568
+ )
569
+ continue
570
+ }
571
+ if (result.state === 'output-available' && responseSchema !== undefined) {
532
572
  await pushSchemaIssues({
533
573
  request: input,
534
574
  errors,
@@ -539,13 +579,16 @@ export async function validateInterruptResumeBatch(
539
579
  label: `Tool ${binding.toolName} output is invalid`,
540
580
  })
541
581
  }
542
- if (tool.outputSchema !== undefined) {
582
+ if (
583
+ result.state === 'output-available' &&
584
+ tool.outputSchema !== undefined
585
+ ) {
543
586
  await pushSchemaIssues({
544
587
  request: input,
545
588
  errors,
546
589
  interruptId: record.interruptId,
547
590
  schema: tool.outputSchema,
548
- value: entry.payload,
591
+ value: result.output,
549
592
  code: 'invalid-tool-output',
550
593
  label: `Tool ${binding.toolName} output is invalid`,
551
594
  })
@@ -682,6 +725,7 @@ export async function validateInterruptResumeBatch(
682
725
  const canonical = canonicalizeInterruptResolutions(input.resume ?? [])
683
726
  const approvals = new Map<string, ToolApprovalResolution>()
684
727
  const clientToolResults = new Map<string, unknown>()
728
+ const clientToolErrors = new Map<string, string>()
685
729
  const genericInterrupts = new Map<
686
730
  string,
687
731
  | { interruptId: string; status: 'resolved'; payload: unknown }
@@ -738,7 +782,13 @@ export async function validateInterruptResumeBatch(
738
782
  continue
739
783
  }
740
784
  if (binding.kind === 'client-tool-execution') {
741
- clientToolResults.set(binding.toolCallId, entry.payload)
785
+ const result = clientToolResult(entry)
786
+ if (!result) continue
787
+ if (result.state === 'output-error') {
788
+ clientToolErrors.set(binding.toolCallId, result.errorText)
789
+ } else {
790
+ clientToolResults.set(binding.toolCallId, result.output)
791
+ }
742
792
  continue
743
793
  }
744
794
  const envelope = objectValue(entry.payload)
@@ -781,6 +831,7 @@ export async function validateInterruptResumeBatch(
781
831
  resumeToolState: {
782
832
  approvals,
783
833
  clientToolResults,
834
+ clientToolErrors,
784
835
  genericInterrupts,
785
836
  deniedToolResults,
786
837
  cancelledToolCallIds,