@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
@@ -4,23 +4,33 @@ import {
4
4
  serializeDataOrUrl,
5
5
  toModelMessageDataOrUrl,
6
6
  serializeMessage,
7
- serializeNewMessagesInStep,
7
+ serializeResponseMessages,
8
8
  toModelMessage,
9
9
  serializeContent,
10
10
  toModelMessageContent,
11
+ toUIFilePart,
11
12
  autoDenyUnresolvedApprovals,
13
+ serializeWarnings,
12
14
  } from "./mapping.js";
13
- import { api } from "./component/_generated/api.js";
15
+ import { api } from "../component/_generated/api.js";
14
16
  import type { AgentComponent, ActionCtx } from "./client/types.js";
15
- import { vMessage, vToolResultPart } from "./validators.js";
17
+ import { vMessage, vToolResultPart } from "../validators.js";
16
18
  import fs from "fs";
17
19
  import path from "path";
18
20
  import type { SerializedContent } from "./mapping.js";
19
21
  import { validate } from "convex-helpers/validators";
20
- import type { ModelMessage, StepResult, ToolResultPart, ToolSet } from "ai";
22
+ import type {
23
+ FilePart,
24
+ ModelMessage,
25
+ StepResult,
26
+ ToolResultPart,
27
+ ToolSet,
28
+ CallWarning,
29
+ } from "ai";
21
30
  import type { Infer } from "convex/values";
31
+ import { mockModel } from "./client/mockModel.js";
22
32
 
