@convex-dev/agent 0.0.16-alpha.0 → 0.0.17-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 (73) hide show
  1. package/README.md +92 -71
  2. package/dist/commonjs/client/index.d.ts +1012 -920
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +188 -31
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/client/playground.d.ts +251 -116
  7. package/dist/commonjs/client/playground.d.ts.map +1 -1
  8. package/dist/commonjs/client/playground.js +33 -11
  9. package/dist/commonjs/client/playground.js.map +1 -1
  10. package/dist/commonjs/component/messages.d.ts +683 -571
  11. package/dist/commonjs/component/messages.d.ts.map +1 -1
  12. package/dist/commonjs/component/messages.js +56 -33
  13. package/dist/commonjs/component/messages.js.map +1 -1
  14. package/dist/commonjs/component/schema.d.ts +1080 -1324
  15. package/dist/commonjs/component/schema.d.ts.map +1 -1
  16. package/dist/commonjs/component/schema.js +20 -14
  17. package/dist/commonjs/component/schema.js.map +1 -1
  18. package/dist/commonjs/component/vector/index.d.ts.map +1 -1
  19. package/dist/commonjs/component/vector/index.js +4 -6
  20. package/dist/commonjs/component/vector/index.js.map +1 -1
  21. package/dist/commonjs/component/vector/tables.d.ts +10 -10
  22. package/dist/commonjs/component/vector/tables.d.ts.map +1 -1
  23. package/dist/commonjs/component/vector/tables.js.map +1 -1
  24. package/dist/commonjs/mapping.d.ts +7 -1
  25. package/dist/commonjs/mapping.d.ts.map +1 -1
  26. package/dist/commonjs/mapping.js +39 -18
  27. package/dist/commonjs/mapping.js.map +1 -1
  28. package/dist/commonjs/validators.d.ts +1669 -2336
  29. package/dist/commonjs/validators.d.ts.map +1 -1
  30. package/dist/commonjs/validators.js +35 -25
  31. package/dist/commonjs/validators.js.map +1 -1
  32. package/dist/esm/client/index.d.ts +1012 -920
  33. package/dist/esm/client/index.d.ts.map +1 -1
  34. package/dist/esm/client/index.js +188 -31
  35. package/dist/esm/client/index.js.map +1 -1
  36. package/dist/esm/client/playground.d.ts +251 -116
  37. package/dist/esm/client/playground.d.ts.map +1 -1
  38. package/dist/esm/client/playground.js +33 -11
  39. package/dist/esm/client/playground.js.map +1 -1
  40. package/dist/esm/component/messages.d.ts +683 -571
  41. package/dist/esm/component/messages.d.ts.map +1 -1
  42. package/dist/esm/component/messages.js +56 -33
  43. package/dist/esm/component/messages.js.map +1 -1
  44. package/dist/esm/component/schema.d.ts +1080 -1324
  45. package/dist/esm/component/schema.d.ts.map +1 -1
  46. package/dist/esm/component/schema.js +20 -14
  47. package/dist/esm/component/schema.js.map +1 -1
  48. package/dist/esm/component/vector/index.d.ts.map +1 -1
  49. package/dist/esm/component/vector/index.js +4 -6
  50. package/dist/esm/component/vector/index.js.map +1 -1
  51. package/dist/esm/component/vector/tables.d.ts +10 -10
  52. package/dist/esm/component/vector/tables.d.ts.map +1 -1
  53. package/dist/esm/component/vector/tables.js.map +1 -1
  54. package/dist/esm/mapping.d.ts +7 -1
  55. package/dist/esm/mapping.d.ts.map +1 -1
  56. package/dist/esm/mapping.js +39 -18
  57. package/dist/esm/mapping.js.map +1 -1
  58. package/dist/esm/validators.d.ts +1669 -2336
  59. package/dist/esm/validators.d.ts.map +1 -1
  60. package/dist/esm/validators.js +35 -25
  61. package/dist/esm/validators.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/client/index.test.ts +68 -0
  64. package/src/client/index.ts +219 -35
  65. package/src/client/playground.ts +49 -23
  66. package/src/component/_generated/api.d.ts +311 -323
  67. package/src/component/messages.test.ts +23 -0
  68. package/src/component/messages.ts +75 -42
  69. package/src/component/schema.ts +26 -13
  70. package/src/component/vector/index.ts +5 -6
  71. package/src/component/vector/tables.ts +15 -15
  72. package/src/mapping.ts +65 -32
  73. package/src/validators.ts +47 -27
