convex-durable-agents 0.2.2 → 0.2.4

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 (75) hide show
  1. package/README.md +81 -7
  2. package/dist/client/api.d.ts.map +1 -1
  3. package/dist/client/api.js +26 -6
  4. package/dist/client/api.js.map +1 -1
  5. package/dist/client/handler.d.ts +20 -0
  6. package/dist/client/handler.d.ts.map +1 -1
  7. package/dist/client/handler.js +239 -116
  8. package/dist/client/handler.js.map +1 -1
  9. package/dist/client/streamer.js +1 -1
  10. package/dist/client/streamer.js.map +1 -1
  11. package/dist/client/tools.d.ts +17 -0
  12. package/dist/client/tools.d.ts.map +1 -1
  13. package/dist/client/tools.js +16 -0
  14. package/dist/client/tools.js.map +1 -1
  15. package/dist/client/types.d.ts +75 -1
  16. package/dist/client/types.d.ts.map +1 -1
  17. package/dist/client/types.js +11 -0
  18. package/dist/client/types.js.map +1 -1
  19. package/dist/component/_generated/component.d.ts +90 -0
  20. package/dist/component/_generated/component.d.ts.map +1 -1
  21. package/dist/component/agent.d.ts.map +1 -1
  22. package/dist/component/agent.js +21 -2
  23. package/dist/component/agent.js.map +1 -1
  24. package/dist/component/messages.d.ts +1 -0
  25. package/dist/component/messages.d.ts.map +1 -1
  26. package/dist/component/messages.js +9 -0
  27. package/dist/component/messages.js.map +1 -1
  28. package/dist/component/schema.d.ts +70 -2
  29. package/dist/component/schema.d.ts.map +1 -1
  30. package/dist/component/schema.js +21 -0
  31. package/dist/component/schema.js.map +1 -1
  32. package/dist/component/streams.js +2 -2
  33. package/dist/component/streams.js.map +1 -1
  34. package/dist/component/threads.d.ts +92 -2
  35. package/dist/component/threads.d.ts.map +1 -1
  36. package/dist/component/threads.js +83 -2
  37. package/dist/component/threads.js.map +1 -1
  38. package/dist/component/tool_calls.d.ts +54 -3
  39. package/dist/component/tool_calls.d.ts.map +1 -1
  40. package/dist/component/tool_calls.js +358 -40
  41. package/dist/component/tool_calls.js.map +1 -1
  42. package/dist/utils/logger.d.ts.map +1 -0
  43. package/dist/utils/logger.js.map +1 -0
  44. package/dist/utils/retry.d.ts +69 -0
  45. package/dist/utils/retry.d.ts.map +1 -0
  46. package/dist/utils/retry.js +404 -0
  47. package/dist/utils/retry.js.map +1 -0
  48. package/dist/utils/streaming.d.ts +4 -0
  49. package/dist/utils/streaming.d.ts.map +1 -0
  50. package/dist/utils/streaming.js +4 -0
  51. package/dist/utils/streaming.js.map +1 -0
  52. package/package.json +2 -2
  53. package/src/client/api.ts +26 -7
  54. package/src/client/handler.ts +308 -132
  55. package/src/client/streamer.ts +1 -1
  56. package/src/client/tools.ts +43 -1
  57. package/src/client/types.ts +60 -0
  58. package/src/component/_generated/component.ts +104 -0
  59. package/src/component/agent.ts +24 -2
  60. package/src/component/messages.ts +9 -0
  61. package/src/component/schema.ts +22 -0
  62. package/src/component/streams.ts +2 -2
  63. package/src/component/threads.ts +92 -3
  64. package/src/component/tool_calls.ts +433 -49
  65. package/src/utils/retry.ts +528 -0
  66. package/src/{streaming.ts → utils/streaming.ts} +2 -3
  67. package/dist/logger.d.ts.map +0 -1
  68. package/dist/logger.js.map +0 -1
  69. package/dist/streaming.d.ts +0 -3
  70. package/dist/streaming.d.ts.map +0 -1
  71. package/dist/streaming.js +0 -4
  72. package/dist/streaming.js.map +0 -1
  73. /package/dist/{logger.d.ts → utils/logger.d.ts} +0 -0
  74. /package/dist/{logger.js → utils/logger.js} +0 -0
  75. /package/src/{logger.ts → utils/logger.ts} +0 -0
