@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
@@ -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
  });