@convex-dev/agent 0.1.6 → 0.1.7-alpha.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.
Files changed (106) hide show
  1. package/README.md +23 -12
  2. package/dist/commonjs/client/_generated/_ignore.d.ts +1 -0
  3. package/dist/commonjs/client/_generated/_ignore.d.ts.map +1 -0
  4. package/dist/commonjs/client/_generated/_ignore.js +3 -0
  5. package/dist/commonjs/client/_generated/_ignore.js.map +1 -0
  6. package/dist/commonjs/client/files.d.ts +57 -0
  7. package/dist/commonjs/client/files.d.ts.map +1 -0
  8. package/dist/commonjs/client/files.js +124 -0
  9. package/dist/commonjs/client/files.js.map +1 -0
  10. package/dist/commonjs/client/index.d.ts +487 -318
  11. package/dist/commonjs/client/index.d.ts.map +1 -1
  12. package/dist/commonjs/client/index.js +652 -550
  13. package/dist/commonjs/client/index.js.map +1 -1
  14. package/dist/commonjs/client/types.d.ts +35 -5
  15. package/dist/commonjs/client/types.d.ts.map +1 -1
  16. package/dist/commonjs/component/_generated/api.d.ts +47 -711
  17. package/dist/commonjs/component/files.d.ts +66 -21
  18. package/dist/commonjs/component/files.d.ts.map +1 -1
  19. package/dist/commonjs/component/files.js +71 -26
  20. package/dist/commonjs/component/files.js.map +1 -1
  21. package/dist/commonjs/component/messages.d.ts +31 -499
  22. package/dist/commonjs/component/messages.d.ts.map +1 -1
  23. package/dist/commonjs/component/messages.js +45 -125
  24. package/dist/commonjs/component/messages.js.map +1 -1
  25. package/dist/commonjs/component/schema.d.ts +949 -3785
  26. package/dist/commonjs/component/schema.d.ts.map +1 -1
  27. package/dist/commonjs/component/schema.js +9 -25
  28. package/dist/commonjs/component/schema.js.map +1 -1
  29. package/dist/commonjs/component/streams.d.ts +3 -3
  30. package/dist/commonjs/mapping.d.ts +28 -19
  31. package/dist/commonjs/mapping.d.ts.map +1 -1
  32. package/dist/commonjs/mapping.js +136 -81
  33. package/dist/commonjs/mapping.js.map +1 -1
  34. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  35. package/dist/commonjs/react/toUIMessages.js +12 -6
  36. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  37. package/dist/commonjs/react/usePaginatedQuery.d.ts.map +1 -1
  38. package/dist/commonjs/react/usePaginatedQuery.js +0 -19
  39. package/dist/commonjs/react/usePaginatedQuery.js.map +1 -1
  40. package/dist/commonjs/validators.d.ts +851 -3388
  41. package/dist/commonjs/validators.d.ts.map +1 -1
  42. package/dist/commonjs/validators.js +11 -37
  43. package/dist/commonjs/validators.js.map +1 -1
  44. package/dist/commonjs.tsbuildinfo +1 -1
  45. package/dist/esm/client/_generated/_ignore.d.ts +1 -0
  46. package/dist/esm/client/_generated/_ignore.d.ts.map +1 -0
  47. package/dist/esm/client/_generated/_ignore.js +3 -0
  48. package/dist/esm/client/_generated/_ignore.js.map +1 -0
  49. package/dist/esm/client/files.d.ts +57 -0
  50. package/dist/esm/client/files.d.ts.map +1 -0
  51. package/dist/esm/client/files.js +124 -0
  52. package/dist/esm/client/files.js.map +1 -0
  53. package/dist/esm/client/index.d.ts +487 -318
  54. package/dist/esm/client/index.d.ts.map +1 -1
  55. package/dist/esm/client/index.js +652 -550
  56. package/dist/esm/client/index.js.map +1 -1
  57. package/dist/esm/client/types.d.ts +35 -5
  58. package/dist/esm/client/types.d.ts.map +1 -1
  59. package/dist/esm/component/_generated/api.d.ts +47 -711
  60. package/dist/esm/component/files.d.ts +66 -21
  61. package/dist/esm/component/files.d.ts.map +1 -1
  62. package/dist/esm/component/files.js +71 -26
  63. package/dist/esm/component/files.js.map +1 -1
  64. package/dist/esm/component/messages.d.ts +31 -499
  65. package/dist/esm/component/messages.d.ts.map +1 -1
  66. package/dist/esm/component/messages.js +45 -125
  67. package/dist/esm/component/messages.js.map +1 -1
  68. package/dist/esm/component/schema.d.ts +949 -3785
  69. package/dist/esm/component/schema.d.ts.map +1 -1
  70. package/dist/esm/component/schema.js +9 -25
  71. package/dist/esm/component/schema.js.map +1 -1
  72. package/dist/esm/component/streams.d.ts +3 -3
  73. package/dist/esm/mapping.d.ts +28 -19
  74. package/dist/esm/mapping.d.ts.map +1 -1
  75. package/dist/esm/mapping.js +136 -81
  76. package/dist/esm/mapping.js.map +1 -1
  77. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  78. package/dist/esm/react/toUIMessages.js +12 -6
  79. package/dist/esm/react/toUIMessages.js.map +1 -1
  80. package/dist/esm/react/usePaginatedQuery.d.ts.map +1 -1
  81. package/dist/esm/react/usePaginatedQuery.js +0 -19
  82. package/dist/esm/react/usePaginatedQuery.js.map +1 -1
  83. package/dist/esm/validators.d.ts +851 -3388
  84. package/dist/esm/validators.d.ts.map +1 -1
  85. package/dist/esm/validators.js +11 -37
  86. package/dist/esm/validators.js.map +1 -1
  87. package/dist/esm.tsbuildinfo +1 -1
  88. package/package.json +11 -9
  89. package/src/client/_generated/_ignore.ts +1 -0
  90. package/src/client/files.ts +167 -0
  91. package/src/client/index.test.ts +127 -1
  92. package/src/client/index.ts +970 -812
  93. package/src/client/setup.test.ts +29 -0
  94. package/src/client/types.ts +41 -3
  95. package/src/component/_generated/api.d.ts +47 -711
  96. package/src/component/files.test.ts +130 -0
  97. package/src/component/files.ts +83 -27
  98. package/src/component/messages.test.ts +2 -1
  99. package/src/component/messages.ts +47 -137
  100. package/src/component/schema.ts +8 -28
  101. package/src/mapping.test.ts +163 -0
  102. package/src/mapping.ts +173 -103
  103. package/src/react/deltas.test.ts +4 -1
  104. package/src/react/toUIMessages.ts +12 -6
  105. package/src/react/usePaginatedQuery.ts +0 -31
  106. package/src/validators.ts +33 -47
