@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,497 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { MessageDoc } from "./client/index.js";
3
+ import type { UIMessage } from "./UIMessages.js";
4
+ import { fromUIMessages, toUIMessages } from "./UIMessages.js";
5
+
6
+ // Helper to create a base message doc
7
+ function baseMessageDoc<T = unknown>(
8
+ overrides: Partial<MessageDoc & { streaming?: boolean; metadata?: T }> = {},
9
+ ): MessageDoc & { streaming?: boolean; metadata?: T } {
10
+ return {
11
+ _id: "msg1",
12
+ _creationTime: Date.now(),
13
+ order: 0,
14
+ stepOrder: 0,
15
+ status: "success",
16
+ threadId: "thread1",
17
+ tool: false,
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ describe("fromUIMessages round-trip tests", () => {
23
+ it("preserves essential data for simple user message", async () => {
24
+ const originalMessages = [
25
+ baseMessageDoc({
26
+ message: {
27
+ role: "user",
28
+ content: "Hello world!",
29
+ },
30
+ text: "Hello world!",
31
+ }),
32
+ ];
33
+
34
+ const uiMessages = toUIMessages(originalMessages);
35
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
36
+ threadId: "thread1",
37
+ });
38
+
39
+ expect(uiMessages).toHaveLength(1);
40
+ expect(uiMessages[0].role).toBe("user");
41
+ expect(uiMessages[0].text).toBe("Hello world!");
42
+
43
+ expect(backToMessageDocs).toHaveLength(1);
44
+ expect(backToMessageDocs[0].text).toBe("Hello world!");
45
+ expect(backToMessageDocs[0].threadId).toBe("thread1");
46
+
47
+ // Content gets normalized to array format
48
+ expect(Array.isArray(backToMessageDocs[0].message?.content)).toBe(true);
49
+ if (Array.isArray(backToMessageDocs[0].message?.content)) {
50
+ expect(backToMessageDocs[0].message.content[0]).toMatchObject({
51
+ type: "text",
52
+ text: "Hello world!",
53
+ });
54
+ }
55
+ });
56
+
57
+ it("preserves essential data for assistant message", async () => {
58
+ const originalMessages = [
59
+ baseMessageDoc({
60
+ message: {
61
+ role: "assistant",
62
+ content: "Hi there! How can I help?",
63
+ },
64
+ text: "Hi there! How can I help?",
65
+ }),
66
+ ];
67
+
68
+ const uiMessages = toUIMessages(originalMessages);
69
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
70
+ threadId: "thread1",
71
+ });
72
+
73
+ expect(uiMessages).toHaveLength(1);
74
+ expect(uiMessages[0].role).toBe("assistant");
75
+ expect(uiMessages[0].text).toBe("Hi there! How can I help?");
76
+
77
+ expect(backToMessageDocs).toHaveLength(1);
78
+ expect(backToMessageDocs[0].text).toBe("Hi there! How can I help?");
79
+ });
80
+
81
+ it("preserves system messages correctly", async () => {
82
+ const originalMessages = [
83
+ baseMessageDoc({
84
+ message: {
85
+ role: "system",
86
+ content: "You are a helpful assistant.",
87
+ },
88
+ text: "You are a helpful assistant.",
89
+ }),
90
+ ];
91
+
92
+ const uiMessages = toUIMessages(originalMessages);
93
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
94
+ threadId: "thread1",
95
+ });
96
+
97
+ expect(uiMessages).toHaveLength(1);
98
+ expect(uiMessages[0].role).toBe("system");
99
+ expect(uiMessages[0].text).toBe("You are a helpful assistant.");
100
+
101
+ expect(backToMessageDocs).toHaveLength(1);
102
+ expect(backToMessageDocs[0].text).toBe("You are a helpful assistant.");
103
+ expect(backToMessageDocs[0].message?.role).toBe("system");
104
+
105
+ // System content stays as string
106
+ expect(backToMessageDocs[0].message?.content).toBe(
107
+ "You are a helpful assistant.",
108
+ );
109
+ });
110
+
111
+ it("preserves reasoning in assistant messages", async () => {
112
+ const originalMessages = [
113
+ baseMessageDoc({
114
+ message: {
115
+ role: "assistant",
116
+ content: [
117
+ {
118
+ type: "reasoning",
119
+ text: "Let me think about this...",
120
+ },
121
+ {
122
+ type: "text",
123
+ text: "Here's my response.",
124
+ },
125
+ ],
126
+ },
127
+ text: "Here's my response.",
128
+ reasoning: "Let me think about this...",
129
+ }),
130
+ ];
131
+
132
+ const uiMessages = toUIMessages(originalMessages);
133
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
134
+ threadId: "thread1",
135
+ });
136
+
137
+ expect(uiMessages).toHaveLength(1);
138
+ expect(uiMessages[0].text).toBe("Here's my response.");
139
+
140
+ // Check that reasoning parts are preserved in UI message
141
+ const reasoningParts = uiMessages[0].parts.filter(
142
+ (part) => part.type === "reasoning",
143
+ );
144
+ expect(reasoningParts).toHaveLength(1);
145
+ expect(reasoningParts[0]).toMatchObject({
146
+ type: "reasoning",
147
+ text: "Let me think about this...",
148
+ });
149
+
150
+ expect(backToMessageDocs).toHaveLength(1);
151
+ expect(backToMessageDocs[0].text).toBe("Here's my response.");
152
+ expect(backToMessageDocs[0].reasoning).toBe("Let me think about this...");
153
+ });
154
+
155
+ it("handles tool calls and groups them correctly", async () => {
156
+ // Tool calls get grouped into single UI message but expanded back to multiple message docs
157
+ const originalMessages = [
158
+ baseMessageDoc({
159
+ _id: "msg1",
160
+ order: 1,
161
+ stepOrder: 1,
162
+ message: {
163
+ role: "assistant",
164
+ content: [
165
+ {
166
+ type: "tool-call",
167
+ toolName: "calculator",
168
+ toolCallId: "call1",
169
+ input: { operation: "add", a: 2, b: 3 },
170
+ args: { operation: "add", a: 2, b: 3 },
171
+ },
172
+ ],
173
+ },
174
+ tool: true,
175
+ }),
176
+ baseMessageDoc({
177
+ _id: "msg2",
178
+ order: 1,
179
+ stepOrder: 2,
180
+ message: {
181
+ role: "tool",
182
+ content: [
183
+ {
184
+ type: "tool-result",
185
+ toolCallId: "call1",
186
+ toolName: "calculator",
187
+ output: {
188
+ type: "json",
189
+ value: { result: 5 },
190
+ },
191
+ },
192
+ ],
193
+ },
194
+ tool: true,
195
+ }),
196
+ ];
197
+ const toTest = [originalMessages, [...originalMessages].reverse()];
198
+ for (const messages of toTest) {
199
+ const uiMessages = toUIMessages(messages);
200
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
201
+ threadId: "thread1",
202
+ });
203
+
204
+ // Should be grouped into single UI message
205
+ expect(uiMessages).toHaveLength(1);
206
+ const uiMessage = uiMessages[0];
207
+ expect(uiMessage.role).toBe("assistant");
208
+ expect(uiMessage.id).toBe("msg1");
209
+
210
+ // Check tool parts exist
211
+ const toolParts = uiMessage.parts.filter(
212
+ (part) => part.type === "tool-calculator",
213
+ );
214
+ expect(toolParts).toHaveLength(1);
215
+ expect(toolParts[0]).toMatchObject({
216
+ type: "tool-calculator",
217
+ toolCallId: "call1",
218
+ state: "output-available",
219
+ input: { operation: "add", a: 2, b: 3 },
220
+ output: { result: 5 },
221
+ });
222
+
223
+ // Should expand back to multiple message docs
224
+ expect(backToMessageDocs.length).toBeGreaterThanOrEqual(1);
225
+
226
+ // Check that tool information is preserved
227
+ const toolMessages = backToMessageDocs.filter((msg) => msg.tool);
228
+ expect(toolMessages.length).toBeGreaterThan(0);
229
+ expect(toolMessages[0].stepOrder).toBe(1);
230
+ expect(toolMessages[1].stepOrder).toBe(2);
231
+ }
232
+ });
233
+
234
+ it("preserves file attachments in user messages", async () => {
235
+ const originalMessages = [
236
+ baseMessageDoc({
237
+ message: {
238
+ role: "user",
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: "What's in this image?",
243
+ },
244
+ {
245
+ type: "file",
246
+ mimeType: "image/png",
247
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
248
+ },
249
+ ],
250
+ },
251
+ text: "What's in this image?",
252
+ }),
253
+ ];
254
+
255
+ const uiMessages = toUIMessages(originalMessages);
256
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
257
+ threadId: "thread1",
258
+ });
259
+
260
+ expect(uiMessages).toHaveLength(1);
261
+ expect(uiMessages[0].role).toBe("user");
262
+ expect(uiMessages[0].text).toBe("What's in this image?");
263
+
264
+ // Check file parts exist in UI message
265
+ const fileParts = uiMessages[0].parts.filter(
266
+ (part) => part.type === "file",
267
+ );
268
+ expect(fileParts).toHaveLength(1);
269
+
270
+ expect(backToMessageDocs).toHaveLength(1);
271
+ expect(backToMessageDocs[0].text).toBe("What's in this image?");
272
+
273
+ // Check file content is preserved
274
+ const content = backToMessageDocs[0].message?.content;
275
+ expect(Array.isArray(content)).toBe(true);
276
+ if (Array.isArray(content)) {
277
+ const fileContent = content.find((c) => c.type === "file");
278
+ expect(fileContent).toBeDefined();
279
+ expect(fileContent).toMatchObject({
280
+ type: "file",
281
+ mediaType: "image/png",
282
+ });
283
+ }
284
+ });
285
+
286
+ it("preserves sources correctly", async () => {
287
+ const originalMessages = [
288
+ baseMessageDoc({
289
+ message: {
290
+ role: "assistant",
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: "I found some relevant sources.",
295
+ },
296
+ ],
297
+ },
298
+ text: "I found some relevant sources.",
299
+ sources: [
300
+ {
301
+ type: "source",
302
+ sourceType: "url",
303
+ id: "source1",
304
+ url: "https://example.com",
305
+ title: "Example Source",
306
+ },
307
+ {
308
+ type: "source",
309
+ sourceType: "document",
310
+ id: "source2",
311
+ mediaType: "application/pdf",
312
+ title: "Document Source",
313
+ },
314
+ ],
315
+ }),
316
+ ];
317
+
318
+ const uiMessages = toUIMessages(originalMessages);
319
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
320
+ threadId: "thread1",
321
+ });
322
+
323
+ expect(uiMessages).toHaveLength(1);
324
+
325
+ // Check source parts exist in UI message
326
+ const sourceParts = uiMessages[0].parts.filter(
327
+ (part) => part.type === "source-url" || part.type === "source-document",
328
+ );
329
+ expect(sourceParts).toHaveLength(2);
330
+
331
+ expect(backToMessageDocs).toHaveLength(1);
332
+ expect(backToMessageDocs[0].sources).toHaveLength(2);
333
+ expect(backToMessageDocs[0].sources![0]).toMatchObject({
334
+ type: "source",
335
+ sourceType: "url",
336
+ id: "source1",
337
+ url: "https://example.com",
338
+ title: "Example Source",
339
+ });
340
+ });
341
+
342
+ it("preserves metadata when provided", async () => {
343
+ const testMetadata = {
344
+ customField: "customValue",
345
+ timestamp: Date.now(),
346
+ };
347
+
348
+ const originalMessages = [
349
+ baseMessageDoc({
350
+ message: {
351
+ role: "user",
352
+ content: "Test message",
353
+ },
354
+ text: "Test message",
355
+ metadata: testMetadata,
356
+ }),
357
+ ];
358
+
359
+ const uiMessages = toUIMessages(originalMessages);
360
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
361
+ threadId: "thread1",
362
+ });
363
+
364
+ expect(uiMessages).toHaveLength(1);
365
+ expect(uiMessages[0].metadata).toEqual(testMetadata);
366
+
367
+ expect(backToMessageDocs).toHaveLength(1);
368
+ expect(backToMessageDocs[0].metadata).toEqual(testMetadata);
369
+ });
370
+
371
+ it("handles streaming status correctly", async () => {
372
+ const originalMessages = [
373
+ baseMessageDoc({
374
+ message: {
375
+ role: "assistant",
376
+ content: "Streaming response...",
377
+ },
378
+ text: "Streaming response...",
379
+ streaming: true,
380
+ status: "pending",
381
+ }),
382
+ ];
383
+
384
+ const uiMessages = toUIMessages(originalMessages);
385
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
386
+ threadId: "thread1",
387
+ });
388
+
389
+ expect(uiMessages).toHaveLength(1);
390
+ expect(uiMessages[0].status).toBe("streaming");
391
+
392
+ expect(backToMessageDocs).toHaveLength(1);
393
+ expect(backToMessageDocs[0].streaming).toBe(true);
394
+ expect(backToMessageDocs[0].status).toBe("pending");
395
+ });
396
+ });
397
+
398
+ describe("fromUIMessages functionality tests", () => {
399
+ it("handles empty messages array", async () => {
400
+ const uiMessages: UIMessage[] = [];
401
+ const result = await fromUIMessages(uiMessages, { threadId: "thread1" });
402
+ expect(result).toHaveLength(0);
403
+ });
404
+
405
+ it("correctly assigns thread ID", async () => {
406
+ const uiMessage: UIMessage = {
407
+ id: "test-id",
408
+ _creationTime: Date.now(),
409
+ order: 0,
410
+ stepOrder: 0,
411
+ status: "success",
412
+ key: "test-key",
413
+ text: "Hello",
414
+ role: "user",
415
+ parts: [{ type: "text", text: "Hello" }],
416
+ };
417
+
418
+ const result = await fromUIMessages([uiMessage], {
419
+ threadId: "custom-thread-id",
420
+ });
421
+ expect(result).toHaveLength(1);
422
+ expect(result[0].threadId).toBe("custom-thread-id");
423
+ });
424
+
425
+ it("correctly determines tool status", async () => {
426
+ const toolUIMessage: UIMessage = {
427
+ id: "tool-id",
428
+ _creationTime: Date.now(),
429
+ order: 0,
430
+ stepOrder: 0,
431
+ status: "success",
432
+ key: "tool-key",
433
+ text: "",
434
+ role: "assistant",
435
+ parts: [
436
+ {
437
+ type: "tool-calculator",
438
+ toolCallId: "call1",
439
+ input: { a: 1, b: 2 },
440
+ state: "output-available",
441
+ output: { result: 3 },
442
+ },
443
+ ],
444
+ };
445
+
446
+ const result = await fromUIMessages([toolUIMessage], {
447
+ threadId: "thread1",
448
+ });
449
+ expect(result.length).toBeGreaterThan(0);
450
+
451
+ // Should have tool messages
452
+ const toolMessages = result.filter((msg) => msg.tool);
453
+ expect(toolMessages.length).toBeGreaterThan(0);
454
+ });
455
+
456
+ it("handles tool calls without responses", async () => {
457
+ const toolUIMessage: UIMessage = {
458
+ id: "tool-id",
459
+ _creationTime: Date.now(),
460
+ order: 0,
461
+ stepOrder: 0,
462
+ status: "success",
463
+ key: "tool-key",
464
+ text: "",
465
+ role: "assistant",
466
+ parts: [
467
+ { type: "text", text: "Tool call" },
468
+ {
469
+ type: "tool-calculator",
470
+ toolCallId: "call1",
471
+ input: { a: 1, b: 2 },
472
+ state: "input-available",
473
+ },
474
+ ],
475
+ };
476
+
477
+ const result = await fromUIMessages([toolUIMessage], {
478
+ threadId: "thread1",
479
+ });
480
+ expect(result.length).toBeGreaterThan(0);
481
+
482
+ // Should have tool messages
483
+ const toolMessages = result.filter((msg) => msg.tool);
484
+ expect(toolMessages.length).toBe(1);
485
+ expect(toolMessages[0].message?.role).toBe("assistant");
486
+ expect(toolMessages[0].message?.content[0]).toMatchObject({
487
+ type: "text",
488
+ text: "Tool call",
489
+ });
490
+ expect(toolMessages[0].message?.content[1]).toMatchObject({
491
+ args: { a: 1, b: 2 },
492
+ toolCallId: "call1",
493
+ toolName: "calculator",
494
+ type: "tool-call",
495
+ });
496
+ });
497
+ });
@@ -2,18 +2,21 @@ import { describe, test, expect } from "vitest";
2
2
  import {
3
3
  guessMimeType,
4
4
  serializeDataOrUrl,
5
- deserializeUrl,
5
+ toModelMessageDataOrUrl,
6
6
  serializeMessage,
7
- deserializeMessage,
7
+ toModelMessage,
8
8
  serializeContent,
9
+ toModelMessageContent,
9
10
  } from "./mapping.js";