@@ -5,9 +5,32 @@ import { convexTest } from "convex-test";
5
5
  import schema from "./schema.js";
6
6
  import { api } from "./_generated/api.js";
7
7
  import { modules } from "./setup.test.js";
8
+ import { Id } from "./_generated/dataModel.js";
9
+ import { getMaxMessage } from "./messages.js";
8
10
 
9
11
  describe("agent", () => {
10
12
  test("add and subtract", async () => {
11
13
  const t = convexTest(schema, modules);
12
14
  });
15
+ test("getMaxMessage works for threads", async () => {
16
+ const t = convexTest(schema, modules);
17
+ const thread = await t.mutation(api.threads.createThread, {
18
+ userId: "test",
19
+ });
20
+ const { messages } = await t.mutation(api.messages.addMessages, {
21
+ threadId: thread._id as Id<"threads">,
22
+ messages: [
23
+ { message: { role: "user", content: "hello" } },
24
+ { message: { role: "assistant", content: "world" } },
25
+ ],
26
+ });
27
+ const maxMessage = await t.run(async (ctx) => {
28
+ return await getMaxMessage(ctx, thread._id as Id<"threads">, "test");
29
+ });
30
+ expect(maxMessage).toMatchObject({
31
+ _id: messages.at(-1)!._id,
32
+ order: 1,
33
+ stepOrder: 0,
34
+ });
35
+ });
13
36
  });