@@ -0,0 +1,130 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import { convexTest } from "convex-test";
4
+ import { describe, expect, test } from "vitest";
5
+ import { api } from "./_generated/api.js";
6
+ import schema from "./schema.js";
7
+ import { modules } from "./setup.test.js";
8
+ import type { Doc } from "./_generated/dataModel.js";
9
+ import type { PaginationResult } from "convex/server";
10
+
11
+ describe("files", () => {
12
+ test("addFile increments refcount and does not create a new entry", async () => {
13
+ const t = convexTest(schema, modules);
14
+ const storageId = "storage-1";
15
+ const hash = "hash-1";
16
+ const filename = "file.txt";
17
+ // Add the file for the first time
18
+ const { fileId, storageId: returnedStorageId } = await t.mutation(
19
+ api.files.addFile,
20
+ {
21
+ storageId,
22
+ hash,
23
+ filename,
24
+ mimeType: "text/plain",
25
+ }
26
+ );
27
+ expect(fileId).toBeTruthy();
28
+ expect(returnedStorageId).toBe(storageId);
29
+ // Add the same file again
30
+ const { fileId: fileId2 } = await t.mutation(api.files.addFile, {
31
+ storageId,
32
+ hash,
33
+ filename,
34
+ mimeType: "text/plain",
35
+ });
36
+ expect(fileId2).toBe(fileId);
37
+ // Add the same file with a different filename (should create a new entry)
38
+ const { fileId: fileId3 } = await t.mutation(api.files.addFile, {
39
+ storageId,
40
+ hash,
41
+ filename: "other.txt",
42
+ mimeType: "text/plain",
43
+ });
44
+ expect(fileId3).not.toBe(fileId);
45
+ // Add the same file with undefined filename (should create a new entry)
46
+ const { fileId: fileId4 } = await t.mutation(api.files.addFile, {
47
+ storageId,
48
+ hash,
49
+ filename: undefined,
50
+ mimeType: "text/plain",
51
+ });
52
+ expect(fileId4).not.toBe(fileId);
53
+ });
54
+
55
+ test("useExistingFile only matches files with the same hash and filename", async () => {
56
+ const t = convexTest(schema, modules);
57
+ const storageId = "storage-2";
58
+ const hash = "hash-2";
59
+ const filename = "file2.txt";
60
+ // Add a file
61
+ const { fileId } = await t.mutation(api.files.addFile, {
62
+ storageId,
63
+ hash,
64
+ filename,
65
+ mimeType: "text/plain",
66
+ });
67
+ // Should match
68
+ const fileId2 = await t.mutation(api.files.useExistingFile, {
69
+ hash,
70
+ filename,
71
+ });
72
+ expect(fileId2?.fileId).toBe(fileId);
73
+ // Should not match with different filename
74
+ const fileId3 = await t.mutation(api.files.useExistingFile, {
75
+ hash,
76
+ filename: "other2.txt",
77
+ });
78
+ expect(fileId3).toBeNull();
79
+ // Should not match with undefined filename
80
+ const fileId4 = await t.mutation(api.files.useExistingFile, {
81
+ hash,
82
+ });
83
+ expect(fileId4).toBeNull();
84
+ });
85
+
86
+ test("getFilesToDelete paginates through files with refcount 0 one at a time", async () => {
87
+ const t = convexTest(schema, modules);
88
+ // Add 3 files with refcount 0
89
+ const files = [];
90
+ for (let i = 0; i < 3; i++) {
91
+ const { fileId } = await t.mutation(api.files.addFile, {
92
+ storageId: `storage-del-${i}`,
93
+ hash: `hash-del-${i}`,
94
+ filename: `file-del-${i}.txt`,
95
+ mimeType: "text/plain",
96
+ });
97
+ // Manually set refcount to 0
98
+ await t.run(async (ctx) => {
99
+ await ctx.db.patch(fileId, { refcount: 0 });
100
+ });
101
+ files.push(fileId);
102
+ }
103
+ // Paginate through files to delete one at a time
104
+ let cursor: string | null = null;
105
+ const seen: string[] = [];
106
+ for (let i = 0; i < 3; i++) {
107
+ const { page, continueCursor, isDone }: PaginationResult<Doc<"files">> =
108
+ await t.query(api.files.getFilesToDelete, {
109
+ paginationOpts: {
110
+ numItems: 1,
111
+ cursor,
112
+ },
113
+ });
114
+ expect(page.length).toBe(1);
115
+ seen.push(page[0]._id);
116
+ cursor = continueCursor;
117
+ expect(isDone).toBe(false);
118
+ }
119
+ const { page, isDone } = await t.query(api.files.getFilesToDelete, {
120
+ paginationOpts: {
121
+ numItems: 1,
122
+ cursor,
123
+ },
124
+ });
125
+ expect(page.length).toBe(0);
126
+ expect(isDone).toBe(true);
127
+ // All fileIds should be seen
128
+ expect(seen.sort()).toEqual(files.sort());
129
+ });
130
+ });
@@ -2,71 +2,99 @@ import { paginator } from "convex-helpers/server/pagination";
2
2
  import type { Id } from "./_generated/dataModel.js";