@@ -33,6 +33,17 @@ export type PaginationResult<T> = {
33
33
 
34
34
  export type ThreadStatus = "streaming" | "awaiting_tool_results" | "completed" | "failed" | "stopped";
35
35
 
36
+ export type RetryState = {
37
+ scope: "stream";
38
+ attempt: number;
39
+ maxAttempts: number;
40
+ nextRetryAt: number;
41
+ error: string;
42
+ kind?: string;
43
+ retryable: boolean;
44
+ requiresExplicitHandling: boolean;
45
+ };
46
+
36
47
  export type ThreadDoc = {
37
48
  _id: string;
38
49
  _creationTime: number;
@@ -40,6 +51,7 @@ export type ThreadDoc = {
40
51
  stopSignal: boolean;
41
52
  streamId?: string | null;
42
53
  streamFnHandle: string;
54
+ retryState?: RetryState;
43
55
  };
44
56
 
45
57
  const vThreadStatus = v.union(
@@ -59,6 +71,19 @@ export const _vClientThreadDoc = v.object({
59
71
  streamFnHandle: v.string(),
60
72
  workpoolEnqueueAction: v.optional(v.string()),
61
73
  toolExecutionWorkpoolEnqueueAction: v.optional(v.string()),
74
+ retryState: v.optional(
75
+ v.object({
76
+ scope: v.literal("stream"),
77
+ attempt: v.number(),
78
+ maxAttempts: v.number(),
79
+ nextRetryAt: v.number(),
80
+ error: v.string(),
81
+ kind: v.optional(v.string()),
82
+ retryable: v.boolean(),
83
+ requiresExplicitHandling: v.boolean(),
84
+ retryFnId: v.optional(v.string()),
85
+ }),
86
+ ),
62
87
  });
63
88
 
64
89
  export type MessageDoc = UIMessage<any> & {
@@ -77,12 +102,47 @@ export function messageDocToUIMessage(message: MessageDoc): UIMessage {
77
102
  };
78
103
  }
79
104
 
105
+ export type RetryBackoffConfig =
106
+ | {
107
+ strategy?: "fixed";
108
+ delayMs: number;
109
+ jitter?: boolean;
110
+ }
111
+ | {
112
+ strategy: "exponential";
113
+ initialDelayMs: number;
114
+ multiplier?: number;
115
+ maxDelayMs?: number;
116
+ jitter?: boolean;
117
+ };
118
+
119
+ export type SyncToolRetryOptions = {
120
+ /**
121
+ * Opt-in to retry for this sync tool.
122
+ */
123
+ enabled: true;
124
+ /**
125
+ * Maximum execution attempts including the initial attempt.
126
+ */
127
+ maxAttempts?: number;
128
+ /**
129
+ * Retry backoff policy.
130
+ */
131
+ backoff?: RetryBackoffConfig;
132
+ /**
133
+ * Optional function to classify whether an error is retryable.
134
+ * Receives { threadId, toolCallId, toolName, args, error, attempt, maxAttempts }.
135
+ */
136
+ shouldRetryError?: FunctionReference<"action", "internal" | "public">;
137
+ };
138
+
80
139
  // Sync durable tool definition - handler returns the result directly
81
140
  export type SyncTool<INPUT = unknown, OUTPUT = unknown> = {
82
141
  type: "sync";
83
142
  description: string;
84
143
  parameters: unknown; // JSON Schema
85
144
  handler: FunctionReference<"action", "internal" | "public">;
145
+ retry?: SyncToolRetryOptions;
86
146
  _inputType?: INPUT;
87
147
  _outputType?: OUTPUT;
88
148
  };
@@ -52,6 +52,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
52
52
  role: "system" | "user" | "assistant";
53
53
  };
54
54
  overwrite?: boolean;
55
+ streaming?: boolean;
55
56
  threadId: string;
56
57
  },
57
58
  string,
@@ -162,6 +163,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
162
163
  >;
163
164
  };
