@tanstack/ai 0.59.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 (123) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/adapter.d.ts +9 -0
  3. package/dist/esm/activities/chat/adapter.js +1 -0
  4. package/dist/esm/activities/chat/adapter.js.map +1 -1
  5. package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
  6. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +8 -5
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
  10. package/dist/esm/activities/chat/index.js +289 -78
  11. package/dist/esm/activities/chat/index.js.map +1 -1
  12. package/dist/esm/activities/chat/messages.js +35 -19
  13. package/dist/esm/activities/chat/messages.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
  15. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
  17. package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
  18. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  19. package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
  20. package/dist/esm/activities/chat/stream/processor.js +51 -28
  21. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  22. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
  23. package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
  24. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  25. package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
  26. package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
  27. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  28. package/dist/esm/activities/embed/adapter.d.ts +7 -0
  29. package/dist/esm/activities/embed/adapter.js +1 -0
  30. package/dist/esm/activities/embed/adapter.js.map +1 -1
  31. package/dist/esm/activities/embed/index.js +2 -0
  32. package/dist/esm/activities/embed/index.js.map +1 -1
  33. package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
  34. package/dist/esm/activities/evaluate/adapter.js.map +1 -1
  35. package/dist/esm/activities/evaluate/index.d.ts +4 -0
  36. package/dist/esm/activities/evaluate/index.js +3 -1
  37. package/dist/esm/activities/evaluate/index.js.map +1 -1
  38. package/dist/esm/activities/files/adapter.d.ts +97 -0
  39. package/dist/esm/activities/files/adapter.js +45 -0
  40. package/dist/esm/activities/files/adapter.js.map +1 -0
  41. package/dist/esm/activities/files/index.d.ts +66 -0
  42. package/dist/esm/activities/files/index.js +78 -0
  43. package/dist/esm/activities/files/index.js.map +1 -0
  44. package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
  45. package/dist/esm/activities/generateImage/adapter.js +1 -0
  46. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  47. package/dist/esm/activities/generateImage/index.js +2 -0
  48. package/dist/esm/activities/generateImage/index.js.map +1 -1
  49. package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
  50. package/dist/esm/activities/generateVideo/adapter.js +1 -0
  51. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  52. package/dist/esm/activities/generateVideo/index.js +3 -0
  53. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  54. package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
  55. package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
  56. package/dist/esm/activities/generateWorld/index.d.ts +4 -3
  57. package/dist/esm/activities/generateWorld/index.js +5 -4
  58. package/dist/esm/activities/generateWorld/index.js.map +1 -1
  59. package/dist/esm/activities/index.d.ts +6 -3
  60. package/dist/esm/activities/index.js +13 -11
  61. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
  62. package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
  63. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  64. package/dist/esm/client.d.ts +3 -1
  65. package/dist/esm/client.js +2 -1
  66. package/dist/esm/client.js.map +1 -1
  67. package/dist/esm/index.d.ts +4 -3
  68. package/dist/esm/index.js +5 -3
  69. package/dist/esm/interrupt-resume.js +29 -4
  70. package/dist/esm/interrupt-resume.js.map +1 -1
  71. package/dist/esm/middlewares/otel.d.ts +5 -2
  72. package/dist/esm/middlewares/otel.js +114 -0
  73. package/dist/esm/middlewares/otel.js.map +1 -1
  74. package/dist/esm/types.d.ts +114 -14
  75. package/dist/esm/utilities/ag-ui-wire.js +39 -16
  76. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  77. package/dist/esm/utilities/content-source.d.ts +60 -0
  78. package/dist/esm/utilities/content-source.js +85 -0
  79. package/dist/esm/utilities/content-source.js.map +1 -0
  80. package/dist/esm/utilities/provider-executed.d.ts +7 -0
  81. package/dist/esm/utilities/provider-executed.js +10 -1
  82. package/dist/esm/utilities/provider-executed.js.map +1 -1
  83. package/dist/esm/utilities/tool-result.d.ts +14 -3
  84. package/dist/esm/utilities/tool-result.js +26 -3
  85. package/dist/esm/utilities/tool-result.js.map +1 -1
  86. package/package.json +4 -4
  87. package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
  88. package/skills/ai-core/chat-experience/SKILL.md +134 -0
  89. package/skills/ai-core/media-generation/SKILL.md +8 -0
  90. package/skills/ai-core/tool-calling/SKILL.md +103 -0
  91. package/src/activities/chat/adapter.ts +10 -0
  92. package/src/activities/chat/agents/define-agent.ts +20 -3
  93. package/src/activities/chat/agents/spawn.ts +20 -12
  94. package/src/activities/chat/index.ts +458 -99
  95. package/src/activities/chat/messages.ts +52 -6
  96. package/src/activities/chat/middleware/types.ts +1 -0
  97. package/src/activities/chat/stream/message-updaters.ts +27 -2
  98. package/src/activities/chat/stream/processor.ts +81 -49
  99. package/src/activities/chat/tools/tool-calls.ts +104 -9
  100. package/src/activities/chat/tools/tool-definition.ts +8 -0
  101. package/src/activities/embed/adapter.ts +7 -0
  102. package/src/activities/embed/index.ts +5 -0
  103. package/src/activities/evaluate/adapter.ts +4 -0
  104. package/src/activities/evaluate/index.ts +6 -0
  105. package/src/activities/files/adapter.ts +120 -0
  106. package/src/activities/files/index.ts +113 -0
  107. package/src/activities/generateImage/adapter.ts +8 -0
  108. package/src/activities/generateImage/index.ts +4 -0
  109. package/src/activities/generateVideo/adapter.ts +8 -0
  110. package/src/activities/generateVideo/index.ts +7 -0
  111. package/src/activities/generateWorld/adapter.ts +4 -2
  112. package/src/activities/generateWorld/index.ts +7 -6
  113. package/src/activities/index.ts +25 -1
  114. package/src/activities/summarize/chat-stream-summarize.ts +22 -12
  115. package/src/client.ts +8 -0
  116. package/src/index.ts +17 -0
  117. package/src/interrupt-resume.ts +55 -4
  118. package/src/middlewares/otel.ts +161 -3
  119. package/src/types.ts +114 -14
  120. package/src/utilities/ag-ui-wire.ts +72 -17
  121. package/src/utilities/content-source.ts +138 -0
  122. package/src/utilities/provider-executed.ts +13 -0
  123. package/src/utilities/tool-result.ts +45 -3