23
- const testAssetsDir = path.join(__dirname, "../test-assets");
33
+ const testAssetsDir = path.join(__dirname, "../../test-assets");
24
34
  const testFiles = [
25
35
  "book.svg",
26
36
  "bump.jpeg",
@@ -36,6 +46,21 @@ function fileToArrayBuffer(filePath: string): ArrayBuffer {
36
46
  }
37
47
 
38
48
  describe("mapping", () => {
49
+ test("serializes every AI SDK 7 warning discriminant", () => {
50
+ const warnings: CallWarning[] = [
51
+ { type: "unsupported", feature: "feature", details: "details" },
52
+ { type: "compatibility", feature: "feature", details: "details" },
53
+ {
54
+ type: "deprecated",
55
+ setting: "setting",
56
+ message: "message",
57
+ },
58
+ { type: "other", message: "message" },
59
+ ];
60
+
61
+ expect(serializeWarnings(warnings)).toEqual(warnings);
62
+ });
63
+
39
64
  test("infers correct mimeType for all test-assets", () => {
40
65
  const expected: { [key: string]: string } = {
41
66
  "book.svg": "image/svg+xml", // <svg
@@ -135,6 +160,266 @@ describe("mapping", () => {
135
160
  expect(serialized).toMatchObject(expected);
136
161
  });
137
162
 
163
+ test("legacy result JSON with a type field is not treated as SDK output", async () => {
164
+ const weather = { type: "weather", temperature: 72 };
165
+ const toolResult = {
166
+ type: "tool-result" as const,
167
+ toolCallId: "tool-call-id",
168
+ toolName: "weather",
169
+ result: weather,
170
+ } satisfies Infer<typeof vToolResultPart>;
171
+ const expected = { type: "json", value: weather };
172
+
173
+ const [deserialized] = toModelMessageContent([
174
+ toolResult,
175
+ ]) as ToolResultPart[];
176
+ expect(deserialized.output).toEqual(expected);
177
+ const { content } = await serializeContent(
178
+ {} as ActionCtx,
179
+ {} as AgentComponent,
180
+ [toolResult],
181
+ );
182
+ expect((content[0] as Infer<typeof vToolResultPart>).output).toEqual(
183
+ expected,
184
+ );
185
+ });
186
+
187
+ test.each([
188
+ [
189
+ "tagged data",
190
+ {
191
+ type: "file",
192
+ data: { type: "data", data: new Uint8Array([1, 2]) },
193
+ mediaType: "application/octet-stream",
194
+ },
195
+ { type: "data" },
196
+ ],
197
+ [
198
+ "tagged URL",
199
+ {
200
+ type: "file",
201
+ data: { type: "url", url: new URL("https://example.com/file") },
202
+ mediaType: "application/pdf",
203
+ },
204
+ { type: "url", url: new URL("https://example.com/file") },
205
+ ],
206
+ [
207
+ "tagged text",
208
+ {
209
+ type: "file",
210
+ data: { type: "text", text: "hello" },
211
+ mediaType: "text/plain",
212
+ },
213
+ { type: "text", text: "hello" },
214
+ ],
215
+ [
216
+ "tagged reference",
217
+ {
218
+ type: "file",
219
+ data: { type: "reference", reference: { openai: "file-1" } },
220
+ mediaType: "application/pdf",
221
+ },
222
+ { type: "reference", reference: { openai: "file-1" } },
223
+ ],
224
+ [
225
+ "legacy file-data",
226
+ {
227
+ type: "file-data",
228
+ data: "AQI=",
229
+ mediaType: "application/octet-stream",
230
+ },
231
+ { type: "data", data: "AQI=" },
232
+ ],
233
+ [
234
+ "legacy file-url",
235
+ {
236
+ type: "file-url",
237
+ url: "https://example.com/file",
238
+ mediaType: "application/pdf",
239
+ },
240
+ { type: "url", url: new URL("https://example.com/file") },
241
+ ],
242
+ [
243
+ "legacy file ID",
244
+ { type: "file-id", fileId: "file-1" },
245
+ { type: "file-id", fileId: "file-1" },
246
+ ],
247
+ [
248
+ "provider-keyed legacy file ID",
249
+ { type: "file-id", fileId: { openai: "file-1" } },
250
+ { type: "reference", reference: { openai: "file-1" } },
251
+ ],
252
+ [
253
+ "legacy file reference",
254
+ { type: "file-reference", providerReference: { openai: "file-1" } },
255
+ { type: "reference", reference: { openai: "file-1" } },
256
+ ],
257
+ [
258
+ "legacy image data",
259
+ { type: "image-data", data: "AQI=", mediaType: "image/png" },
260
+ { type: "data", data: "AQI=" },
261
+ ],
262
+ [
263
+ "legacy image URL",
264
+ { type: "image-url", url: "https://example.com/image" },
265
+ { type: "url", url: new URL("https://example.com/image") },
266
+ ],
267
+ [
268
+ "legacy image ID",
269
+ { type: "image-file-id", fileId: "image-1" },
270
+ { type: "image-file-id", fileId: "image-1" },
271
+ ],
272
+ [
273
+ "provider-keyed legacy image ID",
274
+ { type: "image-file-id", fileId: { openai: "image-1" } },
275
+ { type: "reference", reference: { openai: "image-1" } },
276
+ ],
277
+ [
278
+ "legacy image reference",
279
+ {
280
+ type: "image-file-reference",
281
+ providerReference: { openai: "image-1" },
282
+ },
283
+ { type: "reference", reference: { openai: "image-1" } },
284
+ ],
285
+ ])("round-trips AI SDK 7 tool-result %s", async (_name, value, expected) => {
286
+ const result = {
287
+ type: "tool-result",
288
+ toolCallId: "call-1",
289
+ toolName: "lookup",
290
+ output: { type: "content", value: [value] },
291
+ } as ToolResultPart;
292
+ const { content } = await serializeContent(
293
+ {} as ActionCtx,
294
+ {} as AgentComponent,
295
+ [result],
296
+ );
297
+ const [restored] = toModelMessageContent(content) as ToolResultPart[];
298
+ const restoredPart = (restored.output as { value: unknown[] }).value[0]!;
299
+ if ("fileId" in expected) {
300
+ expect(restoredPart).toMatchObject(expected);
301
+ } else {
302
+ expect((restoredPart as { data: unknown }).data).toMatchObject(expected);
303
+ }
304
+ });
305
+
306
+ test("deserializes persisted legacy media tool output to a canonical file", () => {
307
+ const [restored] = toModelMessageContent([
308
+ {
309
+ type: "tool-result",
310
+ toolCallId: "call-1",
311
+ toolName: "render",
312
+ output: {
313
+ type: "content",
314
+ value: [{ type: "media", data: "AQI=", mediaType: "image/png" }],
315
+ },
316
+ },
317
+ ]) as ToolResultPart[];
318
+ expect(restored.output).toEqual({
319
+ type: "content",
320
+ value: [
321
+ {
322
+ type: "file",
323
+ data: { type: "data", data: "AQI=" },
324
+ mediaType: "image/png",
325
+ },
326
+ ],
327
+ });
328
+ });
329
+
330
+ test("reasoning files persist tagged data and URLs, and reject missing data", async () => {
331
+ const { content } = await serializeContent(
332
+ {} as ActionCtx,
333
+ {} as AgentComponent,
334
+ [
335
+ {
336
+ type: "reasoning-file",
337
+ data: { type: "url", url: new URL("https://example.com/reasoning") },
338
+ mediaType: "text/plain",
339
+ },
340
+ {
341
+ type: "reasoning-file",
342
+ data: { type: "data", data: new Uint8Array([1, 2]) },
343
+ mediaType: "application/octet-stream",
344
+ },
345
+ { type: "custom", kind: "provider.annotation" },
346
+ ] as ModelMessage["content"],
347
+ );
348
+ expect(content).toMatchObject([
349
+ { type: "reasoning-file", url: "https://example.com/reasoning" },
350
+ { type: "reasoning-file", data: expect.any(ArrayBuffer) },
351
+ { type: "custom", kind: "provider.annotation" },
352
+ ]);
353
+ expect(toModelMessageContent(content)).toMatchObject([
354
+ {
355
+ type: "reasoning-file",
356
+ data: { type: "url", url: new URL("https://example.com/reasoning") },
357
+ },
358
+ {
359
+ type: "reasoning-file",
360
+ data: { type: "data", data: expect.any(ArrayBuffer) },
361
+ },
362
+ { type: "custom", kind: "provider.annotation" },
363
+ ]);
364
+ expect(
365
+ (await serializeContent({} as ActionCtx, {} as AgentComponent, content))
366
+ .content,
367
+ ).toEqual(content);
368
+ expect(() =>
369
+ toModelMessageContent([
370
+ { type: "reasoning-file", mediaType: "text/plain" },
371
+ ] as SerializedContent),
372
+ ).toThrow("reasoning-file requires data or url");
373
+ });
374
+
375
+ test("reserializing persisted provider fields preserves them", async () => {
376
+ const content: SerializedContent = [
377
+ { type: "reasoning", text: "private", signature: "signed" },
378
+ {
379
+ type: "tool-result",
380
+ toolCallId: "provider-call",
381
+ toolName: "search",
382
+ providerExecuted: true,
383
+ output: { type: "text", value: "found" },
384
+ },
385
+ ];
386
+ await expect(
387
+ serializeContent({} as ActionCtx, {} as AgentComponent, content),
388
+ ).resolves.toEqual({ content, fileIds: undefined });
389
+ });
390
+
391
+ test.each([
392
+ [
393
+ "ArrayBuffer",
394
+ {
395
+ type: "file",
396
+ data: new Uint8Array([1, 2]).buffer,
397
+ mediaType: "application/octet-stream",
398
+ },
399
+ { url: "data:application/octet-stream;base64,AQI=" },
400
+ ],
401
+ [
402
+ "tagged data",
403
+ {
404
+ type: "file",
405
+ data: { type: "data", data: new Uint8Array([1, 2]) },
406
+ mediaType: "application/octet-stream",
407
+ },
408
+ { url: "data:application/octet-stream;base64,AQI=" },
409
+ ],
410
+ [
411
+ "provider reference",
412
+ {
413
+ type: "file",
414
+ data: { type: "reference", reference: { openai: "file-1" } },
415
+ mediaType: "application/pdf",
416
+ },
417
+ { url: "", providerReference: { openai: "file-1" } },
418
+ ],
419
+ ])("renders file UI part for %s", (_name, part, expected) => {
420
+ expect(toUIFilePart(part as FilePart)).toMatchObject(expected);
421
+ });
422
+
138
423
  test("saving files returns fileIds when too big", async () => {
139
424
  // Make a big file
140
425
  const bigArr = new Uint8Array(1024 * 65).fill(1);
@@ -225,6 +510,7 @@ describe("mapping", () => {
225
510
  );
226
511
  expect(content).toHaveLength(1);
227
512
  expect((content as unknown[])[0]).toMatchObject(approvalRequest);
513
+ expect(toModelMessageContent(content)).toMatchObject([approvalRequest]);
228
514
  });
229
515
 
230
516
  test("tool-approval-response with approved: true is preserved", async () => {
@@ -260,7 +546,23 @@ describe("mapping", () => {
260
546
  expect((content as unknown[])[0]).toMatchObject(approvalResponse);
261
547
  });
262
548
 
263
- describe("serializeNewMessagesInStep", () => {
549
+ test("stored reasoning-file URLs survive serialization", async () => {
550
+ const reasoningFile: SerializedContent = [
551
+ {
552
+ type: "reasoning-file",
553
+ url: "https://example.com/reasoning",
554
+ mediaType: "text/plain",
555
+ },
556
+ ];
557
+ const { content } = await serializeContent(
558
+ {} as ActionCtx,
559
+ {} as AgentComponent,
560
+ reasoningFile,
561
+ );
562
+ expect(content).toEqual(reasoningFile);
563
+ });
564
+
565
+ describe("serializeResponseMessages", () => {
264
566
  const ctx = {
265
567
  runAction: async () => undefined,
266
568
  runMutation: async () => undefined,
@@ -276,7 +578,12 @@ describe("mapping", () => {
276
578
  {
277
579
  role: "assistant",
278
580
  content: [
279
- { type: "tool-call", toolCallId: "c1", toolName: "search", input: {} },
581
+ {
582
+ type: "tool-call",
583
+ toolCallId: "c1",
584
+ toolName: "search",
585
+ input: {},
586
+ },
280
587
  ],
281
588
  },
282
589
  {
@@ -292,15 +599,18 @@ describe("mapping", () => {
292
599
  },
293
600
  ];
294
601
  const step1Messages: ModelMessage[] = [
295
- ...step0Messages,
296
602
  { role: "assistant", content: [{ type: "text", text: "thinking" }] },
297
603
  ];
298
604
  const step2Messages: ModelMessage[] = [
299
- ...step1Messages,
300
605
  {
301
606
  role: "assistant",
302
607
  content: [
303
- { type: "tool-call", toolCallId: "c2", toolName: "search", input: {} },
608
+ {
609
+ type: "tool-call",
610
+ toolCallId: "c2",
611
+ toolName: "search",
612
+ input: {},
613
+ },
304
614
  ],
305
615
  },
306
616
  {
@@ -350,13 +660,24 @@ describe("mapping", () => {
350
660
  return c.map((p: { type?: string }) => p.type ?? "?");
351
661
  };
352
662
 
353
- test("first step (count=0) serializes all response messages", async () => {
354
- const res = await serializeNewMessagesInStep(
663
+ test("explicitly provided empty response messages stay empty", async () => {
664
+ const res = await serializeResponseMessages(
665
+ ctx,
666
+ component,
667
+ makeStep([]),
668
+ undefined,
669
+ [],
670
+ );
671
+ expect(res.messages).toEqual([]);
672
+ });
673
+
674
+ test("serializes all response messages for a step", async () => {
675
+ const res = await serializeResponseMessages(
355
676
  ctx,
356
677
  component,
357
678
  makeStep(step0Messages),
358
679
  undefined,
359
- 0,
680
+ step0Messages,
360
681
  );
361
682
  expect(res.messages).toHaveLength(2);
362
683
  expect(res.messages[0].message.role).toBe("assistant");
@@ -365,26 +686,50 @@ describe("mapping", () => {
365
686
  expect(contentTypes(res.messages[1].message)).toEqual(["tool-result"]);
366
687
  });
367
688
 
368
- test("middle step (count=2) serializes only the new text message", async () => {
369
- const res = await serializeNewMessagesInStep(
689
+ test("serializes the text response from a later step", async () => {
690
+ const res = await serializeResponseMessages(
370
691
  ctx,
371
692
  component,
372
693
  makeStep(step1Messages),
373
694
  undefined,
374
- 2,
695
+ step1Messages,
375
696
  );
376
697
  expect(res.messages).toHaveLength(1);
377
698
  expect(res.messages[0].message.role).toBe("assistant");
378
699
  expect(contentTypes(res.messages[0].message)).toEqual(["text"]);
379
700
  });
380
701
 
381
- test("multi-message step (count=3) serializes the new tool-call + tool-result pair", async () => {
382
- const res = await serializeNewMessagesInStep(
702
+ test("persists the model that generated an SDK 7 step over the fallback", async () => {
703
+ const routedModel = mockModel({
704
+ provider: "routed-provider",
705
+ modelId: "routed-model",
706
+ });
707
+ const step = {
708
+ ...makeStep(step1Messages),
709
+ model: routedModel,
710
+ } as StepResult<ToolSet>;
711
+
712
+ const res = await serializeResponseMessages(
713
+ ctx,
714
+ component,
715
+ step,
716
+ { provider: "fallback-provider", model: "fallback-model" },
717
+ step1Messages,
718
+ );
719
+
720
+ expect(res.messages[0]).toMatchObject({
721
+ model: "routed-model",
722
+ provider: "routed-provider",
723
+ });
724
+ });
725
+
726
+ test("serializes a tool-call and tool-result from the same step", async () => {
727
+ const res = await serializeResponseMessages(
383
728
  ctx,
384
729
  component,
385
730
  makeStep(step2Messages),
386
731
  undefined,
387
- 3,
732
+ step2Messages,
388
733
  );
389
734
  expect(res.messages).toHaveLength(2);
390
735
  expect(res.messages[0].message.role).toBe("assistant");
@@ -393,19 +738,21 @@ describe("mapping", () => {
393
738
  expect(contentTypes(res.messages[1].message)).toEqual(["tool-result"]);
394
739
  });
395
740
 
396
- // Regression test for the actually-broken shape: a single step appended
397
- // assistant(text) + assistant(tool-call) + tool(tool-result), so the new
398
- // tail has length 3 and the last message is a tool message. The old
399
- // heuristic took `slice(-2)` whenever the last role was "tool" and would
400
- // have dropped the leading text. The watermark returns all three.
401
- test("returns all three messages when a step adds text + tool-call + tool-result", async () => {
741
+ test("preserves text, a tool call, and its result from one step", async () => {
402
742
  const stepMessages: ModelMessage[] = [
403
- ...step0Messages, // length 2
404
- { role: "assistant", content: [{ type: "text", text: "Let me check..." }] },
743
+ {
744
+ role: "assistant",
745
+ content: [{ type: "text", text: "Let me check..." }],
746
+ },
405
747
  {
406
748
  role: "assistant",
407
749
  content: [
408
- { type: "tool-call", toolCallId: "c3", toolName: "search", input: {} },
750
+ {
751
+ type: "tool-call",
752
+ toolCallId: "c3",
753
+ toolName: "search",
754
+ input: {},
755
+ },
409
756
  ],
410
757
  },
411
758
  {
@@ -420,12 +767,12 @@ describe("mapping", () => {
420
767
  ],
421
768
  },
422
769
  ];
423
- const res = await serializeNewMessagesInStep(
770
+ const res = await serializeResponseMessages(
424
771
  ctx,
425
772
  component,
426
773
  makeStep(stepMessages),
427
774
  undefined,
428
- step0Messages.length,
775
+ stepMessages,
429
776
  );
430
777
  expect(res.messages).toHaveLength(3);
431
778
  expect(res.messages[0].message.role).toBe("assistant");
@@ -435,68 +782,6 @@ describe("mapping", () => {
435
782
  expect(res.messages[2].message.role).toBe("tool");
436
783
  expect(contentTypes(res.messages[2].message)).toEqual(["tool-result"]);
437
784
  });
438
-
439
- test("empty response messages slice falls back to synthetic empty assistant", async () => {
440
- const res = await serializeNewMessagesInStep(
441
- ctx,
442
- component,
443
- makeStep(step1Messages),
444
- undefined,
445
- step1Messages.length,
446
- );
447
- expect(res.messages).toHaveLength(1);
448
- expect(res.messages[0].message.role).toBe("assistant");
449
- expect(res.messages[0].message.content).toEqual([]);
450
- });
451
-
452
- // Pin the caller-drift behavior: if the watermark is past the end of
453
- // response.messages (e.g. the caller mistracked), the slice is empty and
454
- // we fall through to the synthetic anchor. Future "fixes" should not
455
- // accidentally change this without intent.
456
- test("watermark beyond response.messages.length returns the synthetic fallback", async () => {
457
- const res = await serializeNewMessagesInStep(
458
- ctx,
459
- component,
460
- makeStep(step1Messages),
461
- undefined,
462
- step1Messages.length + 5,
463
- );
464
- expect(res.messages).toHaveLength(1);
465
- expect(res.messages[0].message.role).toBe("assistant");
466
- expect(res.messages[0].message.content).toEqual([]);
467
- });
468
-
469
- // AI SDK v6 makes step.response.messages cumulative across steps:
470
- // step N's array contains all messages from steps 0..N. Without the
471
- // previousResponseMessageCount watermark, every multi-step save duplicates
472
- // all prior messages. These tests demonstrate the bug and the fix.
473
- describe("multi-step loop — previousStep watermark", () => {
474
- test("without watermark, step 2 re-saves all cumulative messages (demonstrates the bug)", async () => {
475
- // step2Messages = step0 (2 msgs) + step1 (1 msg) + step2 new (2 msgs) = 5 total
476
- const res = await serializeNewMessagesInStep(
477
- ctx,
478
- component,
479
- makeStep(step2Messages),
480
- undefined,
481
- 0,
482
- );
483
- expect(res.messages).toHaveLength(5);
484
- });
485
-
486
- test("with watermark, step 2 saves only its 2 new messages", async () => {
487
- const step1 = makeStep(step1Messages);
488
- const res = await serializeNewMessagesInStep(
489
- ctx,
490
- component,
491
- makeStep(step2Messages),
492
- undefined,
493
- step1.response.messages.length,
494
- );
495
- expect(res.messages).toHaveLength(2);
496
- expect(contentTypes(res.messages[0].message)).toEqual(["tool-call"]);
497
- expect(contentTypes(res.messages[1].message)).toEqual(["tool-result"]);
498
- });
499
- });
500
785
  });
501
786
 
502
787
  describe("autoDenyUnresolvedApprovals", () => {