@convex-dev/agent 0.7.0 → 0.7.1
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/component/_generated/component.d.ts +5 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/files.d.ts +6 -4
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +53 -42
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +7 -3
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +58 -6
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +12 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +10 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +72 -17
- package/dist/component/streams.js.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts +8 -2
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.js +34 -3
- package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -1
- package/dist/vercel/UIMessages.js +1 -1
- package/dist/vercel/UIMessages.js.map +1 -1
- package/dist/vercel/client/files.d.ts.map +1 -1
- package/dist/vercel/client/files.js +51 -27
- package/dist/vercel/client/files.js.map +1 -1
- package/dist/vercel/client/streamText.d.ts.map +1 -1
- package/dist/vercel/client/streamText.js +2 -0
- package/dist/vercel/client/streamText.js.map +1 -1
- package/dist/vercel/client/streaming.d.ts +14 -0
- package/dist/vercel/client/streaming.d.ts.map +1 -1
- package/dist/vercel/client/streaming.js +35 -13
- package/dist/vercel/client/streaming.js.map +1 -1
- package/dist/vercel/fileMaterialization.d.ts +24 -0
- package/dist/vercel/fileMaterialization.d.ts.map +1 -0
- package/dist/vercel/fileMaterialization.js +118 -0
- package/dist/vercel/fileMaterialization.js.map +1 -0
- package/dist/vercel/mapping.d.ts.map +1 -1
- package/dist/vercel/mapping.js +7 -1
- package/dist/vercel/mapping.js.map +1 -1
- package/package.json +1 -1
- package/src/component/_generated/component.ts +8 -2
- package/src/component/files.test.ts +81 -6
- package/src/component/files.ts +69 -41
- package/src/component/messages.test.ts +92 -0
- package/src/component/messages.ts +11 -3
- package/src/component/schema.ts +16 -2
- package/src/component/streams.test.ts +109 -0
- package/src/component/streams.ts +103 -20
- package/src/streaming/materializePersistedUIMessageChunks.test.ts +33 -0
- package/src/streaming/materializePersistedUIMessageChunks.ts +37 -0
- package/src/vercel/UIMessages.ts +1 -1
- package/src/vercel/client/files.test.ts +56 -0
- package/src/vercel/client/files.ts +53 -28
- package/src/vercel/client/streamText.ts +3 -0
- package/src/vercel/client/streaming.test.ts +71 -0
- package/src/vercel/client/streaming.ts +50 -16
- package/src/vercel/fileMaterialization.ts +174 -0
- package/src/vercel/mapping.test.ts +117 -0
- package/src/vercel/mapping.ts +15 -1
- package/src/vercel/toUIMessages.test.ts +7 -1
|
@@ -204,6 +204,12 @@ export class DeltaStreamer<T> {
|
|
|
204
204
|
throttleMs: number;
|
|
205
205
|
onAsyncAbort: (reason: string) => Promise<void>;
|
|
206
206
|
compress: ((parts: T[]) => T[]) | null;
|
|
207
|
+
materialize:
|
|
208
|
+
| ((parts: T[]) => Promise<{
|
|
209
|
+
parts: T[];
|
|
210
|
+
fileRefs: Array<{ url: string; fileId: string }>;
|
|
211
|
+
}>)
|
|
212
|
+
| null;
|
|
207
213
|
};
|
|
208
214
|
#nextParts: T[] = [];
|
|
209
215
|
#latestWrite: number = 0;
|
|
@@ -223,6 +229,10 @@ export class DeltaStreamer<T> {
|
|
|
223
229
|
onAsyncAbort: (reason: string) => Promise<void>;
|
|
224
230
|
abortSignal: AbortSignal | undefined;
|
|
225
231
|
compress: ((parts: T[]) => T[]) | null;
|
|
232
|
+
materialize?: (parts: T[]) => Promise<{
|
|
233
|
+
parts: T[];
|
|
234
|
+
fileRefs: Array<{ url: string; fileId: string }>;
|
|
235
|
+
}>;
|
|
226
236
|
},
|
|
227
237
|
public readonly metadata: {
|
|
228
238
|
threadId: string;
|
|
@@ -240,6 +250,7 @@ export class DeltaStreamer<T> {
|
|
|
240
250
|
throttleMs: config.throttleMs ?? DEFAULT_STREAMING_OPTIONS.throttleMs,
|
|
241
251
|
onAsyncAbort: config.onAsyncAbort,
|
|
242
252
|
compress: config.compress,
|
|
253
|
+
materialize: config.materialize ?? null,
|
|
243
254
|
};
|
|
244
255
|
this.#nextParts = [];
|
|
245
256
|
this.abortController = new AbortController();
|
|
@@ -300,8 +311,18 @@ export class DeltaStreamer<T> {
|
|
|
300
311
|
}
|
|
301
312
|
|
|
302
313
|
public async consumeStream(stream: AsyncIterableStream<T>) {
|
|
303
|
-
|
|
304
|
-
await
|
|
314
|
+
try {
|
|
315
|
+
for await (const chunk of stream) {
|
|
316
|
+
await this.addParts([chunk]);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// A provider can throw while responding to an abort. Join the durable
|
|
320
|
+
// abort transition here, outside the active delta writer, before
|
|
321
|
+
// preserving the provider error for the caller.
|
|
322
|
+
await this.#abort(
|
|
323
|
+
error instanceof Error ? error.message : "stream consumption failed",
|
|
324
|
+
).catch(() => {});
|
|
325
|
+
throw error;
|
|
305
326
|
}
|
|
306
327
|
// Skip finish if it will be handled externally (atomically with message save)
|
|
307
328
|
// or if the stream was aborted (e.g., due to a failed delta write).
|
|
@@ -335,21 +356,19 @@ export class DeltaStreamer<T> {
|
|
|
335
356
|
if (this.abortController.signal.aborted) {
|
|
336
357
|
return;
|
|
337
358
|
}
|
|
338
|
-
const delta = this.#createDelta();
|
|
339
|
-
if (!delta) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
this.#latestWrite = Date.now();
|
|
343
359
|
let success: boolean;
|
|
344
360
|
try {
|
|
361
|
+
const delta = await this.#createDelta();
|
|
362
|
+
if (!delta) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.#latestWrite = Date.now();
|
|
345
366
|
success = await this.ctx.runMutation(
|
|
346
367
|
this.component.streams.addDelta,
|
|
347
368
|
delta,
|
|
348
369
|
);
|
|
349
370
|
} catch (e) {
|
|
350
|
-
await this.#abortDelta(
|
|
351
|
-
e instanceof Error ? e.message : "unknown error",
|
|
352
|
-
);
|
|
371
|
+
await this.#abortDelta(e instanceof Error ? e.message : "unknown error");
|
|
353
372
|
return;
|
|
354
373
|
}
|
|
355
374
|
if (!success) {
|
|
@@ -375,21 +394,36 @@ export class DeltaStreamer<T> {
|
|
|
375
394
|
}
|
|
376
395
|
}
|
|
377
396
|
|
|
378
|
-
#createDelta():
|
|
397
|
+
async #createDelta(): Promise<
|
|
398
|
+
| (StreamDelta & { fileRefs?: Array<{ url: string; fileId: string }> })
|
|
399
|
+
| undefined
|
|
400
|
+
> {
|
|
379
401
|
if (this.#nextParts.length === 0) {
|
|
380
402
|
return undefined;
|
|
381
403
|
}
|
|
382
404
|
const start = this.#cursor;
|
|
383
|
-
const
|
|
405
|
+
const pendingParts = this.#nextParts;
|
|
406
|
+
const end = start + pendingParts.length;
|
|
384
407
|
this.#cursor = end;
|
|
385
|
-
const parts = this.config.compress
|
|
386
|
-
? this.config.compress(this.#nextParts)
|
|
387
|
-
: this.#nextParts;
|
|
388
408
|
this.#nextParts = [];
|
|
409
|
+
const materialized = this.config.materialize
|
|
410
|
+
? await this.config.materialize(pendingParts)
|
|
411
|
+
: { parts: pendingParts, fileRefs: [] };
|
|
412
|
+
const parts = this.config.compress
|
|
413
|
+
? this.config.compress(materialized.parts)
|
|
414
|
+
: materialized.parts;
|
|
389
415
|
if (!this.streamId) {
|
|
390
416
|
throw new Error("Creating a delta before the stream is created");
|
|
391
417
|
}
|
|
392
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
streamId: this.streamId,
|
|
420
|
+
start,
|
|
421
|
+
end,
|
|
422
|
+
parts,
|
|
423
|
+
...(materialized.fileRefs.length > 0
|
|
424
|
+
? { fileRefs: materialized.fileRefs }
|
|
425
|
+
: {}),
|
|
426
|
+
};
|
|
393
427
|
}
|
|
394
428
|
|
|
395
429
|
public async finish() {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { UIMessageChunk } from "ai";
|
|
2
|
+
import { MAX_FILE_SIZE, storeFile } from "./client/files.js";
|
|
3
|
+
import type { ActionCtx, AgentComponent, MutationCtx } from "./client/types.js";
|
|
4
|
+
|
|
5
|
+
export type MaterializedFileRef = { url: string; fileId: string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Replaces oversized inline files in persisted UI chunks with storage URLs.
|
|
9
|
+
* Recovery uses the accompanying URL-to-file ownership references to attach
|
|
10
|
+
* the stored files to the durable messages it creates.
|
|
11
|
+
*/
|
|
12
|
+
export async function materializeUIMessageChunkFiles(
|
|
13
|
+
ctx: ActionCtx,
|
|
14
|
+
component: AgentComponent,
|
|
15
|
+
parts: readonly UIMessageChunk[],
|
|
16
|
+
): Promise<{ parts: UIMessageChunk[]; fileRefs: MaterializedFileRef[] }> {
|
|
17
|
+
const fileRefs: MaterializedFileRef[] = [];
|
|
18
|
+
const materialized = await Promise.all(
|
|
19
|
+
parts.map(async (part): Promise<UIMessageChunk> => {
|
|
20
|
+
if (part.type === "tool-output-available") {
|
|
21
|
+
const result = await materializeCanonicalToolResultContentFiles(
|
|
22
|
+
ctx,
|
|
23
|
+
component,
|
|
24
|
+
part.output,
|
|
25
|
+
);
|
|
26
|
+
fileRefs.push(...result.fileRefs);
|
|
27
|
+
return { ...part, output: result.output };
|
|
28
|
+
}
|
|
29
|
+
if (part.type !== "file" && part.type !== "reasoning-file") {
|
|
30
|
+
return { ...part };
|
|
31
|
+
}
|
|
32
|
+
const file = await materializeInlineFile(
|
|
33
|
+
ctx,
|
|
34
|
+
component,
|
|
35
|
+
part.url,
|
|
36
|
+
part.mediaType,
|
|
37
|
+
);
|
|
38
|
+
if (!file) {
|
|
39
|
+
return { ...part };
|
|
40
|
+
}
|
|
41
|
+
fileRefs.push({ url: file.url, fileId: file.fileId });
|
|
42
|
+
return { ...part, url: file.url };
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
return { parts: materialized, fileRefs };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Materializes files in the AI SDK's canonical tool-result content output.
|
|
50
|
+
* Other tool outputs are application-defined and intentionally remain opaque.
|
|
51
|
+
*/
|
|
52
|
+
export async function materializeCanonicalToolResultContentFiles(
|
|
53
|
+
ctx: ActionCtx | MutationCtx,
|
|
54
|
+
component: AgentComponent,
|
|
55
|
+
output: unknown,
|
|
56
|
+
): Promise<{ output: unknown; fileRefs: MaterializedFileRef[] }> {
|
|
57
|
+
if (!isCanonicalToolResultContent(output)) {
|
|
58
|
+
return { output, fileRefs: [] };
|
|
59
|
+
}
|
|
60
|
+
const fileRefs: MaterializedFileRef[] = [];
|
|
61
|
+
const value = await Promise.all(
|
|
62
|
+
output.value.map(async (part): Promise<unknown> => {
|
|
63
|
+
if (!isCanonicalToolResultFile(part)) return part;
|
|
64
|
+
const file = await materializeInlineFile(
|
|
65
|
+
ctx,
|
|
66
|
+
component,
|
|
67
|
+
part.data.type === "url"
|
|
68
|
+
? part.data.url
|
|
69
|
+
: part.data.type === "data"
|
|
70
|
+
? part.data.data
|
|
71
|
+
: new TextEncoder().encode(part.data.text),
|
|
72
|
+
part.mediaType,
|
|
73
|
+
part.filename,
|
|
74
|
+
);
|
|
75
|
+
if (!file) return part;
|
|
76
|
+
fileRefs.push({ url: file.url, fileId: file.fileId });
|
|
77
|
+
return {
|
|
78
|
+
...part,
|
|
79
|
+
data: { type: "url", url: file.url },
|
|
80
|
+
};
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
return { output: { ...output, value }, fileRefs };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function materializeInlineFile(
|
|
87
|
+
ctx: ActionCtx | MutationCtx,
|
|
88
|
+
component: AgentComponent,
|
|
89
|
+
data: unknown,
|
|
90
|
+
mediaType: string,
|
|
91
|
+
filename?: string,
|
|
92
|
+
) {
|
|
93
|
+
const bytes = decodeInlineFileData(data);
|
|
94
|
+
if (!bytes) return undefined;
|
|
95
|
+
if (bytes.byteLength <= MAX_FILE_SIZE) return undefined;
|
|
96
|
+
const blobBytes = bytes.buffer.slice(
|
|
97
|
+
bytes.byteOffset,
|
|
98
|
+
bytes.byteOffset + bytes.byteLength,
|
|
99
|
+
) as ArrayBuffer;
|
|
100
|
+
const { file } = await storeFile(
|
|
101
|
+
ctx,
|
|
102
|
+
component,
|
|
103
|
+
new Blob([blobBytes], {
|
|
104
|
+
type: mediaType || "application/octet-stream",
|
|
105
|
+
}),
|
|
106
|
+
filename ? { filename } : {},
|
|
107
|
+
);
|
|
108
|
+
return file;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isCanonicalToolResultContent(
|
|
112
|
+
value: unknown,
|
|
113
|
+
): value is { type: "content"; value: unknown[] } {
|
|
114
|
+
return (
|
|
115
|
+
isRecord(value) && value.type === "content" && Array.isArray(value.value)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isCanonicalToolResultFile(value: unknown): value is {
|
|
120
|
+
type: "file";
|
|
121
|
+
data:
|
|
122
|
+
| { type: "url"; url: string }
|
|
123
|
+
| { type: "data"; data: unknown }
|
|
124
|
+
| { type: "text"; text: string };
|
|
125
|
+
mediaType: string;
|
|
126
|
+
filename?: string;
|
|
127
|
+
} {
|
|
128
|
+
return (
|
|
129
|
+
isRecord(value) &&
|
|
130
|
+
value.type === "file" &&
|
|
131
|
+
typeof value.mediaType === "string" &&
|
|
132
|
+
isRecord(value.data) &&
|
|
133
|
+
((value.data.type === "url" && typeof value.data.url === "string") ||
|
|
134
|
+
value.data.type === "data" ||
|
|
135
|
+
(value.data.type === "text" && typeof value.data.text === "string"))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
140
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function decodeInlineFileData(value: unknown): Uint8Array | undefined {
|
|
144
|
+
if (typeof value === "string") {
|
|
145
|
+
if (!value.startsWith("data:")) {
|
|
146
|
+
// Remote and already-stored files are references, not inline payloads.
|
|
147
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return undefined;
|
|
148
|
+
return decodeBase64(value);
|
|
149
|
+
}
|
|
150
|
+
const separator = value.indexOf(",");
|
|
151
|
+
if (separator === -1) return undefined;
|
|
152
|
+
const metadata = value.slice(5, separator);
|
|
153
|
+
const payload = value.slice(separator + 1);
|
|
154
|
+
try {
|
|
155
|
+
return metadata.includes(";base64")
|
|
156
|
+
? decodeBase64(payload)
|
|
157
|
+
: new TextEncoder().encode(decodeURIComponent(payload));
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (value instanceof Uint8Array) return value;
|
|
163
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function decodeBase64(value: string): Uint8Array | undefined {
|
|
168
|
+
try {
|
|
169
|
+
const binary = atob(value);
|
|
170
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
171
|
+
} catch {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -465,6 +465,123 @@ describe("mapping", () => {
|
|
|
465
465
|
);
|
|
466
466
|
});
|
|
467
467
|
|
|
468
|
+
test("materializes oversized canonical tool-result files", async () => {
|
|
469
|
+
const bytes = new Uint8Array(1024 * 65).fill(1);
|
|
470
|
+
const ctx = {
|
|
471
|
+
runAction: async () => undefined,
|
|
472
|
+
runMutation: async () => ({
|
|
473
|
+
fileId: "file-123",
|
|
474
|
+
storageId: "storage-123",
|
|
475
|
+
}),
|
|
476
|
+
storage: {
|
|
477
|
+
getUrl: async () => "https://example.com/file",
|
|
478
|
+
},
|
|
479
|
+
} as unknown as ActionCtx;
|
|
480
|
+
const result = {
|
|
481
|
+
type: "tool-result" as const,
|
|
482
|
+
toolCallId: "call-1",
|
|
483
|
+
toolName: "render",
|
|
484
|
+
output: {
|
|
485
|
+
type: "content" as const,
|
|
486
|
+
value: [
|
|
487
|
+
{
|
|
488
|
+
type: "file" as const,
|
|
489
|
+
data: { type: "data" as const, data: bytes },
|
|
490
|
+
mediaType: "application/octet-stream",
|
|
491
|
+
filename: "result.bin",
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
} satisfies ToolResultPart;
|
|
496
|
+
|
|
497
|
+
const { content, fileIds } = await serializeContent(
|
|
498
|
+
ctx,
|
|
499
|
+
api as unknown as AgentComponent,
|
|
500
|
+
[result],
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
expect(fileIds).toEqual(["file-123"]);
|
|
504
|
+
expect(content).toMatchObject([
|
|
505
|
+
{
|
|
506
|
+
type: "tool-result",
|
|
507
|
+
output: {
|
|
508
|
+
type: "content",
|
|
509
|
+
value: [
|
|
510
|
+
{
|
|
511
|
+
type: "file",
|
|
512
|
+
data: { type: "url", url: "https://example.com/file" },
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
const opaque = { attachment: "data:application/octet-stream,opaque" };
|
|
520
|
+
await expect(
|
|
521
|
+
serializeContent(ctx, api as unknown as AgentComponent, [
|
|
522
|
+
{
|
|
523
|
+
type: "tool-result",
|
|
524
|
+
toolCallId: "call-2",
|
|
525
|
+
toolName: "render",
|
|
526
|
+
result: opaque,
|
|
527
|
+
} satisfies Infer<typeof vToolResultPart>,
|
|
528
|
+
]),
|
|
529
|
+
).resolves.toMatchObject({
|
|
530
|
+
content: [{ output: { type: "json", value: opaque } }],
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("materializes oversized canonical tool-result text files", async () => {
|
|
535
|
+
const ctx = {
|
|
536
|
+
runAction: async () => undefined,
|
|
537
|
+
runMutation: async () => ({
|
|
538
|
+
fileId: "file-123",
|
|
539
|
+
storageId: "storage-123",
|
|
540
|
+
}),
|
|
541
|
+
storage: {
|
|
542
|
+
getUrl: async () => "https://example.com/file",
|
|
543
|
+
},
|
|
544
|
+
} as unknown as ActionCtx;
|
|
545
|
+
const result = {
|
|
546
|
+
type: "tool-result" as const,
|
|
547
|
+
toolCallId: "call-1",
|
|
548
|
+
toolName: "render",
|
|
549
|
+
output: {
|
|
550
|
+
type: "content" as const,
|
|
551
|
+
value: [
|
|
552
|
+
{
|
|
553
|
+
type: "file" as const,
|
|
554
|
+
data: { type: "text" as const, text: "x".repeat(1024 * 65) },
|
|
555
|
+
mediaType: "text/plain",
|
|
556
|
+
filename: "result.txt",
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
} satisfies ToolResultPart;
|
|
561
|
+
|
|
562
|
+
const { content, fileIds } = await serializeContent(
|
|
563
|
+
ctx,
|
|
564
|
+
api as unknown as AgentComponent,
|
|
565
|
+
[result],
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(fileIds).toEqual(["file-123"]);
|
|
569
|
+
expect(content).toMatchObject([
|
|
570
|
+
{
|
|
571
|
+
type: "tool-result",
|
|
572
|
+
output: {
|
|
573
|
+
type: "content",
|
|
574
|
+
value: [
|
|
575
|
+
{
|
|
576
|
+
type: "file",
|
|
577
|
+
data: { type: "url", url: "https://example.com/file" },
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
]);
|
|
583
|
+
});
|
|
584
|
+
|
|
468
585
|
test("sanity: fileIds are not returned for small files", async () => {
|
|
469
586
|
const arr = new Uint8Array([1, 2, 3, 4, 5]);
|
|
470
587
|
const ab = arr.buffer.slice(
|
package/src/vercel/mapping.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import type { ActionCtx, AgentComponent } from "./client/types.js";
|
|
45
45
|
import type { MutationCtx } from "./client/types.js";
|
|
46
46
|
import { MAX_FILE_SIZE, storeFile } from "./client/files.js";
|
|
47
|
+
import { materializeCanonicalToolResultContentFiles } from "./fileMaterialization.js";
|
|
47
48
|
import type { Infer } from "convex/values";
|
|
48
49
|
import {
|
|
49
50
|
convertUint8ArrayToBase64,
|
|
@@ -466,7 +467,20 @@ export async function serializeContent(
|
|
|
466
467
|
} satisfies Infer<typeof vToolCallPart>;
|
|
467
468
|
}
|
|
468
469
|
case "tool-result": {
|
|
469
|
-
|
|
470
|
+
const output =
|
|
471
|
+
"output" in part && part.output !== undefined
|
|
472
|
+
? part.output
|
|
473
|
+
: normalizeToolOutput("result" in part ? part.result : undefined);
|
|
474
|
+
const materialized = await materializeCanonicalToolResultContentFiles(
|
|
475
|
+
ctx,
|
|
476
|
+
component,
|
|
477
|
+
output,
|
|
478
|
+
);
|
|
479
|
+
fileIds.push(...materialized.fileRefs.map((ref) => ref.fileId));
|
|
480
|
+
return serializeToolResult(
|
|
481
|
+
{ ...part, output: materialized.output } as ToolResultPart,
|
|
482
|
+
metadata,
|
|
483
|
+
);
|
|
470
484
|
}
|
|
471
485
|
case "reasoning": {
|
|
472
486
|
return {
|
|
@@ -26,13 +26,19 @@ describe("toUIMessages", () => {
|
|
|
26
26
|
content: "Hello!",
|
|
27
27
|
},
|
|
28
28
|
text: "Hello!",
|
|
29
|
+
providerMetadata: { testProvider: { traceId: "trace-123" } },
|
|
29
30
|
}),
|
|
30
31
|
];
|
|
31
32
|
const uiMessages = toUIMessages(messages);
|
|
32
33
|
expect(uiMessages).toHaveLength(1);
|
|
33
34
|
expect(uiMessages[0].role).toBe("user");
|
|
34
35
|
expect(uiMessages[0].text).toBe("Hello!");
|
|
35
|
-
expect(uiMessages[0].parts[0]).toEqual({
|
|
36
|
+
expect(uiMessages[0].parts[0]).toEqual({
|
|
37
|
+
type: "text",
|
|
38
|
+
text: "Hello!",
|
|
39
|
+
state: "done",
|
|
40
|
+
providerMetadata: { testProvider: { traceId: "trace-123" } },
|
|
41
|
+
});
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
it("handles assistant message", () => {
|