@@ -57,10 +57,11 @@ export async function deleteMessage(
57
57
  if (messageDoc.embeddingId) {
58
58
  await ctx.db.delete(messageDoc.embeddingId);
59
59
  }
60
- if (messageDoc.fileId) {
61
- const file = await ctx.db.get(messageDoc.fileId);
60
+ for (const { fileId } of messageDoc.files ?? []) {
61
+ if (!fileId) continue;
62
+ const file = await ctx.db.get(fileId);
62
63
  if (file) {
63
- await ctx.db.patch(messageDoc.fileId, { refcount: file.refcount - 1 });
64
+ await ctx.db.patch(fileId, { refcount: file.refcount - 1 });
64
65
  }
65
66
  }
66
67
  }
@@ -117,9 +118,11 @@ async function addMessagesHandler(
117
118
  }
118
119
  const maxMessage = await getMaxMessage(ctx, threadId, userId);
119
120
  let order = maxMessage?.order ?? -1;
121
+ let stepOrder = maxMessage?.stepOrder ?? 0;
122
+ let lastMessageIsTool = maxMessage?.tool ?? false;
120
123
  const toReturn: Doc<"messages">[] = [];
121
124
  if (messages.length > 0) {
122
- for (const { message, fileId, embedding, ...fields } of messages) {
125
+ for (const { message, files, embedding, ...fields } of messages) {
123
126
  let embeddingId: VectorTableId | undefined;
124
127
  if (embedding) {
125
128
  embeddingId = await insertVector(ctx, embedding.dimension, {
@@ -131,9 +134,13 @@ async function addMessagesHandler(
131
134
  });
132
135
  }
133
136
  const tool = isTool(message);
134
- if (!tool) {
137
+ if (lastMessageIsTool) {
138
+ stepOrder++;
139
+ } else {
135
140
  order++;
141
+ stepOrder = 0;
136
142
  }
143
+ lastMessageIsTool = tool;
137
144
  const text = extractText(message);
138
145
  const messageId = await ctx.db.insert("messages", {
139
146
  ...rest,
@@ -145,11 +152,17 @@ async function addMessagesHandler(
145
152
  order,
146
153
  tool,
147
154
  text,
148
- fileId,
155
+ files,
149
156
  status: pending ? "pending" : "success",
150
- stepOrder: 0,
157
+ stepOrder,
151
158
  });
152
- if (fileId) {
159
+ if (!fields.id) {
160
+ await ctx.db.patch(messageId, {
161
+ id: messageId,
162
+ });
163
+ }
164
+ for (const { fileId } of files ?? []) {
165
+ if (!fileId) continue;
153
166
  await ctx.db.patch(fileId, {
154
167
  refcount: (await ctx.db.get(fileId))!.refcount + 1,
155
168
  });
@@ -160,7 +173,8 @@ async function addMessagesHandler(
160
173
  return { messages: toReturn };
161
174
  }
162
175
 
163
- async function getMaxMessage(
176
+ // exported for tests
177
+ export async function getMaxMessage(
164
178
  ctx: QueryCtx,
165
179
  threadId: Id<"threads"> | undefined,
166
180
  userId: string | undefined
@@ -168,39 +182,32 @@ async function getMaxMessage(
168
182
  assert(threadId || userId, "One of threadId or userId is required");
169
183
  if (threadId) {
170
184
  return mergedStream(
171
- ["success" as const, "pending" as const].map((status) =>
172
- stream(ctx.db, schema)
173
- .query("messages")
174
- .withIndex("threadId_status_tool_order_stepOrder", (q) =>
175
- q.eq("threadId", threadId).eq("status", status).eq("tool", false)
176
- )
177
- .order("desc")
185
+ [true, false].flatMap((tool) =>
186
+ ["success" as const, "pending" as const].map((status) =>
187
+ stream(ctx.db, schema)
188
+ .query("messages")
189
+ .withIndex("threadId_status_tool_order_stepOrder", (q) =>
190
+ q.eq("threadId", threadId).eq("status", status).eq("tool", tool)
191
+ )
192
+ .order("desc")
193
+ )
178
194
  ),
179
195
  ["order", "stepOrder"]
180
196
  ).first();
181
197
  } else {
182
- // DO explicitly
183
- const maxPending = await ctx.db
184
- .query("messages")
185
- .withIndex("userId_status_tool_order_stepOrder", (q) =>
186
- q.eq("userId", userId).eq("status", "pending").eq("tool", false)
187
- )
188
- .order("desc")
189
- .first();
190
- const maxSuccess = await ctx.db
191
- .query("messages")
192
- .withIndex("userId_status_tool_order_stepOrder", (q) =>
193
- q.eq("userId", userId).eq("status", "success").eq("tool", false)
194
- )
195
- .order("desc")
196
- .first();
197
- return maxPending
198
- ? maxSuccess
199
- ? maxPending.order > maxSuccess.order
200
- ? maxPending
201
- : maxSuccess
202
- : maxPending
203
- : maxSuccess ?? null;
198
+ return mergedStream(
199
+ [true, false].flatMap((tool) =>
200
+ ["success" as const, "pending" as const].map((status) =>
201
+ stream(ctx.db, schema)
202
+ .query("messages")
203
+ .withIndex("userId_status_tool_order_stepOrder", (q) =>
204
+ q.eq("userId", userId).eq("status", status).eq("tool", tool)
205
+ )
206
+ .order("desc")
207
+ )
208
+ ),
209
+ ["order", "stepOrder"]
210
+ ).first();
204
211
  }
205
212
  }
206
213
 
@@ -225,6 +232,7 @@ async function addStepHandler(
225
232
  assert(parentMessage, `Message ${args.parentMessageId} not found`);
226
233
  const order = parentMessage.order;
227
234
  assert(order !== undefined, `${args.parentMessageId} has no order`);
235
+ // TODO: only fetch the last one if we aren't failing pending steps
228
236
  let steps = await ctx.db
229
237
  .query("steps")
230
238
  .withIndex("parentMessageId_order_stepOrder", (q) =>
@@ -376,6 +384,12 @@ export const listMessagesByThreadId = query({
376
384
  return qq;
377
385
  })
378
386
  .order(order)
387
+ .filterWith(
388
+ async (m) =>
389
+ !before ||
390
+ m.order < before.order ||
391
+ (m.order === before.order && m.stepOrder < before.stepOrder)
392
+ )
379
393
  )
380
394
  );
381
395
  const messages = await mergedStream(streams, [
@@ -401,7 +415,6 @@ export const getThreadMessages = query({
401
415
  returns: paginationResultValidator(v.doc("messages")),
402
416
  });
403
417
 
404
-
405
418
  export const searchMessages = action({
406
419
  args: {
407
420
  userId: v.optional(v.string()),
@@ -420,6 +433,7 @@ export const searchMessages = action({
420
433
  threadId: args.threadId,
421
434
  text: args.text,
422
435
  limit,
436
+ beforeMessageId: args.beforeMessageId,
423
437
  });
424
438
  }
425
439
  if (args.vector) {
@@ -506,7 +520,10 @@ export const _fetchSearchMessages = internalQuery({
506
520
  m !== undefined &&
507
521
  m !== null &&
508
522
  !m.tool &&
509
- (!beforeMessage || m.order <= beforeMessage.order)
523
+ (!beforeMessage ||
524
+ m.order < beforeMessage.order ||
525
+ (m.order === beforeMessage.order &&
526
+ m.stepOrder < beforeMessage.stepOrder))
510
527
  );
511
528
  messages.push(...(args.textSearchMessages ?? []));
512
529
  // TODO: prioritize more recent messages
@@ -597,9 +614,13 @@ export const textSearch = query({
597
614
  userId: v.optional(v.string()),
598
615
  text: v.string(),
599
616
  limit: v.number(),
617
+ beforeMessageId: v.optional(v.id("messages")),
600
618
  },
601
619
  handler: async (ctx, args) => {
602
620
  assert(args.userId || args.threadId, "Specify userId or threadId");
621
+ const beforeMessage =
622
+ args.beforeMessageId && (await ctx.db.get(args.beforeMessageId));
623
+ const order = beforeMessage?.order;
603
624
  const messages = await ctx.db
604
625
  .query("messages")
605
626
  .withSearchIndex("text_search", (q) =>
@@ -608,9 +629,21 @@ export const textSearch = query({
608
629
  : q.search("text", args.text).eq("threadId", args.threadId!)
609
630
  )
610
631
  // Just in case tool messages slip through
611
- .filter((q) => q.eq(q.field("tool"), false))
632
+ .filter((q) => {
633
+ const qq = q.eq(q.field("tool"), false);
634
+ if (order) {
635
+ return q.and(qq, q.lte(q.field("order"), order));
636
+ }
637
+ return qq;
638
+ })
612
639
  .take(args.limit);
613
- return messages;
640
+ return messages.filter(
641
+ (m) =>
642
+ !beforeMessage ||
643
+ m.order < beforeMessage.order ||
644
+ (m.order === beforeMessage.order &&
645
+ m.stepOrder < beforeMessage.stepOrder)
646
+ );
614
647
  },
615
648
  returns: v.array(v.doc("messages")),
616
649
  });
@@ -9,6 +9,10 @@ import {
9
9
  vSource,
10
10
  vLanguageModelV1CallWarning,
11
11
  vFinishReason,
12
+ vProviderOptions,
13
+ vProviderMetadata,
14
+ vReasoningDetails,
15
+ vFile,
12
16
  } from "../validators.js";
13
17
  import { typedV } from "convex-helpers/validators";
14
18
  import vectorTables, { vVectorId } from "./vector/tables.js";
@@ -29,35 +33,44 @@ export const schema = defineSchema({
29
33
  id: v.optional(v.string()), // external id, e.g. from Vercel AI SDK
30
34
  userId: v.optional(v.string()), // useful for future indexes (text search)
31
35
  threadId: v.id("threads"),
36
+ // TODO: is this redunant with message at last step @ order - 1?
32
37
  parentMessageId: v.optional(v.id("messages")),
33
38
  stepId: v.optional(v.id("steps")),
34
- agentName: v.optional(v.string()),
35
- message: v.optional(vMessage),
39
+ // Repeats until a non-tool message.
40
+ order: v.number(),
41
+ stepOrder: v.number(),
42
+ embeddingId: v.optional(vVectorId),
36
43
  error: v.optional(v.string()),
44
+ status: vMessageStatus,
45
+
46
+ // Context on how it was generated
47
+ agentName: v.optional(v.string()),
37
48
  model: v.optional(v.string()),
38
49
  provider: v.optional(v.string()),
50
+ providerOptions: v.optional(vProviderOptions), // Sent to model
51
+
52
+ // The result
53
+ message: v.optional(vMessage),
54
+ // Convenience fields extracted from the message
55
+ tool: v.boolean(), // either tool call (assistant) or tool result (tool)
39
56
  text: v.optional(v.string()),
40
- embeddingId: v.optional(vVectorId),
41
- // TODO: add sub-messages back in? or be able to skip them?
42
- tool: v.boolean(),
43
- // Repeats until a non-tool message.
44
- // Unset if it's not in a thread.
45
- order: v.number(),
46
- stepOrder: v.number(),
57
+ files: v.optional(v.array(vFile)),
58
+
59
+ // Result metadata
47
60
  usage: v.optional(vUsage),
48
- finishReason: v.optional(vFinishReason),
49
- providerMetadata: v.optional(v.record(v.string(), v.any())),
61
+ providerMetadata: v.optional(vProviderMetadata), // Received from model
50
62
  sources: v.optional(v.array(vSource)),
51
63
  reasoning: v.optional(v.string()),
64
+ reasoningDetails: v.optional(vReasoningDetails),
52
65
  warnings: v.optional(v.array(vLanguageModelV1CallWarning)),
53
- fileId: v.optional(v.id("files")),
54
- status: vMessageStatus,
66
+ finishReason: v.optional(vFinishReason),
55
67
  })
56
68
  // Allows finding successful visible messages in order
57
69
  // Also surface pending messages separately to e.g. stream
58
70
  .index("threadId_status_tool_order_stepOrder", [
59
71
  "threadId",
60
72
  "status",
73
+ // TODO: we might not need this to be in the index..
61
74
  "tool",
62
75
  "order",
63
76
  "stepOrder",
@@ -65,14 +65,13 @@ export const deleteBatchForThread = mutation({
65
65
  handler: async (ctx, args) => {
66
66
  const tableName = getVectorTableName(args.vectorDimension);
67
67
  const vectors = await mergedStream(
68
- ["thread", "memory"].map((kind) =>
68
+ ["thread", "memory"].map((table) =>
69
69
  stream(ctx.db, schema)
70
70
  .query(tableName)
71
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
- .withIndex("model_kind_threadId" as any, (q) =>
73
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
- (q.eq("model", args.model) as any)
75
- .eq("kind", kind)
71
+ .withIndex("model_table_threadId", (q) =>
72
+ q
73
+ .eq("model", args.model)
74
+ .eq("table", table)
76
75
  .eq("threadId", args.threadId)
77
76
  )
78
77
  ),
@@ -47,6 +47,21 @@ function table<D extends number>(dimensions: D): Table<D> {
47
47
  .index("model_table_threadId", ["model", "table", "threadId"]);
48
48
  }
49
49
 
50
+ type Table<D extends number> = TableDefinition<
51
+ VObject<ObjectType<typeof embeddings>, typeof embeddings>,
52
+ { model_table_threadId: ["model", "table", "threadId", "_creationTime"] },
53
+ GenericTableSearchIndexes,
54
+ VectorIndex<D>
55
+ >;
56
+
57
+ type VectorIndex<D extends number> = {
58
+ vector: {
59
+ vectorField: "vector";
60
+ dimensions: D;
61
+ filterFields: "model_table_userId" | "model_table_threadId";
62
+ };
63
+ };
64
+
50
65
  export type VectorSchema = SchemaDefinition<
51
66
  { [key in VectorTableName]: Table<128> },
52
67
  true
@@ -80,21 +95,6 @@ export const vVectorId = v.union(
80
95
  VId<(typeof VectorTableNames)[number]>[]
81
96
  >;
82
97
 
83
- type Table<D extends number> = TableDefinition<
84
- VObject<ObjectType<typeof embeddings>, typeof embeddings>,
85
- { id: ["id"] },
86
- GenericTableSearchIndexes,
87
- VectorIndex<D>
88
- >;
89
-
90
- type VectorIndex<D extends number> = {
91
- vector: {
92
- vectorField: "vector";
93
- dimensions: D;
94
- filterFields: string;
95
- };
96
- };
97
-
98
98
  export function getVectorTableName(dimension: VectorDimension) {
99
99
  return `embeddings_${dimension}` as VectorTableName;
100
100
  }
package/src/mapping.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  type Message as AIMessage,
12
12
  type UserContent,
13
13
  } from "ai";
14
+ import type { FileUIPart } from "@ai-sdk/ui-utils";
14
15
  import { assert } from "convex-helpers";
15
16
  import {
16
17
  MessageWithMetadata,
@@ -39,9 +40,11 @@ export type SerializedMessage = SerializeUrlsAndUint8Arrays<CoreMessage>;
39
40
  export function serializeMessage(
40
41
  messageWithId: CoreMessage & { id?: string }
41
42
  ): SerializedMessage {
42
- const { id: _, ...message } = messageWithId;
43
+ const { id: _, experimental_providerMetadata, ...message } = messageWithId;
43
44
  const content = message.content;
44
45
  return {
46
+ // for backwards compatibility
47
+ providerOptions: experimental_providerMetadata,
45
48
  ...message,
46
49
  content: serializeContent(content),
47
50
  } as SerializedMessage;
@@ -90,6 +93,7 @@ export function serializeNewMessagesInStep<TOOLS extends ToolSet>(
90
93
  provider: metadata.provider,
91
94
  providerMetadata: step.providerMetadata,
92
95
  reasoning: step.reasoning,
96
+ reasoningDetails: step.reasoningDetails,
93
97
  usage: step.usage,
94
98
  warnings: step.warnings,
95
99
  finishReason: step.finishReason,
@@ -101,33 +105,41 @@ export function serializeNewMessagesInStep<TOOLS extends ToolSet>(
101
105
  step.toolResults.length > 0
102
106
  ? step.response.messages.slice(-2)
103
107
  : step.response.messages.slice(-1)
104
- ).map((message) => ({
105
- message: serializeMessage(message),
106
- id: message.id,
107
- ...(message.role === "tool" ? toolFields : assistantFields),
108
- // fileId: message.fileId,
109
- }));
108
+ ).map(
109
+ (message): MessageWithMetadata => ({
110
+ message: serializeMessage(message),
111
+ id: message.id,
112
+ ...(message.role === "tool" ? toolFields : assistantFields),
113
+ text: step.text,
114
+ // fileId: message.fileId,
115
+ files: step.files.map((file) => ({
116
+ mimeType: file.mimeType,
117
+ data: serializeDataOrUrl(file.uint8Array ?? file.base64),
118
+ // TODO: if the file is big, store it and populate url, fileId
119
+ })),
120
+ })
121
+ );
110
122
  return messages;
111
123
  }
112
124
 
113
125
  export function serializeObjectResult(
114
- step: GenerateObjectResult<unknown>,
126
+ result: GenerateObjectResult<unknown>,
115
127
  metadata: { model: string; provider: string }
116
128
  ): StepWithMessagesWithMetadata {
117
- const text = JSON.stringify(step.object);
129
+ const text = JSON.stringify(result.object);
118
130
 
119
131
  return {
120
132
  messages: [
121
133
  {
122
134
  message: { role: "assistant" as const, content: text },
123
- id: step.response.id,
135
+ id: result.response.id,
124
136
  model: metadata.model,
125
137
  provider: metadata.provider,
126
- providerMetadata: step.providerMetadata,
127
- finishReason: step.finishReason,
138
+ providerMetadata: result.providerMetadata,
139
+ finishReason: result.finishReason,
128
140
  text,
129
- usage: step.usage,
130
- warnings: step.warnings,
141
+ usage: result.usage,
142
+ warnings: result.warnings,
131
143
  },
132
144
  ],
133
145
  step: {
@@ -136,23 +148,22 @@ export function serializeObjectResult(
136
148
  stepType: "initial",
137
149
  toolCalls: [],
138
150
  toolResults: [],
139
- usage: step.usage,
140
- warnings: step.warnings,
141
- finishReason: step.finishReason,
142
- request: step.request,
151
+ usage: result.usage,
152
+ warnings: result.warnings,
153
+ finishReason: result.finishReason,
154
+ providerMetadata: result.providerMetadata,
155
+ request: result.request,
143
156
  response: {
144
- ...step.response,
145
- timestamp: step.response.timestamp.getTime(),
157
+ ...result.response,
158
+ timestamp: result.response.timestamp.getTime(),
146
159
  messages: [
147
160
  serializeMessageWithId({
148
161
  role: "assistant" as const,
149
162
  content: text,
150
- id: step.response.id,
163
+ id: result.response.id,
151
164
  }),
152
165
  ],
153
166
  },
154
- providerMetadata: step.providerMetadata,
155
- experimental_providerMetadata: step.experimental_providerMetadata,
156
167
  },
157
168
  };
158
169
  }
@@ -161,16 +172,19 @@ export function serializeContent(content: Content): SerializedContent {
161
172
  if (typeof content === "string") {
162
173
  return content;
163
174
  }
164
- const serialized = content.map((part) => {
165
- switch (part.type) {
166
- case "image":
167
- return { ...part, image: serializeDataOrUrl(part.image) };
168
- case "file":
169
- return { ...part, file: serializeDataOrUrl(part.data) };
170
- default:
171
- return part;
175
+ const serialized = content.map(
176
+ ({ experimental_providerMetadata, ...rest }) => {
177
+ const part = { providerOptions: experimental_providerMetadata, ...rest };
178
+ switch (part.type) {
179
+ case "image":
180
+ return { ...part, image: serializeDataOrUrl(part.image) };
181
+ case "file":
182
+ return { ...part, file: serializeDataOrUrl(part.data) };
183
+ default:
184
+ return part;
185
+ }
172
186
  }
173
- });
187
+ );
174
188
  return serialized as SerializedContent;
175
189
  }
176
190
 
@@ -222,6 +236,25 @@ function deserializeUrl(urlOrString: string | ArrayBuffer): URL | DataContent {
222
236
  return urlOrString;
223
237
  }
224
238
 
239
+ export function toUIFilePart(file: {
240
+ data?: ArrayBuffer | string;
241
+ url?: string;
242
+ mimeType: string;
243
+ }): FileUIPart {
244
+ return {
245
+ type: "file",
246
+ data:
247
+ file.data instanceof ArrayBuffer
248
+ ? encodeBase64(file.data)
249
+ : file.url ?? file.data ?? "",
250
+ mimeType: file.mimeType,
251
+ };
252
+ }
253
+
254
+ function encodeBase64(data: ArrayBuffer): string {
255
+ return Buffer.from(data).toString("base64");
256
+ }
257
+
225
258
  export function promptOrMessagesToCoreMessages(args: {
226
259
  prompt?: string;
227
260
  messages?: CoreMessage[] | AIMessageWithoutId[];