@convex-dev/agent 0.7.0 → 0.7.2

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 (116) hide show
  1. package/README.md +5 -8
  2. package/dist/client/messages.d.ts +10 -0
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +1 -0
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +15 -8
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/files.d.ts +6 -4
  9. package/dist/component/files.d.ts.map +1 -1
  10. package/dist/component/files.js +53 -42
  11. package/dist/component/files.js.map +1 -1
  12. package/dist/component/messages.d.ts +340 -338
  13. package/dist/component/messages.d.ts.map +1 -1
  14. package/dist/component/messages.js +80 -16
  15. package/dist/component/messages.js.map +1 -1
  16. package/dist/component/schema.d.ts +782 -626
  17. package/dist/component/schema.d.ts.map +1 -1
  18. package/dist/component/schema.js +12 -2
  19. package/dist/component/schema.js.map +1 -1
  20. package/dist/component/streams.d.ts +16 -2
  21. package/dist/component/streams.d.ts.map +1 -1
  22. package/dist/component/streams.js +86 -30
  23. package/dist/component/streams.js.map +1 -1
  24. package/dist/component/vector/index.d.ts +9 -9
  25. package/dist/component/vector/index.d.ts.map +1 -1
  26. package/dist/component/vector/tables.d.ts +5 -5
  27. package/dist/component/vector/tables.d.ts.map +1 -1
  28. package/dist/component/vector/tables.js +1 -1
  29. package/dist/component/vector/tables.js.map +1 -1
  30. package/dist/errors.d.ts +2 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +106 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/streaming/materializePersistedUIMessageChunks.d.ts +8 -2
  35. package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -1
  36. package/dist/streaming/materializePersistedUIMessageChunks.js +34 -3
  37. package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -1
  38. package/dist/validators.d.ts +1952 -1952
  39. package/dist/vercel/UIMessages.js +1 -1
  40. package/dist/vercel/UIMessages.js.map +1 -1
  41. package/dist/vercel/client/definePlaygroundAPI.d.ts +132 -132
  42. package/dist/vercel/client/files.d.ts.map +1 -1
  43. package/dist/vercel/client/files.js +51 -27
  44. package/dist/vercel/client/files.js.map +1 -1
  45. package/dist/vercel/client/messages.d.ts +50 -32
  46. package/dist/vercel/client/messages.d.ts.map +1 -1
  47. package/dist/vercel/client/messages.js +2 -0
  48. package/dist/vercel/client/messages.js.map +1 -1
  49. package/dist/vercel/client/search.d.ts +475 -32
  50. package/dist/vercel/client/search.d.ts.map +1 -1
  51. package/dist/vercel/client/search.js +34 -9
  52. package/dist/vercel/client/search.js.map +1 -1
  53. package/dist/vercel/client/start.d.ts.map +1 -1
  54. package/dist/vercel/client/start.js +1 -0
  55. package/dist/vercel/client/start.js.map +1 -1
  56. package/dist/vercel/client/streamText.d.ts +1 -2
  57. package/dist/vercel/client/streamText.d.ts.map +1 -1
  58. package/dist/vercel/client/streamText.js +49 -17
  59. package/dist/vercel/client/streamText.js.map +1 -1
  60. package/dist/vercel/client/streaming.d.ts +318 -301
  61. package/dist/vercel/client/streaming.d.ts.map +1 -1
  62. package/dist/vercel/client/streaming.js +56 -21
  63. package/dist/vercel/client/streaming.js.map +1 -1
  64. package/dist/vercel/client/types.d.ts +2 -0
  65. package/dist/vercel/client/types.d.ts.map +1 -1
  66. package/dist/vercel/client/types.js.map +1 -1
  67. package/dist/vercel/client/utils.d.ts +9 -5
  68. package/dist/vercel/client/utils.d.ts.map +1 -1
  69. package/dist/vercel/client/utils.js +13 -13
  70. package/dist/vercel/client/utils.js.map +1 -1
  71. package/dist/vercel/fileMaterialization.d.ts +24 -0
  72. package/dist/vercel/fileMaterialization.d.ts.map +1 -0
  73. package/dist/vercel/fileMaterialization.js +118 -0
  74. package/dist/vercel/fileMaterialization.js.map +1 -0
  75. package/dist/vercel/index.d.ts +91 -90
  76. package/dist/vercel/index.d.ts.map +1 -1
  77. package/dist/vercel/index.js +2 -0
  78. package/dist/vercel/index.js.map +1 -1
  79. package/dist/vercel/mapping.d.ts.map +1 -1
  80. package/dist/vercel/mapping.js +7 -1
  81. package/dist/vercel/mapping.js.map +1 -1
  82. package/package.json +16 -9
  83. package/src/client/messages.ts +12 -0
  84. package/src/component/_generated/component.ts +18 -2
  85. package/src/component/files.test.ts +81 -6
  86. package/src/component/files.ts +69 -41
  87. package/src/component/messages.test.ts +496 -0
  88. package/src/component/messages.ts +105 -15
  89. package/src/component/schema.ts +16 -2
  90. package/src/component/streams.test.ts +109 -0
  91. package/src/component/streams.ts +123 -35
  92. package/src/component/vector/tables.ts +1 -1
  93. package/src/errors.test.ts +76 -0
  94. package/src/errors.ts +113 -0
  95. package/src/streaming/materializePersistedUIMessageChunks.test.ts +33 -0
  96. package/src/streaming/materializePersistedUIMessageChunks.ts +37 -0
  97. package/src/vercel/UIMessages.ts +1 -1
  98. package/src/vercel/client/files.test.ts +56 -0
  99. package/src/vercel/client/files.ts +53 -28
  100. package/src/vercel/client/index.test.ts +47 -0
  101. package/src/vercel/client/messages.ts +20 -0
  102. package/src/vercel/client/search.test.ts +183 -8
  103. package/src/vercel/client/search.ts +46 -12
  104. package/src/vercel/client/start.ts +1 -0
  105. package/src/vercel/client/streamText.test.ts +113 -17
  106. package/src/vercel/client/streamText.ts +55 -28
  107. package/src/vercel/client/streaming.test.ts +142 -7
  108. package/src/vercel/client/streaming.ts +73 -19
  109. package/src/vercel/client/types.ts +2 -0
  110. package/src/vercel/client/utils.test.ts +97 -0
  111. package/src/vercel/client/utils.ts +15 -15
  112. package/src/vercel/fileMaterialization.ts +174 -0
  113. package/src/vercel/index.ts +2 -0
  114. package/src/vercel/mapping.test.ts +117 -0
  115. package/src/vercel/mapping.ts +15 -1
  116. package/src/vercel/toUIMessages.test.ts +7 -1
@@ -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
+ }
@@ -818,6 +818,7 @@ export class Agent<
818
818
  metadata: args.metadata ? [args.metadata] : undefined,
819
819
  skipEmbeddings: args.skipEmbeddings,
820
820
  promptMessageId: args.promptMessageId,
821
+ order: args.order,
821
822
  pendingMessageId: args.pendingMessageId,
822
823
  });
823
824
  const message = messages.at(-1)!;
@@ -1765,6 +1766,7 @@ export class Agent<
1765
1766
  threadId: v.string(),
1766
1767
  userId: v.optional(v.string()),
1767
1768
  promptMessageId: v.optional(v.string()),
1769
+ order: v.optional(v.union(v.number(), v.literal("next"))),
1768
1770
  messages: v.array(vMessageWithMetadata),
1769
1771
  failPendingSteps: v.optional(v.boolean()),
1770
1772
  embeddings: v.optional(vMessageEmbeddings),
@@ -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(
@@ -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
- return serializeToolResult(part, metadata);
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({ type: "text", text: "Hello!" });
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", () => {