@convex-dev/agent 0.5.0-alpha.1 → 0.6.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 (233) hide show
  1. package/README.md +32 -27
  2. package/dist/UIMessages.d.ts +46 -0
  3. package/dist/UIMessages.d.ts.map +1 -0
  4. package/dist/UIMessages.js +546 -0
  5. package/dist/UIMessages.js.map +1 -0
  6. package/dist/client/createTool.d.ts +129 -27
  7. package/dist/client/createTool.d.ts.map +1 -1
  8. package/dist/client/createTool.js +66 -12
  9. package/dist/client/createTool.js.map +1 -1
  10. package/dist/client/defaultComponent.d.ts +11 -0
  11. package/dist/client/defaultComponent.d.ts.map +1 -0
  12. package/dist/client/defaultComponent.js +7 -0
  13. package/dist/client/defaultComponent.js.map +1 -0
  14. package/dist/client/definePlaygroundAPI.d.ts +1323 -192
  15. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  16. package/dist/client/definePlaygroundAPI.js +52 -28
  17. package/dist/client/definePlaygroundAPI.js.map +1 -1
  18. package/dist/client/files.d.ts +20 -7
  19. package/dist/client/files.d.ts.map +1 -1
  20. package/dist/client/files.js +68 -11
  21. package/dist/client/files.js.map +1 -1
  22. package/dist/client/index.d.ts +1056 -965
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +242 -748
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/messages.d.ts +461 -0
  27. package/dist/client/messages.d.ts.map +1 -0
  28. package/dist/client/messages.js +106 -0
  29. package/dist/client/messages.js.map +1 -0
  30. package/dist/client/mockModel.d.ts +42 -0
  31. package/dist/client/mockModel.d.ts.map +1 -0
  32. package/dist/client/mockModel.js +175 -0
  33. package/dist/client/mockModel.js.map +1 -0
  34. package/dist/client/saveInputMessages.d.ts +20 -0
  35. package/dist/client/saveInputMessages.d.ts.map +1 -0
  36. package/dist/client/saveInputMessages.js +58 -0
  37. package/dist/client/saveInputMessages.js.map +1 -0
  38. package/dist/client/search.d.ts +346 -35
  39. package/dist/client/search.d.ts.map +1 -1
  40. package/dist/client/search.js +350 -39
  41. package/dist/client/search.js.map +1 -1
  42. package/dist/client/start.d.ts +84 -0
  43. package/dist/client/start.d.ts.map +1 -0
  44. package/dist/client/start.js +171 -0
  45. package/dist/client/start.js.map +1 -0
  46. package/dist/client/streamText.d.ts +46 -0
  47. package/dist/client/streamText.d.ts.map +1 -0
  48. package/dist/client/streamText.js +93 -0
  49. package/dist/client/streamText.js.map +1 -0
  50. package/dist/client/streaming.d.ts +3705 -32
  51. package/dist/client/streaming.d.ts.map +1 -1
  52. package/dist/client/streaming.js +141 -59
  53. package/dist/client/streaming.js.map +1 -1
  54. package/dist/client/threads.d.ts +46 -0
  55. package/dist/client/threads.d.ts.map +1 -0
  56. package/dist/client/threads.js +49 -0
  57. package/dist/client/threads.js.map +1 -0
  58. package/dist/client/types.d.ts +265 -128
  59. package/dist/client/types.d.ts.map +1 -1
  60. package/dist/client/utils.d.ts +4 -0
  61. package/dist/client/utils.d.ts.map +1 -0
  62. package/dist/client/utils.js +21 -0
  63. package/dist/client/utils.js.map +1 -0
  64. package/dist/component/_generated/api.d.ts +24 -2178
  65. package/dist/component/_generated/api.d.ts.map +1 -1
  66. package/dist/component/_generated/api.js +10 -1
  67. package/dist/component/_generated/api.js.map +1 -1
  68. package/dist/component/_generated/component.d.ts +3119 -0
  69. package/dist/component/_generated/component.d.ts.map +1 -0
  70. package/dist/component/_generated/component.js +11 -0
  71. package/dist/component/_generated/component.js.map +1 -0
  72. package/dist/component/_generated/dataModel.d.ts +4 -18
  73. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  74. package/dist/component/_generated/dataModel.js +11 -0
  75. package/dist/component/_generated/dataModel.js.map +1 -0
  76. package/dist/component/_generated/server.d.ts +10 -38
  77. package/dist/component/_generated/server.d.ts.map +1 -1
  78. package/dist/component/_generated/server.js +9 -5
  79. package/dist/component/_generated/server.js.map +1 -1
  80. package/dist/component/files.d.ts +16 -10
  81. package/dist/component/files.d.ts.map +1 -1
  82. package/dist/component/files.js +10 -2
  83. package/dist/component/files.js.map +1 -1
  84. package/dist/component/messages.d.ts +2553 -342
  85. package/dist/component/messages.d.ts.map +1 -1
  86. package/dist/component/messages.js +387 -154
  87. package/dist/component/messages.js.map +1 -1
  88. package/dist/component/schema.d.ts +5697 -3584
  89. package/dist/component/schema.d.ts.map +1 -1
  90. package/dist/component/schema.js +18 -41
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/streams.d.ts +35 -335
  93. package/dist/component/streams.d.ts.map +1 -1
  94. package/dist/component/streams.js +114 -73
  95. package/dist/component/streams.js.map +1 -1
  96. package/dist/component/threads.d.ts +16 -16
  97. package/dist/component/users.d.ts +4 -4
  98. package/dist/component/vector/index.d.ts +1 -1
  99. package/dist/component/vector/index.d.ts.map +1 -1
  100. package/dist/component/vector/index.js +1 -3
  101. package/dist/component/vector/index.js.map +1 -1
  102. package/dist/deltas.d.ts +43 -0
  103. package/dist/deltas.d.ts.map +1 -0
  104. package/dist/deltas.js +447 -0
  105. package/dist/deltas.js.map +1 -0
  106. package/dist/mapping.d.ts +20 -20
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +313 -96
  109. package/dist/mapping.js.map +1 -1
  110. package/dist/react/SmoothText.d.ts +5 -0
  111. package/dist/react/SmoothText.d.ts.map +1 -0
  112. package/dist/react/SmoothText.js +6 -0
  113. package/dist/react/SmoothText.js.map +1 -0
  114. package/dist/react/index.d.ts +5 -77
  115. package/dist/react/index.d.ts.map +1 -1
  116. package/dist/react/index.js +6 -160
  117. package/dist/react/index.js.map +1 -1
  118. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  119. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  120. package/dist/react/optimisticallySendMessage.js +35 -9
  121. package/dist/react/optimisticallySendMessage.js.map +1 -1
  122. package/dist/react/types.d.ts +4 -18
  123. package/dist/react/types.d.ts.map +1 -1
  124. package/dist/react/useDeltaStreams.d.ts +10 -0
  125. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  126. package/dist/react/useDeltaStreams.js +101 -0
  127. package/dist/react/useDeltaStreams.js.map +1 -0
  128. package/dist/react/useSmoothText.d.ts +13 -12
  129. package/dist/react/useSmoothText.d.ts.map +1 -1
  130. package/dist/react/useSmoothText.js +32 -15
  131. package/dist/react/useSmoothText.js.map +1 -1
  132. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  133. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  134. package/dist/react/useStreamingUIMessages.js +92 -0
  135. package/dist/react/useStreamingUIMessages.js.map +1 -0
  136. package/dist/react/useThreadMessages.d.ts +104 -0
  137. package/dist/react/useThreadMessages.d.ts.map +1 -0
  138. package/dist/react/useThreadMessages.js +148 -0
  139. package/dist/react/useThreadMessages.js.map +1 -0
  140. package/dist/react/useUIMessages.d.ts +96 -0
  141. package/dist/react/useUIMessages.d.ts.map +1 -0
  142. package/dist/react/useUIMessages.js +108 -0
  143. package/dist/react/useUIMessages.js.map +1 -0
  144. package/dist/shared.d.ts +20 -4
  145. package/dist/shared.d.ts.map +1 -1
  146. package/dist/shared.js +45 -8
  147. package/dist/shared.js.map +1 -1
  148. package/dist/validators.d.ts +22981 -5666
  149. package/dist/validators.d.ts.map +1 -1
  150. package/dist/validators.js +245 -137
  151. package/dist/validators.js.map +1 -1
  152. package/package.json +98 -50
  153. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  154. package/src/UIMessages.test.ts +273 -0
  155. package/src/UIMessages.ts +739 -0
  156. package/src/client/createTool.ts +293 -76
  157. package/src/client/defaultComponent.ts +17 -0
  158. package/src/client/definePlaygroundAPI.ts +67 -31
  159. package/src/client/files.ts +100 -20
  160. package/src/client/index.test.ts +40 -85
  161. package/src/client/index.ts +520 -1290
  162. package/src/client/messages.ts +237 -0
  163. package/src/client/mockModel.ts +245 -0
  164. package/src/client/saveInputMessages.test.ts +583 -0
  165. package/src/client/saveInputMessages.ts +101 -0
  166. package/src/client/search.test.ts +1207 -0
  167. package/src/client/search.ts +577 -70
  168. package/src/client/start.ts +310 -0
  169. package/src/client/streamText.ts +163 -0
  170. package/src/client/streaming.test.ts +186 -0
  171. package/src/client/streaming.ts +219 -97
  172. package/src/client/threads.ts +83 -0
  173. package/src/client/types.ts +368 -219
  174. package/src/client/utils.ts +27 -0
  175. package/src/component/_generated/api.ts +64 -0
  176. package/src/component/_generated/component.ts +4913 -0
  177. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  178. package/src/component/files.ts +11 -2
  179. package/src/component/messages.test.ts +195 -51
  180. package/src/component/messages.ts +490 -201
  181. package/src/component/schema.ts +20 -46
  182. package/src/component/setup.test.ts +7 -0
  183. package/src/component/streams.ts +184 -83
  184. package/src/component/users.test.ts +0 -1
  185. package/src/component/vector/index.ts +1 -3
  186. package/src/deltas.test.ts +626 -0
  187. package/src/deltas.ts +570 -0
  188. package/src/fromUIMessages.test.ts +497 -0
  189. package/src/mapping.test.ts +103 -6
  190. package/src/mapping.ts +422 -161
  191. package/src/react/SmoothText.tsx +9 -0
  192. package/src/react/index.ts +10 -230
  193. package/src/react/optimisticallySendMessage.ts +55 -12
  194. package/src/react/types.ts +6 -39
  195. package/src/react/useDeltaStreams.ts +154 -0
  196. package/src/react/useSmoothText.ts +56 -36
  197. package/src/react/useStreamingUIMessages.ts +143 -0
  198. package/src/react/useThreadMessages.ts +262 -0
  199. package/src/react/useUIMessages.test.ts +255 -0
  200. package/src/react/useUIMessages.ts +195 -0
  201. package/src/shared.ts +88 -12
  202. package/src/test.ts +18 -0
  203. package/src/toUIMessages.test.ts +1269 -0
  204. package/src/validators.test.ts +18 -19
  205. package/src/validators.ts +325 -185
  206. package/dist/client/_generated/_ignore.d.ts +0 -1
  207. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  208. package/dist/client/_generated/_ignore.js +0 -3
  209. package/dist/client/_generated/_ignore.js.map +0 -1
  210. package/dist/client/listMessages.d.ts +0 -22
  211. package/dist/client/listMessages.d.ts.map +0 -1
  212. package/dist/client/listMessages.js +0 -25
  213. package/dist/client/listMessages.js.map +0 -1
  214. package/dist/package.json +0 -3
  215. package/dist/react/deltas.d.ts +0 -26
  216. package/dist/react/deltas.d.ts.map +0 -1
  217. package/dist/react/deltas.js +0 -384
  218. package/dist/react/deltas.js.map +0 -1
  219. package/dist/react/toUIMessages.d.ts +0 -15
  220. package/dist/react/toUIMessages.d.ts.map +0 -1
  221. package/dist/react/toUIMessages.js +0 -211
  222. package/dist/react/toUIMessages.js.map +0 -1
  223. package/src/client/listMessages.ts +0 -38
  224. package/src/component/_generated/api.d.ts +0 -2202
  225. package/src/component/_generated/api.js +0 -23
  226. package/src/component/_generated/server.js +0 -90
  227. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  228. package/src/react/deltas.test.ts +0 -315
  229. package/src/react/deltas.ts +0 -478
  230. package/src/react/toUIMessages.test.ts +0 -420
  231. package/src/react/toUIMessages.ts +0 -253
  232. package/src/vitest.config.ts +0 -7
  233. /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
