@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.
- package/dist/UIMessages.d.ts +3 -2
- package/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js +100 -19
- package/dist/UIMessages.js.map +1 -1
- package/dist/client/createTool.d.ts +129 -25
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +65 -8
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +560 -35
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +4 -2
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +462 -37
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +26 -7
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +114 -9
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/mockModel.d.ts +17 -17
- package/dist/client/mockModel.d.ts.map +1 -1
- package/dist/client/mockModel.js +8 -6
- package/dist/client/mockModel.js.map +1 -1
- package/dist/client/saveInputMessages.d.ts +1 -1
- package/dist/client/saveInputMessages.d.ts.map +1 -1
- package/dist/client/saveInputMessages.js +1 -1
- package/dist/client/saveInputMessages.js.map +1 -1
- package/dist/client/search.d.ts +132 -14
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +66 -19
- package/dist/client/search.js.map +1 -1
- package/dist/client/streamText.d.ts +4 -4
- package/dist/client/streamText.d.ts.map +1 -1
- package/dist/client/streamText.js.map +1 -1
- package/dist/client/streaming.d.ts +1491 -87
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/types.d.ts +26 -6
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +862 -19
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/files.d.ts +12 -6
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +1247 -92
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/schema.d.ts +2823 -217
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.js +1 -1
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +10 -10
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +40 -5
- package/dist/deltas.js.map +1 -1
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +117 -25
- package/dist/mapping.js.map +1 -1
- package/dist/react/useThreadMessages.d.ts.map +1 -1
- package/dist/react/useThreadMessages.js +17 -9
- package/dist/react/useThreadMessages.js.map +1 -1
- package/dist/shared.d.ts +2 -2
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +12196 -624
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +152 -9
- package/dist/validators.js.map +1 -1
- package/package.json +24 -22
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.ts +184 -66
- package/src/client/createTool.ts +291 -67
- package/src/client/files.ts +4 -2
- package/src/client/index.test.ts +1 -0
- package/src/client/index.ts +46 -25
- package/src/client/mockModel.ts +36 -34
- package/src/client/saveInputMessages.ts +2 -2
- package/src/client/search.test.ts +166 -0
- package/src/client/search.ts +121 -38
- package/src/client/streamText.ts +5 -5
- package/src/client/types.ts +31 -18
- package/src/component/_generated/component.ts +2329 -338
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +7 -7
- package/src/component/schema.ts +3 -1
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +1 -1
- package/src/deltas.test.ts +90 -0
- package/src/deltas.ts +47 -8
- package/src/fromUIMessages.test.ts +32 -27
- package/src/mapping.test.ts +48 -0
- package/src/mapping.ts +234 -142
- package/src/react/useThreadMessages.ts +21 -9
- package/src/shared.ts +2 -0
- package/src/toUIMessages.test.ts +539 -1
- package/src/validators.ts +179 -20
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
package/src/component/files.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
+
input: { a: 2, b: 3 },
|
|
426
426
|
toolCallId: "1",
|
|
427
427
|
toolName: "tool",
|
|
428
428
|
},
|
package/src/component/schema.ts
CHANGED
|
@@ -149,7 +149,9 @@ export const schema = defineSchema({
|
|
|
149
149
|
|
|
150
150
|
files: defineTable({
|
|
151
151
|
storageId: v.string(),
|
|
152
|
-
|
|
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", () => {});
|
package/src/component/streams.ts
CHANGED
|
@@ -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) => {
|
package/src/deltas.test.ts
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
//
|
|
486
|
-
|
|
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
|
-
|
|
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], {
|
|
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], {
|
|
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
|
package/src/mapping.test.ts
CHANGED
|
@@ -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
|
});
|