164
165
  threads: {
166
+ clearRetryState: FunctionReference<
167
+ "mutation",
168
+ "internal",
169
+ { threadId: string },
170
+ null,
171
+ Name
172
+ >;
165
173
  clearStreamId: FunctionReference<
166
174
  "mutation",
167
175
  "internal",
@@ -181,6 +189,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
181
189
  {
182
190
  _creationTime: number;
183
191
  _id: string;
192
+ retryState?: {
193
+ attempt: number;
194
+ error: string;
195
+ kind?: string;
196
+ maxAttempts: number;
197
+ nextRetryAt: number;
198
+ requiresExplicitHandling: boolean;
199
+ retryFnId?: string;
200
+ retryable: boolean;
201
+ scope: "stream";
202
+ };
184
203
  status:
185
204
  | "streaming"
186
205
  | "awaiting_tool_results"
@@ -219,6 +238,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
219
238
  {
220
239
  _creationTime: number;
221
240
  _id: string;
241
+ retryState?: {
242
+ attempt: number;
243
+ error: string;
244
+ kind?: string;
245
+ maxAttempts: number;
246
+ nextRetryAt: number;
247
+ requiresExplicitHandling: boolean;
248
+ retryFnId?: string;
249
+ retryable: boolean;
250
+ scope: "stream";
251
+ };
222
252
  status:
223
253
  | "streaming"
224
254
  | "awaiting_tool_results"
@@ -240,6 +270,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
240
270
  Array<{
241
271
  _creationTime: number;
242
272
  _id: string;
273
+ retryState?: {
274
+ attempt: number;
275
+ error: string;
276
+ kind?: string;
277
+ maxAttempts: number;
278
+ nextRetryAt: number;
279
+ requiresExplicitHandling: boolean;
280
+ retryFnId?: string;
281
+ retryable: boolean;
282
+ scope: "stream";
283
+ };
243
284
  status:
244
285
  | "streaming"
245
286
  | "awaiting_tool_results"
@@ -275,6 +316,23 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
275
316
  null,
276
317
  Name
277
318
  >;
319
+ scheduleRetry: FunctionReference<
320
+ "mutation",
321
+ "internal",
322
+ {
323
+ attempt: number;
324
+ error: string;
325
+ kind?: string;
326
+ maxAttempts: number;
327
+ nextRetryAt: number;
328
+ requiresExplicitHandling: boolean;
329
+ retryable: boolean;
330
+ scope: "stream";
331
+ threadId: string;
332
+ },
333
+ null,
334
+ Name
335
+ >;
278
336
  setStatus: FunctionReference<
279
337
  "mutation",
280
338
  "internal",
@@ -320,7 +378,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
320
378
  {
321
379
  args: any;
322
380
  callback?: string;
381
+ handler?: string;
323
382
  msgId: string;
383
+ retry?: any;
324
384
  saveDelta: boolean;
325
385
  threadId: string;
326
386
  toolCallId: string;
@@ -330,9 +390,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
330
390
  _creationTime: number;
331
391
  _id: string;
332
392
  args: any;
393
+ callbackAttempt?: number;
394
+ callbackLastError?: string;
333
395
  error?: string;
396
+ executionAttempt?: number;
397
+ executionLastError?: string;
398
+ executionMaxAttempts?: number;
399
+ executionRetryPolicy?: any;
400
+ handler?: string;
334
401
  msgId: string;
402
+ nextRetryAt?: number;
335
403
  result?: any;
404
+ status: "pending" | "completed" | "failed";
336
405
  threadId: string;
337
406
  toolCallId: string;
338
407
  toolName: string;
@@ -347,9 +416,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
347
416
  _creationTime: number;
348
417
  _id: string;
349
418
  args: any;
419
+ callbackAttempt?: number;
420
+ callbackLastError?: string;
350
421
  error?: string;
422
+ executionAttempt?: number;
423
+ executionLastError?: string;
424
+ executionMaxAttempts?: number;
425
+ executionRetryPolicy?: any;
426
+ handler?: string;
351
427
  msgId: string;
428
+ nextRetryAt?: number;
352
429
  result?: any;
430
+ status: "pending" | "completed" | "failed";
353
431
  threadId: string;
354
432
  toolCallId: string;
355
433
  toolName: string;
@@ -364,9 +442,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
364
442
  _creationTime: number;
365
443
  _id: string;
366
444
  args: any;
445
+ callbackAttempt?: number;
446
+ callbackLastError?: string;
367
447
  error?: string;
448
+ executionAttempt?: number;
449
+ executionLastError?: string;
450
+ executionMaxAttempts?: number;
451
+ executionRetryPolicy?: any;
452
+ handler?: string;
368
453
  msgId: string;
454
+ nextRetryAt?: number;
369
455
  result?: any;
456
+ status: "pending" | "completed" | "failed";
370
457
  threadId: string;
371
458
  toolCallId: string;
372
459
  toolName: string;
@@ -381,15 +468,31 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
381
468
  _creationTime: number;
382
469
  _id: string;
383
470
  args: any;
471
+ callbackAttempt?: number;
472
+ callbackLastError?: string;
384
473
  error?: string;
474
+ executionAttempt?: number;
475
+ executionLastError?: string;
476
+ executionMaxAttempts?: number;
477
+ executionRetryPolicy?: any;
478
+ handler?: string;
385
479
  msgId: string;
480
+ nextRetryAt?: number;
386
481
  result?: any;
482
+ status: "pending" | "completed" | "failed";
387
483
  threadId: string;
388
484
  toolCallId: string;
389
485
  toolName: string;
390
486
  }>,
391
487
  Name
392
488
  >;
489
+ resumePendingSyncToolExecutions: FunctionReference<
490
+ "mutation",
491
+ "internal",
492
+ { limit?: number },
493
+ number,
494
+ Name
495
+ >;
393
496
  scheduleAsyncToolCall: FunctionReference<
394
497
  "mutation",
395
498
  "internal",
@@ -412,6 +515,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
412
515
  args: any;
413
516
  handler: string;
414
517
  msgId: string;
518
+ retry?: any;
415
519
  saveDelta: boolean;
416
520
  threadId: string;
417
521
  toolCallId: string;
@@ -1,7 +1,7 @@
1
1
  import type { FunctionHandle, FunctionReference, GenericDataModel, GenericMutationCtx } from "convex/server";
2
2
  import { v } from "convex/values";
3
- import { Logger } from "../logger.js";
4
- import { STREAM_LIVENESS_THRESHOLD_MS } from "../streaming.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { STREAM_LIVENESS_THRESHOLD_MS } from "../utils/streaming.js";
5
5
  import { api, internal } from "./_generated/api.js";
6
6
  import { action, mutation } from "./_generated/server.js";
7
7
  import { cancelStream, isAlive } from "./streams.js";
@@ -69,6 +69,7 @@ export const continueStream = mutation({
69
69
  status: "stopped",
70
70
  activeStream: null,
71
71
  continue: false,
72
+ retryState: undefined,
72
73
  });
73
74
  if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
74
75
  await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
@@ -93,6 +94,27 @@ export const continueStream = mutation({
93
94
  return null;
94
95
  }
95
96
 
97
+ if (thread.retryState?.scope === "stream") {
98
+ const now = Date.now();
99
+ if (thread.retryState.nextRetryAt > now) {
100
+ const delayMs = Math.max(0, thread.retryState.nextRetryAt - now);
101
+ logger.debug(
102
+ `Retry pending for thread=${args.threadId}; nextRetryAt=${thread.retryState.nextRetryAt}, now=${now}`,
103
+ );
104
+ const retryFnId = await ctx.scheduler.runAfter(delayMs, api.agent.continueStream, {
105
+ threadId: args.threadId,
106
+ });
107
+ await ctx.db.patch(thread._id, {
108
+ retryState: {
109
+ ...thread.retryState,
110
+ retryFnId,
111
+ },
112
+ });
113
+ return null;
114
+ }
115
+ logger.debug(`Retry window reached for thread=${args.threadId}; continuing with persisted retryState`);
116
+ }
117
+
96
118
  // Check for pending tool calls
97
119
  const pendingToolCalls = await ctx.runQuery(api.tool_calls.listPending, {
98
120
  threadId: args.threadId,
@@ -44,12 +44,21 @@ export const vMessageDoc = vUIMessage.extend({
44
44
  export const add = mutation({
45
45
  args: {
46
46
  threadId: v.id("threads"),
47
+ streaming: v.optional(v.boolean()),
47
48
  msg: vUIMessageOptId,
48
49
  overwrite: v.optional(v.boolean()),
49
50
  committedSeq: v.optional(v.number()),
50
51
  },
51
52
  returns: v.id("messages"),
52
53
  handler: async (ctx, args): Promise<Id<"messages">> => {
54
+ if (!args.streaming) {
55
+ const thread = await ctx.db.get(args.threadId);
56
+ if (!thread) throw new Error(`Thread ${args.threadId} not found`);
57
+ if (thread.status === "streaming" || thread.status === "awaiting_tool_results") {
58
+ throw new Error(`Thread ${args.threadId} is ${thread.status}, cannot add message`);
59
+ }
60
+ }
61
+
53
62
  const existingMessage = args.msg.id
54
63
  ? await ctx.db
55
64
  .query("messages")
@@ -10,6 +10,18 @@ export const vThreadStatus = v.union(
10
10
  v.literal("stopped"),
11
11
  );
12
12
 
13
+ export const vRetryState = v.object({
14
+ scope: v.literal("stream"),
15
+ attempt: v.number(),
16
+ maxAttempts: v.number(),
17
+ nextRetryAt: v.number(),
18
+ error: v.string(),
19
+ kind: v.optional(v.string()),
20
+ retryable: v.boolean(),
21
+ requiresExplicitHandling: v.boolean(),
22
+ retryFnId: v.optional(v.id("_scheduled_functions")),
23
+ });
24
+
13
25
  // AI SDK message content - supports both string and array of parts
14
26
  export const vMessageContent = v.union(
15
27
  v.string(),
@@ -70,6 +82,8 @@ const schema = defineSchema({
70
82
  onStatusChangeHandle: v.optional(v.string()),
71
83
  // Monotonically increasing sequence number for streams of this thread
72
84
  seq: v.number(),
85
+ // Pending retry metadata for stream retries.
86
+ retryState: v.optional(vRetryState),
73
87
  }).index("by_status", ["status"]),
74
88
 
75
89
  // AI SDK compatible message storage
@@ -95,6 +109,13 @@ const schema = defineSchema({
95
109
  callback: v.optional(v.string()),
96
110
  callbackAttempt: v.optional(v.number()),
97
111
  callbackLastError: v.optional(v.string()),
112
+ executionAttempt: v.optional(v.number()),
113
+ executionMaxAttempts: v.optional(v.number()),
114
+ executionLastError: v.optional(v.string()),
115
+ executionRetryPolicy: v.optional(v.any()),
116
+ nextRetryAt: v.optional(v.number()),
117
+ executionRetryFnId: v.optional(v.id("_scheduled_functions")),
118
+ handler: v.optional(v.string()),
98
119
  timeoutMs: v.optional(v.union(v.number(), v.null())),
99
120
  expiresAt: v.optional(v.union(v.number(), v.null())),
100
121
  timeoutFnId: v.optional(v.id("_scheduled_functions")),
@@ -105,6 +126,7 @@ const schema = defineSchema({
105
126
  saveDelta: v.optional(v.boolean()),
106
127
  })
107
128
  .index("by_thread", ["threadId"])
129
+ .index("by_status_only", ["status"])
108
130
  .index("by_status", ["threadId", "status"])
109
131
  .index("by_thread_tool_call_id", ["threadId", "toolCallId"])
110
132
  .index("by_tool_call_id", ["toolCallId"]),
@@ -1,7 +1,7 @@
1
1
  import type { UIMessageChunk } from "ai";
2
2
  import { type Infer, v } from "convex/values";
3
- import { Logger } from "../logger.js";
4
- import { STREAM_LIVENESS_THRESHOLD_MS } from "../streaming.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { STREAM_LIVENESS_THRESHOLD_MS } from "../utils/streaming.js";
5
5
  import { api, internal } from "./_generated/api";
6
6
  import type { Doc, Id } from "./_generated/dataModel";
7
7
  import { internalMutation, type MutationCtx, mutation, query } from "./_generated/server";
@@ -1,9 +1,10 @@
1
1
  import type { FunctionHandle } from "convex/server";
2
2
  import { v } from "convex/values";
3
- import { Logger } from "../logger.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { api } from "./_generated/api.js";
4
5
  import type { Doc, Id } from "./_generated/dataModel.js";
5
- import { internalQuery, mutation, query } from "./_generated/server.js";
6
- import { vThreadStatus } from "./schema.js";
6
+ import { internalQuery, type MutationCtx, mutation, query } from "./_generated/server.js";
7
+ import { vRetryState, vThreadStatus } from "./schema.js";
7
8
 
8
9
  const logger = new Logger("threads");
9
10
  const FINALIZER_MISMATCH_ALERT_WINDOW_MS = 5 * 60 * 1000;
@@ -64,6 +65,7 @@ export type ThreadDoc = {
64
65
  streamFnHandle: string;
65
66
  workpoolEnqueueAction?: string;
66
67
  toolExecutionWorkpoolEnqueueAction?: string;
68
+ retryState?: Doc<"threads">["retryState"];
67
69
  };
68
70
 
69
71
  function publicThread(thread: Doc<"threads">): ThreadDoc {
@@ -76,6 +78,7 @@ function publicThread(thread: Doc<"threads">): ThreadDoc {
76
78
  streamFnHandle: thread.streamFnHandle,
77
79
  workpoolEnqueueAction: thread.workpoolEnqueueAction,
78
80
  toolExecutionWorkpoolEnqueueAction: thread.toolExecutionWorkpoolEnqueueAction,
81
+ retryState: thread.retryState,
79
82
  };
80
83
  }
81
84
 
@@ -89,6 +92,7 @@ export const vThreadDoc = v.object({
89
92
  streamFnHandle: v.string(),
90
93
  workpoolEnqueueAction: v.optional(v.string()),
91
94
  toolExecutionWorkpoolEnqueueAction: v.optional(v.string()),
95
+ retryState: v.optional(vRetryState),
92
96
  });
93
97
 
94
98
  export const vThreadDocWithStreamFnHandle = v.object({
@@ -104,8 +108,19 @@ export const vThreadDocWithStreamFnHandle = v.object({
104
108
  activeStream: v.optional(v.union(v.id("streams"), v.null())),
105
109
  continue: v.optional(v.boolean()),
106
110
  seq: v.number(),
111
+ retryState: v.optional(vRetryState),
107
112
  });
108
113
 
114
+ async function cancelRetryFnIfPending(ctx: MutationCtx, thread: Doc<"threads">): Promise<void> {
115
+ if (!thread.retryState?.retryFnId) {
116
+ return;
117
+ }
118
+ const retryFn = await ctx.db.system.get(thread.retryState.retryFnId);
119
+ if (retryFn?.state.kind === "pending") {
120
+ await ctx.scheduler.cancel(thread.retryState.retryFnId);
121
+ }
122
+ }
123
+
109
124
  export const create = mutation({
110
125
  args: {
111
126
  streamFnHandle: v.string(),
@@ -149,6 +164,74 @@ export const getWithStreamFnHandle = internalQuery({
149
164
  },
150
165
  });
151
166
 
167
+ export const scheduleRetry = mutation({
168
+ args: {
169
+ threadId: v.id("threads"),
170
+ scope: v.literal("stream"),
171
+ attempt: v.number(),
172
+ maxAttempts: v.number(),
173
+ nextRetryAt: v.number(),
174
+ error: v.string(),
175
+ kind: v.optional(v.string()),
176
+ retryable: v.boolean(),
177
+ requiresExplicitHandling: v.boolean(),
178
+ },
179
+ returns: v.null(),
180
+ handler: async (ctx, args) => {
181
+ const thread = await ctx.db.get(args.threadId);
182
+ if (!thread) {
183
+ throw new Error(`Thread ${args.threadId} not found`);
184
+ }
185
+
186
+ await cancelRetryFnIfPending(ctx, thread);
187
+
188
+ if (thread.stopSignal) {
189
+ await ctx.db.patch(args.threadId, {
190
+ retryState: undefined,
191
+ });
192
+ return null;
193
+ }
194
+
195
+ const delayMs = Math.max(0, args.nextRetryAt - Date.now());
196
+ const retryFnId = await ctx.scheduler.runAfter(delayMs, api.agent.continueStream, {
197
+ threadId: args.threadId,
198
+ });
199
+
200
+ await ctx.db.patch(args.threadId, {
201
+ retryState: {
202
+ scope: args.scope,
203
+ attempt: args.attempt,
204
+ maxAttempts: args.maxAttempts,
205
+ nextRetryAt: args.nextRetryAt,
206
+ error: args.error,
207
+ kind: args.kind,
208
+ retryable: args.retryable,
209
+ requiresExplicitHandling: args.requiresExplicitHandling,
210
+ retryFnId,
211
+ },
212
+ });
213
+ return null;
214
+ },
215
+ });
216
+
217
+ export const clearRetryState = mutation({
218
+ args: {
219
+ threadId: v.id("threads"),
220
+ },
221
+ returns: v.null(),
222
+ handler: async (ctx, args) => {
223
+ const thread = await ctx.db.get(args.threadId);
224
+ if (!thread) {
225
+ throw new Error(`Thread ${args.threadId} not found`);
226
+ }
227
+ await cancelRetryFnIfPending(ctx, thread);
228
+ await ctx.db.patch(args.threadId, {
229
+ retryState: undefined,
230
+ });
231
+ return null;
232
+ },
233
+ });
234
+
152
235
  export const resume = mutation({
153
236
  args: {
154
237
  threadId: v.id("threads"),
@@ -344,6 +427,11 @@ export const setStopSignal = mutation({
344
427
  if (!thread) {
345
428
  throw new Error(`Thread ${args.threadId} not found`);
346
429
  }
430
+ if (args.stopSignal) {
431
+ await cancelRetryFnIfPending(ctx, thread);
432
+ await ctx.db.patch(args.threadId, { stopSignal: args.stopSignal, retryState: undefined });
433
+ return null;
434
+ }
347
435
  await ctx.db.patch(args.threadId, { stopSignal: args.stopSignal });
348
436
  return null;
349
437
  },
@@ -383,6 +471,7 @@ export const remove = mutation({
383
471
  if (!thread) {
384
472
  throw new Error(`Thread ${args.threadId} not found`);
385
473
  }
474
+ await cancelRetryFnIfPending(ctx, thread);
386
475
  // Delete all messages for this thread
387
476
  const messages = await ctx.db
388
477
  .query("messages")