@convex-dev/agent 0.6.4 → 0.7.0-alpha.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 (225) hide show
  1. package/MIGRATION.md +60 -116
  2. package/dist/client/messages.d.ts +1 -398
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +3 -47
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/client/types.d.ts +1 -455
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/component/_generated/component.d.ts +805 -45
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/messages.d.ts +1252 -201
  11. package/dist/component/messages.d.ts.map +1 -1
  12. package/dist/component/messages.js +49 -23
  13. package/dist/component/messages.js.map +1 -1
  14. package/dist/component/schema.d.ts +2593 -332
  15. package/dist/component/schema.d.ts.map +1 -1
  16. package/dist/component/streams.d.ts +7 -1
  17. package/dist/component/streams.d.ts.map +1 -1
  18. package/dist/component/streams.js +24 -30
  19. package/dist/component/streams.js.map +1 -1
  20. package/dist/component/threads.d.ts.map +1 -1
  21. package/dist/component/threads.js +1 -2
  22. package/dist/component/threads.js.map +1 -1
  23. package/dist/react/index.d.ts +5 -5
  24. package/dist/react/index.d.ts.map +1 -1
  25. package/dist/react/index.js +5 -5
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/shared.d.ts +19 -7
  28. package/dist/shared.d.ts.map +1 -1
  29. package/dist/shared.js +4 -4
  30. package/dist/shared.js.map +1 -1
  31. package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
  32. package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
  33. package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
  34. package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
  35. package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
  36. package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
  37. package/dist/streaming/persistedUIMessageChunks.js +475 -0
  38. package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
  39. package/dist/validators.d.ts +10026 -1055
  40. package/dist/validators.d.ts.map +1 -1
  41. package/dist/validators.js +71 -9
  42. package/dist/validators.js.map +1 -1
  43. package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
  44. package/dist/vercel/UIMessages.d.ts.map +1 -0
  45. package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
  46. package/dist/vercel/UIMessages.js.map +1 -0
  47. package/dist/{client → vercel/client}/createTool.d.ts +23 -16
  48. package/dist/vercel/client/createTool.d.ts.map +1 -0
  49. package/dist/{client → vercel/client}/createTool.js +2 -0
  50. package/dist/vercel/client/createTool.js.map +1 -0
  51. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +533 -79
  52. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
  53. package/dist/{client → vercel/client}/definePlaygroundAPI.js +8 -7
  54. package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
  55. package/dist/{client → vercel/client}/files.d.ts +2 -2
  56. package/dist/vercel/client/files.d.ts.map +1 -0
  57. package/dist/vercel/client/files.js.map +1 -0
  58. package/dist/vercel/client/messages.d.ts +542 -0
  59. package/dist/vercel/client/messages.d.ts.map +1 -0
  60. package/dist/vercel/client/messages.js +66 -0
  61. package/dist/vercel/client/messages.js.map +1 -0
  62. package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
  63. package/dist/vercel/client/mockModel.d.ts.map +1 -0
  64. package/dist/{client → vercel/client}/mockModel.js +12 -13
  65. package/dist/vercel/client/mockModel.js.map +1 -0
  66. package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
  67. package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
  68. package/dist/vercel/client/saveInputMessages.js.map +1 -0
  69. package/dist/{client → vercel/client}/search.d.ts +119 -22
  70. package/dist/vercel/client/search.d.ts.map +1 -0
  71. package/dist/{client → vercel/client}/search.js +2 -2
  72. package/dist/vercel/client/search.js.map +1 -0
  73. package/dist/{client → vercel/client}/start.d.ts +26 -11
  74. package/dist/vercel/client/start.d.ts.map +1 -0
  75. package/dist/{client → vercel/client}/start.js +64 -22
  76. package/dist/vercel/client/start.js.map +1 -0
  77. package/dist/{client → vercel/client}/streamText.d.ts +13 -12
  78. package/dist/vercel/client/streamText.d.ts.map +1 -0
  79. package/dist/{client → vercel/client}/streamText.js +43 -9
  80. package/dist/vercel/client/streamText.js.map +1 -0
  81. package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
  82. package/dist/vercel/client/streaming.d.ts.map +1 -0
  83. package/dist/{client → vercel/client}/streaming.js +120 -52
  84. package/dist/vercel/client/streaming.js.map +1 -0
  85. package/dist/vercel/client/types.d.ts +456 -0
  86. package/dist/vercel/client/types.d.ts.map +1 -0
  87. package/dist/vercel/client/types.js +3 -0
  88. package/dist/vercel/client/types.js.map +1 -0
  89. package/dist/{client → vercel/client}/utils.d.ts +3 -2
  90. package/dist/vercel/client/utils.d.ts.map +1 -0
  91. package/dist/{client → vercel/client}/utils.js +2 -2
  92. package/dist/vercel/client/utils.js.map +1 -0
  93. package/dist/{deltas.d.ts → vercel/deltas.d.ts} +3 -4
  94. package/dist/vercel/deltas.d.ts.map +1 -0
  95. package/dist/{deltas.js → vercel/deltas.js} +195 -99
  96. package/dist/vercel/deltas.js.map +1 -0
  97. package/dist/{client → vercel}/index.d.ts +482 -106
  98. package/dist/vercel/index.d.ts.map +1 -0
  99. package/dist/{client → vercel}/index.js +64 -46
  100. package/dist/vercel/index.js.map +1 -0
  101. package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -19
  102. package/dist/vercel/mapping.d.ts.map +1 -0
  103. package/dist/{mapping.js → vercel/mapping.js} +411 -114
  104. package/dist/vercel/mapping.js.map +1 -0
  105. package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
  106. package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
  107. package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
  108. package/dist/{react → vercel/react}/types.d.ts +1 -1
  109. package/dist/vercel/react/types.d.ts.map +1 -0
  110. package/dist/{react → vercel/react}/types.js.map +1 -1
  111. package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
  112. package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
  113. package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
  114. package/dist/vercel/react/useDeltaStreams.js.map +1 -0
  115. package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
  116. package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
  117. package/dist/{react → vercel/react}/useStreamingUIMessages.js +7 -3
  118. package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
  119. package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
  120. package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
  121. package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
  122. package/dist/vercel/react/useThreadMessages.js.map +1 -0
  123. package/dist/{react → vercel/react}/useUIMessages.d.ts +1 -1
  124. package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
  125. package/dist/{react → vercel/react}/useUIMessages.js +1 -1
  126. package/dist/vercel/react/useUIMessages.js.map +1 -0
  127. package/package.json +29 -22
  128. package/src/client/messages.ts +5 -107
  129. package/src/client/types.ts +0 -608
  130. package/src/component/_generated/component.ts +1089 -48
  131. package/src/component/messages.test.ts +273 -1
  132. package/src/component/messages.ts +62 -32
  133. package/src/component/streams.ts +47 -47
  134. package/src/component/threads.ts +5 -2
  135. package/src/eslintConfig.test.ts +44 -0
  136. package/src/react/index.ts +8 -5
  137. package/src/shared.ts +26 -36
  138. package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
  139. package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
  140. package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
  141. package/src/streaming/persistedUIMessageChunks.ts +658 -0
  142. package/src/validators.test.ts +4 -1
  143. package/src/validators.ts +89 -8
  144. package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
  145. package/src/{UIMessages.ts → vercel/UIMessages.ts} +95 -3
  146. package/src/{client → vercel/client}/approval.test.ts +23 -13
  147. package/src/{client → vercel/client}/createTool.ts +62 -22
  148. package/src/{client → vercel/client}/definePlaygroundAPI.ts +15 -16
  149. package/src/{client → vercel/client}/files.ts +2 -2
  150. package/src/{client → vercel/client}/index.test.ts +99 -31
  151. package/src/vercel/client/messages.ts +182 -0
  152. package/src/{client → vercel/client}/mockModel.ts +38 -39
  153. package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
  154. package/src/{client → vercel/client}/saveInputMessages.ts +3 -3
  155. package/src/{client → vercel/client}/search.test.ts +3 -3
  156. package/src/{client → vercel/client}/search.ts +4 -4
  157. package/src/{client → vercel/client}/setup.test.ts +6 -3
  158. package/src/vercel/client/start.test.ts +14 -0
  159. package/src/{client → vercel/client}/start.ts +147 -36
  160. package/src/vercel/client/streamText.test.ts +223 -0
  161. package/src/{client → vercel/client}/streamText.ts +102 -31
  162. package/src/{client → vercel/client}/streaming.integration.test.ts +6 -3
  163. package/src/{client → vercel/client}/streaming.test.ts +178 -4
  164. package/src/{client → vercel/client}/streaming.ts +114 -52
  165. package/src/vercel/client/types.test.ts +13 -0
  166. package/src/vercel/client/types.ts +679 -0
  167. package/src/{client → vercel/client}/utils.ts +13 -7
  168. package/src/{deltas.test.ts → vercel/deltas.test.ts} +384 -93
  169. package/src/{deltas.ts → vercel/deltas.ts} +218 -115
  170. package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
  171. package/src/{client → vercel}/index.ts +167 -84
  172. package/src/{mapping.test.ts → vercel/mapping.test.ts} +377 -92
  173. package/src/{mapping.ts → vercel/mapping.ts} +501 -137
  174. package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
  175. package/src/{react → vercel/react}/types.ts +1 -1
  176. package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
  177. package/src/{react → vercel/react}/useStreamingUIMessages.ts +7 -3
  178. package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
  179. package/src/{react → vercel/react}/useUIMessages.ts +2 -2
  180. package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
  181. package/dist/UIMessages.d.ts.map +0 -1
  182. package/dist/UIMessages.js.map +0 -1
  183. package/dist/client/createTool.d.ts.map +0 -1
  184. package/dist/client/createTool.js.map +0 -1
  185. package/dist/client/definePlaygroundAPI.js.map +0 -1
  186. package/dist/client/files.d.ts.map +0 -1
  187. package/dist/client/files.js.map +0 -1
  188. package/dist/client/index.d.ts.map +0 -1
  189. package/dist/client/index.js.map +0 -1
  190. package/dist/client/mockModel.d.ts.map +0 -1
  191. package/dist/client/mockModel.js.map +0 -1
  192. package/dist/client/saveInputMessages.d.ts.map +0 -1
  193. package/dist/client/saveInputMessages.js.map +0 -1
  194. package/dist/client/search.d.ts.map +0 -1
  195. package/dist/client/search.js.map +0 -1
  196. package/dist/client/start.d.ts.map +0 -1
  197. package/dist/client/start.js.map +0 -1
  198. package/dist/client/streamText.d.ts.map +0 -1
  199. package/dist/client/streamText.js.map +0 -1
  200. package/dist/client/streaming.d.ts.map +0 -1
  201. package/dist/client/streaming.js.map +0 -1
  202. package/dist/client/utils.d.ts.map +0 -1
  203. package/dist/client/utils.js.map +0 -1
  204. package/dist/deltas.d.ts.map +0 -1
  205. package/dist/deltas.js.map +0 -1
  206. package/dist/mapping.d.ts.map +0 -1
  207. package/dist/mapping.js.map +0 -1
  208. package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
  209. package/dist/react/optimisticallySendMessage.js.map +0 -1
  210. package/dist/react/types.d.ts.map +0 -1
  211. package/dist/react/useDeltaStreams.d.ts.map +0 -1
  212. package/dist/react/useDeltaStreams.js.map +0 -1
  213. package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
  214. package/dist/react/useStreamingUIMessages.js.map +0 -1
  215. package/dist/react/useThreadMessages.d.ts.map +0 -1
  216. package/dist/react/useThreadMessages.js.map +0 -1
  217. package/dist/react/useUIMessages.d.ts.map +0 -1
  218. package/dist/react/useUIMessages.js.map +0 -1
  219. package/src/client/streamText.test.ts +0 -102
  220. /package/dist/{client → vercel/client}/files.js +0 -0
  221. /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
  222. /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
  223. /package/dist/{react → vercel/react}/types.js +0 -0
  224. /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
  225. /package/src/{react → vercel/react}/useUIMessages.test.ts +0 -0