3
3
  import { mutation, type MutationCtx, query } from "./_generated/server.js";
4
4
  import { schema, v } from "./schema.js";
5
+ import { paginationOptsValidator } from "convex/server";
6
+ import type { Infer } from "convex/values";
7
+
8
+ const addFileArgs = v.object({
9
+ storageId: v.string(),
10
+ hash: v.string(),
11
+ filename: v.optional(v.string()),
12
+ mimeType: v.string(),
13
+ });
5
14
 
6
15
  export const addFile = mutation({
7
- args: {
8
- storageId: v.string(),
9
- hash: v.string(),
10
- },
16
+ args: addFileArgs,
11
17
  handler: addFileHandler,
12
18
  returns: {
13
19
  fileId: v.id("files"),
14
- storageIdUnused: v.boolean(),
20
+ storageId: v.string(),
15
21
  },
16
22
  });
17
23
 
18
24
  export async function addFileHandler(
19
25
  ctx: MutationCtx,
20
- args: { storageId: string; hash: string }
26
+ args: Infer<typeof addFileArgs>
21
27
  ) {
22
28
  const existingFile = await ctx.db
23
29
  .query("files")
24
30
  .withIndex("hash", (q) => q.eq("hash", args.hash))
31
+ .filter((q) => q.eq(q.field("filename"), args.filename))
25
32
  .first();
26
33
  if (existingFile) {
27
34
  // increment the refcount
28
35
  await ctx.db.patch(existingFile._id, {
29
36
  refcount: existingFile.refcount + 1,
37
+ lastTouchedAt: Date.now(),
30
38
  });
31
39
  return {
32
40
  fileId: existingFile._id,
33
- storageIdUnused: existingFile.storageId !== args.storageId,
41
+ storageId: existingFile.storageId,
34
42
  };
35
43
  }
36
44
  const fileId = await ctx.db.insert("files", {
37
- storageId: args.storageId,
38
- hash: args.hash,
39
- refcount: 1,
45
+ ...args,
46
+ // We start out with it unused - when it's saved in a message we increment.
47
+ refcount: 0,
48
+ lastTouchedAt: Date.now(),
40
49
  });
41
50
  return {
42
51
  fileId,
43
- storageIdUnused: false,
52
+ storageId: args.storageId,
44
53
  };
45
54
  }
