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.
- package/README.md +81 -7
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +26 -6
- package/dist/client/api.js.map +1 -1
- package/dist/client/handler.d.ts +20 -0
- package/dist/client/handler.d.ts.map +1 -1
- package/dist/client/handler.js +239 -116
- package/dist/client/handler.js.map +1 -1
- package/dist/client/streamer.js +1 -1
- package/dist/client/streamer.js.map +1 -1
- package/dist/client/tools.d.ts +17 -0
- package/dist/client/tools.d.ts.map +1 -1
- package/dist/client/tools.js +16 -0
- package/dist/client/tools.js.map +1 -1
- package/dist/client/types.d.ts +75 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js +11 -0
- package/dist/client/types.js.map +1 -1
- package/dist/component/_generated/component.d.ts +90 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/agent.d.ts.map +1 -1
- package/dist/component/agent.js +21 -2
- package/dist/component/agent.js.map +1 -1
- package/dist/component/messages.d.ts +1 -0
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +9 -0
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +70 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +21 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.js +2 -2
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +92 -2
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +83 -2
- package/dist/component/threads.js.map +1 -1
- package/dist/component/tool_calls.d.ts +54 -3
- package/dist/component/tool_calls.d.ts.map +1 -1
- package/dist/component/tool_calls.js +358 -40
- package/dist/component/tool_calls.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +69 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +404 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/streaming.d.ts +4 -0
- package/dist/utils/streaming.d.ts.map +1 -0
- package/dist/utils/streaming.js +4 -0
- package/dist/utils/streaming.js.map +1 -0
- package/package.json +2 -2
- package/src/client/api.ts +26 -7
- package/src/client/handler.ts +308 -132
- package/src/client/streamer.ts +1 -1
- package/src/client/tools.ts +43 -1
- package/src/client/types.ts +60 -0
- package/src/component/_generated/component.ts +104 -0
- package/src/component/agent.ts +24 -2
- package/src/component/messages.ts +9 -0
- package/src/component/schema.ts +22 -0
- package/src/component/streams.ts +2 -2
- package/src/component/threads.ts +92 -3
- package/src/component/tool_calls.ts +433 -49
- package/src/utils/retry.ts +528 -0
- package/src/{streaming.ts → utils/streaming.ts} +2 -3
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/streaming.d.ts +0 -3
- package/dist/streaming.d.ts.map +0 -1
- package/dist/streaming.js +0 -4
- package/dist/streaming.js.map +0 -1
- /package/dist/{logger.d.ts → utils/logger.d.ts} +0 -0
- /package/dist/{logger.js → utils/logger.js} +0 -0
- /package/src/{logger.ts → utils/logger.ts} +0 -0
package/src/client/types.ts
CHANGED
|
@@ -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;
|
package/src/component/agent.ts
CHANGED
|
@@ -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")
|
package/src/component/schema.ts
CHANGED
|
@@ -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"]),
|
package/src/component/streams.ts
CHANGED
|
@@ -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";
|
package/src/component/threads.ts
CHANGED
|
@@ -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")
|