10
11
  import { api } from "./component/_generated/api.js";
11
12
  import type { AgentComponent, ActionCtx } from "./client/types.js";
12
- import { vMessage } from "./validators.js";
13
+ import { vMessage, vToolResultPart } from "./validators.js";
13
14
  import fs from "fs";
14
15
  import path from "path";
15
16
  import type { SerializedContent } from "./mapping.js";
16
17
  import { validate } from "convex-helpers/validators";
18
+ import type { ToolResultPart } from "ai";
19
+ import type { Infer } from "convex/values";
17
20
 
18
21
  const testAssetsDir = path.join(__dirname, "../test-assets");
19
22
  const testFiles = [
@@ -54,8 +57,8 @@ describe("mapping", () => {
54
57
  const ser = serializeDataOrUrl(arr);
55
58
  expect(ser).toBeInstanceOf(ArrayBuffer);
56
59
  expect(new Uint8Array(ser as ArrayBuffer)).toEqual(arr);
57
- // deserializeUrl should return the same ArrayBuffer
58
- const deser = deserializeUrl(ser);
60
+ // toModelMessageDataOrUrl should return the same ArrayBuffer
61
+ const deser = toModelMessageDataOrUrl(ser);
59
62
  expect(deser).toBeInstanceOf(ArrayBuffer);
60
63
  expect(new Uint8Array(deser as ArrayBuffer)).toEqual(arr);
61
64
  });
@@ -80,10 +83,56 @@ describe("mapping", () => {
80
83
  const { message: ser } = await serializeMessage(ctx, component, message);
81
84
  // Use is for type validation
82
85
  expect(validate(vMessage, ser)).toBeTruthy();
83
- const round = deserializeMessage(ser);
86
+ const round = toModelMessage(ser);
84
87
  expect(round).toEqual(message);
85
88
  });
86
89
 
90
+ test("tool output round-trips", async () => {
91
+ const toolResult = {
92
+ type: "tool-result" as const,
93
+ toolCallId: "tool-call-id",
94
+ toolName: "tool-name",
95
+ output: {
96
+ type: "text",
97
+ value: "hello world",
98
+ },
99
+ } satisfies ToolResultPart;
100
+ const [result] = toModelMessageContent([toolResult]);
101
+ expect(result).toMatchObject(toolResult);
102
+ const {
103
+ content: [roundtrip],
104
+ } = await serializeContent({} as ActionCtx, {} as AgentComponent, [
105
+ result as ToolResultPart,
106
+ ]);
107
+ expect(roundtrip).toMatchObject(toolResult);
108
+ });
109
+
110
+ test("tool results get normalized to output", async () => {
111
+ const toolResult = {
112
+ type: "tool-result" as const,
113
+ toolCallId: "tool-call-id",
114
+ toolName: "tool-name",
115
+ result: "hello world",
116
+ } satisfies Infer<typeof vToolResultPart>;
117
+ const expected = {
118
+ type: "tool-result",
119
+ toolCallId: "tool-call-id",
120
+ toolName: "tool-name",
121
+ output: {
122
+ type: "text",
123
+ value: "hello world",
124
+ },
125
+ };
126
+ const [deserialized] = toModelMessageContent([toolResult]);
127
+ expect(deserialized).toMatchObject(expected);
128
+ const {
129
+ content: [serialized],
130
+ } = await serializeContent({} as ActionCtx, {} as AgentComponent, [
131
+ toolResult,
132
+ ]);
133
+ expect(serialized).toMatchObject(expected);
134
+ });
135
+
87
136
  test("saving files returns fileIds when too big", async () => {
88
137
  // Make a big file
89
138
  const bigArr = new Uint8Array(1024 * 65).fill(1);
@@ -160,4 +209,52 @@ describe("mapping", () => {
160
209
  const { fileIds } = await serializeContent(ctx, component, content);
161
210
  expect(fileIds).toBeUndefined();
162
211
  });
212
+
213
+ test("tool-approval-request is preserved after serialization", async () => {
214
+ const approvalRequest = {
215
+ type: "tool-approval-request" as const,
216
+ approvalId: "approval-123",
217
+ toolCallId: "tool-call-456",
218
+ };
219
+ const { content } = await serializeContent(
220
+ {} as ActionCtx,
221
+ {} as AgentComponent,
222
+ [approvalRequest],
223
+ );
224
+ expect(content).toHaveLength(1);
225
+ expect((content as unknown[])[0]).toMatchObject(approvalRequest);
226
+ });
227
+
228
+ test("tool-approval-response with approved: true is preserved", async () => {
229
+ const approvalResponse = {
230
+ type: "tool-approval-response" as const,
231
+ approvalId: "approval-123",
232
+ approved: true,
233
+ reason: "User approved",
234
+ };
235
+ const { content } = await serializeContent(
236
+ {} as ActionCtx,
237
+ {} as AgentComponent,
238
+ [approvalResponse],
239
+ );
240
+ expect(content).toHaveLength(1);
241
+ expect((content as unknown[])[0]).toMatchObject(approvalResponse);
242
+ });
243
+
244
+ test("tool-approval-response with approved: false is preserved", async () => {
245
+ const approvalResponse = {
246
+ type: "tool-approval-response" as const,
247
+ approvalId: "approval-123",
248
+ approved: false,
249
+ reason: "User denied",
250
+ providerExecuted: false,
251
+ };
252
+ const { content } = await serializeContent(
253
+ {} as ActionCtx,
254
+ {} as AgentComponent,
255
+ [approvalResponse],
256
+ );
257
+ expect(content).toHaveLength(1);
258
+ expect((content as unknown[])[0]).toMatchObject(approvalResponse);
259
+ });
163
260
  });