46
55
 
56
+ export const get = query({
57
+ args: {
58
+ fileId: v.id("files"),
59
+ },
60
+ returns: v.union(v.null(), v.doc("files")),
61
+ handler: async (ctx, args) => {
62
+ return ctx.db.get(args.fileId);
63
+ },
64
+ });
65
+
47
66
  /**
48
67
  * If you plan to have the same file added over and over without a reference to
49
68
  * the fileId, you can use this query to get the fileId of the existing file.
50
- * And if it's
69
+ * Note: this will not increment the refcount. only saving messages does that.
70
+ * It will only match if the filename is the same (or both are undefined).
51
71
  */
52
72
  export const useExistingFile = mutation({
53
73
  args: {
54
74
  hash: v.string(),
75
+ filename: v.optional(v.string()),
55
76
  },
56
77
  handler: async (ctx, args) => {
57
78
  const file = await ctx.db
58
79
  .query("files")
59
80
  .withIndex("hash", (q) => q.eq("hash", args.hash))
81
+ .filter((q) => q.eq(q.field("filename"), args.filename))
60
82
  .first();
61
83
  if (!file) {
62
84
  return null;
63
85
  }
64
86
  await ctx.db.patch(file._id, {
65
- refcount: file.refcount + 1,
87
+ lastTouchedAt: Date.now(),
66
88
  });
67
- return file._id;
89
+ return { fileId: file._id, storageId: file.storageId };
68
90
  },
69
- returns: v.union(v.id("files"), v.null()),
91
+ returns: v.union(
92
+ v.null(),
93
+ v.object({
94
+ fileId: v.id("files"),
95
+ storageId: v.string(),
96
+ })
97
+ ),
70
98
  });
71
99
 