@@ -1,9 +1,16 @@
1
- import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
1
+ import {
2
+ isAssistantSegmentOf,
3
+ isProviderExecutedToolCall,
4
+ } from '../../utilities/provider-executed'
2
5
  import {
3
6
  isContentPartArray,
7
+ isToolResultOutcome,
4
8
  normalizeToolResult,
5
9
  } from '../../utilities/tool-result'
6
- import { tanstackMetadata } from '../../utilities/merge-metadata'
10
+ import {
11
+ tanstackMetadata,
12
+ withTanstackMetadata,
13
+ } from '../../utilities/merge-metadata'
7
14
  import {
8
15
  splitSubagentWire,
9
16
  subagentWireText,
@@ -26,6 +33,7 @@ import type {
26
33
  function isRecord(value: unknown): value is Record<string, unknown> {
27
34
  return typeof value === 'object' && value !== null && !Array.isArray(value)
28
35
  }
36
+
29
37
  // ===========================
30
38
  // Message Converters
31
39
  // ===========================
@@ -543,6 +551,7 @@ function assistantMetadata(
543
551
  const tanstack: TanStackMessageMetadata = {}
544
552
  if (previous?.model !== undefined) tanstack.model = previous.model
545
553
  if (previous?.runId !== undefined) tanstack.runId = previous.runId
554
+ if (previous?.run?.id !== undefined) tanstack.run = { id: previous.run.id }
546
555
  if (previous?.signature !== undefined) tanstack.signature = previous.signature
547
556
  if (fromParts.length > 0) tanstack.uiResources = fromParts
548
557
  const result = { ...current }
@@ -726,6 +735,11 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
726
735
  (part.state === 'complete' || part.state === 'error') &&
727
736
  !emittedToolResultIds.has(part.toolCallId)
728
737
  ) {
738
+ const metadata =
739
+ part.outcome === undefined
740
+ ? part.metadata
741
+ : withTanstackMetadata(part, { toolResultOutcome: part.outcome })
742
+ .metadata
729
743
  messageList.push({
730
744
  ...(part.id !== undefined && { id: part.id }),
731
745
  ...optionalCreatedAt(part),
@@ -733,7 +747,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
733
747
  content: part.content,
734
748
  toolCallId: part.toolCallId,
735
749
  ...(part.name !== undefined && { name: part.name }),
736
- ...(part.metadata !== undefined && { metadata: part.metadata }),
750
+ ...(metadata !== undefined && { metadata }),
737
751
  ...(part.error !== undefined && { error: part.error }),
738
752
  })
739
753
  emittedToolResultIds.add(part.toolCallId)
@@ -838,6 +852,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
838
852
  : 'User denied this action',
839
853
  }),
840
854
  toolCallId: part.id,
855
+ ...(approved === false && {
856
+ metadata: { tanstack: { toolResultOutcome: 'denied' } },
857
+ }),
841
858
  })
