@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1

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 (235) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +32 -27
  3. package/dist/UIMessages.d.ts +46 -0
  4. package/dist/UIMessages.d.ts.map +1 -0
  5. package/dist/UIMessages.js +546 -0
  6. package/dist/UIMessages.js.map +1 -0
  7. package/dist/client/createTool.d.ts +126 -27
  8. package/dist/client/createTool.d.ts.map +1 -1
  9. package/dist/client/createTool.js +67 -12
  10. package/dist/client/createTool.js.map +1 -1
  11. package/dist/client/defaultComponent.d.ts +11 -0
  12. package/dist/client/defaultComponent.d.ts.map +1 -0
  13. package/dist/client/defaultComponent.js +7 -0
  14. package/dist/client/defaultComponent.js.map +1 -0
  15. package/dist/client/definePlaygroundAPI.d.ts +1335 -204
  16. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  17. package/dist/client/definePlaygroundAPI.js +52 -28
  18. package/dist/client/definePlaygroundAPI.js.map +1 -1
  19. package/dist/client/files.d.ts +20 -7
  20. package/dist/client/files.d.ts.map +1 -1
  21. package/dist/client/files.js +68 -11
  22. package/dist/client/files.js.map +1 -1
  23. package/dist/client/index.d.ts +1116 -978
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +332 -747
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/messages.d.ts +461 -0
  28. package/dist/client/messages.d.ts.map +1 -0
  29. package/dist/client/messages.js +106 -0
  30. package/dist/client/messages.js.map +1 -0
  31. package/dist/client/mockModel.d.ts +42 -0
  32. package/dist/client/mockModel.d.ts.map +1 -0
  33. package/dist/client/mockModel.js +182 -0
  34. package/dist/client/mockModel.js.map +1 -0
  35. package/dist/client/saveInputMessages.d.ts +20 -0
  36. package/dist/client/saveInputMessages.d.ts.map +1 -0
  37. package/dist/client/saveInputMessages.js +58 -0
  38. package/dist/client/saveInputMessages.js.map +1 -0
  39. package/dist/client/search.d.ts +350 -39
  40. package/dist/client/search.d.ts.map +1 -1
  41. package/dist/client/search.js +350 -39
  42. package/dist/client/search.js.map +1 -1
  43. package/dist/client/start.d.ts +84 -0
  44. package/dist/client/start.d.ts.map +1 -0
  45. package/dist/client/start.js +185 -0
  46. package/dist/client/start.js.map +1 -0
  47. package/dist/client/streamText.d.ts +46 -0
  48. package/dist/client/streamText.d.ts.map +1 -0
  49. package/dist/client/streamText.js +117 -0
  50. package/dist/client/streamText.js.map +1 -0
  51. package/dist/client/streaming.d.ts +3716 -32
  52. package/dist/client/streaming.d.ts.map +1 -1
  53. package/dist/client/streaming.js +161 -59
  54. package/dist/client/streaming.js.map +1 -1
  55. package/dist/client/threads.d.ts +46 -0
  56. package/dist/client/threads.d.ts.map +1 -0
  57. package/dist/client/threads.js +49 -0
  58. package/dist/client/threads.js.map +1 -0
  59. package/dist/client/types.d.ts +266 -128
  60. package/dist/client/types.d.ts.map +1 -1
  61. package/dist/client/utils.d.ts +4 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +21 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +24 -2178
  66. package/dist/component/_generated/api.d.ts.map +1 -1
  67. package/dist/component/_generated/api.js +10 -1
  68. package/dist/component/_generated/api.js.map +1 -1
  69. package/dist/component/_generated/component.d.ts +3120 -0
  70. package/dist/component/_generated/component.d.ts.map +1 -0
  71. package/dist/component/_generated/component.js +11 -0
  72. package/dist/component/_generated/component.js.map +1 -0
  73. package/dist/component/_generated/dataModel.d.ts +4 -18
  74. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  75. package/dist/component/_generated/dataModel.js +11 -0
  76. package/dist/component/_generated/dataModel.js.map +1 -0
  77. package/dist/component/_generated/server.d.ts +10 -38
  78. package/dist/component/_generated/server.d.ts.map +1 -1
  79. package/dist/component/_generated/server.js +9 -5
  80. package/dist/component/_generated/server.js.map +1 -1
  81. package/dist/component/files.d.ts +16 -10
  82. package/dist/component/files.d.ts.map +1 -1
  83. package/dist/component/files.js +10 -2
  84. package/dist/component/files.js.map +1 -1
  85. package/dist/component/messages.d.ts +2578 -366
  86. package/dist/component/messages.d.ts.map +1 -1
  87. package/dist/component/messages.js +397 -154
  88. package/dist/component/messages.js.map +1 -1
  89. package/dist/component/schema.d.ts +5697 -3584
  90. package/dist/component/schema.d.ts.map +1 -1
  91. package/dist/component/schema.js +18 -41
  92. package/dist/component/schema.js.map +1 -1
  93. package/dist/component/streams.d.ts +39 -339
  94. package/dist/component/streams.d.ts.map +1 -1
  95. package/dist/component/streams.js +114 -73
  96. package/dist/component/streams.js.map +1 -1
  97. package/dist/component/threads.d.ts +13 -13
  98. package/dist/component/users.d.ts +7 -7
  99. package/dist/component/vector/index.d.ts +1 -1
  100. package/dist/component/vector/index.d.ts.map +1 -1
  101. package/dist/component/vector/index.js +1 -3
  102. package/dist/component/vector/index.js.map +1 -1
  103. package/dist/deltas.d.ts +43 -0
  104. package/dist/deltas.d.ts.map +1 -0
  105. package/dist/deltas.js +446 -0
  106. package/dist/deltas.js.map +1 -0
  107. package/dist/mapping.d.ts +38 -20
  108. package/dist/mapping.d.ts.map +1 -1
  109. package/dist/mapping.js +365 -97
  110. package/dist/mapping.js.map +1 -1
  111. package/dist/react/SmoothText.d.ts +5 -0
  112. package/dist/react/SmoothText.d.ts.map +1 -0
  113. package/dist/react/SmoothText.js +6 -0
  114. package/dist/react/SmoothText.js.map +1 -0
  115. package/dist/react/index.d.ts +5 -77
  116. package/dist/react/index.d.ts.map +1 -1
  117. package/dist/react/index.js +6 -160
  118. package/dist/react/index.js.map +1 -1
  119. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  120. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  121. package/dist/react/optimisticallySendMessage.js +35 -9
  122. package/dist/react/optimisticallySendMessage.js.map +1 -1
  123. package/dist/react/types.d.ts +4 -18
  124. package/dist/react/types.d.ts.map +1 -1
  125. package/dist/react/useDeltaStreams.d.ts +10 -0
  126. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  127. package/dist/react/useDeltaStreams.js +106 -0
  128. package/dist/react/useDeltaStreams.js.map +1 -0
  129. package/dist/react/useSmoothText.d.ts +13 -12
  130. package/dist/react/useSmoothText.d.ts.map +1 -1
  131. package/dist/react/useSmoothText.js +32 -15
  132. package/dist/react/useSmoothText.js.map +1 -1
  133. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  134. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  135. package/dist/react/useStreamingUIMessages.js +92 -0
  136. package/dist/react/useStreamingUIMessages.js.map +1 -0
  137. package/dist/react/useThreadMessages.d.ts +104 -0
  138. package/dist/react/useThreadMessages.d.ts.map +1 -0
  139. package/dist/react/useThreadMessages.js +148 -0
  140. package/dist/react/useThreadMessages.js.map +1 -0
  141. package/dist/react/useUIMessages.d.ts +96 -0
  142. package/dist/react/useUIMessages.d.ts.map +1 -0
  143. package/dist/react/useUIMessages.js +108 -0
  144. package/dist/react/useUIMessages.js.map +1 -0
  145. package/dist/shared.d.ts +20 -4
  146. package/dist/shared.d.ts.map +1 -1
  147. package/dist/shared.js +45 -8
  148. package/dist/shared.js.map +1 -1
  149. package/dist/validators.d.ts +22981 -5666
  150. package/dist/validators.d.ts.map +1 -1
  151. package/dist/validators.js +245 -137
  152. package/dist/validators.js.map +1 -1
  153. package/package.json +101 -51
  154. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  155. package/src/UIMessages.test.ts +273 -0
  156. package/src/UIMessages.ts +739 -0
  157. package/src/client/approval.test.ts +350 -0
  158. package/src/client/createTool.ts +291 -76
  159. package/src/client/defaultComponent.ts +17 -0
  160. package/src/client/definePlaygroundAPI.ts +67 -31
  161. package/src/client/files.ts +100 -20
  162. package/src/client/index.test.ts +40 -85
  163. package/src/client/index.ts +638 -1289
  164. package/src/client/messages.ts +237 -0
  165. package/src/client/mockModel.ts +252 -0
  166. package/src/client/saveInputMessages.test.ts +583 -0
  167. package/src/client/saveInputMessages.ts +101 -0
  168. package/src/client/search.test.ts +1207 -0
  169. package/src/client/search.ts +581 -70
  170. package/src/client/start.ts +327 -0
  171. package/src/client/streamText.ts +187 -0
  172. package/src/client/streaming.test.ts +186 -0
  173. package/src/client/streaming.ts +241 -97
  174. package/src/client/threads.ts +83 -0
  175. package/src/client/types.ts +370 -219
  176. package/src/client/utils.ts +27 -0
  177. package/src/component/_generated/api.ts +64 -0
  178. package/src/component/_generated/component.ts +4902 -0
  179. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  180. package/src/component/files.ts +11 -2
  181. package/src/component/messages.test.ts +195 -51
  182. package/src/component/messages.ts +500 -201
  183. package/src/component/schema.ts +20 -46
  184. package/src/component/setup.test.ts +7 -0
  185. package/src/component/streams.ts +184 -83
  186. package/src/component/users.test.ts +0 -1
  187. package/src/component/vector/index.ts +1 -3
  188. package/src/deltas.test.ts +626 -0
  189. package/src/deltas.ts +569 -0
  190. package/src/fromUIMessages.test.ts +497 -0
  191. package/src/mapping.test.ts +180 -6
  192. package/src/mapping.ts +479 -162
  193. package/src/react/SmoothText.tsx +9 -0
  194. package/src/react/index.ts +10 -230
  195. package/src/react/optimisticallySendMessage.ts +55 -12
  196. package/src/react/types.ts +6 -39
  197. package/src/react/useDeltaStreams.ts +160 -0
  198. package/src/react/useSmoothText.ts +56 -36
  199. package/src/react/useStreamingUIMessages.ts +143 -0
  200. package/src/react/useThreadMessages.ts +262 -0
  201. package/src/react/useUIMessages.test.ts +255 -0
  202. package/src/react/useUIMessages.ts +195 -0
  203. package/src/shared.ts +88 -12
  204. package/src/test.ts +18 -0
  205. package/src/toUIMessages.test.ts +1269 -0
  206. package/src/validators.test.ts +18 -19
  207. package/src/validators.ts +325 -185
  208. package/dist/client/_generated/_ignore.d.ts +0 -1
  209. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  210. package/dist/client/_generated/_ignore.js +0 -3
  211. package/dist/client/_generated/_ignore.js.map +0 -1
  212. package/dist/client/listMessages.d.ts +0 -22
  213. package/dist/client/listMessages.d.ts.map +0 -1
  214. package/dist/client/listMessages.js +0 -25
  215. package/dist/client/listMessages.js.map +0 -1
  216. package/dist/package.json +0 -3
  217. package/dist/react/deltas.d.ts +0 -26
  218. package/dist/react/deltas.d.ts.map +0 -1
  219. package/dist/react/deltas.js +0 -384
  220. package/dist/react/deltas.js.map +0 -1
  221. package/dist/react/toUIMessages.d.ts +0 -15
  222. package/dist/react/toUIMessages.d.ts.map +0 -1
  223. package/dist/react/toUIMessages.js +0 -211
  224. package/dist/react/toUIMessages.js.map +0 -1
  225. package/src/client/listMessages.ts +0 -38
  226. package/src/component/_generated/api.d.ts +0 -2202
  227. package/src/component/_generated/api.js +0 -23
  228. package/src/component/_generated/server.js +0 -90
  229. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  230. package/src/react/deltas.test.ts +0 -315
  231. package/src/react/deltas.ts +0 -478
  232. package/src/react/toUIMessages.test.ts +0 -420
  233. package/src/react/toUIMessages.ts +0 -253
  234. package/src/vitest.config.ts +0 -7
  235. /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
+ });