@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
@@ -4,9 +4,14 @@ 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'
14
+ import { isRedactedThinkingId } from '../../utilities/reasoning-encrypted-value'
10
15
  import {
11
16
  splitSubagentWire,
12
17
  subagentWireText,
@@ -29,6 +34,7 @@ import type {
29
34
  function isRecord(value: unknown): value is Record<string, unknown> {
30
35
  return typeof value === 'object' && value !== null && !Array.isArray(value)
31
36
  }
37
+
32
38
  // ===========================
33
39
  // Message Converters
34
40
  // ===========================
@@ -67,6 +73,13 @@ function encryptedValueFrom(value: object): string | undefined {
67
73
  return nonEmptyString(tanstackMetadata(value)?.signature)
68
74
  }
69
75
 
76
+ /** `{ redacted: true }` when a reasoning message's id marks a redacted block. */
77
+ function redactedFrom(value: object) {
78
+ return 'id' in value && isRedactedThinkingId(value.id)
79
+ ? { redacted: true }
80
+ : {}
81
+ }
82
+
70
83
  function toolCallFromWire(toolCall: ToolCall, bag: unknown): ToolCall {
71
84
  const fromBag =
72
85
  bag != null && typeof bag === 'object' && !Array.isArray(bag)
@@ -241,7 +254,7 @@ function convertOwnMessages(
241
254
  }
242
255
 
243
256
  const modelMessages: Array<ModelMessage> = []
244
- let pendingThinking: Array<{ content: string; signature?: string }> = []
257
+ let pendingThinking: NonNullable<ModelMessage['thinking']> = []
245
258
  for (const msg of messages) {
246
259
  if ('parts' in msg) {
247
260
  modelMessages.push(...uiMessageToModelMessages(msg))
@@ -268,6 +281,7 @@ function convertOwnMessages(
268
281
  pendingThinking.push({
269
282
  content: typeof content === 'string' ? content : '',
270
283
  ...(signature !== undefined ? { signature } : {}),
284
+ ...redactedFrom(msg),
271
285
  })
272
286
  }
273
287
  continue
@@ -546,6 +560,7 @@ function assistantMetadata(
546
560
  const tanstack: TanStackMessageMetadata = {}
547
561
  if (previous?.model !== undefined) tanstack.model = previous.model
548
562
  if (previous?.runId !== undefined) tanstack.runId = previous.runId
563
+ if (previous?.run?.id !== undefined) tanstack.run = { id: previous.run.id }
549
564
  if (previous?.signature !== undefined) tanstack.signature = previous.signature
550
565
  if (fromParts.length > 0) tanstack.uiResources = fromParts
551
566
  const result = { ...current }
@@ -657,7 +672,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
657
672
  // shared UI id on each one so persistence can retain the original identity.
658
673
  const messageList: Array<ModelMessage> = []
659
674
  let current = createSegment()
660
- let pendingThinking: Array<{ content: string; signature?: string }> = []
675
+ let pendingThinking: NonNullable<ModelMessage['thinking']> = []
661
676
 
662
677
  // Track emitted tool result IDs to avoid duplicates.
663
678
  // A tool call can have BOTH an explicit tool-result part AND an output
@@ -729,6 +744,11 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
729
744
  (part.state === 'complete' || part.state === 'error') &&
730
745
  !emittedToolResultIds.has(part.toolCallId)
731
746
  ) {
747
+ const metadata =
748
+ part.outcome === undefined
749
+ ? part.metadata
750
+ : withTanstackMetadata(part, { toolResultOutcome: part.outcome })
751
+ .metadata
732
752
  messageList.push({
733
753
  ...(part.id !== undefined && { id: part.id }),
734
754
  ...optionalCreatedAt(part),
@@ -736,7 +756,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
736
756
  content: part.content,
737
757
  toolCallId: part.toolCallId,
738
758
  ...(part.name !== undefined && { name: part.name }),
739
- ...(part.metadata !== undefined && { metadata: part.metadata }),
759
+ ...(metadata !== undefined && { metadata }),
740
760
  ...(part.error !== undefined && { error: part.error }),
741
761
  })
742
762
  emittedToolResultIds.add(part.toolCallId)
@@ -753,6 +773,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
753
773
  pendingThinking.push({
754
774
  content: part.content,
755
775
  ...(part.signature && { signature: part.signature }),
776
+ ...(part.redacted && { redacted: true }),
756
777
  })
757
778
  }
758
779
  break
@@ -841,6 +862,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
841
862
  : 'User denied this action',
842
863
  }),
843
864
  toolCallId: part.id,
865
+ ...(approved === false && {
866
+ metadata: { tanstack: { toolResultOutcome: 'denied' } },
867
+ }),
844
868
  })
845
869
  emittedToolResultIds.add(part.id)
846
870
  }