842
859
  emittedToolResultIds.add(part.id)
843
860
  }
@@ -905,11 +922,21 @@ export function modelMessageToUIMessage(
905
922
  }
906
923
  parts.push(structuredOutput)
907
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'
908
934
  parts.push({
909
935
  type: 'tool-result',
910
936
  toolCallId: modelMessage.toolCallId,
911
937
  content: toolResultContent(modelMessage.content),
912
- state: modelMessage.error === undefined ? 'complete' : 'error',
938
+ state: resultState,
939
+ ...(toolResultOutcome !== undefined && { outcome: toolResultOutcome }),
913
940
  ...(modelMessage.id !== undefined && { id: modelMessage.id }),
914
941
  ...(modelMessage.name !== undefined && { name: modelMessage.name }),
915
942
  ...(modelMessage.metadata !== undefined && {
@@ -1225,6 +1252,14 @@ export function modelMessagesToUIMessages(
1225
1252
  currentAssistantMessage.role === 'assistant'
1226
1253
  ) {
1227
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'
1228
1263
  const toolCallPart = currentAssistantMessage.parts.find(
1229
1264
  (part): part is ToolCallPart =>
1230
1265
  part.type === 'tool-call' && part.id === msg.toolCallId,
@@ -1235,14 +1270,17 @@ export function modelMessagesToUIMessages(
1235
1270
  typeof content === 'string'
1236
1271
  ? parseToolResultContent(content)
1237
1272
  : content
1238
- toolCallPart.state = msg.error === undefined ? 'complete' : 'error'
1273
+ toolCallPart.state = resultState
1239
1274
  }
1240
1275
 
1241
1276
  currentAssistantMessage.parts.push({
1242
1277
  type: 'tool-result',
1243
1278
  toolCallId: msg.toolCallId,
1244
1279
  content,
1245
- state: msg.error === undefined ? 'complete' : 'error',
1280
+ state: resultState,
1281
+ ...(toolResultOutcome !== undefined && {
1282
+ outcome: toolResultOutcome,
1283
+ }),
1246
1284
  ...(msg.id !== undefined &&
1247
1285
  msg.id !== currentAssistantMessage.id && { id: msg.id }),
1248
1286
  ...(msg.name !== undefined && { name: msg.name }),
@@ -1259,6 +1297,14 @@ export function modelMessagesToUIMessages(
1259
1297
  // Regular message. Preserve a persisted stable id so a hydrated message
1260
1298
  // keeps the same identity as its live stream (enables in-place resume).
1261
1299
  const uiMessage = modelMessageToUIMessage(msg, msg.id)
1300
+ if (
1301
+ msg.role === 'assistant' &&
1302
+ currentAssistantMessage &&
1303
+ isAssistantSegmentOf(msg.id, currentAssistantMessage.id)
1304
+ ) {
1305
+ currentAssistantMessage.parts.push(...uiMessage.parts)
1306
+ continue
1307
+ }
1262
1308
  uiMessages.push(uiMessage)
1263
1309
 
1264
1310
  // Track assistant messages for potential tool result merging
@@ -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
 
@@ -455,15 +458,37 @@ export function updateThinkingPart(
455
458
  }
456
459
 
457
460
  const parts = [...msg.parts]
458
- const thinkingPartIndex = parts.findIndex(
461
+ let thinkingPartIndex = parts.findIndex(
459
462
  (p) => p.type === 'thinking' && p.stepId === stepId,
460
463
  )
461
464
 
465
+ // A hydrated message carries its thinking without a stepId: the stored form
466
+ // has no field for one, so `modelMessageToUIMessage` cannot put it back.
467
+ // When a run is rejoined mid-stream the replayed reasoning is keyed by
468
+ // stepId, matches nothing, and gets appended -- leaving a second thinking
469
+ // part sitting after the answer text. Adopt the first stepId-less thinking
470
+ // part instead, so the replay lands on the part it belongs to. Live
471
+ // streaming always writes a stepId, so the only parts this can match are
472
+ // hydrated ones.
473
+ let adopted: ThinkingPart | undefined
474
+ if (thinkingPartIndex < 0) {
475
+ thinkingPartIndex = parts.findIndex(
476
+ (p) => p.type === 'thinking' && p.stepId === undefined,
477
+ )
478
+ const candidate = parts[thinkingPartIndex]
479
+ if (candidate?.type === 'thinking') adopted = candidate
480
+ }
481
+
482
+ // Keep the signature the hydrated part already had when this update does
483
+ // not carry one; losing it would strip the provider's encrypted reasoning
484
+ // from a message that is about to be sent back.
485
+ const nextSignature = signature ?? adopted?.signature
486
+
462
487
  const thinkingPart: ThinkingPart = {
463
488
  type: 'thinking',
464
489
  content,
465
490
  stepId,
466
- ...(signature && { signature }),
491
+ ...(nextSignature && { signature: nextSignature }),
467
492
  }
468
493
 
469
494
  if (thinkingPartIndex >= 0) {
@@ -25,14 +25,22 @@ import {
25
25
  uiMessageToModelMessages,
26
26
  } from '../messages.js'
27
27
  import { runErrorEventToError } from '../../../utilities/errors'
28
- import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
28
+ import {
29
+ isAssistantSegmentOf,
30
+ isProviderExecutedToolCall,
31
+ } from '../../../utilities/provider-executed'
29
32
  import {
30
33
  mergeMetadata,
31
34
  tanstackMetadata,
32
35
  } from '../../../utilities/merge-metadata'
33
36
  import { getChunkRunId } from '../../../utilities/chunk-ids'
34
37
  import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
35
- import { normalizeToolResult } from '../../../utilities/tool-result'
38
+ import {
39
+ isContentPartArray,
40
+ isToolResultOutcome,
41
+ normalizeToolResult,
42
+ toolResultErrorText,
43
+ } from '../../../utilities/tool-result'
36
44
  import { defaultJSONParser } from './json-parser'
37
45
  import {
38
46
  appendStructuredOutputDelta,
@@ -175,6 +183,23 @@ function interruptBatchHasGeneric(interrupts: Array<Interrupt>): boolean {
175
183
  })
176
184
  }
177
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
+
178
203
  /**
179
204
  * StreamProcessor - State machine for processing AI response streams
180
205
  *
@@ -1518,20 +1543,30 @@ export class StreamProcessor {
1518
1543
  pending.push(msg)
1519
1544
  continue
1520
1545
  }
1546
+ let next = msg
1521
1547
  if (
1522
1548
  msg.role === 'assistant' &&
1523
1549
  pending.length > 0 &&
1524
1550
  !isToolResultOnly(msg)
1525
1551
  ) {
1526
- out.push({
1552
+ next = {
1527
1553
  ...msg,
1528
1554
  parts: [...pending.flatMap(thinkingParts), ...msg.parts],
1529
- })
1555
+ }
1530
1556
  pending = []
1557
+ } else {
1558
+ flushPending()
1559
+ }
1560
+ const prev = out.at(-1)
1561
+ if (
1562
+ prev?.role === 'assistant' &&
1563
+ next.role === 'assistant' &&
1564
+ isAssistantSegmentOf(next.id, prev.id)
1565
+ ) {
1566
+ out[out.length - 1] = { ...prev, parts: [...prev.parts, ...next.parts] }
1531
1567
  continue
1532
1568
  }
1533
- flushPending()
1534
- out.push(msg)
1569
+ out.push(next)
1535
1570
  }
1536
1571
  flushPending()
1537
1572
  return out
@@ -1705,9 +1740,7 @@ export class StreamProcessor {
1705
1740
  }
1706
1741
  }
1707
1742
  const errorText =
1708
- result.state === 'error'
1709
- ? this.extractToolResultError(output)
1710
- : undefined
1743
+ result.state === 'error' ? toolResultErrorText(output) : undefined
1711
1744
  next = {
1712
1745
  ...next,
1713
1746
  output: errorText ? { error: errorText } : output,
@@ -1939,11 +1972,13 @@ export class StreamProcessor {
1939
1972
  * Handle TOOL_CALL_END event — arguments are finalized (input-complete).
1940
1973
  * Tool output arrives on TOOL_CALL_RESULT, not on this event.
1941
1974
  *
1942
- * If TOOL_CALL_END carries parsed `input`, use it as the canonical arguments:
1943
- * back-fill the accumulated string when no TOOL_CALL_ARGS deltas were seen
1944
- * (adapters that deliver the whole input on END — e.g. Anthropic
1945
- * server_tool_use / web_search — issue #839) and override the rendered part's
1946
- * `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.
1947
1982
  *
1948
1983
  * @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
1949
1984
  */
@@ -1966,16 +2001,15 @@ export class StreamProcessor {
1966
2001
  // Transition the tool call to input-complete (the authoritative completion signal)
1967
2002
  const existingToolCall = msgState.toolCalls.get(chunk.toolCallId)
1968
2003
  if (existingToolCall && existingToolCall.state !== 'input-complete') {
1969
- // Back-fill the arguments string from the parsed input when no
1970
- // TOOL_CALL_ARGS deltas were received, so completeToolCall's strict parse
1971
- // surfaces the correct value on the ToolCallPart.
1972
- if (input !== undefined && !existingToolCall.arguments) {
1973
- try {
1974
- existingToolCall.arguments = JSON.stringify(input)
1975
- } catch {
1976
- // circular refs, BigInt, etc. — leave arguments empty rather than
1977
- // aborting stream processing
1978
- }
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
1979
2013
  }
1980
2014
 
1981
2015
  const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
@@ -1984,7 +2018,7 @@ export class StreamProcessor {
1984
2018
  // Canonicalize on the parsed input: overrides the accumulated-args parse
1985
2019
  // that completeToolCall wrote (adapters may coerce values differently
1986
2020
  // between streamed args and the final structured input).
1987
- if (input !== undefined) {
2021
+ if (serializedInput !== undefined) {
1988
2022
  existingToolCall.parsedArguments = input
1989
2023
  this.messages = updateToolCallPart(this.messages, messageId, {
1990
2024
  id: existingToolCall.id,
@@ -2001,18 +2035,6 @@ export class StreamProcessor {
2001
2035
  }
2002
2036
  }
2003
2037
 
2004
- private extractToolResultError(output: unknown): string {
2005
- if (
2006
- output &&
2007
- typeof output === 'object' &&
2008
- 'error' in output &&
2009
- typeof output.error === 'string'
2010
- ) {
2011
- return output.error
2012
- }
2013
- return typeof output === 'string' ? output : 'Tool execution failed'
2014
- }
2015
-
2016
2038
  /**
2017
2039
  * Handle TOOL_CALL_RESULT event (AG-UI spec).
2018
2040
  *
@@ -2040,9 +2062,14 @@ export class StreamProcessor {
2040
2062
  if (!messageId) return
2041
2063
 
2042
2064
  const extra = chunk as AdapterYieldChunk
2065
+ const rawToolResultOutcome = tanstackMetadata(chunk)?.toolResultOutcome
2066
+ const toolResultOutcome = isToolResultOutcome(rawToolResultOutcome)
2067
+ ? rawToolResultOutcome
2068
+ : undefined
2043
2069
  const isOutputError =
2044
2070
  extra.state === 'output-error' ||
2045
- tanstackMetadata(chunk)?.state === 'output-error'
2071
+ tanstackMetadata(chunk)?.state === 'output-error' ||
2072
+ toolResultOutcome !== undefined
2046
2073
 
2047
2074
  // Step 1: Update the tool-call part's output field
2048
2075
  let output: unknown
@@ -2067,9 +2094,14 @@ export class StreamProcessor {
2067
2094
  this.messages,
2068
2095
  messageId,
2069
2096
  chunk.toolCallId,
2070
- 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),
2071
2102
  resultState,
2072
- resultState === 'error' ? this.extractToolResultError(output) : undefined,
2103
+ resultState === 'error' ? toolResultErrorText(output) : undefined,
2104
+ toolResultOutcome,
2073
2105
  )
2074
2106
  this.emitMessagesChange()
2075
2107
  }
@@ -2676,11 +2708,11 @@ export class StreamProcessor {
2676
2708
  toolCall.parsedArguments = undefined
2677
2709
  }
2678
2710
 
2679
- // Don't downgrade the rendered part of a call that already reached the
2680
- // 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
2681
2713
  // without a preceding TOOL_CALL_END). The RUN_FINISHED / finalizeStream
2682
- // safety net must not clobber a failed call back to 'input-complete'.
2683
- if (this.isToolCallPartErrored(toolCall.id)) {
2714
+ // safety net must not clobber a finished call back to 'input-complete'.
2715
+ if (this.isToolCallPartTerminal(toolCall.id)) {
2684
2716
  return
2685
2717
  }
2686
2718
 
@@ -2726,11 +2758,11 @@ export class StreamProcessor {
2726
2758
  }
2727
2759
 
2728
2760
  /**
2729
- * Whether the rendered tool-call part for the given id has reached the
2730
- * terminal 'error' state. Used to prevent the completion safety net from
2731
- * 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'.
2732
2764
  */
2733
- private isToolCallPartErrored(toolCallId: string): boolean {
2765
+ private isToolCallPartTerminal(toolCallId: string): boolean {
2734
2766
  // `initialMessages` may be ModelMessage-shaped (no `parts`) — e.g. the
2735
2767
  // common pattern of seeding a processor with the same messages passed to
2736
2768
  // `chat()`. Guard the access so iterating them never throws.
@@ -2740,7 +2772,7 @@ export class StreamProcessor {
2740
2772
  (part) =>
2741
2773
  part.type === 'tool-call' &&
2742
2774
  part.id === toolCallId &&
2743
- part.state === 'error',
2775
+ (part.state === 'error' || part.state === 'complete'),
2744
2776
  ),
2745
2777
  )
2746
2778
  }