@convex-dev/agent 0.3.2 → 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 (99) hide show
  1. package/dist/UIMessages.d.ts +3 -2
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +100 -19
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +129 -25
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +65 -8
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/definePlaygroundAPI.d.ts +560 -35
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/files.d.ts.map +1 -1
  12. package/dist/client/files.js +4 -2
  13. package/dist/client/files.js.map +1 -1
  14. package/dist/client/index.d.ts +462 -37
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/index.js +26 -7
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/messages.d.ts +114 -9
  19. package/dist/client/messages.d.ts.map +1 -1
  20. package/dist/client/mockModel.d.ts +17 -17
  21. package/dist/client/mockModel.d.ts.map +1 -1
  22. package/dist/client/mockModel.js +8 -6
  23. package/dist/client/mockModel.js.map +1 -1
  24. package/dist/client/saveInputMessages.d.ts +1 -1
  25. package/dist/client/saveInputMessages.d.ts.map +1 -1
  26. package/dist/client/saveInputMessages.js +1 -1
  27. package/dist/client/saveInputMessages.js.map +1 -1
  28. package/dist/client/search.d.ts +132 -14
  29. package/dist/client/search.d.ts.map +1 -1
  30. package/dist/client/search.js +66 -19
  31. package/dist/client/search.js.map +1 -1
  32. package/dist/client/streamText.d.ts +4 -4
  33. package/dist/client/streamText.d.ts.map +1 -1
  34. package/dist/client/streamText.js.map +1 -1
  35. package/dist/client/streaming.d.ts +1491 -87
  36. package/dist/client/streaming.d.ts.map +1 -1
  37. package/dist/client/types.d.ts +26 -6
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/component.d.ts +862 -19
  40. package/dist/component/_generated/component.d.ts.map +1 -1
  41. package/dist/component/files.d.ts +12 -6
  42. package/dist/component/files.d.ts.map +1 -1
  43. package/dist/component/files.js +10 -2
  44. package/dist/component/files.js.map +1 -1
  45. package/dist/component/messages.d.ts +1247 -92
  46. package/dist/component/messages.d.ts.map +1 -1
  47. package/dist/component/schema.d.ts +2823 -217
  48. package/dist/component/schema.d.ts.map +1 -1
  49. package/dist/component/schema.js +3 -1
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/streams.js +1 -1
  52. package/dist/component/streams.js.map +1 -1
  53. package/dist/component/threads.d.ts +10 -10
  54. package/dist/component/vector/index.d.ts +1 -1
  55. package/dist/deltas.d.ts.map +1 -1
  56. package/dist/deltas.js +40 -5
  57. package/dist/deltas.js.map +1 -1
  58. package/dist/mapping.d.ts.map +1 -1
  59. package/dist/mapping.js +117 -25
  60. package/dist/mapping.js.map +1 -1
  61. package/dist/react/useThreadMessages.d.ts.map +1 -1
  62. package/dist/react/useThreadMessages.js +17 -9
  63. package/dist/react/useThreadMessages.js.map +1 -1
  64. package/dist/shared.d.ts +2 -2
  65. package/dist/shared.d.ts.map +1 -1
  66. package/dist/shared.js.map +1 -1
  67. package/dist/validators.d.ts +12196 -624
  68. package/dist/validators.d.ts.map +1 -1
  69. package/dist/validators.js +152 -9
  70. package/dist/validators.js.map +1 -1
  71. package/package.json +24 -22
  72. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  73. package/src/UIMessages.ts +184 -66
  74. package/src/client/createTool.ts +291 -67
  75. package/src/client/files.ts +4 -2
  76. package/src/client/index.test.ts +1 -0
  77. package/src/client/index.ts +46 -25
  78. package/src/client/mockModel.ts +36 -34
  79. package/src/client/saveInputMessages.ts +2 -2
  80. package/src/client/search.test.ts +166 -0
  81. package/src/client/search.ts +121 -38
  82. package/src/client/streamText.ts +5 -5
  83. package/src/client/types.ts +31 -18
  84. package/src/component/_generated/component.ts +2329 -338
  85. package/src/component/files.ts +11 -2
  86. package/src/component/messages.test.ts +7 -7
  87. package/src/component/schema.ts +3 -1
  88. package/src/component/setup.test.ts +7 -0
  89. package/src/component/streams.ts +1 -1
  90. package/src/deltas.test.ts +90 -0
  91. package/src/deltas.ts +47 -8
  92. package/src/fromUIMessages.test.ts +32 -27
  93. package/src/mapping.test.ts +48 -0
  94. package/src/mapping.ts +234 -142
  95. package/src/react/useThreadMessages.ts +21 -9
  96. package/src/shared.ts +2 -0
  97. package/src/toUIMessages.test.ts +539 -1
  98. package/src/validators.ts +179 -20
  99. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -9,7 +9,9 @@ const addFileArgs = v.object({
9
9
  storageId: v.string(),
10
10
  hash: v.string(),
11
11
  filename: v.optional(v.string()),
12
- mimeType: v.string(),
12
+ mediaType: v.optional(v.string()),
13
+ /** @deprecated Use `mediaType` instead. */
14
+ mimeType: v.optional(v.string()),
13
15
  });