@@ -1,4 +1,5 @@
1
- import type { StepResult, StopCondition } from "ai";
1
+ import type { Context } from "@ai-sdk/provider-utils";
2
+ import type { StepResult, StopCondition, ToolSet } from "ai";
2
3
 
3
4
  /**
4
5
  * A stop condition that only matches tool calls which completed
@@ -14,18 +15,23 @@ export function hasSuccessfulToolCall(toolName: string): StopCondition<any> {
14
15
  ) ?? false;
15
16
  }
16
17
 
17
- export async function willContinue(
18
- steps: StepResult<any>[],
19
-
20
- stopWhen: StopCondition<any> | Array<StopCondition<any>> | undefined,
18
+ export async function willContinue<
19
+ TOOLS extends ToolSet,
20
+ RUNTIME_CONTEXT extends Context,
21
+ >(
22
+ steps: StepResult<TOOLS, RUNTIME_CONTEXT>[],
23
+ stopWhen:
24
+ | StopCondition<TOOLS, RUNTIME_CONTEXT>
25
+ | Array<StopCondition<TOOLS, RUNTIME_CONTEXT>>
26
+ | undefined,
21
27
  ): Promise<boolean> {
22
28
  const step = steps.at(-1)!;
23
29
  // we aren't doing another round after a tool result
24
30
  // TODO: whether to handle continuing after too much context used..
25
31
  if (step.finishReason !== "tool-calls") return false;
26
32
  // Count both successful results and errors as completed outputs.
27
- // In AI SDK v6, failed tool calls produce tool-error content parts
28
- // instead of tool-result, so only checking toolResults misses them.
33
+ // Failed tool calls are represented as tool-error content parts, so only
34
+ // checking toolResults misses them.
29
35
  const completedOutputs =
30
36
  step.content?.filter(
31
37
  (p) => p.type === "tool-result" || p.type === "tool-error",
@@ -4,9 +4,8 @@ import {
4
4
  blankUIMessage,
5
5
  emptyIncrementalStreamState,
6
6
  getParts,
7
- updateFromUIMessageChunks,
8
7
  } from "./deltas.js";
9
- import type { StreamDelta } from "./validators.js";
8
+ import type { StreamDelta } from "../validators.js";
10
9
  import type { ToolUIPart, UIMessageChunk } from "ai";
11
10
 
12
11
  describe("UIMessageChunks", () => {
@@ -24,80 +23,84 @@ describe("UIMessageChunks", () => {
24
23
  );
25
24
  expect(uiMessage.text).toBe("");
26
25
  expect(uiMessage.parts).toEqual([]);
27
- const updatedMessage = await updateFromUIMessageChunks(uiMessage, [
28
- { type: "start" },
29
- { type: "start-step" },
30
- { type: "reasoning-start", id: "reasoning-0" },
31
- { type: "reasoning-delta", id: "reasoning-0", delta: "Okay" },
32
- {
33
- type: "reasoning-delta",
34
- id: "reasoning-0",
35
- delta: ", the user is asking...",
36
- },
37
- { type: "text-start", id: "txt-1" },
38
- {
39
- type: "text-delta",
40
- id: "txt-1",
41
- delta: "Hey ho.",
42
- },
43
- { type: "reasoning-end", id: "reasoning-0" },
44
- { type: "text-end", id: "txt-1" },
45
- { type: "tool-input-start", toolCallId: "0ychh9k6f", toolName: "say" },
46
- {
47
- type: "tool-input-delta",
48
- toolCallId: "0ychh9k6f",
49
- inputTextDelta:
50
- '{"question":"What is your favorite flavor of ice cream?"}',
51
- },
52
- {
53
- type: "tool-input-available",
54
- toolCallId: "0ychh9k6f",
55
- toolName: "say",
56
- input: { question: "What is your favorite flavor of ice cream?" },
57
- providerMetadata: { openai: { itemId: "123" } },
58
- },
59
- {
60
- type: "tool-output-available",
61
- toolCallId: "0ychh9k6f",
62
- output: "I'm sorry I can't help you. Stop asking me questions.",
63
- },
64
- { type: "finish-step" },
65
- { type: "start-step" },
66
- { type: "tool-input-start", toolCallId: "1ychh9k6f", toolName: "say" },
67
- {
68
- type: "tool-input-delta",
69
- toolCallId: "1ychh9k6f",
70
- inputTextDelta:
71
- '{"question":"What is your favorite flavor of ice cream?"}',
72
- },
73
- {
74
- type: "tool-input-available",
75
- toolCallId: "1ychh9k6f",
76
- toolName: "say",
77
- input: { question: "What is your favorite flavor of ice cream?" },
78
- },
79
- {
80
- type: "tool-output-available",
81
- toolCallId: "1ychh9k6f",
82
- output: "I'm serious.",
83
- },
84
- { type: "finish-step" },
85
- { type: "start-step" },
86
- { type: "text-start", id: "msg_0" },
87
- {
88
- type: "text-delta",
89
- id: "msg_0",
90
- delta: "The best ice cream flavor is vanilla",
91
- },
92
- {
93
- type: "text-delta",
94
- id: "msg_0",
95
- delta: ".",
96
- },
97
- { type: "text-end", id: "msg_0" },
98
- { type: "finish-step" },
99
- { type: "finish" },
100
- ]);
26
+ const updatedMessage = applyUIMessageChunksIncremental(
27
+ uiMessage,
28
+ [
29
+ { type: "start" },
30
+ { type: "start-step" },
31
+ { type: "reasoning-start", id: "reasoning-0" },
32
+ { type: "reasoning-delta", id: "reasoning-0", delta: "Okay" },
33
+ {
34
+ type: "reasoning-delta",
35
+ id: "reasoning-0",
36
+ delta: ", the user is asking...",
37
+ },
38
+ { type: "text-start", id: "txt-1" },
39
+ {
40
+ type: "text-delta",
41
+ id: "txt-1",
42
+ delta: "Hey ho.",
43
+ },
44
+ { type: "reasoning-end", id: "reasoning-0" },
45
+ { type: "text-end", id: "txt-1" },
46
+ { type: "tool-input-start", toolCallId: "0ychh9k6f", toolName: "say" },
47
+ {
48
+ type: "tool-input-delta",
49
+ toolCallId: "0ychh9k6f",
50
+ inputTextDelta:
51
+ '{"question":"What is your favorite flavor of ice cream?"}',
52
+ },
53
+ {
54
+ type: "tool-input-available",
55
+ toolCallId: "0ychh9k6f",
56
+ toolName: "say",
57
+ input: { question: "What is your favorite flavor of ice cream?" },
58
+ providerMetadata: { openai: { itemId: "123" } },
59
+ },
60
+ {
61
+ type: "tool-output-available",
62
+ toolCallId: "0ychh9k6f",
63
+ output: "I'm sorry I can't help you. Stop asking me questions.",
64
+ },
65
+ { type: "finish-step" },
66
+ { type: "start-step" },
67
+ { type: "tool-input-start", toolCallId: "1ychh9k6f", toolName: "say" },
68
+ {
69
+ type: "tool-input-delta",
70
+ toolCallId: "1ychh9k6f",
71
+ inputTextDelta:
72
+ '{"question":"What is your favorite flavor of ice cream?"}',
73
+ },
74
+ {
75
+ type: "tool-input-available",
76
+ toolCallId: "1ychh9k6f",
77
+ toolName: "say",
78
+ input: { question: "What is your favorite flavor of ice cream?" },
79
+ },
80
+ {
81
+ type: "tool-output-available",
82
+ toolCallId: "1ychh9k6f",
83
+ output: "I'm serious.",
84
+ },
85
+ { type: "finish-step" },
86
+ { type: "start-step" },
87
+ { type: "text-start", id: "msg_0" },
88
+ {
89
+ type: "text-delta",
90
+ id: "msg_0",
91
+ delta: "The best ice cream flavor is vanilla",
92
+ },
93
+ {
94
+ type: "text-delta",
95
+ id: "msg_0",
96
+ delta: ".",
97
+ },
98
+ { type: "text-end", id: "msg_0" },
99
+ { type: "finish-step" },
100
+ { type: "finish" },
101
+ ],
102
+ emptyIncrementalStreamState(),
103
+ ).message;
101
104
  expect(updatedMessage.text).toBe(
102
105
  "Hey ho. The best ice cream flavor is vanilla.",
103
106
  );
@@ -163,9 +166,8 @@ describe("UIMessageChunks - continuation stream", () => {
163
166
  // User approves
164
167
  // Stream B: tool-result (referencing tool-call from Stream A) -> this test
165
168
  //
166
- // The AI SDK's readUIMessageStream expects tool-call before tool-result,
167
- // but they're in different streams. The onError handler should gracefully
168
- // ignore this error since stored messages provide the fallback.
169
+ // The continuation stream does not repeat the prior tool call. The reducer
170
+ // tolerates the orphan result because stored messages provide that context.
169
171
  const uiMessage = blankUIMessage(
170
172
  {
171
173
  streamId: "continuation-stream",
@@ -180,17 +182,21 @@ describe("UIMessageChunks - continuation stream", () => {
180
182
 
181
183
  // Send a tool-result without the corresponding tool-call in this stream
182
184
  // This would normally throw "No tool invocation found" error
183
- const updatedMessage = await updateFromUIMessageChunks(uiMessage, [
184
- { type: "start" },
185
- { type: "start-step" },
186
- {
187
- type: "tool-output-available",
188
- toolCallId: "call_from_previous_stream",
189
- output: "Tool execution result",
190
- },
191
- { type: "finish-step" },
192
- { type: "finish" },
193
- ]);
185
+ const updatedMessage = applyUIMessageChunksIncremental(
186
+ uiMessage,
187
+ [
188
+ { type: "start" },
189
+ { type: "start-step" },
190
+ {
191
+ type: "tool-output-available",
192
+ toolCallId: "call_from_previous_stream",
193
+ output: "Tool execution result",
194
+ },
195
+ { type: "finish-step" },
196
+ { type: "finish" },
197
+ ],
198
+ emptyIncrementalStreamState(),
199
+ ).message;
194
200
 
195
201
  // The message should NOT be marked as failed - the error should be suppressed
196
202
  expect(updatedMessage.status).not.toBe("failed");
@@ -370,6 +376,276 @@ describe("mergeDeltas", () => {
370
376
  expect((toolPart as { providerExecuted?: boolean }).providerExecuted).toBe(
371
377
  true,
372
378
  );
379
+
380
+ ({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
381
+ msg,
382
+ [
383
+ {
384
+ type: "tool-output-available",
385
+ toolCallId: "c1",
386
+ output: { result: "final" },
387
+ },
388
+ ] as UIMessageChunk[],
389
+ state,
390
+ ));
391
+ const completedToolPart = msg.parts.find(
392
+ (p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === "c1",
393
+ );
394
+ expect(
395
+ (completedToolPart as { preliminary?: boolean }).preliminary,
396
+ ).toBeUndefined();
397
+
398
+ ({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
399
+ msg,
400
+ [
401
+ {
402
+ type: "tool-output-error",
403
+ toolCallId: "c1",
404
+ errorText: "final failure",
405
+ },
406
+ ] as UIMessageChunk[],
407
+ state,
408
+ ));
409
+
410
+ const failedToolPart = msg.parts.find(
411
+ (p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === "c1",
412
+ );
413
+ expect(failedToolPart).toMatchObject({ state: "output-error" });
414
+ expect((failedToolPart as { output?: unknown }).output).toBeUndefined();
415
+ expect(
416
+ (failedToolPart as { preliminary?: boolean }).preliminary,
417
+ ).toBeUndefined();
418
+ });
419
+
420
+ it("keeps a denied approval distinct from its later denied output", () => {
421
+ const streamMessage = {
422
+ streamId: "s-tool-approval",
423
+ status: "streaming" as const,
424
+ order: 0,
425
+ stepOrder: 0,
426
+ format: "UIMessageChunk" as const,
427
+ agentName: "a",
428
+ };
429
+ let message = blankUIMessage(streamMessage, "thread-tool-approval");
430
+ let streamState = emptyIncrementalStreamState();
431
+ ({ message, streamState } = applyUIMessageChunksIncremental(
432
+ message,
433
+ [
434
+ {
435
+ type: "tool-input-start",
436
+ toolCallId: "call-1",
437
+ toolName: "deleteFile",
438
+ providerExecuted: true,
439
+ providerMetadata: { openai: { request: "call-1" } },
440
+ },
441
+ {
442
+ type: "tool-input-available",
443
+ toolCallId: "call-1",
444
+ toolName: "deleteFile",
445
+ input: { path: "/tmp/file" },
446
+ },
447
+ {
448
+ type: "tool-approval-request",
449
+ toolCallId: "call-1",
450
+ approvalId: "approval-1",
451
+ },
452
+ {
453
+ type: "tool-approval-response",
454
+ approvalId: "approval-1",
455
+ approved: false,
456
+ reason: "User declined",
457
+ },
458
+ ] as UIMessageChunk[],
459
+ streamState,
460
+ ));
461
+
462
+ expect(message.parts).toContainEqual(
463
+ expect.objectContaining({
464
+ toolCallId: "call-1",
465
+ state: "approval-responded",
466
+ approval: {
467
+ id: "approval-1",
468
+ approved: false,
469
+ reason: "User declined",
470
+ },
471
+ providerExecuted: true,
472
+ callProviderMetadata: { openai: { request: "call-1" } },
473
+ }),
474
+ );
475
+
476
+ ({ message, streamState } = applyUIMessageChunksIncremental(
477
+ message,
478
+ [{ type: "tool-output-denied", toolCallId: "call-1" }],
479
+ streamState,
480
+ ));
481
+
482
+ expect(message.parts).toContainEqual(
483
+ expect.objectContaining({
484
+ toolCallId: "call-1",
485
+ state: "output-denied",
486
+ }),
487
+ );
488
+ });
489
+
490
+ it("matches SDK 7 provider metadata replacement and dynamic tool initialization", () => {
491
+ const streamMessage = {
492
+ streamId: "s-metadata",
493
+ status: "streaming" as const,
494
+ order: 0,
495
+ stepOrder: 0,
496
+ format: "UIMessageChunk" as const,
497
+ agentName: "a",
498
+ };
499
+ const { message } = applyUIMessageChunksIncremental(
500
+ blankUIMessage(streamMessage, "thread-metadata"),
501
+ [
502
+ {
503
+ type: "text-start",
504
+ id: "t",
505
+ providerMetadata: { openai: { first: true } },
506
+ },
507
+ {
508
+ type: "text-delta",
509
+ id: "t",
510
+ delta: "x",
511
+ providerMetadata: { openai: { second: true } },
512
+ },
513
+ {
514
+ type: "tool-input-start",
515
+ toolCallId: "dynamic-1",
516
+ toolName: "runtimeTool",
517
+ dynamic: true,
518
+ providerExecuted: true,
519
+ title: "Runtime tool",
520
+ toolMetadata: { source: "provider" },
521
+ providerMetadata: { openai: { itemId: "item-1" } },
522
+ },
523
+ ] as UIMessageChunk[],
524
+ emptyIncrementalStreamState(),
525
+ );
526
+
527
+ expect(message.parts.find((part) => part.type === "text")).toMatchObject({
528
+ providerMetadata: { openai: { second: true } },
529
+ });
530
+ expect(
531
+ message.parts.find(
532
+ (part) => "toolCallId" in part && part.toolCallId === "dynamic-1",
533
+ ),
534
+ ).toMatchObject({
535
+ type: "dynamic-tool",
536
+ providerExecuted: true,
537
+ title: "Runtime tool",
538
+ toolMetadata: { source: "provider" },
539
+ callProviderMetadata: { openai: { itemId: "item-1" } },
540
+ });
541
+ });
542
+
543
+ it("creates tool parts from standalone tool-input-available chunks", () => {
544
+ const { message } = applyUIMessageChunksIncremental(
545
+ blankUIMessage(
546
+ {
547
+ streamId: "s-available",
548
+ status: "streaming",
549
+ order: 0,
550
+ stepOrder: 0,
551
+ format: "UIMessageChunk",
552
+ agentName: "a",
553
+ },
554
+ "thread-available",
555
+ ),
556
+ [
557
+ {
558
+ type: "tool-input-available",
559
+ toolCallId: "static-call",
560
+ toolName: "weather",
561
+ input: { city: "Boston" },
562
+ providerExecuted: true,
563
+ },
564
+ {
565
+ type: "tool-input-available",
566
+ toolCallId: "dynamic-call",
567
+ toolName: "runtimeTool",
568
+ dynamic: true,
569
+ input: { id: 1 },
570
+ },
571
+ ] as UIMessageChunk[],
572
+ emptyIncrementalStreamState(),
573
+ );
574
+
575
+ expect(message.parts).toMatchObject([
576
+ {
577
+ type: "tool-weather",
578
+ toolCallId: "static-call",
579
+ state: "input-available",
580
+ input: { city: "Boston" },
581
+ providerExecuted: true,
582
+ },
583
+ {
584
+ type: "dynamic-tool",
585
+ toolCallId: "dynamic-call",
586
+ toolName: "runtimeTool",
587
+ state: "input-available",
588
+ input: { id: 1 },
589
+ },
590
+ ]);
591
+ });
592
+
593
+ it("creates output-error parts from standalone tool-input-error chunks", () => {
594
+ const { message } = applyUIMessageChunksIncremental(
595
+ blankUIMessage(
596
+ {
597
+ streamId: "s-error",
598
+ status: "streaming",
599
+ order: 0,
600
+ stepOrder: 0,
601
+ format: "UIMessageChunk",
602
+ agentName: "a",
603
+ },
604
+ "thread-error",
605
+ ),
606
+ [
607
+ {
608
+ type: "tool-input-error",
609
+ toolCallId: "static-error",
610
+ toolName: "weather",
611
+ input: { city: "Boston" },
612
+ errorText: "Invalid weather request",
613
+ providerExecuted: true,
614
+ providerMetadata: { openai: { result: "static" } },
615
+ },
616
+ {
617
+ type: "tool-input-error",
618
+ toolCallId: "dynamic-error",
619
+ toolName: "runtimeTool",
620
+ dynamic: true,
621
+ input: { id: 1 },
622
+ errorText: "Invalid runtime request",
623
+ providerMetadata: { anthropic: { result: "dynamic" } },
624
+ },
625
+ ] as UIMessageChunk[],
626
+ emptyIncrementalStreamState(),
627
+ );
628
+
629
+ expect(message.parts).toMatchObject([
630
+ {
631
+ type: "tool-weather",
632
+ toolCallId: "static-error",
633
+ state: "output-error",
634
+ rawInput: { city: "Boston" },
635
+ errorText: "Invalid weather request",
636
+ providerExecuted: true,
637
+ resultProviderMetadata: { openai: { result: "static" } },
638
+ },
639
+ {
640
+ type: "dynamic-tool",
641
+ toolCallId: "dynamic-error",
642
+ toolName: "runtimeTool",
643
+ state: "output-error",
644
+ input: { id: 1 },
645
+ errorText: "Invalid runtime request",
646
+ resultProviderMetadata: { anthropic: { result: "dynamic" } },
647
+ },
648
+ ]);
373
649
  });
374
650
 
375
651
  it("applyUIMessageChunksIncremental: tool-input-error sets rawInput and clears input for static tools", async () => {
@@ -388,7 +664,15 @@ describe("mergeDeltas", () => {
388
664
  [
389
665
  { type: "start" },
390
666
  { type: "start-step" },
391
- { type: "tool-input-start", toolCallId: "c2", toolName: "myTool" },
667
+ {
668
+ type: "tool-input-start",
669
+ toolCallId: "c2",
670
+ toolName: "myTool",
671
+ providerExecuted: true,
672
+ title: "My tool",
673
+ toolMetadata: { source: "test" },
674
+ providerMetadata: { openai: { itemId: "call-1" } },
675
+ },
392
676
  ] as UIMessageChunk[],
393
677
  state,
394
678
  ));
@@ -417,6 +701,12 @@ describe("mergeDeltas", () => {
417
701
  expect((toolPart as { rawInput?: unknown }).rawInput).toEqual({
418
702
  bad: "args",
419
703
  });
704
+ expect(toolPart).toMatchObject({
705
+ providerExecuted: true,
706
+ title: "My tool",
707
+ toolMetadata: { source: "test" },
708
+ callProviderMetadata: { openai: { itemId: "call-1" } },
709
+ });
420
710
  });
421
711
 
422
712
  it("accumulates tool input across a batch boundary", async () => {
@@ -588,10 +878,11 @@ describe("mergeDeltas", () => {
588
878
  ];
589
879
 
590
880
  // SDK: process the entire stream at once.
591
- const sdkMsg = await updateFromUIMessageChunks(
881
+ const sdkMsg = applyUIMessageChunksIncremental(
592
882
  blankUIMessage(streamMessage, "thread-equiv"),
593
883
  batches.flat(),
594
- );
884
+ emptyIncrementalStreamState(),
885
+ ).message;
595
886
 
596
887
  // Incremental: process batch by batch, threading state.
597
888
  let incMsg = blankUIMessage(streamMessage, "thread-equiv");