@@ -0,0 +1,626 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ blankUIMessage,
4
+ deriveUIMessagesFromTextStreamParts,
5
+ updateFromTextStreamParts,
6
+ updateFromUIMessageChunks,
7
+ } from "./deltas.js";
8
+ import type { StreamMessage, StreamDelta } from "./validators.js";
9
+ import { omit } from "convex-helpers";
10
+ import type { Tool, ToolUIPart, TypedToolResult } from "ai";
11
+
12
+ describe("UIMessageChunks", () => {
13
+ it("updates a UIMessage with a tool call and follow up", async () => {
14
+ const uiMessage = blankUIMessage(
15
+ {
16
+ streamId: "s1",
17
+ status: "streaming",
18
+ order: 0,
19
+ stepOrder: 1,
20
+ format: "UIMessageChunk",
21
+ agentName: "agent1",
22
+ },
23
+ "thread1",
24
+ );
25
+ expect(uiMessage.text).toBe("");
26
+ 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
+ ]);
101
+ expect(updatedMessage.text).toBe(
102
+ "Hey ho. The best ice cream flavor is vanilla.",
103
+ );
104
+ const expectedParts = [
105
+ {
106
+ type: "step-start",
107
+ },
108
+ {
109
+ state: "done",
110
+ text: "Okay, the user is asking...",
111
+ type: "reasoning",
112
+ },
113
+ {
114
+ state: "done",
115
+ text: "Hey ho.",
116
+ type: "text",
117
+ },
118
+ {
119
+ callProviderMetadata: {
120
+ openai: {
121
+ itemId: "123",
122
+ },
123
+ },
124
+ input: {
125
+ question: "What is your favorite flavor of ice cream?",
126
+ },
127
+ output: "I'm sorry I can't help you. Stop asking me questions.",
128
+
129
+ state: "output-available",
130
+ toolCallId: "0ychh9k6f",
131
+ type: "tool-say",
132
+ },
133
+ {
134
+ type: "step-start",
135
+ },
136
+ {
137
+ input: {
138
+ question: "What is your favorite flavor of ice cream?",
139
+ },
140
+ output: "I'm serious.",
141
+ state: "output-available",
142
+ toolCallId: "1ychh9k6f",
143
+ type: "tool-say",
144
+ },
145
+ {
146
+ type: "step-start",
147
+ },
148
+ {
149
+ state: "done",
150
+ text: "The best ice cream flavor is vanilla.",
151
+ type: "text",
152
+ },
153
+ ];
154
+ expect(updatedMessage.parts).toEqual(expectedParts);
155
+ expect(updatedMessage.parts).toHaveLength(8);
156
+ });
157
+ });
158
+
159
+ describe("UIMessageChunks - continuation stream", () => {
160
+ it("gracefully handles tool-result without tool-call in continuation stream after approval", async () => {
161
+ // This simulates what happens after tool approval:
162
+ // Stream A: tool-call, tool-approval-request -> finishes
163
+ // User approves
164
+ // Stream B: tool-result (referencing tool-call from Stream A) -> this test
165
+ //
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
+ const uiMessage = blankUIMessage(
170
+ {
171
+ streamId: "continuation-stream",
172
+ status: "streaming",
173
+ order: 1,
174
+ stepOrder: 0,
175
+ format: "UIMessageChunk",
176
+ agentName: "agent1",
177
+ },
178
+ "thread1",
179
+ );
180
+
181
+ // Send a tool-result without the corresponding tool-call in this stream
182
+ // 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
+ ]);
194
+
195
+ // The message should NOT be marked as failed - the error should be suppressed
196
+ expect(updatedMessage.status).not.toBe("failed");
197
+ // The stream still processes (even if tool-output isn't reflected without tool-input)
198
+ expect(updatedMessage).toBeDefined();
199
+ });
200
+ });
201
+
202
+ describe("mergeDeltas", () => {
203
+ it("merges a single text-delta into a message", () => {
204
+ const streamId = "s1";
205
+ const deltas = [
206
+ {
207
+ streamId,
208
+ start: 0,
209
+ end: 5,
210
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
211
+ } satisfies StreamDelta,
212
+ ];
213
+ const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
214
+ "thread1",
215
+ [{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
216
+ [],
217
+ deltas,
218
+ );
219
+ expect(messages).toHaveLength(1);
220
+ expect(messages[0].text).toBe("Hello");
221
+ expect(messages[0].role).toBe("assistant");
222
+ expect(changed).toBe(true);
223
+ expect(newStreams[0].cursor).toBe(5);
224
+ });
225
+
226
+ it("merges multiple deltas for the same stream", () => {
227
+ const streamId = "s1";
228
+ const deltas = [
229
+ {
230
+ streamId,
231
+ start: 0,
232
+ end: 5,
233
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
234
+ },
235
+ {
236
+ streamId,
237
+ start: 5,
238
+ end: 11,
239
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
240
+ },
241
+ ];
242
+ const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
243
+ "thread1",
244
+ [{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
245
+ [],
246
+ deltas,
247
+ );
248
+ expect(messages).toHaveLength(1);
249
+ expect(messages[0].text).toBe("Hello World!");
250
+ expect(changed).toBe(true);
251
+ expect(newStreams[0].cursor).toBe(11);
252
+ });
253
+
254
+ it("handles tool-call and tool-result parts", () => {
255
+ const streamId = "s2";
256
+ const deltas = [
257
+ {
258
+ streamId,
259
+ start: 0,
260
+ end: 1,
261
+ parts: [
262
+ {
263
+ type: "tool-call",
264
+ toolCallId: "call1",
265
+ toolName: "myTool",
266
+ input: "What's the meaning of life?",
267
+ },
268
+ ],
269
+ } satisfies StreamDelta,
270
+ {
271
+ streamId,
272
+ start: 1,
273
+ end: 2,
274
+ parts: [
275
+ {
276
+ type: "tool-result",
277
+ toolCallId: "call1",
278
+ toolName: "myTool",
279
+ input: undefined,
280
+ output: "42",
281
+ } satisfies TypedToolResult<{ myTool: Tool }>,
282
+ ],
283
+ } satisfies StreamDelta,
284
+ ];
285
+ const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
286
+ "thread1",
287
+ [{ streamId, order: 2, stepOrder: 0, status: "streaming" }],
288
+ [],
289
+ deltas,
290
+ );
291
+ expect(message).toBeDefined();
292
+ expect(message.role).toBe("assistant");
293
+ const content = message.parts;
294
+ expect(content).toEqual([
295
+ {
296
+ type: "tool-myTool",
297
+ toolCallId: "call1",
298
+ input: "What's the meaning of life?",
299
+ output: "42",
300
+ state: "output-available",
301
+ } satisfies ToolUIPart,
302
+ ]);
303
+ expect(changed).toBe(true);
304
+ });
305
+
306
+ it("returns changed=false if no new deltas", () => {
307
+ const streamId = "s3";
308
+ const deltas: StreamDelta[] = [];
309
+ const [, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
310
+ "thread1",
311
+ [{ streamId, order: 3, stepOrder: 0, status: "streaming" }],
312
+ [],
313
+ deltas,
314
+ );
315
+ expect(changed).toBe(false);
316
+ expect(newStreams[0].cursor).toBe(0);
317
+ });
318
+
319
+ it("handles multiple streams and sorts by order/stepOrder", () => {
320
+ const deltas = [
321
+ {
322
+ streamId: "s2",
323
+ start: 0,
324
+ end: 3,
325
+ parts: [{ type: "text-delta", id: "1", text: "B" }],
326
+ } satisfies StreamDelta,
327
+ {
328
+ streamId: "s1",
329
+ start: 0,
330
+ end: 3,
331
+ parts: [{ type: "text-delta", id: "2", text: "A" }],
332
+ } satisfies StreamDelta,
333
+ ];
334
+ const [messages, _, changed] = deriveUIMessagesFromTextStreamParts(
335
+ "thread1",
336
+ [
337
+ { streamId: "s1", order: 1, stepOrder: 0, status: "streaming" },
338
+ { streamId: "s2", order: 2, stepOrder: 0, status: "streaming" },
339
+ ],
340
+ [],
341
+ deltas,
342
+ );
343
+ expect(messages).toHaveLength(2);
344
+ expect(messages[0].text).toBe("A");
345
+ expect(messages[1].text).toBe("B");
346
+ expect(changed).toBe(true);
347
+ // Sorted by order
348
+ expect(messages[0].order).toBe(1);
349
+ expect(messages[1].order).toBe(2);
350
+ });
351
+
352
+ it("does not duplicate text content when merging sequential text-deltas", () => {
353
+ const streamId = "s4";
354
+ const deltas = [
355
+ {
356
+ streamId,
357
+ start: 0,
358
+ end: 5,
359
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
360
+ },
361
+ {
362
+ streamId,
363
+ start: 5,
364
+ end: 11,
365
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
366
+ },
367
+ {
368
+ streamId,
369
+ start: 11,
370
+ end: 12,
371
+ parts: [{ type: "text-delta", id: "3", text: "!" }],
372
+ },
373
+ ] satisfies StreamDelta[];
374
+ const [messages] = deriveUIMessagesFromTextStreamParts(
375
+ "thread1",
376
+ [{ streamId, order: 4, stepOrder: 0, status: "streaming" }],
377
+ [],
378
+ deltas,
379
+ );
380
+ expect(messages).toHaveLength(1);
381
+ expect(messages[0].text).toBe("Hello World!!");
382
+ // There should only be one text part per message
383
+ const content = messages[0].parts;
384
+ if (Array.isArray(content)) {
385
+ const textParts = content.filter((p) => p.type === "text");
386
+ expect(textParts).toHaveLength(1);
387
+ expect(textParts[0].text).toBe("Hello World!!");
388
+ }
389
+ });
390
+
391
+ it("does not duplicate reasoning parts", () => {
392
+ const streamId = "s6";
393
+ const deltas = [
394
+ {
395
+ streamId,
396
+ start: 0,
397
+ end: 1,
398
+ parts: [
399
+ { type: "reasoning-start", id: "1" },
400
+ { type: "reasoning-delta", id: "1", text: "I'm thinking..." },
401
+ ],
402
+ },
403
+ {
404
+ streamId,
405
+ start: 1,
406
+ end: 2,
407
+ parts: [
408
+ { type: "reasoning-delta", id: "1", text: " Still thinking..." },
409
+ ],
410
+ },
411
+ {
412
+ streamId,
413
+ start: 2,
414
+ end: 3,
415
+ parts: [{ type: "reasoning-end", id: "1" }],
416
+ },
417
+ ];
418
+ const [messages] = deriveUIMessagesFromTextStreamParts(
419
+ "thread1",
420
+ [{ streamId, order: 6, stepOrder: 0, status: "streaming" }],
421
+ [],
422
+ deltas,
423
+ );
424
+ expect(messages).toHaveLength(1);
425
+ if (Array.isArray(messages[0].parts)) {
426
+ const reasoningParts = messages[0].parts.filter(
427
+ (p) => p.type === "reasoning",
428
+ );
429
+ expect(reasoningParts).toHaveLength(1);
430
+ expect(reasoningParts[0].text).toBe("I'm thinking... Still thinking...");
431
+ expect(reasoningParts[0].state).toBe("done");
432
+ }
433
+ });
434
+
435
+ it("applyDeltasToStreamMessage is idempotent and does not duplicate content", () => {
436
+ const streamId = "s7";
437
+ const streamMessage = {
438
+ streamId,
439
+ order: 7,
440
+ stepOrder: 0,
441
+ status: "streaming",
442
+ } satisfies StreamMessage;
443
+ const deltas = [
444
+ {
445
+ streamId,
446
+ start: 0,
447
+ end: 5,
448
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
449
+ },
450
+ {
451
+ streamId,
452
+ start: 5,
453
+ end: 11,
454
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
455
+ },
456
+ ];
457
+ // First call: apply both deltas
458
+ let [result, changed] = updateFromTextStreamParts(
459
+ "thread1",
460
+ streamMessage,
461
+ undefined,
462
+ deltas,
463
+ );
464
+ expect(result.message.text).toBe("Hello World!");
465
+ // Second call: re-apply the same deltas (should not duplicate)
466
+ [result, changed] = updateFromTextStreamParts(
467
+ "thread1",
468
+ streamMessage,
469
+ result,
470
+ deltas,
471
+ );
472
+ expect(result.message.text).toBe("Hello World!");
473
+ // Third call: add a new delta
474
+ const moreDeltas = [
475
+ ...deltas,
476
+ {
477
+ streamId,
478
+ start: 11,
479
+ end: 12,
480
+ parts: [{ type: "text-delta", id: "3", text: "!" }],
481
+ },
482
+ ];
483
+ [result, changed] = updateFromTextStreamParts(
484
+ "thread1",
485
+ streamMessage,
486
+ result,
487
+ moreDeltas,
488
+ );
489
+ expect(changed).toBe(true);
490
+ expect(result.message.text).toBe("Hello World!!");
491
+ // Re-apply all deltas again (should still not duplicate)
492
+ [result, changed] = updateFromTextStreamParts(
493
+ "thread1",
494
+ streamMessage,
495
+ result,
496
+ moreDeltas,
497
+ );
498
+ expect(changed).toBe(false);
499
+ expect(result.message.text).toBe("Hello World!!");
500
+ });
501
+
502
+ it("mergeDeltas is pure and does not mutate inputs", () => {
503
+ const streamId = "s8";
504
+ const streamMessages = [
505
+ { streamId, order: 8, stepOrder: 0, status: "streaming" },
506
+ ] satisfies StreamMessage[];
507
+ const deltas = [
508
+ {
509
+ streamId,
510
+ start: 0,
511
+ end: 5,
512
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
513
+ },
514
+ {
515
+ streamId,
516
+ start: 5,
517
+ end: 11,
518
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
519
+ },
520
+ ];
521
+ // Deep freeze inputs to catch mutation
522
+ function deepFreeze(obj: unknown): unknown {
523
+ if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
524
+ Object.freeze(obj);
525
+ for (const key of Object.keys(obj)) {
526
+ deepFreeze((obj as Record<string, unknown>)[key]);
527
+ }
528
+ }
529
+ return obj;
530
+ }
531
+ deepFreeze(streamMessages);
532
+ deepFreeze(deltas);
533
+ const [messages1, streams1, changed1] = deriveUIMessagesFromTextStreamParts(
534
+ "thread1",
535
+ streamMessages,
536
+ [],
537
+ deltas,
538
+ );
539
+ const [messages2, streams2, changed2] = deriveUIMessagesFromTextStreamParts(
540
+ "thread1",
541
+ streamMessages,
542
+ [],
543
+ deltas,
544
+ );
545
+ expect(messages1.map((m) => omit(m, ["_creationTime"]))).toEqual(
546
+ messages2.map((m) => omit(m, ["_creationTime"])),
547
+ );
548
+ expect(
549
+ streams1.map((s) => ({
550
+ ...s,
551
+ message: omit(s.message, ["_creationTime"]),
552
+ })),
553
+ ).toEqual(
554
+ streams2.map((s) => ({
555
+ ...s,
556
+ message: omit(s.message, ["_creationTime"]),
557
+ })),
558
+ );
559
+ expect(changed1).toBe(changed2);
560
+ // Inputs should remain unchanged
561
+ expect(streamMessages).toMatchObject([
562
+ { streamId, order: 8, stepOrder: 0, status: "streaming" },
563
+ ]);
564
+ expect(deltas).toEqual([
565
+ {
566
+ streamId,
567
+ start: 0,
568
+ end: 5,
569
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
570
+ },
571
+ {
572
+ streamId,
573
+ start: 5,
574
+ end: 11,
575
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
576
+ },
577
+ ]);
578
+ });
579
+
580
+ it("handles streaming tool-approval-request and updates tool state", () => {
581
+ const streamId = "s10";
582
+ const deltas = [
583
+ {
584
+ streamId,
585
+ start: 0,
586
+ end: 1,
587
+ parts: [
588
+ {
589
+ type: "tool-call",
590
+ toolCallId: "call1",
591
+ toolName: "dangerousTool",
592
+ input: { action: "delete" },
593
+ },
594
+ ],
595
+ } satisfies StreamDelta,
596
+ {
597
+ streamId,
598
+ start: 1,
599
+ end: 2,
600
+ parts: [
601
+ {
602
+ type: "tool-approval-request",
603
+ toolCallId: "call1",
604
+ approvalId: "approval1",
605
+ },
606
+ ],
607
+ } satisfies StreamDelta,
608
+ ];
609
+ const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
610
+ "thread1",
611
+ [{ streamId, order: 10, stepOrder: 0, status: "streaming" }],
612
+ [],
613
+ deltas,
614
+ );
615
+ expect(message).toBeDefined();
616
+ expect(message.role).toBe("assistant");
617
+ expect(changed).toBe(true);
618
+
619
+ const toolPart = message.parts.find(
620
+ (p) => p.type === "tool-dangerousTool",
621
+ ) as any;
622
+ expect(toolPart).toBeDefined();
623
+ expect(toolPart.state).toBe("approval-requested");
624
+ expect(toolPart.approval).toEqual({ id: "approval1" });
625
+ });
626
+ });