@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.
- package/README.md +5 -8
- package/dist/client/messages.d.ts +10 -0
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +1 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/component/_generated/component.d.ts +15 -8
- 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 +340 -338
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +80 -16
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +782 -626
- 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 +16 -2
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +86 -30
- package/dist/component/streams.js.map +1 -1
- package/dist/component/vector/index.d.ts +9 -9
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/tables.d.ts +5 -5
- package/dist/component/vector/tables.d.ts.map +1 -1
- package/dist/component/vector/tables.js +1 -1
- package/dist/component/vector/tables.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +106 -0
- package/dist/errors.js.map +1 -0
- 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/validators.d.ts +1952 -1952
- package/dist/vercel/UIMessages.js +1 -1
- package/dist/vercel/UIMessages.js.map +1 -1
- package/dist/vercel/client/definePlaygroundAPI.d.ts +132 -132
- 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/messages.d.ts +50 -32
- package/dist/vercel/client/messages.d.ts.map +1 -1
- package/dist/vercel/client/messages.js +2 -0
- package/dist/vercel/client/messages.js.map +1 -1
- package/dist/vercel/client/search.d.ts +475 -32
- package/dist/vercel/client/search.d.ts.map +1 -1
- package/dist/vercel/client/search.js +34 -9
- package/dist/vercel/client/search.js.map +1 -1
- package/dist/vercel/client/start.d.ts.map +1 -1
- package/dist/vercel/client/start.js +1 -0
- package/dist/vercel/client/start.js.map +1 -1
- package/dist/vercel/client/streamText.d.ts +1 -2
- package/dist/vercel/client/streamText.d.ts.map +1 -1
- package/dist/vercel/client/streamText.js +49 -17
- package/dist/vercel/client/streamText.js.map +1 -1
- package/dist/vercel/client/streaming.d.ts +318 -301
- package/dist/vercel/client/streaming.d.ts.map +1 -1
- package/dist/vercel/client/streaming.js +56 -21
- package/dist/vercel/client/streaming.js.map +1 -1
- package/dist/vercel/client/types.d.ts +2 -0
- package/dist/vercel/client/types.d.ts.map +1 -1
- package/dist/vercel/client/types.js.map +1 -1
- package/dist/vercel/client/utils.d.ts +9 -5
- package/dist/vercel/client/utils.d.ts.map +1 -1
- package/dist/vercel/client/utils.js +13 -13
- package/dist/vercel/client/utils.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/index.d.ts +91 -90
- package/dist/vercel/index.d.ts.map +1 -1
- package/dist/vercel/index.js +2 -0
- package/dist/vercel/index.js.map +1 -1
- 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 +16 -9
- package/src/client/messages.ts +12 -0
- package/src/component/_generated/component.ts +18 -2
- package/src/component/files.test.ts +81 -6
- package/src/component/files.ts +69 -41
- package/src/component/messages.test.ts +496 -0
- package/src/component/messages.ts +105 -15
- package/src/component/schema.ts +16 -2
- package/src/component/streams.test.ts +109 -0
- package/src/component/streams.ts +123 -35
- package/src/component/vector/tables.ts +1 -1
- package/src/errors.test.ts +76 -0
- package/src/errors.ts +113 -0
- 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/index.test.ts +47 -0
- package/src/vercel/client/messages.ts +20 -0
- package/src/vercel/client/search.test.ts +183 -8
- package/src/vercel/client/search.ts +46 -12
- package/src/vercel/client/start.ts +1 -0
- package/src/vercel/client/streamText.test.ts +113 -17
- package/src/vercel/client/streamText.ts +55 -28
- package/src/vercel/client/streaming.test.ts +142 -7
- package/src/vercel/client/streaming.ts +73 -19
- package/src/vercel/client/types.ts +2 -0
- package/src/vercel/client/utils.test.ts +97 -0
- package/src/vercel/client/utils.ts +15 -15
- package/src/vercel/fileMaterialization.ts +174 -0
- package/src/vercel/index.ts +2 -0
- package/src/vercel/mapping.test.ts +117 -0
- package/src/vercel/mapping.ts +15 -1
- package/src/vercel/toUIMessages.test.ts +7 -1
|
@@ -5,6 +5,7 @@ import { describe, expect, test, vi } from "vitest";
|
|
|
5
5
|
import { api } from "./_generated/api.js";
|
|
6
6
|
import type { Id } from "./_generated/dataModel.js";
|
|
7
7
|
import { getMaxMessage } from "./messages.js";
|
|
8
|
+
import { timeoutStreamHandler } from "./streams.js";
|
|
8
9
|
import schema from "./schema.js";
|
|
9
10
|
import { initConvexTest, modules } from "./setup.test.js";
|
|
10
11
|
|
|
@@ -163,6 +164,249 @@ describe("agent", () => {
|
|
|
163
164
|
});
|
|
164
165
|
});
|
|
165
166
|
|
|
167
|
+
test("an explicit order starts, appends, and avoids later order collisions", async () => {
|
|
168
|
+
const t = convexTest(schema, modules);
|
|
169
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
170
|
+
userId: "test",
|
|
171
|
+
});
|
|
172
|
+
await t.mutation(api.messages.addMessages, {
|
|
173
|
+
threadId: thread._id as Id<"threads">,
|
|
174
|
+
messages: [
|
|
175
|
+
{ message: { role: "user", content: "hello" } },
|
|
176
|
+
{ message: { role: "assistant", content: "agent reply" } },
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const { messages: firstHumanReply } = await t.mutation(
|
|
181
|
+
api.messages.addMessages,
|
|
182
|
+
{
|
|
183
|
+
threadId: thread._id as Id<"threads">,
|
|
184
|
+
order: 1,
|
|
185
|
+
agentName: "human:Alex",
|
|
186
|
+
messages: [{ message: { role: "assistant", content: "human reply" } }],
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
expect(firstHumanReply[0]).toMatchObject({
|
|
190
|
+
order: 1,
|
|
191
|
+
stepOrder: 0,
|
|
192
|
+
agentName: "human:Alex",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const { messages: secondHumanReply } = await t.mutation(
|
|
196
|
+
api.messages.addMessages,
|
|
197
|
+
{
|
|
198
|
+
threadId: thread._id as Id<"threads">,
|
|
199
|
+
order: 1,
|
|
200
|
+
agentName: "human:Sam",
|
|
201
|
+
messages: [{ message: { role: "assistant", content: "follow-up" } }],
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
expect(secondHumanReply[0]).toMatchObject({
|
|
205
|
+
order: 1,
|
|
206
|
+
stepOrder: 1,
|
|
207
|
+
agentName: "human:Sam",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const { messages: backdatedBatch } = await t.mutation(
|
|
211
|
+
api.messages.addMessages,
|
|
212
|
+
{
|
|
213
|
+
threadId: thread._id as Id<"threads">,
|
|
214
|
+
order: 0,
|
|
215
|
+
messages: [
|
|
216
|
+
{ message: { role: "assistant", content: "backdated reply" } },
|
|
217
|
+
{ message: { role: "user", content: "new user turn" } },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
expect(
|
|
222
|
+
backdatedBatch.map(({ order, stepOrder }) => [order, stepOrder]),
|
|
223
|
+
).toEqual([
|
|
224
|
+
[0, 2],
|
|
225
|
+
[2, 0],
|
|
226
|
+
]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("concurrent saves to an explicit order receive distinct step orders", async () => {
|
|
230
|
+
const t = convexTest(schema, modules);
|
|
231
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
232
|
+
userId: "test",
|
|
233
|
+
});
|
|
234
|
+
const saveReply = (agentName: string) =>
|
|
235
|
+
t.mutation(api.messages.addMessages, {
|
|
236
|
+
threadId: thread._id as Id<"threads">,
|
|
237
|
+
order: 3,
|
|
238
|
+
agentName,
|
|
239
|
+
messages: [
|
|
240
|
+
{ message: { role: "assistant" as const, content: agentName } },
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const replies = await Promise.all([saveReply("Alex"), saveReply("Sam")]);
|
|
245
|
+
|
|
246
|
+
expect(
|
|
247
|
+
replies
|
|
248
|
+
.map(({ messages }) => messages[0].stepOrder)
|
|
249
|
+
.sort((a, b) => a - b),
|
|
250
|
+
).toEqual([0, 1]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("next order is allocated after the latest message", async () => {
|
|
254
|
+
const t = convexTest(schema, modules);
|
|
255
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
256
|
+
userId: "test",
|
|
257
|
+
});
|
|
258
|
+
await t.mutation(api.messages.addMessages, {
|
|
259
|
+
threadId: thread._id as Id<"threads">,
|
|
260
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
264
|
+
threadId: thread._id as Id<"threads">,
|
|
265
|
+
order: "next",
|
|
266
|
+
messages: [{ message: { role: "assistant", content: "separate reply" } }],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(messages[0]).toMatchObject({ order: 1, stepOrder: 0 });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("concurrent next orders receive distinct orders", async () => {
|
|
273
|
+
const t = convexTest(schema, modules);
|
|
274
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
275
|
+
userId: "test",
|
|
276
|
+
});
|
|
277
|
+
await t.mutation(api.messages.addMessages, {
|
|
278
|
+
threadId: thread._id as Id<"threads">,
|
|
279
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
280
|
+
});
|
|
281
|
+
const saveReply = (agentName: string) =>
|
|
282
|
+
t.mutation(api.messages.addMessages, {
|
|
283
|
+
threadId: thread._id as Id<"threads">,
|
|
284
|
+
order: "next",
|
|
285
|
+
agentName,
|
|
286
|
+
messages: [
|
|
287
|
+
{ message: { role: "assistant" as const, content: agentName } },
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const replies = await Promise.all([saveReply("Alex"), saveReply("Sam")]);
|
|
292
|
+
|
|
293
|
+
expect(
|
|
294
|
+
replies
|
|
295
|
+
.map(({ messages }) => messages[0])
|
|
296
|
+
.sort((a, b) => a.order - b.order)
|
|
297
|
+
.map(({ order, stepOrder }) => [order, stepOrder]),
|
|
298
|
+
).toEqual([
|
|
299
|
+
[1, 0],
|
|
300
|
+
[2, 0],
|
|
301
|
+
]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("an explicit order ahead of the thread places the batch there", async () => {
|
|
305
|
+
const t = convexTest(schema, modules);
|
|
306
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
307
|
+
userId: "test",
|
|
308
|
+
});
|
|
309
|
+
await t.mutation(api.messages.addMessages, {
|
|
310
|
+
threadId: thread._id as Id<"threads">,
|
|
311
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
315
|
+
threadId: thread._id as Id<"threads">,
|
|
316
|
+
order: 5,
|
|
317
|
+
messages: [
|
|
318
|
+
{ message: { role: "assistant", content: "imported reply" } },
|
|
319
|
+
{ message: { role: "user", content: "imported question" } },
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
expect(messages.map(({ order, stepOrder }) => [order, stepOrder])).toEqual([
|
|
323
|
+
[5, 0],
|
|
324
|
+
[6, 0],
|
|
325
|
+
]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("an explicit order cannot conflict with another placement argument", async () => {
|
|
329
|
+
const t = convexTest(schema, modules);
|
|
330
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
331
|
+
userId: "test",
|
|
332
|
+
});
|
|
333
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
334
|
+
threadId: thread._id as Id<"threads">,
|
|
335
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await expect(
|
|
339
|
+
t.mutation(api.messages.addMessages, {
|
|
340
|
+
threadId: thread._id as Id<"threads">,
|
|
341
|
+
order: 1,
|
|
342
|
+
promptMessageId: messages[0]._id as Id<"messages">,
|
|
343
|
+
messages: [{ message: { role: "assistant", content: "reply" } }],
|
|
344
|
+
}),
|
|
345
|
+
).rejects.toThrow("order and promptMessageId cannot both be provided");
|
|
346
|
+
|
|
347
|
+
await expect(
|
|
348
|
+
t.mutation(api.messages.addMessages, {
|
|
349
|
+
threadId: thread._id as Id<"threads">,
|
|
350
|
+
order: "next",
|
|
351
|
+
promptMessageId: messages[0]._id as Id<"messages">,
|
|
352
|
+
messages: [{ message: { role: "assistant", content: "reply" } }],
|
|
353
|
+
}),
|
|
354
|
+
).rejects.toThrow("order and promptMessageId cannot both be provided");
|
|
355
|
+
|
|
356
|
+
for (const order of [-1, 1.5, Number.MAX_SAFE_INTEGER]) {
|
|
357
|
+
await expect(
|
|
358
|
+
t.mutation(api.messages.addMessages, {
|
|
359
|
+
threadId: thread._id as Id<"threads">,
|
|
360
|
+
order,
|
|
361
|
+
messages: [{ message: { role: "assistant", content: "reply" } }],
|
|
362
|
+
}),
|
|
363
|
+
).rejects.toThrow("order must be a non-negative safe integer");
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("derived message positions cannot exceed safe integers", async () => {
|
|
368
|
+
const t = convexTest(schema, modules);
|
|
369
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
370
|
+
userId: "test",
|
|
371
|
+
});
|
|
372
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
373
|
+
threadId: thread._id as Id<"threads">,
|
|
374
|
+
order: 1,
|
|
375
|
+
messages: [{ message: { role: "assistant", content: "reply" } }],
|
|
376
|
+
});
|
|
377
|
+
const messageId = messages[0]._id as Id<"messages">;
|
|
378
|
+
|
|
379
|
+
await t.run(async (ctx) => {
|
|
380
|
+
await ctx.db.patch("messages", messageId, {
|
|
381
|
+
stepOrder: Number.MAX_SAFE_INTEGER,
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
await expect(
|
|
385
|
+
t.mutation(api.messages.addMessages, {
|
|
386
|
+
threadId: thread._id as Id<"threads">,
|
|
387
|
+
order: 1,
|
|
388
|
+
messages: [{ message: { role: "assistant", content: "follow-up" } }],
|
|
389
|
+
}),
|
|
390
|
+
).rejects.toThrow(
|
|
391
|
+
"stepOrder cannot be incremented past Number.MAX_SAFE_INTEGER",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
await t.run(async (ctx) => {
|
|
395
|
+
await ctx.db.patch("messages", messageId, {
|
|
396
|
+
order: Number.MAX_SAFE_INTEGER,
|
|
397
|
+
stepOrder: 0,
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
await expect(
|
|
401
|
+
t.mutation(api.messages.addMessages, {
|
|
402
|
+
threadId: thread._id as Id<"threads">,
|
|
403
|
+
messages: [{ message: { role: "user", content: "new turn" } }],
|
|
404
|
+
}),
|
|
405
|
+
).rejects.toThrow(
|
|
406
|
+
"order cannot be incremented past Number.MAX_SAFE_INTEGER",
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
166
410
|
test("order is incremented for user messages on to addMessages for the same promptMessageId", async () => {
|
|
167
411
|
const t = convexTest(schema, modules);
|
|
168
412
|
const thread = await t.mutation(api.threads.createThread, {
|
|
@@ -924,4 +1168,256 @@ describe("agent", () => {
|
|
|
924
1168
|
expect(state.messages[0].embeddingId).toBeUndefined();
|
|
925
1169
|
expect(state.embedding).toBeNull();
|
|
926
1170
|
});
|
|
1171
|
+
|
|
1172
|
+
test("transfers recovered file ownership and releases timed-out streams", async () => {
|
|
1173
|
+
const t = initConvexTest();
|
|
1174
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
1175
|
+
userId: "stream-file-recovery",
|
|
1176
|
+
});
|
|
1177
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
1178
|
+
threadId: thread._id as Id<"threads">,
|
|
1179
|
+
messages: [
|
|
1180
|
+
{ message: { role: "assistant", content: [] }, status: "pending" },
|
|
1181
|
+
],
|
|
1182
|
+
});
|
|
1183
|
+
const pending = messages[0]!;
|
|
1184
|
+
const { fileId: recoveredFileId } = await t.mutation(api.files.addFile, {
|
|
1185
|
+
storageId: "recovered-storage",
|
|
1186
|
+
hash: "recovered-hash",
|
|
1187
|
+
filename: "recovered.txt",
|
|
1188
|
+
});
|
|
1189
|
+
await t.mutation(api.files.copyFile, { fileId: recoveredFileId });
|
|
1190
|
+
const recoveredStreamId = await t.run((ctx) =>
|
|
1191
|
+
ctx.db.insert("streamingMessages", {
|
|
1192
|
+
threadId: thread._id as Id<"threads">,
|
|
1193
|
+
order: pending.order,
|
|
1194
|
+
stepOrder: pending.stepOrder,
|
|
1195
|
+
format: "UIMessageChunk",
|
|
1196
|
+
state: { kind: "aborted", reason: "interrupted" },
|
|
1197
|
+
fileRefs: [
|
|
1198
|
+
{ url: "https://files.example/recovered", fileId: recoveredFileId },
|
|
1199
|
+
],
|
|
1200
|
+
}),
|
|
1201
|
+
);
|
|
1202
|
+
await t.run((ctx) =>
|
|
1203
|
+
ctx.db.insert("streamDeltas", {
|
|
1204
|
+
streamId: recoveredStreamId,
|
|
1205
|
+
start: 0,
|
|
1206
|
+
end: 3,
|
|
1207
|
+
parts: [
|
|
1208
|
+
{ type: "start" },
|
|
1209
|
+
{
|
|
1210
|
+
type: "file",
|
|
1211
|
+
url: "https://files.example/recovered",
|
|
1212
|
+
mediaType: "text/plain",
|
|
1213
|
+
},
|
|
1214
|
+
{ type: "finish" },
|
|
1215
|
+
],
|
|
1216
|
+
}),
|
|
1217
|
+
);
|
|
1218
|
+
await t.mutation(api.messages.finalizeMessage, {
|
|
1219
|
+
messageId: pending._id as Id<"messages">,
|
|
1220
|
+
result: { status: "success" },
|
|
1221
|
+
});
|
|
1222
|
+
await expect(
|
|
1223
|
+
t.query(api.files.get, { fileId: recoveredFileId }),
|
|
1224
|
+
).resolves.toMatchObject({
|
|
1225
|
+
refcount: 1,
|
|
1226
|
+
});
|
|
1227
|
+
expect(
|
|
1228
|
+
(await t.run((ctx) => ctx.db.get("streamingMessages", recoveredStreamId)))
|
|
1229
|
+
?.fileRefs,
|
|
1230
|
+
).toBeUndefined();
|
|
1231
|
+
|
|
1232
|
+
const { fileId: timedOutFileId } = await t.mutation(api.files.addFile, {
|
|
1233
|
+
storageId: "timeout-storage",
|
|
1234
|
+
hash: "timeout-hash",
|
|
1235
|
+
filename: "timeout.txt",
|
|
1236
|
+
});
|
|
1237
|
+
await t.mutation(api.files.copyFile, { fileId: timedOutFileId });
|
|
1238
|
+
const timedOutStreamId = await t.run((ctx) =>
|
|
1239
|
+
ctx.db.insert("streamingMessages", {
|
|
1240
|
+
threadId: thread._id as Id<"threads">,
|
|
1241
|
+
order: pending.order + 1,
|
|
1242
|
+
stepOrder: 0,
|
|
1243
|
+
format: "UIMessageChunk",
|
|
1244
|
+
state: { kind: "streaming", lastHeartbeat: Date.now() },
|
|
1245
|
+
fileRefs: [
|
|
1246
|
+
{ url: "https://files.example/timeout", fileId: timedOutFileId },
|
|
1247
|
+
],
|
|
1248
|
+
}),
|
|
1249
|
+
);
|
|
1250
|
+
await t.run((ctx) =>
|
|
1251
|
+
timeoutStreamHandler(ctx, { streamId: timedOutStreamId }),
|
|
1252
|
+
);
|
|
1253
|
+
await t.mutation(api.streams.deleteStreamSync, {
|
|
1254
|
+
streamId: timedOutStreamId,
|
|
1255
|
+
});
|
|
1256
|
+
await expect(
|
|
1257
|
+
t.query(api.files.get, { fileId: timedOutFileId }),
|
|
1258
|
+
).resolves.toMatchObject({
|
|
1259
|
+
refcount: 0,
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
describe("late saves racing a failed pending message (issue #320)", () => {
|
|
1265
|
+
const PROVIDER_ERROR = "invalid_prompt: Invalid prompt: flagged by policy.";
|
|
1266
|
+
|
|
1267
|
+
test("keeps the first durable failure authoritative", async () => {
|
|
1268
|
+
const t = initConvexTest();
|
|
1269
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
1270
|
+
userId: "u1",
|
|
1271
|
+
});
|
|
1272
|
+
const threadId = thread._id as Id<"threads">;
|
|
1273
|
+
|
|
1274
|
+
const { messages: seeded } = await t.mutation(api.messages.addMessages, {
|
|
1275
|
+
threadId,
|
|
1276
|
+
messages: [
|
|
1277
|
+
{ message: { role: "user", content: "hello" } },
|
|
1278
|
+
{ message: { role: "assistant", content: [] }, status: "pending" },
|
|
1279
|
+
],
|
|
1280
|
+
});
|
|
1281
|
+
const pending = seeded.at(-1)!;
|
|
1282
|
+
expect(pending.status).toBe("pending");
|
|
1283
|
+
|
|
1284
|
+
const streamId = await t.mutation(api.streams.create, {
|
|
1285
|
+
threadId,
|
|
1286
|
+
order: pending.order,
|
|
1287
|
+
stepOrder: pending.stepOrder,
|
|
1288
|
+
format: "UIMessageChunk",
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
await t.mutation(api.messages.finalizeMessage, {
|
|
1292
|
+
messageId: pending._id as Id<"messages">,
|
|
1293
|
+
result: { status: "failed", error: PROVIDER_ERROR },
|
|
1294
|
+
});
|
|
1295
|
+
await t.mutation(api.streams.abort, { streamId, reason: PROVIDER_ERROR });
|
|
1296
|
+
|
|
1297
|
+
const { messages: late } = await t.mutation(api.messages.addMessages, {
|
|
1298
|
+
threadId,
|
|
1299
|
+
pendingMessageId: pending._id as Id<"messages">,
|
|
1300
|
+
finishStreamId: streamId,
|
|
1301
|
+
failPendingSteps: false,
|
|
1302
|
+
messages: [
|
|
1303
|
+
{ message: { role: "assistant", content: "partial response" } },
|
|
1304
|
+
],
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
const assistants = (
|
|
1308
|
+
await t.run(async (ctx) =>
|
|
1309
|
+
ctx.db
|
|
1310
|
+
.query("messages")
|
|
1311
|
+
.withIndex("threadId_status_tool_order_stepOrder", (q) =>
|
|
1312
|
+
q.eq("threadId", threadId),
|
|
1313
|
+
)
|
|
1314
|
+
.collect(),
|
|
1315
|
+
)
|
|
1316
|
+
).filter((message) => message.message?.role === "assistant");
|
|
1317
|
+
|
|
1318
|
+
expect(late).toHaveLength(1);
|
|
1319
|
+
expect(assistants).toHaveLength(1);
|
|
1320
|
+
expect(assistants[0]!._id).toBe(pending._id);
|
|
1321
|
+
expect(assistants[0]!.status).toBe("failed");
|
|
1322
|
+
expect(assistants[0]!.error).toBe(PROVIDER_ERROR);
|
|
1323
|
+
expect(assistants[0]!.text).toBe("partial response");
|
|
1324
|
+
|
|
1325
|
+
const stream = await t.run((ctx) =>
|
|
1326
|
+
ctx.db.get("streamingMessages", streamId),
|
|
1327
|
+
);
|
|
1328
|
+
expect(stream?.state.kind).toBe("aborted");
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
describe("deleting a message aborts generation writing to it (issue #300)", () => {
|
|
1333
|
+
test("a stream at the deleted order is aborted", async () => {
|
|
1334
|
+
const t = initConvexTest();
|
|
1335
|
+
const thread = await t.mutation(api.threads.createThread, { userId: "u" });
|
|
1336
|
+
const threadId = thread._id as Id<"threads">;
|
|
1337
|
+
|
|
1338
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
1339
|
+
threadId,
|
|
1340
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
1341
|
+
});
|
|
1342
|
+
const prompt = messages[0];
|
|
1343
|
+
|
|
1344
|
+
await t.mutation(api.streams.create, {
|
|
1345
|
+
threadId,
|
|
1346
|
+
order: prompt.order,
|
|
1347
|
+
stepOrder: prompt.stepOrder + 1,
|
|
1348
|
+
userId: "u",
|
|
1349
|
+
agentName: "a",
|
|
1350
|
+
model: "m",
|
|
1351
|
+
provider: "p",
|
|
1352
|
+
format: "UIMessageChunk",
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
await t.mutation(api.messages.deleteByIds, {
|
|
1356
|
+
messageIds: [prompt._id as Id<"messages">],
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
const streaming = await t.query(api.streams.list, {
|
|
1360
|
+
threadId,
|
|
1361
|
+
statuses: ["streaming"],
|
|
1362
|
+
});
|
|
1363
|
+
const aborted = await t.query(api.streams.list, {
|
|
1364
|
+
threadId,
|
|
1365
|
+
statuses: ["aborted"],
|
|
1366
|
+
});
|
|
1367
|
+
expect(streaming).toHaveLength(0);
|
|
1368
|
+
expect(aborted).toHaveLength(1);
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
describe("abandoning a save whose prompt was deleted (issue #300)", () => {
|
|
1373
|
+
test("abandons instead of throwing when the caller opts in", async () => {
|
|
1374
|
+
const t = initConvexTest();
|
|
1375
|
+
const thread = await t.mutation(api.threads.createThread, { userId: "u" });
|
|
1376
|
+
const threadId = thread._id as Id<"threads">;
|
|
1377
|
+
|
|
1378
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
1379
|
+
threadId,
|
|
1380
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
1381
|
+
});
|
|
1382
|
+
const promptMessageId = messages[0]._id as Id<"messages">;
|
|
1383
|
+
|
|
1384
|
+
await t.mutation(api.messages.deleteByIds, { messageIds: [promptMessageId] });
|
|
1385
|
+
|
|
1386
|
+
const saved = await t.mutation(api.messages.addMessages, {
|
|
1387
|
+
threadId,
|
|
1388
|
+
promptMessageId,
|
|
1389
|
+
abandonIfPromptMissing: true,
|
|
1390
|
+
messages: [{ message: { role: "assistant", content: "answer" } }],
|
|
1391
|
+
});
|
|
1392
|
+
expect(saved.messages).toEqual([]);
|
|
1393
|
+
|
|
1394
|
+
// Nothing was grafted onto the thread.
|
|
1395
|
+
const all = await t.query(api.messages.listMessagesByThreadId, {
|
|
1396
|
+
threadId,
|
|
1397
|
+
order: "asc",
|
|
1398
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
1399
|
+
});
|
|
1400
|
+
expect(all.page).toHaveLength(0);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
test("still throws for a caller that did not opt in", async () => {
|
|
1404
|
+
const t = initConvexTest();
|
|
1405
|
+
const thread = await t.mutation(api.threads.createThread, { userId: "u" });
|
|
1406
|
+
const threadId = thread._id as Id<"threads">;
|
|
1407
|
+
|
|
1408
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
1409
|
+
threadId,
|
|
1410
|
+
messages: [{ message: { role: "user", content: "hello" } }],
|
|
1411
|
+
});
|
|
1412
|
+
const promptMessageId = messages[0]._id as Id<"messages">;
|
|
1413
|
+
await t.mutation(api.messages.deleteByIds, { messageIds: [promptMessageId] });
|
|
1414
|
+
|
|
1415
|
+
await expect(
|
|
1416
|
+
t.mutation(api.messages.addMessages, {
|
|
1417
|
+
threadId,
|
|
1418
|
+
promptMessageId,
|
|
1419
|
+
messages: [{ message: { role: "assistant", content: "answer" } }],
|
|
1420
|
+
}),
|
|
1421
|
+
).rejects.toThrow("not found");
|
|
1422
|
+
});
|
|
927
1423
|
});
|