72
100
  export const copyFile = mutation({
@@ -87,31 +115,59 @@ export async function copyFileHandler(
87
115
  }
88
116
  await ctx.db.patch(args.fileId, {
89
117
  refcount: file.refcount + 1,
118
+ lastTouchedAt: Date.now(),
90
119
  });
91
120
  }
92
121
 
122
+ /**
123
+ * Get files that are unused and can be deleted.
124
+ * This is useful for cleaning up files that are no longer needed.
125
+ * Note: recently added files that have not been saved yet will show up here.
126
+ * You can inspect the `lastTouchedAt` field to see how recently it was used.
127
+ * I'd recommend not deleting anything touched in the last 24 hours.
128
+ */
93
129
  export const getFilesToDelete = query({
94
130
  args: {
95
- cursor: v.optional(v.string()),
96
- limit: v.optional(v.number()),
131
+ paginationOpts: paginationOptsValidator,
97
132
  },
98
133
  handler: async (ctx, args) => {
99
134
  const files = await paginator(ctx.db, schema)
100
135
  .query("files")
101
136
  .withIndex("refcount", (q) => q.eq("refcount", 0))
102
- .paginate({
103
- numItems: args.limit ?? 100,
104
- cursor: args.cursor ?? null,
105
- });
106
- return {
107
- files: files.page,
108
- continueCursor: files.continueCursor,
109
- isDone: files.isDone,
110
- };
137
+ .paginate(args.paginationOpts);
138
+ return files;
111
139
  },
112
140
  returns: v.object({
113
- files: v.array(v.doc("files")),
141
+ page: v.array(v.doc("files")),
114
142
  continueCursor: v.string(),
115
143
  isDone: v.boolean(),
116
144
  }),
117
145
  });
146
+
147
+ export const deleteFiles = mutation({
148
+ args: {
149
+ fileIds: v.array(v.id("files")),
150
+ force: v.optional(v.boolean()),
151
+ },
152
+ handler: async (ctx, args) => {
153
+ await Promise.all(
154
+ args.fileIds.map(async (fileId) => {
155
+ const file = await ctx.db.get(fileId);
156
+ if (!file) {
157
+ console.error(`File ${fileId} not found when deleting, skipping...`);
158
+ return;
159
+ }
160
+ if (file.refcount && file.refcount > 0) {
161
+ if (!args.force) {
162
+ console.error(
163
+ `File ${fileId} has refcount ${file.refcount} > 0, skipping...`
164
+ );
165
+ return;
166
+ }
167
+ }
168
+ await ctx.db.delete(fileId);
169
+ })
170
+ );
171
+ },
172
+ returns: v.null(),
173
+ });
@@ -129,7 +129,8 @@ describe("agent", () => {
129
129
  const { messages: messages2 } = await t.mutation(api.messages.addMessages, {
130
130
  threadId: thread._id as Id<"threads">,
131
131
  messages: [{ message: { role: "user", content: "hello" } }],
132
- promptMessageId: messages.at(-1)!._id,
132
+ agentName: "test",
133
+ promptMessageId: messages.at(-1)!._id as Id<"messages">,
133
134
  });
134
135
  const maxMessage2 = await t.run(async (ctx) => {
135
136
  return await getMaxMessage(ctx, thread._id as Id<"threads">);
@@ -11,10 +11,9 @@ import {
11
11
  import {
12
12
  vMessageEmbeddings,
13
13
  vMessageStatus,
14
- vMessageWithMetadata,
14
+ vMessageWithMetadataInternal,
15
15
  vPaginationResult,
16
16
  vSearchOptions,
17
- vStepWithMessages,
18
17
  } from "../validators.js";
19
18
  import { api, internal } from "./_generated/api.js";
20
19
  import type { Doc, Id } from "./_generated/dataModel.js";
@@ -51,7 +50,7 @@ export const getThread = _getThread;
51
50
  export const updateThread = _updateThread;
52
51
 
53
52
  function publicMessage(message: Doc<"messages">): MessageDoc {
54
- return omit(message, ["parentMessageId", "stepId"]);
53
+ return omit(message, ["parentMessageId", "stepId", "files"]);
55
54
  }
56
55
 
57
56
  export async function deleteMessage(
@@ -62,7 +61,7 @@ export async function deleteMessage(
62
61
  if (messageDoc.embeddingId) {
63
62
  await ctx.db.delete(messageDoc.embeddingId);
64
63
  }
65
- for (const { fileId } of messageDoc.files ?? []) {
64
+ for (const fileId of messageDoc.fileIds ?? []) {
66
65
  if (!fileId) continue;
67
66
  const file = await ctx.db.get(fileId);
68
67
  if (file) {
@@ -78,10 +77,9 @@ export const messageStatuses = vMessageDoc.fields.status.members.map(
78
77
  const addMessagesArgs = {
79
78
  userId: v.optional(v.string()),
80
79
  threadId: v.id("threads"),
81
- stepId: v.optional(v.id("steps")),
82
80
  promptMessageId: v.optional(v.id("messages")),
83
81
  agentName: v.optional(v.string()),
84
- messages: v.array(vMessageWithMetadata),
82
+ messages: v.array(vMessageWithMetadataInternal),
85
83
  embeddings: v.optional(vMessageEmbeddings),
86
84
  pending: v.optional(v.boolean()),
87
85
  failPendingSteps: v.optional(v.boolean()),
@@ -91,7 +89,6 @@ export const addMessages = mutation({
91
89
  handler: addMessagesHandler,
92
90
  returns: v.object({
93
91
  messages: v.array(vMessageDoc),
94
- pending: v.optional(vMessageDoc),
95
92
  }),
96
93
  });
97
94
  async function addMessagesHandler(
@@ -147,55 +144,53 @@ async function addMessagesHandler(
147
144
  stepOrder = -1;
148
145
  }
149
146
  const toReturn: Doc<"messages">[] = [];
150
- if (messages.length > 0) {
151
- if (embeddings) {
152
- assert(
153
- embeddings.vectors.length === messages.length,
154
- "embeddings.vectors.length must match messages.length"
155
- );
156
- }
157
- for (let i = 0; i < messages.length; i++) {
158
- const message = messages[i];
159
- let embeddingId: VectorTableId | undefined;
160
- if (embeddings && embeddings.vectors[i]) {
161
- embeddingId = await insertVector(ctx, embeddings.dimension, {
162
- vector: embeddings.vectors[i]!,
163
- model: embeddings.model,
164
- table: "messages",
165
- userId,
166
- threadId,
167
- });
168
- }
169
- stepOrder++;
170
- const messageId = await ctx.db.insert("messages", {
171
- ...rest,
172
- ...message,
173
- embeddingId,
174
- parentMessageId: promptMessageId,
147
+ if (embeddings) {
148
+ assert(
149
+ embeddings.vectors.length === messages.length,
150
+ "embeddings.vectors.length must match messages.length"
151
+ );
152
+ }
153
+ for (let i = 0; i < messages.length; i++) {
154
+ const message = messages[i];
155
+ let embeddingId: VectorTableId | undefined;
156
+ if (embeddings && embeddings.vectors[i]) {
157
+ embeddingId = await insertVector(ctx, embeddings.dimension, {
158
+ vector: embeddings.vectors[i]!,
159
+ model: embeddings.model,
160
+ table: "messages",
175
161
  userId,
176
- order,
177
- tool: isTool(message.message),
178
- text: extractText(message.message),
179
- status: fail ? "failed" : pending ? "pending" : "success",
180
- error: fail ? "Parent message failed" : undefined,
181
- stepOrder,
162
+ threadId,
163
+ });
164
+ }
165
+ stepOrder++;
166
+ const messageId = await ctx.db.insert("messages", {
167
+ ...rest,
168
+ ...message,
169
+ embeddingId,
170
+ parentMessageId: promptMessageId,
171
+ userId,
172
+ order,
173
+ tool: isTool(message.message),
174
+ text: extractText(message.message),
175
+ status: fail ? "failed" : pending ? "pending" : "success",
176
+ error: fail ? "Parent message failed" : undefined,
177
+ stepOrder,
178
+ });
179
+ // Let's just not set the id field and have it set only in explicit cases.
180
+ // if (!message.id) {
181
+ // await ctx.db.patch(messageId, {
182
+ // id: messageId,
183
+ // });
184
+ // }
185
+ for (const fileId of message.fileIds ?? []) {
186
+ if (!fileId) continue;
187
+ await ctx.db.patch(fileId, {
188
+ refcount: (await ctx.db.get(fileId))!.refcount + 1,
182
189
  });
183
- // Let's just not set the id field and have it set only in explicit cases.
184
- // if (!message.id) {
185
- // await ctx.db.patch(messageId, {
186
- // id: messageId,
187
- // });
188
- // }
189
- for (const { fileId } of message.files ?? []) {
190
- if (!fileId) continue;
191
- await ctx.db.patch(fileId, {
192
- refcount: (await ctx.db.get(fileId))!.refcount + 1,
193
- });
194
- }
195
- toReturn.push((await ctx.db.get(messageId))!);
196
190
  }
191
+ toReturn.push((await ctx.db.get(messageId))!);
197
192
  }
198
- return { messages: toReturn };
193
+ return { messages: toReturn.map(publicMessage) };
199
194
  }
200
195
 
201
196
  // exported for tests
@@ -235,68 +230,6 @@ function orderedMessagesStream(
235
230
  );
236
231
  }
237
232
 
238
- const addStepArgs = {
239
- userId: v.optional(v.string()),
240
- threadId: v.id("threads"),
241
- promptMessageId: v.id("messages"),
242
- step: vStepWithMessages,
243
- failPendingSteps: v.optional(v.boolean()),
244
- };
245
-
246
- export const addStep = mutation({
247
- args: addStepArgs,
248
- returns: v.array(vMessageDoc),
249
- handler: addStepHandler,
250
- });
251
- async function addStepHandler(
252
- ctx: MutationCtx,
253
- args: ObjectType<typeof addStepArgs>
254
- ) {
255
- const parentMessage = await ctx.db.get(args.promptMessageId);
256
- assert(parentMessage, `Message ${args.promptMessageId} not found`);
257
- const order = parentMessage.order;
258
- // TODO: only fetch the last one if we aren't failing pending steps
259
- let steps = await ctx.db
260
- .query("steps")
261
- .withIndex("parentMessageId_order_stepOrder", (q) =>
262
- // TODO: fetch pending, and commit later
263
- q.eq("parentMessageId", args.promptMessageId)
264
- )
265
- .collect();
266
- if (args.failPendingSteps) {
267
- for (const step of steps) {
268
- if (step.status === "pending") {
269
- await ctx.db.patch(step._id, { status: "failed" });
270
- }
271
- }
272
- steps = steps.filter((s) => s.status !== "failed");
273
- }
274
- const { step, messages } = args.step;
275
- const stepId = await ctx.db.insert("steps", {
276
- threadId: args.threadId,
277
- parentMessageId: args.promptMessageId,
278
- order,
279
- stepOrder: (steps.at(-1)?.stepOrder ?? -1) + 1,
280
- status: step.finishReason === "stop" ? "success" : "pending",
281
- step,
282
- });
283
- const added = await addMessagesHandler(ctx, {
284
- userId: args.userId,
285
- threadId: args.threadId,
286
- stepId,
287
- promptMessageId: args.promptMessageId,
288
- agentName: parentMessage.agentName,
289
- messages,
290
- pending: step.finishReason === "stop" ? false : true,
291
- failPendingSteps: false,
292
- });
293
- // We don't commit if the parent is still pending.
294
- if (step.finishReason === "stop") {
295
- await commitMessageHandler(ctx, { messageId: args.promptMessageId });
296
- }
297
- return added.messages.map(publicMessage);
298
- }
299
-
300
233
  export const rollbackMessage = mutation({
301
234
  args: {
302
235
  messageId: v.id("messages"),
@@ -318,18 +251,6 @@ export const rollbackMessage = mutation({
318
251
  }
319
252
  }
320
253
 
321
- const steps = await ctx.db
322
- .query("steps")
323
- .withIndex("parentMessageId_order_stepOrder", (q) =>
324
- // TODO: fetch pending, and commit later
325
- q.eq("parentMessageId", messageId)
326
- )
327
- .collect();
328
- for (const step of steps) {
329
- if (step.status === "pending") {
330
- await ctx.db.patch(step._id, { status: "failed" });
331
- }
332
- }
333
254
  await ctx.db.patch(messageId, {
334
255
  status: "failed",
335
256
  error: error,
@@ -351,17 +272,6 @@ async function commitMessageHandler(
351
272
  const message = await ctx.db.get(messageId);
352
273
  assert(message, `Message ${messageId} not found`);
353
274
 
354
- const allSteps = await ctx.db
355
- .query("steps")
356
- .withIndex("parentMessageId_order_stepOrder", (q) =>
357
- q.eq("parentMessageId", messageId)
358
- )
359
- .collect();
360
- for (const step of allSteps) {
361
- if (step.status === "pending") {
362
- await ctx.db.patch(step._id, { status: "success" });
363
- }
364
- }
365
275
  const order = message.order!;
366
276
  const messages = await mergedStream(
367
277
  [true, false].map((tool) =>
@@ -4,7 +4,6 @@ import {
4
4
  vThreadStatus,
5
5
  vMessage,
6
6
  vMessageStatus,
7
- vStep,
8
7
  vUsage,
9
8
  vSource,
10
9
  vLanguageModelV1CallWarning,
@@ -12,8 +11,6 @@ import {
12
11
  vProviderOptions,
13
12
  vProviderMetadata,
14
13
  vReasoningDetails,
15
- vFile,
16
- vFileWithStringId,
17
14
  vTextStreamPart,
18
15
  } from "../validators.js";
19
16
  import { typedV } from "convex-helpers/validators";
@@ -35,10 +32,10 @@ export const schema = defineSchema({
35
32
  id: v.optional(v.string()), // external id, e.g. from Vercel AI SDK
36
33
  userId: v.optional(v.string()), // useful for searching across threads
37
34
  threadId: v.id("threads"),
38
- // Repeats until a non-tool message.
39
35
  order: v.number(),
40
36
  stepOrder: v.number(),
41
37
  embeddingId: v.optional(vVectorId),
38
+ fileIds: v.optional(v.array(v.id("files"))),
42
39
  error: v.optional(v.string()),
43
40
  status: vMessageStatus,
44
41
 
@@ -53,7 +50,6 @@ export const schema = defineSchema({
53
50
  // Convenience fields extracted from the message
54
51
  tool: v.boolean(), // either tool call (assistant) or tool result (tool)
55
52
  text: v.optional(v.string()),
56
- files: v.optional(v.array(vFile)),
57
53
 
58
54
  // Result metadata
59
55
  usage: v.optional(vUsage),
@@ -65,7 +61,8 @@ export const schema = defineSchema({
65
61
  finishReason: v.optional(vFinishReason),
66
62
  // DEPRECATED
67
63
  parentMessageId: v.optional(v.id("messages")),
68
- stepId: v.optional(v.id("steps")),
64
+ stepId: v.optional(v.string()),
65
+ files: v.optional(v.array(v.any())),
69
66
  })
70
67
  // Allows finding successful visible messages in order
71
68
  // Also surface pending messages separately to e.g. stream
@@ -85,27 +82,6 @@ export const schema = defineSchema({
85
82
  // Allows finding messages by vector embedding id
86
83
  .index("embeddingId", ["embeddingId"]),
87
84
 
88
- steps: defineTable({
89
- threadId: v.id("threads"),
90
- // Could be different from the order if we fail.
91
- parentMessageId: v.id("messages"),
92
- order: v.number(), // parent message order
93
- stepOrder: v.number(), // step order
94
- step: vStep,
95
- status: vMessageStatus,
96
- })
97
- .index("status_threadId_order_stepOrder", [
98
- "status",
99
- "threadId",
100
- "order",
101
- "stepOrder",
102
- ])
103
- .index("parentMessageId_order_stepOrder", [
104
- "parentMessageId",
105
- "order",
106
- "stepOrder",
107
- ]),
108
-
109
85
  // Status: if it's done, it's deleted, then deltas are vacuumed
110
86
  streamingMessages: defineTable({
111
87
  // extra metadata?
@@ -168,8 +144,11 @@ export const schema = defineSchema({
168
144
 
169
145
  files: defineTable({
170
146
  storageId: v.string(),
147
+ mimeType: v.string(),
148
+ filename: v.optional(v.string()),
171
149
  hash: v.string(),
172
150
  refcount: v.number(),
151
+ lastTouchedAt: v.number(),
173
152
  })
174
153
  .index("hash", ["hash"])
175
154
  .index("refcount", ["refcount"]),
@@ -202,12 +181,13 @@ export const vMessageDoc = v.object({
202
181
  ...omit(schema.tables.messages.validator.fields, [
203
182
  "parentMessageId",
204
183
  "stepId",
184
+ "files",
205
185
  ]),
206
186
  // Overwrite all the types that have a v.id validator
207
187
  // Outside of the component, they are strings
208
188
  threadId: v.string(),
209
189
  embeddingId: v.optional(v.string()),
210
- files: v.optional(v.array(vFileWithStringId)),
190
+ fileIds: v.optional(v.array(v.string())),
211
191
  });
212
192
  export type MessageDoc = Infer<typeof vMessageDoc>;
213
193