@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.
- package/README.md +23 -12
- package/dist/commonjs/client/_generated/_ignore.d.ts +1 -0
- package/dist/commonjs/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/commonjs/client/_generated/_ignore.js +3 -0
- package/dist/commonjs/client/_generated/_ignore.js.map +1 -0
- package/dist/commonjs/client/files.d.ts +57 -0
- package/dist/commonjs/client/files.d.ts.map +1 -0
- package/dist/commonjs/client/files.js +124 -0
- package/dist/commonjs/client/files.js.map +1 -0
- package/dist/commonjs/client/index.d.ts +487 -318
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +652 -550
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/client/types.d.ts +35 -5
- package/dist/commonjs/client/types.d.ts.map +1 -1
- package/dist/commonjs/component/_generated/api.d.ts +47 -711
- package/dist/commonjs/component/files.d.ts +66 -21
- package/dist/commonjs/component/files.d.ts.map +1 -1
- package/dist/commonjs/component/files.js +71 -26
- package/dist/commonjs/component/files.js.map +1 -1
- package/dist/commonjs/component/messages.d.ts +31 -499
- package/dist/commonjs/component/messages.d.ts.map +1 -1
- package/dist/commonjs/component/messages.js +45 -125
- package/dist/commonjs/component/messages.js.map +1 -1
- package/dist/commonjs/component/schema.d.ts +949 -3785
- package/dist/commonjs/component/schema.d.ts.map +1 -1
- package/dist/commonjs/component/schema.js +9 -25
- package/dist/commonjs/component/schema.js.map +1 -1
- package/dist/commonjs/component/streams.d.ts +3 -3
- package/dist/commonjs/mapping.d.ts +28 -19
- package/dist/commonjs/mapping.d.ts.map +1 -1
- package/dist/commonjs/mapping.js +136 -81
- package/dist/commonjs/mapping.js.map +1 -1
- package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
- package/dist/commonjs/react/toUIMessages.js +12 -6
- package/dist/commonjs/react/toUIMessages.js.map +1 -1
- package/dist/commonjs/react/usePaginatedQuery.d.ts.map +1 -1
- package/dist/commonjs/react/usePaginatedQuery.js +0 -19
- package/dist/commonjs/react/usePaginatedQuery.js.map +1 -1
- package/dist/commonjs/validators.d.ts +851 -3388
- package/dist/commonjs/validators.d.ts.map +1 -1
- package/dist/commonjs/validators.js +11 -37
- package/dist/commonjs/validators.js.map +1 -1
- package/dist/commonjs.tsbuildinfo +1 -1
- package/dist/esm/client/_generated/_ignore.d.ts +1 -0
- package/dist/esm/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/esm/client/_generated/_ignore.js +3 -0
- package/dist/esm/client/_generated/_ignore.js.map +1 -0
- package/dist/esm/client/files.d.ts +57 -0
- package/dist/esm/client/files.d.ts.map +1 -0
- package/dist/esm/client/files.js +124 -0
- package/dist/esm/client/files.js.map +1 -0
- package/dist/esm/client/index.d.ts +487 -318
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +652 -550
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/types.d.ts +35 -5
- package/dist/esm/client/types.d.ts.map +1 -1
- package/dist/esm/component/_generated/api.d.ts +47 -711
- package/dist/esm/component/files.d.ts +66 -21
- package/dist/esm/component/files.d.ts.map +1 -1
- package/dist/esm/component/files.js +71 -26
- package/dist/esm/component/files.js.map +1 -1
- package/dist/esm/component/messages.d.ts +31 -499
- package/dist/esm/component/messages.d.ts.map +1 -1
- package/dist/esm/component/messages.js +45 -125
- package/dist/esm/component/messages.js.map +1 -1
- package/dist/esm/component/schema.d.ts +949 -3785
- package/dist/esm/component/schema.d.ts.map +1 -1
- package/dist/esm/component/schema.js +9 -25
- package/dist/esm/component/schema.js.map +1 -1
- package/dist/esm/component/streams.d.ts +3 -3
- package/dist/esm/mapping.d.ts +28 -19
- package/dist/esm/mapping.d.ts.map +1 -1
- package/dist/esm/mapping.js +136 -81
- package/dist/esm/mapping.js.map +1 -1
- package/dist/esm/react/toUIMessages.d.ts.map +1 -1
- package/dist/esm/react/toUIMessages.js +12 -6
- package/dist/esm/react/toUIMessages.js.map +1 -1
- package/dist/esm/react/usePaginatedQuery.d.ts.map +1 -1
- package/dist/esm/react/usePaginatedQuery.js +0 -19
- package/dist/esm/react/usePaginatedQuery.js.map +1 -1
- package/dist/esm/validators.d.ts +851 -3388
- package/dist/esm/validators.d.ts.map +1 -1
- package/dist/esm/validators.js +11 -37
- package/dist/esm/validators.js.map +1 -1
- package/dist/esm.tsbuildinfo +1 -1
- package/package.json +11 -9
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/files.ts +167 -0
- package/src/client/index.test.ts +127 -1
- package/src/client/index.ts +970 -812
- package/src/client/setup.test.ts +29 -0
- package/src/client/types.ts +41 -3
- package/src/component/_generated/api.d.ts +47 -711
- package/src/component/files.test.ts +130 -0
- package/src/component/files.ts +83 -27
- package/src/component/messages.test.ts +2 -1
- package/src/component/messages.ts +47 -137
- package/src/component/schema.ts +8 -28
- package/src/mapping.test.ts +163 -0
- package/src/mapping.ts +173 -103
- package/src/react/deltas.test.ts +4 -1
- package/src/react/toUIMessages.ts +12 -6
- package/src/react/usePaginatedQuery.ts +0 -31
- 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
|
+
});
|
package/src/component/files.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
+
storageId: v.string(),
|
|
15
21
|
},
|
|
16
22
|
});
|
|
17
23
|
|
|
18
24
|
export async function addFileHandler(
|
|
19
25
|
ctx: MutationCtx,
|
|
20
|
-
args:
|
|
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
|
-
|
|
41
|
+
storageId: existingFile.storageId,
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
44
|
const fileId = await ctx.db.insert("files", {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
refcount:
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
87
|
+
lastTouchedAt: Date.now(),
|
|
66
88
|
});
|
|
67
|
-
return file._id;
|
|
89
|
+
return { fileId: file._id, storageId: file.storageId };
|
|
68
90
|
},
|
|
69
|
-
returns: v.union(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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) =>
|
package/src/component/schema.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
190
|
+
fileIds: v.optional(v.array(v.string())),
|
|
211
191
|
});
|
|
212
192
|
export type MessageDoc = Infer<typeof vMessageDoc>;
|
|
213
193
|
|