@@ -884,6 +908,7 @@ export function modelMessageToUIMessage(
884
908
  type: 'thinking',
885
909
  content: thinking.content,
886
910
  ...(thinking.signature && { signature: thinking.signature }),
911
+ ...(thinking.redacted && { redacted: true }),
887
912
  })
888
913
  }
889
914
  }
@@ -908,11 +933,21 @@ export function modelMessageToUIMessage(
908
933
  }
909
934
  parts.push(structuredOutput)
910
935
  } else if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
936
+ const rawToolResultOutcome =
937
+ tanstackMetadata(modelMessage)?.toolResultOutcome
938
+ const toolResultOutcome = isToolResultOutcome(rawToolResultOutcome)
939
+ ? rawToolResultOutcome
940
+ : undefined
941
+ const resultState =
942
+ modelMessage.error === undefined && toolResultOutcome === undefined
943
+ ? 'complete'
944
+ : 'error'
911
945
  parts.push({
912
946
  type: 'tool-result',
913
947
  toolCallId: modelMessage.toolCallId,
914
948
  content: toolResultContent(modelMessage.content),
915
- state: modelMessage.error === undefined ? 'complete' : 'error',
949
+ state: resultState,
950
+ ...(toolResultOutcome !== undefined && { outcome: toolResultOutcome }),
916
951
  ...(modelMessage.id !== undefined && { id: modelMessage.id }),
917
952
  ...(modelMessage.name !== undefined && { name: modelMessage.name }),
918
953
  ...(modelMessage.metadata !== undefined && {
@@ -1097,6 +1132,7 @@ export function aguiSnapshotMessageToUIMessage(
1097
1132
  type: 'thinking' as const,
1098
1133
  content,
1099
1134
  ...(signature !== undefined ? { signature } : {}),
1135
+ ...redactedFrom(message),
1100
1136
  },
1101
1137
  ]
1102
1138
  : [],
@@ -1228,6 +1264,14 @@ export function modelMessagesToUIMessages(
1228
1264
  currentAssistantMessage.role === 'assistant'
1229
1265
  ) {
1230
1266
  const content = toolResultContent(msg.content)
1267
+ const rawToolResultOutcome = tanstackMetadata(msg)?.toolResultOutcome
1268
+ const toolResultOutcome = isToolResultOutcome(rawToolResultOutcome)
1269
+ ? rawToolResultOutcome
1270
+ : undefined
1271
+ const resultState =
1272
+ msg.error === undefined && toolResultOutcome === undefined
1273
+ ? 'complete'
1274
+ : 'error'
1231
1275
  const toolCallPart = currentAssistantMessage.parts.find(
1232
1276
  (part): part is ToolCallPart =>
1233
1277
  part.type === 'tool-call' && part.id === msg.toolCallId,
@@ -1238,14 +1282,17 @@ export function modelMessagesToUIMessages(
1238
1282
  typeof content === 'string'
1239
1283
  ? parseToolResultContent(content)
1240
1284
  : content
1241
- toolCallPart.state = msg.error === undefined ? 'complete' : 'error'
1285
+ toolCallPart.state = resultState
1242
1286
  }
1243
1287
 
1244
1288
  currentAssistantMessage.parts.push({
1245
1289
  type: 'tool-result',
1246
1290
  toolCallId: msg.toolCallId,
1247
1291
  content,
1248
- state: msg.error === undefined ? 'complete' : 'error',
1292
+ state: resultState,
1293
+ ...(toolResultOutcome !== undefined && {
1294
+ outcome: toolResultOutcome,
1295
+ }),
1249
1296
  ...(msg.id !== undefined &&
1250
1297
  msg.id !== currentAssistantMessage.id && { id: msg.id }),
1251
1298
  ...(msg.name !== undefined && { name: msg.name }),
@@ -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
@@ -5,12 +5,14 @@
5
5
  * These are used by StreamProcessor to manage the message array.
6
6
  */
7
7
 
8
+ import { isRedactedThinkingId } from '../../../utilities/reasoning-encrypted-value'
8
9
  import { parsePartialJSON } from './json-parser'
9
10
  import type {
10
11
  ContentPart,
11
12
  StructuredOutputPart,
12
13
  ThinkingPart,
13
14
  ToolCallPart,
15
+ ToolResultOutcome,
14
16
  ToolResultPart,
15
17
  UIMessage,
16
18
  } from '../../../types'
@@ -117,6 +119,7 @@ export function updateToolResultPart(
117
119
  content: string | Array<ContentPart>,
118
120
  state: ToolResultState,
119
121
  error?: string,
122
+ outcome?: ToolResultOutcome,
120
123
  ): Array<UIMessage> {
121
124
  return messages.map((msg) => {
122
125
  if (msg.id !== messageId) {
@@ -134,6 +137,7 @@ export function updateToolResultPart(
134
137
  toolCallId,
135
138
  content,
136
139
  state,
140
+ ...(outcome !== undefined && { outcome }),
137
141
  ...(error && { error }),
138
142
  }
139
143
 
@@ -480,12 +484,16 @@ export function updateThinkingPart(
480
484
  // not carry one; losing it would strip the provider's encrypted reasoning
481
485
  // from a message that is about to be sent back.
482
486
  const nextSignature = signature ?? adopted?.signature
487
+ // A hydrated part has no stepId to carry the redacted marker, so it keeps
488
+ // its own flag.
489
+ const redacted = isRedactedThinkingId(stepId) || adopted?.redacted === true
483
490
 
484
491
  const thinkingPart: ThinkingPart = {
485
492
  type: 'thinking',
486
493
  content,
487
494
  stepId,
488
495
  ...(nextSignature && { signature: nextSignature }),
496
+ ...(redacted && { redacted: true }),
489
497
  }
490
498
 
491
499
  if (thinkingPartIndex >= 0) {
@@ -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
  }
@@ -230,6 +230,14 @@ export interface ConvertSchemaOptions {
230
230
  * @default false
231
231
  */
232
232
  forStructuredOutput?: boolean
233
+ /**
234
+ * Which view of a Standard JSON Schema to convert. A schema with a
235
+ * transform or a pipe has a different `output` view. Use `'output'` to
236
+ * describe the value that parsing returns.
237
+ *
238
+ * @default 'input'
239
+ */
240
+ io?: 'input' | 'output'
233
241
  }
234
242
 
235
243
  /**
@@ -238,16 +246,20 @@ export interface ConvertSchemaOptions {
238
246
  *
239
247
  * - Standard JSON Schemas are rebuilt structurally (dropping `$schema`, which
240
248
  * LLM providers ignore) and given the explicit `type`/`properties`/`required`
241
- * defaults object shapes need downstream.
249
+ * defaults object shapes need downstream. `io` picks the `input` (default)
250
+ * or `output` view.
242
251
  * - Plain `JSONSchema` inputs are rebuilt into the typed view; non-object inputs
243
252
  * are surfaced untouched (they can't be widened).
244
253
  * - Standard Schema validators lacking a `~standard.jsonSchema` converter throw
245
254
  * with actionable guidance, rather than shipping `{ '~standard': … }` to the
246
255
  * provider and producing an opaque downstream error.
247
256
  */
248
- function toTypedJsonSchema(schema: SchemaInput): JSONSchema | undefined {
257
+ function toTypedJsonSchema(
258
+ schema: SchemaInput,
259
+ io: 'input' | 'output' = 'input',
260
+ ): JSONSchema | undefined {
249
261
  if (isStandardJSONSchema(schema)) {
250
- const jsonSchema = schema['~standard'].jsonSchema.input({
262
+ const jsonSchema = schema['~standard'].jsonSchema[io]({
251
263
  target: 'draft-07',
252
264
  })
253
265
  const result: JSONSchema = toJsonSchema(jsonSchema)
@@ -340,7 +352,7 @@ export function convertSchemaToJsonSchema(
340
352
  ): JSONSchema | undefined {
341
353
  if (!schema) return undefined
342
354
 
343
- const { forStructuredOutput = false } = options
355
+ const { forStructuredOutput = false, io } = options
344
356
 
345
357
  // Plain-JSONSchema passthrough: with no widening requested, return the schema
346
358
  // by reference so callers comparing via `===` keep identity. Only the widening
@@ -353,7 +365,7 @@ export function convertSchemaToJsonSchema(
353
365
  return schema
354
366
  }
355
367
 
356
- const base = toTypedJsonSchema(schema)
368
+ const base = toTypedJsonSchema(schema, io)
357
369
  // Non-object inputs can't be widened; surface them untouched.
358
370
  if (!base || typeof base !== 'object') return base
359
371
  if (!forStructuredOutput) return base