14
16
 
15
17
  export const addFile = mutation({
@@ -25,6 +27,9 @@ export async function addFileHandler(
25
27
  ctx: MutationCtx,
26
28
  args: Infer<typeof addFileArgs>,
27
29
  ) {
30
+ // Support both mediaType (preferred) and mimeType (deprecated)
31
+ const mediaType = args.mediaType ?? args.mimeType;
32
+
28
33
  const existingFile = await ctx.db
29
34
  .query("files")
30
35
  .withIndex("hash", (q) => q.eq("hash", args.hash))
@@ -42,7 +47,11 @@ export async function addFileHandler(
42
47
  };
43
48
  }
44
49
  const fileId = await ctx.db.insert("files", {
45
- ...args,
50
+ storageId: args.storageId,
51
+ hash: args.hash,
52
+ filename: args.filename,
53
+ mediaType,
54
+ mimeType: args.mimeType, // Keep for backwards compatibility
46
55
  // We start out with it unused - when it's saved in a message we increment.
47
56
  refcount: 0,
48
57
  lastTouchedAt: Date.now(),
@@ -6,11 +6,11 @@ import { api } from "./_generated/api.js";
6
6
  import type { Id } from "./_generated/dataModel.js";
7
7
  import { getMaxMessage } from "./messages.js";
8
8
  import schema from "./schema.js";
9
- import { modules } from "./setup.test.js";
9
+ import { initConvexTest, modules } from "./setup.test.js";
10
10
 
11
11
  describe("agent", () => {
12
12
  test("getMaxMessage works for threads", async () => {
13
- const t = convexTest(schema, modules);
13
+ const t = initConvexTest();
14
14
  const thread = await t.mutation(api.threads.createThread, {
15
15
  userId: "test",
16
16
  });
@@ -68,7 +68,7 @@ describe("agent", () => {
68
68
  content: [
69
69
  {
70
70
  type: "tool-call",
71
- args: { a: 1 },
71
+ input: { a: 1 },
72
72
  toolCallId: "1",
73
73
  toolName: "tool",
74
74
  },
@@ -258,7 +258,7 @@ describe("agent", () => {
258
258
  content: [
259
259
  {
260
260
  type: "tool-call",
261
- args: { a: 1 },
261
+ input: { a: 1 },
262
262
  toolCallId: "1",
263
263
  toolName: "tool",
264
264
  },
@@ -389,7 +389,7 @@ describe("agent", () => {
389
389
  content: [
390
390
  {
391
391
  type: "tool-call",
392
- args: { a: 1 },
392
+ input: { a: 1 },
393
393
  toolCallId: "1",
394
394
  toolName: "tool",
395
395
  },
@@ -408,7 +408,7 @@ describe("agent", () => {
408
408
  content: [
409
409
  {
410
410
  type: "tool-call",
411
- args: { a: 2, b: 3 },
411
+ input: { a: 2, b: 3 },
412
412
  toolCallId: "1",
413
413
  toolName: "tool",
414
414
  },
@@ -422,7 +422,7 @@ describe("agent", () => {
422
422
  content: [
423
423
  {
424
424
  type: "tool-call",
425
- args: { a: 2, b: 3 },
425
+ input: { a: 2, b: 3 },
426
426
  toolCallId: "1",
427
427
  toolName: "tool",
428
428
  },
@@ -149,7 +149,9 @@ export const schema = defineSchema({
149
149
 
150
150
  files: defineTable({
151
151
  storageId: v.string(),
152
- mimeType: v.string(),
152
+ mediaType: v.optional(v.string()),
153
+ /** @deprecated Use `mediaType` instead. */
154
+ mimeType: v.optional(v.string()),
153
155
  filename: v.optional(v.string()),
154
156
  hash: v.string(),
155
157
  refcount: v.number(),
@@ -1,5 +1,12 @@
1
1
  /// <reference types="vite/client" />
2
+ import { convexTest } from "convex-test";
2
3
  import { test } from "vitest";
4
+ import schema from "./schema.js";
3
5
  export const modules = import.meta.glob("./**/*.*s");
4
6
 
7
+ export function initConvexTest() {
8
+ const t = convexTest(schema, modules);
9
+ return t;
10
+ }
11
+
5
12
  test("setup", () => {});
@@ -545,7 +545,7 @@ export async function getStreamingMessagesWithMetadata(
545
545
  // We don't save messages that have already been saved
546
546
  const numToSkip = stepOrder - streamingMessage.stepOrder;
547
547
  const messages = await Promise.all(
548
- fromUIMessages(uiMessages, streamingMessage)
548
+ (await fromUIMessages(uiMessages, streamingMessage))
549
549
  .slice(numToSkip)
550
550
  .filter((m) => m.message !== undefined)
551
551
  .map(async (msg) => {
@@ -156,6 +156,49 @@ describe("UIMessageChunks", () => {
156
156
  });
157
157
  });
158
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
+
159
202
  describe("mergeDeltas", () => {
160
203
  it("merges a single text-delta into a message", () => {
161
204
  const streamId = "s1";
@@ -533,4 +576,51 @@ describe("mergeDeltas", () => {
533
576
  },
534
577
  ]);
535
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
+ });
536
626
  });
package/src/deltas.ts CHANGED
@@ -66,22 +66,40 @@ export async function updateFromUIMessageChunks(
66
66
  },
67
67
  });
68
68
  let failed = false;
69
+ let suppressError = false;
69
70
  const messageStream = readUIMessageStream({
70
71
  message: uiMessage,
71
72
  stream: partsStream,
72
73
  onError: (e) => {
74
+ const errorMessage = e instanceof Error ? e.message : String(e);
75
+ // Tool invocation errors can be safely ignored when streaming continuation
76
+ // after tool approval - the stored messages have the complete tool context
77
+ if (errorMessage.toLowerCase().includes("no tool invocation found")) {
78
+ // Silently suppress - this is expected after tool approval when the
79
+ // continuation stream has tool-result without the original tool-call
80
+ suppressError = true;
81
+ return;
82
+ }
73
83
  failed = true;
74
84
  console.error("Error in stream", e);
75
85
  },
76
86
  terminateOnError: true,
77
87
  });
78
88
  let message = uiMessage;
79
- for await (const messagePart of messageStream) {
80
- assert(
81
- messagePart.id === message.id,
82
- `Expecting to only make one UIMessage in a stream`,
83
- );
84
- message = messagePart;
89
+ try {
90
+ for await (const messagePart of messageStream) {
91
+ assert(
92
+ messagePart.id === message.id,
93
+ `Expecting to only make one UIMessage in a stream`,
94
+ );
95
+ message = messagePart;
96
+ }
97
+ } catch (e) {
98
+ // If we've already handled this error in onError and marked it as suppressed,
99
+ // don't rethrow - the stored messages provide the fallback
100
+ if (!suppressError) {
101
+ throw e;
102
+ }
85
103
  }
86
104
  if (failed) {
87
105
  message.status = "failed";
@@ -472,6 +490,25 @@ export function updateFromTextStreamParts(
472
490
  }
473
491
  break;
474
492
  }
493
+ case "tool-approval-request": {
494
+ const typedPart = part as unknown as {
495
+ type: "tool-approval-request";
496
+ toolCallId: string;
497
+ approvalId: string;
498
+ };
499
+ const toolPart = toolPartsById.get(typedPart.toolCallId);
500
+ if (toolPart) {
501
+ toolPart.state = "approval-requested";
502
+ (toolPart as ToolUIPart & { approval?: object }).approval = {
503
+ id: typedPart.approvalId,
504
+ };
505
+ } else {
506
+ console.warn(
507
+ `Expected tool call part ${typedPart.toolCallId} for approval request`,
508
+ );
509
+ }
510
+ break;
511
+ }
475
512
  case "file":
476
513
  case "text-end":
477
514
  case "finish-step":
@@ -482,8 +519,10 @@ export function updateFromTextStreamParts(
482
519
  // ignore
483
520
  break;
484
521
  default: {
485
- // Should never happen
486
- const _: never = part;
522
+ // Exhaustiveness check disabled intentionally for forwards compatibility.
523
+ // New TextStreamPart types from future AI SDK versions will trigger a
524
+ // runtime warning rather than a compile error, allowing graceful degradation.
525
+ // const _: never = part;
487
526
  console.warn(`Received unexpected part: ${JSON.stringify(part)}`);
488
527
  break;
489
528
  }
@@ -20,7 +20,7 @@ function baseMessageDoc<T = unknown>(
20
20
  }
21
21
 
22
22
  describe("fromUIMessages round-trip tests", () => {
23
- it("preserves essential data for simple user message", () => {
23
+ it("preserves essential data for simple user message", async () => {
24
24
  const originalMessages = [
25
25
  baseMessageDoc({
26
26
  message: {
@@ -32,7 +32,7 @@ describe("fromUIMessages round-trip tests", () => {
32
32
  ];
33
33
 
34
34
  const uiMessages = toUIMessages(originalMessages);
35
- const backToMessageDocs = fromUIMessages(uiMessages, {
35
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
36
36
  threadId: "thread1",
37
37
  });
38
38
 
@@ -54,7 +54,7 @@ describe("fromUIMessages round-trip tests", () => {
54
54
  }
55
55
  });
56
56
 
57
- it("preserves essential data for assistant message", () => {
57
+ it("preserves essential data for assistant message", async () => {
58
58
  const originalMessages = [
59
59
  baseMessageDoc({
60
60
  message: {
@@ -66,7 +66,7 @@ describe("fromUIMessages round-trip tests", () => {
66
66
  ];
67
67
 
68
68
  const uiMessages = toUIMessages(originalMessages);
69
- const backToMessageDocs = fromUIMessages(uiMessages, {
69
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
70
70
  threadId: "thread1",
71
71
  });
72
72
 
@@ -78,7 +78,7 @@ describe("fromUIMessages round-trip tests", () => {
78
78
  expect(backToMessageDocs[0].text).toBe("Hi there! How can I help?");
79
79
  });
80
80
 
81
- it("preserves system messages correctly", () => {
81
+ it("preserves system messages correctly", async () => {
82
82
  const originalMessages = [
83
83
  baseMessageDoc({
84
84
  message: {
@@ -90,7 +90,7 @@ describe("fromUIMessages round-trip tests", () => {
90
90
  ];
91
91
 
92
92
  const uiMessages = toUIMessages(originalMessages);
93
- const backToMessageDocs = fromUIMessages(uiMessages, {
93
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
94
94
  threadId: "thread1",
95
95
  });
96
96
 
@@ -108,7 +108,7 @@ describe("fromUIMessages round-trip tests", () => {
108
108
  );
109
109
  });
110
110
 
111
- it("preserves reasoning in assistant messages", () => {
111
+ it("preserves reasoning in assistant messages", async () => {
112
112
  const originalMessages = [
113
113
  baseMessageDoc({
114
114
  message: {
@@ -130,7 +130,7 @@ describe("fromUIMessages round-trip tests", () => {
130
130
  ];
131
131
 
132
132
  const uiMessages = toUIMessages(originalMessages);
133
- const backToMessageDocs = fromUIMessages(uiMessages, {
133
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
134
134
  threadId: "thread1",
135
135
  });
136
136
 
@@ -152,7 +152,7 @@ describe("fromUIMessages round-trip tests", () => {
152
152
  expect(backToMessageDocs[0].reasoning).toBe("Let me think about this...");
153
153
  });
154
154
 
155
- it("handles tool calls and groups them correctly", () => {
155
+ it("handles tool calls and groups them correctly", async () => {
156
156
  // Tool calls get grouped into single UI message but expanded back to multiple message docs
157
157
  const originalMessages = [
158
158
  baseMessageDoc({
@@ -166,6 +166,7 @@ describe("fromUIMessages round-trip tests", () => {
166
166
  type: "tool-call",
167
167
  toolName: "calculator",
168
168
  toolCallId: "call1",
169
+ input: { operation: "add", a: 2, b: 3 },
169
170
  args: { operation: "add", a: 2, b: 3 },
170
171
  },
171
172
  ],
@@ -196,7 +197,7 @@ describe("fromUIMessages round-trip tests", () => {
196
197
  const toTest = [originalMessages, [...originalMessages].reverse()];
197
198
  for (const messages of toTest) {
198
199
  const uiMessages = toUIMessages(messages);
199
- const backToMessageDocs = fromUIMessages(uiMessages, {
200
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
200
201
  threadId: "thread1",
201
202
  });
202
203
 
@@ -230,7 +231,7 @@ describe("fromUIMessages round-trip tests", () => {
230
231
  }
231
232
  });
232
233
 
233
- it("preserves file attachments in user messages", () => {
234
+ it("preserves file attachments in user messages", async () => {
234
235
  const originalMessages = [
235
236
  baseMessageDoc({
236
237
  message: {
@@ -252,7 +253,7 @@ describe("fromUIMessages round-trip tests", () => {
252
253
  ];
253
254
 
254
255
  const uiMessages = toUIMessages(originalMessages);
255
- const backToMessageDocs = fromUIMessages(uiMessages, {
256
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
256
257
  threadId: "thread1",
257
258
  });
258
259
 
@@ -277,12 +278,12 @@ describe("fromUIMessages round-trip tests", () => {
277
278
  expect(fileContent).toBeDefined();
278
279
  expect(fileContent).toMatchObject({
279
280
  type: "file",
280
- mimeType: "image/png",
281
+ mediaType: "image/png",
281
282
  });
282
283
  }
283
284
  });
284
285
 
285
- it("preserves sources correctly", () => {
286
+ it("preserves sources correctly", async () => {
286
287
  const originalMessages = [
287
288
  baseMessageDoc({
288
289
  message: {
@@ -315,7 +316,7 @@ describe("fromUIMessages round-trip tests", () => {
315
316
  ];
316
317
 
317
318
  const uiMessages = toUIMessages(originalMessages);
318
- const backToMessageDocs = fromUIMessages(uiMessages, {
319
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
319
320
  threadId: "thread1",
320
321
  });
321
322
 
@@ -338,7 +339,7 @@ describe("fromUIMessages round-trip tests", () => {
338
339
  });
339
340
  });
340
341
 
341
- it("preserves metadata when provided", () => {
342
+ it("preserves metadata when provided", async () => {
342
343
  const testMetadata = {
343
344
  customField: "customValue",
344
345
  timestamp: Date.now(),
@@ -356,7 +357,7 @@ describe("fromUIMessages round-trip tests", () => {
356
357
  ];
357
358
 
358
359
  const uiMessages = toUIMessages(originalMessages);
359
- const backToMessageDocs = fromUIMessages(uiMessages, {
360
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
360
361
  threadId: "thread1",
361
362
  });
362
363
 
@@ -367,7 +368,7 @@ describe("fromUIMessages round-trip tests", () => {
367
368
  expect(backToMessageDocs[0].metadata).toEqual(testMetadata);
368
369
  });
369
370
 
370
- it("handles streaming status correctly", () => {
371
+ it("handles streaming status correctly", async () => {
371
372
  const originalMessages = [
372
373
  baseMessageDoc({
373
374
  message: {
@@ -381,7 +382,7 @@ describe("fromUIMessages round-trip tests", () => {
381
382
  ];
382
383
 
383
384
  const uiMessages = toUIMessages(originalMessages);
384
- const backToMessageDocs = fromUIMessages(uiMessages, {
385
+ const backToMessageDocs = await fromUIMessages(uiMessages, {
385
386
  threadId: "thread1",
386
387
  });
387
388
 
@@ -395,13 +396,13 @@ describe("fromUIMessages round-trip tests", () => {
395
396
  });
396
397
 
397
398
  describe("fromUIMessages functionality tests", () => {
398
- it("handles empty messages array", () => {
399
+ it("handles empty messages array", async () => {
399
400
  const uiMessages: UIMessage[] = [];
400
- const result = fromUIMessages(uiMessages, { threadId: "thread1" });
401
+ const result = await fromUIMessages(uiMessages, { threadId: "thread1" });
401
402
  expect(result).toHaveLength(0);
402
403
  });
403
404
 
404
- it("correctly assigns thread ID", () => {
405
+ it("correctly assigns thread ID", async () => {
405
406
  const uiMessage: UIMessage = {
406
407
  id: "test-id",
407
408
  _creationTime: Date.now(),
@@ -414,14 +415,14 @@ describe("fromUIMessages functionality tests", () => {
414
415
  parts: [{ type: "text", text: "Hello" }],
415
416
  };
416
417
 
417
- const result = fromUIMessages([uiMessage], {
418
+ const result = await fromUIMessages([uiMessage], {
418
419
  threadId: "custom-thread-id",
419
420
  });
420
421
  expect(result).toHaveLength(1);
421
422
  expect(result[0].threadId).toBe("custom-thread-id");
422
423
  });
423
424
 
424
- it("correctly determines tool status", () => {
425
+ it("correctly determines tool status", async () => {
425
426
  const toolUIMessage: UIMessage = {
426
427
  id: "tool-id",
427
428
  _creationTime: Date.now(),
@@ -442,7 +443,9 @@ describe("fromUIMessages functionality tests", () => {
442
443
  ],
443
444
  };
444
445
 
445
- const result = fromUIMessages([toolUIMessage], { threadId: "thread1" });
446
+ const result = await fromUIMessages([toolUIMessage], {
447
+ threadId: "thread1",
448
+ });
446
449
  expect(result.length).toBeGreaterThan(0);
447
450
 
448
451
  // Should have tool messages
@@ -450,7 +453,7 @@ describe("fromUIMessages functionality tests", () => {
450
453
  expect(toolMessages.length).toBeGreaterThan(0);
451
454
  });
452
455
 
453
- it("handles tool calls without responses", () => {
456
+ it("handles tool calls without responses", async () => {
454
457
  const toolUIMessage: UIMessage = {
455
458
  id: "tool-id",
456
459
  _creationTime: Date.now(),
@@ -471,7 +474,9 @@ describe("fromUIMessages functionality tests", () => {
471
474
  ],
472
475
  };
473
476
 
474
- const result = fromUIMessages([toolUIMessage], { threadId: "thread1" });
477
+ const result = await fromUIMessages([toolUIMessage], {
478
+ threadId: "thread1",
479
+ });
475
480
  expect(result.length).toBeGreaterThan(0);
476
481
 
477
482
  // Should have tool messages
@@ -209,4 +209,52 @@ describe("mapping", () => {
209
209
  const { fileIds } = await serializeContent(ctx, component, content);
210
210
  expect(fileIds).toBeUndefined();
211
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
+ });
212
260
  });