convex-durable-agents 0.2.3 → 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 +23 -4
- 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 -117
- 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 +89 -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/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 +345 -35
- 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 +1 -1
- package/src/client/api.ts +24 -4
- package/src/client/handler.ts +308 -133
- 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 +103 -0
- package/src/component/agent.ts +24 -2
- 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 +421 -44
- 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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { UIMessagePart } from "ai";
|
|
1
|
+
import type { ProviderMetadata, UIMessagePart } from "ai";
|
|
2
2
|
import type { FunctionHandle } from "convex/server";
|
|
3
3
|
import { v } from "convex/values";
|
|
4
|
-
import { Logger } from "../logger.js";
|
|
4
|
+
import { Logger } from "../utils/logger.js";
|
|
5
|
+
import { extractToolErrorInfo, isRetryableDecision, isRetryableToolErrorDefault } from "../utils/retry.js";
|
|
5
6
|
import { api, internal } from "./_generated/api.js";
|
|
6
7
|
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
7
8
|
import { internalAction, internalMutation, type MutationCtx, mutation, query } from "./_generated/server.js";
|
|
@@ -14,6 +15,72 @@ const MINUTE = 60 * SECOND;
|
|
|
14
15
|
const TOOL_CALL_TIMEOUT_MS = 30 * MINUTE;
|
|
15
16
|
const ASYNC_CALLBACK_MAX_ATTEMPTS = 3;
|
|
16
17
|
const ASYNC_CALLBACK_RETRY_BASE_MS = 5 * SECOND;
|
|
18
|
+
const SYNC_TOOL_MAX_ATTEMPTS = 3;
|
|
19
|
+
const SYNC_TOOL_RETRY_INITIAL_BACKOFF_MS = 500;
|
|
20
|
+
|
|
21
|
+
type RetryBackoffPolicy =
|
|
22
|
+
| {
|
|
23
|
+
strategy?: "fixed";
|
|
24
|
+
delayMs: number;
|
|
25
|
+
jitter?: boolean;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
strategy: "exponential";
|
|
29
|
+
initialDelayMs: number;
|
|
30
|
+
multiplier?: number;
|
|
31
|
+
maxDelayMs?: number;
|
|
32
|
+
jitter?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type SyncToolRetryPolicy = {
|
|
36
|
+
enabled: true;
|
|
37
|
+
maxAttempts?: number;
|
|
38
|
+
backoff?: RetryBackoffPolicy;
|
|
39
|
+
shouldRetryError?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function normalizeSyncToolRetryPolicy(value: unknown): SyncToolRetryPolicy | undefined {
|
|
43
|
+
if (value == null || typeof value !== "object") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const obj = value as Record<string, unknown>;
|
|
47
|
+
if (obj.enabled !== true) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
enabled: true,
|
|
52
|
+
maxAttempts: typeof obj.maxAttempts === "number" ? obj.maxAttempts : undefined,
|
|
53
|
+
backoff: (obj.backoff as RetryBackoffPolicy | undefined) ?? undefined,
|
|
54
|
+
shouldRetryError: typeof obj.shouldRetryError === "string" ? obj.shouldRetryError : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clampDelayMs(value: number): number {
|
|
59
|
+
if (!Number.isFinite(value) || value < 0) return 0;
|
|
60
|
+
return Math.floor(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function computeRetryDelayMs(attempt: number, backoff?: RetryBackoffPolicy): number {
|
|
64
|
+
const policy = backoff ?? {
|
|
65
|
+
strategy: "exponential" as const,
|
|
66
|
+
initialDelayMs: SYNC_TOOL_RETRY_INITIAL_BACKOFF_MS,
|
|
67
|
+
multiplier: 2,
|
|
68
|
+
maxDelayMs: 10_000,
|
|
69
|
+
jitter: true,
|
|
70
|
+
};
|
|
71
|
+
if ("delayMs" in policy) {
|
|
72
|
+
const delayMs = clampDelayMs(policy.delayMs);
|
|
73
|
+
if (!policy.jitter) return delayMs;
|
|
74
|
+
return Math.floor(Math.random() * (delayMs + 1));
|
|
75
|
+
}
|
|
76
|
+
const initialDelayMs = clampDelayMs(policy.initialDelayMs);
|
|
77
|
+
const multiplier = Number.isFinite(policy.multiplier ?? 2) ? (policy.multiplier ?? 2) : 2;
|
|
78
|
+
const unbounded = initialDelayMs * multiplier ** Math.max(0, attempt - 1);
|
|
79
|
+
const maxDelayMs = policy.maxDelayMs == null ? unbounded : clampDelayMs(policy.maxDelayMs);
|
|
80
|
+
const delayMs = Math.min(unbounded, maxDelayMs);
|
|
81
|
+
if (!policy.jitter) return delayMs;
|
|
82
|
+
return Math.floor(Math.random() * (delayMs + 1));
|
|
83
|
+
}
|
|
17
84
|
|
|
18
85
|
function normalizeToolCallTimeoutMs(timeoutMs: number): number {
|
|
19
86
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -44,16 +111,29 @@ async function cleanupTimeoutFn(ctx: MutationCtx, toolCall: Doc<"tool_calls">):
|
|
|
44
111
|
}
|
|
45
112
|
}
|
|
46
113
|
|
|
114
|
+
async function cleanupExecutionRetryFn(ctx: MutationCtx, toolCall: Doc<"tool_calls">): Promise<void> {
|
|
115
|
+
if (!toolCall.executionRetryFnId) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
119
|
+
if (retryFn?.state.kind === "pending") {
|
|
120
|
+
await ctx.scheduler.cancel(toolCall.executionRetryFnId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
47
124
|
async function failToolCallIfPending(ctx: MutationCtx, toolCall: Doc<"tool_calls">, error: string): Promise<boolean> {
|
|
48
125
|
const latest = await ctx.db.get(toolCall._id);
|
|
49
126
|
if (!latest || latest.status !== "pending") {
|
|
50
127
|
return false;
|
|
51
128
|
}
|
|
52
129
|
await cleanupTimeoutFn(ctx, latest);
|
|
130
|
+
await cleanupExecutionRetryFn(ctx, latest);
|
|
53
131
|
await ctx.db.patch(latest._id, {
|
|
54
132
|
error,
|
|
55
133
|
status: "failed",
|
|
56
134
|
callbackLastError: error,
|
|
135
|
+
executionRetryFnId: undefined,
|
|
136
|
+
nextRetryAt: undefined,
|
|
57
137
|
});
|
|
58
138
|
|
|
59
139
|
if (latest.saveDelta) {
|
|
@@ -96,6 +176,15 @@ export type ToolCallDoc = {
|
|
|
96
176
|
args: unknown;
|
|
97
177
|
result?: unknown;
|
|
98
178
|
error?: string;
|
|
179
|
+
status: "pending" | "completed" | "failed";
|
|
180
|
+
callbackAttempt?: number;
|
|
181
|
+
callbackLastError?: string;
|
|
182
|
+
handler?: string;
|
|
183
|
+
executionAttempt?: number;
|
|
184
|
+
executionMaxAttempts?: number;
|
|
185
|
+
executionLastError?: string;
|
|
186
|
+
executionRetryPolicy?: unknown;
|
|
187
|
+
nextRetryAt?: number;
|
|
99
188
|
};
|
|
100
189
|
|
|
101
190
|
function publicToolCall(toolCall: Doc<"tool_calls">): ToolCallDoc {
|
|
@@ -109,6 +198,15 @@ function publicToolCall(toolCall: Doc<"tool_calls">): ToolCallDoc {
|
|
|
109
198
|
args: toolCall.args,
|
|
110
199
|
result: toolCall.result,
|
|
111
200
|
error: toolCall.error,
|
|
201
|
+
status: toolCall.status,
|
|
202
|
+
callbackAttempt: toolCall.callbackAttempt,
|
|
203
|
+
callbackLastError: toolCall.callbackLastError,
|
|
204
|
+
handler: toolCall.handler,
|
|
205
|
+
executionAttempt: toolCall.executionAttempt,
|
|
206
|
+
executionMaxAttempts: toolCall.executionMaxAttempts,
|
|
207
|
+
executionLastError: toolCall.executionLastError,
|
|
208
|
+
executionRetryPolicy: toolCall.executionRetryPolicy,
|
|
209
|
+
nextRetryAt: toolCall.nextRetryAt,
|
|
112
210
|
};
|
|
113
211
|
}
|
|
114
212
|
|
|
@@ -123,8 +221,71 @@ export const vToolCallDoc = v.object({
|
|
|
123
221
|
args: v.any(),
|
|
124
222
|
result: v.optional(v.any()),
|
|
125
223
|
error: v.optional(v.string()),
|
|
224
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed")),
|
|
225
|
+
callbackAttempt: v.optional(v.number()),
|
|
226
|
+
callbackLastError: v.optional(v.string()),
|
|
227
|
+
handler: v.optional(v.string()),
|
|
228
|
+
executionAttempt: v.optional(v.number()),
|
|
229
|
+
executionMaxAttempts: v.optional(v.number()),
|
|
230
|
+
executionLastError: v.optional(v.string()),
|
|
231
|
+
executionRetryPolicy: v.optional(v.any()),
|
|
232
|
+
nextRetryAt: v.optional(v.number()),
|
|
126
233
|
});
|
|
127
234
|
|
|
235
|
+
type CreateToolCallArgs = {
|
|
236
|
+
threadId: Id<"threads">;
|
|
237
|
+
msgId: string;
|
|
238
|
+
toolCallId: string;
|
|
239
|
+
toolName: string;
|
|
240
|
+
callback?: string;
|
|
241
|
+
handler?: string;
|
|
242
|
+
retry?: SyncToolRetryPolicy;
|
|
243
|
+
args: unknown;
|
|
244
|
+
saveDelta: boolean;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
async function createToolCallRecord(ctx: MutationCtx, args: CreateToolCallArgs): Promise<Doc<"tool_calls">> {
|
|
248
|
+
const existingToolCall = await ctx.db
|
|
249
|
+
.query("tool_calls")
|
|
250
|
+
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
251
|
+
.first();
|
|
252
|
+
if (existingToolCall) {
|
|
253
|
+
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
254
|
+
}
|
|
255
|
+
logger.debug(
|
|
256
|
+
`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`,
|
|
257
|
+
);
|
|
258
|
+
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
259
|
+
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
260
|
+
threadId: args.threadId,
|
|
261
|
+
msgId: args.msgId,
|
|
262
|
+
toolCallId: args.toolCallId,
|
|
263
|
+
toolName: args.toolName,
|
|
264
|
+
callback: args.callback,
|
|
265
|
+
handler: args.handler,
|
|
266
|
+
callbackAttempt: args.callback ? 0 : undefined,
|
|
267
|
+
executionAttempt: args.retry ? 0 : undefined,
|
|
268
|
+
executionMaxAttempts: args.retry?.maxAttempts,
|
|
269
|
+
executionRetryPolicy: args.retry,
|
|
270
|
+
args: args.args,
|
|
271
|
+
saveDelta: args.saveDelta,
|
|
272
|
+
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
273
|
+
expiresAt,
|
|
274
|
+
status: "pending",
|
|
275
|
+
});
|
|
276
|
+
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
277
|
+
threadId: args.threadId,
|
|
278
|
+
toolCallId: args.toolCallId,
|
|
279
|
+
});
|
|
280
|
+
await ctx.db.patch(toolCallId, { timeoutFnId });
|
|
281
|
+
|
|
282
|
+
const toolCall = await ctx.db.get(toolCallId);
|
|
283
|
+
if (!toolCall) {
|
|
284
|
+
throw new Error(`Tool call ${toolCallId} not found after creation`);
|
|
285
|
+
}
|
|
286
|
+
return toolCall;
|
|
287
|
+
}
|
|
288
|
+
|
|
128
289
|
export const create = mutation({
|
|
129
290
|
args: {
|
|
130
291
|
threadId: v.id("threads"),
|
|
@@ -132,43 +293,25 @@ export const create = mutation({
|
|
|
132
293
|
toolCallId: v.string(),
|
|
133
294
|
toolName: v.string(),
|
|
134
295
|
callback: v.optional(v.string()),
|
|
296
|
+
handler: v.optional(v.string()),
|
|
297
|
+
retry: v.optional(v.any()),
|
|
135
298
|
args: v.any(),
|
|
136
299
|
saveDelta: v.boolean(),
|
|
137
300
|
},
|
|
138
301
|
returns: vToolCallDoc,
|
|
139
302
|
handler: async (ctx, args) => {
|
|
140
|
-
const
|
|
141
|
-
.query("tool_calls")
|
|
142
|
-
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
143
|
-
.first();
|
|
144
|
-
if (existingToolCall) {
|
|
145
|
-
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
146
|
-
}
|
|
147
|
-
logger.debug(
|
|
148
|
-
`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`,
|
|
149
|
-
);
|
|
150
|
-
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
151
|
-
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
303
|
+
const toolCall = await createToolCallRecord(ctx, {
|
|
152
304
|
threadId: args.threadId,
|
|
153
305
|
msgId: args.msgId,
|
|
154
306
|
toolCallId: args.toolCallId,
|
|
155
307
|
toolName: args.toolName,
|
|
156
308
|
callback: args.callback,
|
|
157
|
-
|
|
309
|
+
handler: args.handler,
|
|
310
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
158
311
|
args: args.args,
|
|
159
312
|
saveDelta: args.saveDelta,
|
|
160
|
-
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
161
|
-
expiresAt,
|
|
162
|
-
status: "pending",
|
|
163
|
-
});
|
|
164
|
-
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
165
|
-
threadId: args.threadId,
|
|
166
|
-
toolCallId: args.toolCallId,
|
|
167
313
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const toolCall = await ctx.db.get(toolCallId);
|
|
171
|
-
return publicToolCall(toolCall!);
|
|
314
|
+
return publicToolCall(toolCall);
|
|
172
315
|
},
|
|
173
316
|
});
|
|
174
317
|
|
|
@@ -189,7 +332,13 @@ export const setResult = mutation({
|
|
|
189
332
|
}
|
|
190
333
|
logger.debug(`setResult: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}`);
|
|
191
334
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
192
|
-
await ctx
|
|
335
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
336
|
+
await ctx.db.patch(args.id, {
|
|
337
|
+
result: args.result,
|
|
338
|
+
status: "completed",
|
|
339
|
+
executionRetryFnId: undefined,
|
|
340
|
+
nextRetryAt: undefined,
|
|
341
|
+
});
|
|
193
342
|
return true;
|
|
194
343
|
},
|
|
195
344
|
});
|
|
@@ -211,7 +360,14 @@ export const setError = mutation({
|
|
|
211
360
|
}
|
|
212
361
|
logger.debug(`setError: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}, error=${args.error}`);
|
|
213
362
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
214
|
-
await ctx
|
|
363
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
364
|
+
await ctx.db.patch(args.id, {
|
|
365
|
+
error: args.error,
|
|
366
|
+
status: "failed",
|
|
367
|
+
callbackLastError: args.error,
|
|
368
|
+
executionRetryFnId: undefined,
|
|
369
|
+
nextRetryAt: undefined,
|
|
370
|
+
});
|
|
215
371
|
return true;
|
|
216
372
|
},
|
|
217
373
|
});
|
|
@@ -320,6 +476,7 @@ export const scheduleToolCall = mutation({
|
|
|
320
476
|
toolName: v.string(),
|
|
321
477
|
args: v.any(),
|
|
322
478
|
handler: v.string(),
|
|
479
|
+
retry: v.optional(v.any()),
|
|
323
480
|
saveDelta: v.boolean(),
|
|
324
481
|
},
|
|
325
482
|
returns: v.null(),
|
|
@@ -333,11 +490,13 @@ export const scheduleToolCall = mutation({
|
|
|
333
490
|
logger.debug(`scheduleToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
334
491
|
|
|
335
492
|
// Create the tool call record
|
|
336
|
-
await ctx
|
|
493
|
+
await createToolCallRecord(ctx, {
|
|
337
494
|
threadId: args.threadId,
|
|
338
495
|
msgId: args.msgId,
|
|
339
496
|
toolCallId: args.toolCallId,
|
|
340
497
|
toolName: args.toolName,
|
|
498
|
+
handler: args.handler,
|
|
499
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
341
500
|
args: args.args,
|
|
342
501
|
saveDelta: args.saveDelta,
|
|
343
502
|
});
|
|
@@ -382,7 +541,7 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
382
541
|
logger.debug(`scheduleAsyncToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
383
542
|
|
|
384
543
|
// Create the tool call record (will remain pending until addToolResult is called)
|
|
385
|
-
await ctx
|
|
544
|
+
await createToolCallRecord(ctx, {
|
|
386
545
|
threadId: args.threadId,
|
|
387
546
|
toolCallId: args.toolCallId,
|
|
388
547
|
msgId: args.msgId,
|
|
@@ -412,6 +571,139 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
412
571
|
},
|
|
413
572
|
});
|
|
414
573
|
|
|
574
|
+
export const updateExecutionRetryState = internalMutation({
|
|
575
|
+
args: {
|
|
576
|
+
threadId: v.id("threads"),
|
|
577
|
+
toolCallId: v.string(),
|
|
578
|
+
executionAttempt: v.number(),
|
|
579
|
+
executionLastError: v.optional(v.string()),
|
|
580
|
+
nextRetryAt: v.optional(v.number()),
|
|
581
|
+
executionRetryFnId: v.optional(v.id("_scheduled_functions")),
|
|
582
|
+
clearNextRetryAt: v.optional(v.boolean()),
|
|
583
|
+
clearExecutionRetryFnId: v.optional(v.boolean()),
|
|
584
|
+
},
|
|
585
|
+
returns: v.null(),
|
|
586
|
+
handler: async (ctx, args) => {
|
|
587
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
588
|
+
threadId: args.threadId,
|
|
589
|
+
toolCallId: args.toolCallId,
|
|
590
|
+
});
|
|
591
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const patch: {
|
|
596
|
+
executionAttempt: number;
|
|
597
|
+
executionLastError?: string;
|
|
598
|
+
nextRetryAt?: number | undefined;
|
|
599
|
+
executionRetryFnId?: Id<"_scheduled_functions"> | undefined;
|
|
600
|
+
} = {
|
|
601
|
+
executionAttempt: args.executionAttempt,
|
|
602
|
+
};
|
|
603
|
+
if (args.executionLastError !== undefined) {
|
|
604
|
+
patch.executionLastError = args.executionLastError;
|
|
605
|
+
}
|
|
606
|
+
if (args.nextRetryAt !== undefined) {
|
|
607
|
+
patch.nextRetryAt = args.nextRetryAt;
|
|
608
|
+
}
|
|
609
|
+
if (args.executionRetryFnId !== undefined) {
|
|
610
|
+
patch.executionRetryFnId = args.executionRetryFnId;
|
|
611
|
+
}
|
|
612
|
+
if (args.clearNextRetryAt) {
|
|
613
|
+
patch.nextRetryAt = undefined;
|
|
614
|
+
}
|
|
615
|
+
if (args.clearExecutionRetryFnId) {
|
|
616
|
+
patch.executionRetryFnId = undefined;
|
|
617
|
+
}
|
|
618
|
+
await ctx.db.patch(toolCall._id, patch);
|
|
619
|
+
return null;
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
export const scheduleExecutionRetry = internalMutation({
|
|
624
|
+
args: {
|
|
625
|
+
threadId: v.id("threads"),
|
|
626
|
+
toolCallId: v.string(),
|
|
627
|
+
handler: v.string(),
|
|
628
|
+
executionAttempt: v.number(),
|
|
629
|
+
executionLastError: v.string(),
|
|
630
|
+
nextRetryAt: v.number(),
|
|
631
|
+
},
|
|
632
|
+
returns: v.null(),
|
|
633
|
+
handler: async (ctx, args) => {
|
|
634
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
635
|
+
threadId: args.threadId,
|
|
636
|
+
toolCallId: args.toolCallId,
|
|
637
|
+
});
|
|
638
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
643
|
+
const delayMs = Math.max(0, args.nextRetryAt - Date.now());
|
|
644
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
645
|
+
threadId: args.threadId,
|
|
646
|
+
toolCallId: args.toolCallId,
|
|
647
|
+
handler: args.handler,
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await ctx.db.patch(toolCall._id, {
|
|
651
|
+
executionAttempt: args.executionAttempt,
|
|
652
|
+
executionLastError: args.executionLastError,
|
|
653
|
+
nextRetryAt: args.nextRetryAt,
|
|
654
|
+
executionRetryFnId,
|
|
655
|
+
});
|
|
656
|
+
return null;
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
export const resumePendingSyncToolExecutions = mutation({
|
|
661
|
+
args: {
|
|
662
|
+
limit: v.optional(v.number()),
|
|
663
|
+
},
|
|
664
|
+
returns: v.number(),
|
|
665
|
+
handler: async (ctx, args) => {
|
|
666
|
+
const limit = Math.max(1, Math.floor(args.limit ?? 100));
|
|
667
|
+
const pending = await ctx.db
|
|
668
|
+
.query("tool_calls")
|
|
669
|
+
.withIndex("by_status_only", (q) => q.eq("status", "pending"))
|
|
670
|
+
.take(limit * 2);
|
|
671
|
+
|
|
672
|
+
let resumed = 0;
|
|
673
|
+
const now = Date.now();
|
|
674
|
+
for (const toolCall of pending) {
|
|
675
|
+
if (resumed >= limit) {
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
if (!toolCall.handler) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (toolCall.executionRetryFnId) {
|
|
682
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
683
|
+
if (retryFn?.state.kind === "pending") {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const nextRetryAt = toolCall.nextRetryAt ?? now;
|
|
688
|
+
const delayMs = Math.max(0, nextRetryAt - now);
|
|
689
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
690
|
+
threadId: toolCall.threadId,
|
|
691
|
+
toolCallId: toolCall.toolCallId,
|
|
692
|
+
handler: toolCall.handler,
|
|
693
|
+
});
|
|
694
|
+
await ctx.db.patch(toolCall._id, {
|
|
695
|
+
executionRetryFnId,
|
|
696
|
+
});
|
|
697
|
+
resumed += 1;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (resumed > 0) {
|
|
701
|
+
logger.warn(`resumePendingSyncToolExecutions: resumed ${resumed} pending sync tool call(s)`);
|
|
702
|
+
}
|
|
703
|
+
return resumed;
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
415
707
|
/**
|
|
416
708
|
* Execute a tool call
|
|
417
709
|
*/
|
|
@@ -423,8 +715,6 @@ export const executeToolCall = internalAction({
|
|
|
423
715
|
},
|
|
424
716
|
returns: v.null(),
|
|
425
717
|
handler: async (ctx, args) => {
|
|
426
|
-
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
427
|
-
|
|
428
718
|
// Get the tool call record
|
|
429
719
|
const toolCall = await ctx.runQuery(api.tool_calls.getByToolCallId, {
|
|
430
720
|
threadId: args.threadId,
|
|
@@ -434,34 +724,120 @@ export const executeToolCall = internalAction({
|
|
|
434
724
|
if (!toolCall) {
|
|
435
725
|
throw new Error(`Tool call ${args.toolCallId} not found`);
|
|
436
726
|
}
|
|
727
|
+
if (toolCall.status !== "pending") {
|
|
728
|
+
logger.debug(`executeToolCall: skipping callId=${args.toolCallId}, status already terminal (${toolCall.status})`);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
437
731
|
logger.debug(`executeToolCall: tool=${toolCall.toolName}`);
|
|
438
732
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
733
|
+
const thread = await ctx.runQuery(api.threads.get, {
|
|
734
|
+
threadId: args.threadId,
|
|
735
|
+
});
|
|
736
|
+
if (!thread) {
|
|
737
|
+
throw new Error(`Thread ${args.threadId} not found`);
|
|
738
|
+
}
|
|
739
|
+
if (thread.stopSignal || thread.status === "stopped") {
|
|
740
|
+
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
741
|
+
threadId: args.threadId,
|
|
742
|
+
toolCallId: toolCall.toolCallId,
|
|
743
|
+
error: "Tool execution cancelled because the thread was stopped",
|
|
744
|
+
});
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
445
747
|
|
|
748
|
+
const handler = toolCall.handler ?? args.handler;
|
|
749
|
+
const retryPolicy = normalizeSyncToolRetryPolicy(toolCall.executionRetryPolicy);
|
|
750
|
+
const retryEnabled = retryPolicy?.enabled === true;
|
|
751
|
+
const maxAttempts = retryEnabled ? Math.max(1, retryPolicy.maxAttempts ?? SYNC_TOOL_MAX_ATTEMPTS) : 1;
|
|
752
|
+
const toolArgs = typeof toolCall.args === "object" && toolCall.args !== null ? toolCall.args : {};
|
|
753
|
+
const attempt = Math.max(1, (toolCall.executionAttempt ?? 0) + 1);
|
|
754
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
755
|
+
threadId: args.threadId,
|
|
756
|
+
toolCallId: args.toolCallId,
|
|
757
|
+
executionAttempt: attempt,
|
|
758
|
+
clearNextRetryAt: true,
|
|
759
|
+
clearExecutionRetryFnId: true,
|
|
760
|
+
});
|
|
761
|
+
logger.debug(
|
|
762
|
+
`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}, attempt=${attempt}/${maxAttempts}`,
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
try {
|
|
446
766
|
logger.debug(`executeToolCall: invoking handler for callId=${args.toolCallId}`);
|
|
447
|
-
const result = await ctx.runAction(
|
|
767
|
+
const result = await ctx.runAction(handler as FunctionHandle<"action">, toolArgs as Record<string, unknown>);
|
|
448
768
|
logger.debug(`executeToolCall: handler succeeded for callId=${args.toolCallId}`);
|
|
449
769
|
await ctx.runMutation(api.tool_calls.addToolResult, {
|
|
450
770
|
threadId: args.threadId,
|
|
451
771
|
result,
|
|
452
772
|
toolCallId: toolCall.toolCallId,
|
|
453
773
|
});
|
|
774
|
+
return null;
|
|
454
775
|
} catch (e) {
|
|
455
|
-
const
|
|
456
|
-
|
|
776
|
+
const errorInfo = extractToolErrorInfo(e);
|
|
777
|
+
const error = errorInfo.message;
|
|
778
|
+
logger.debug(`executeToolCall: handler failed for callId=${args.toolCallId} (attempt=${attempt}): ${error}`);
|
|
779
|
+
|
|
780
|
+
let retryable = false;
|
|
781
|
+
if (retryEnabled) {
|
|
782
|
+
if (retryPolicy.shouldRetryError) {
|
|
783
|
+
try {
|
|
784
|
+
const decision = await ctx.runAction(retryPolicy.shouldRetryError as FunctionHandle<"action">, {
|
|
785
|
+
threadId: args.threadId,
|
|
786
|
+
toolCallId: args.toolCallId,
|
|
787
|
+
toolName: toolCall.toolName,
|
|
788
|
+
args: toolCall.args,
|
|
789
|
+
error,
|
|
790
|
+
attempt,
|
|
791
|
+
maxAttempts,
|
|
792
|
+
});
|
|
793
|
+
retryable = isRetryableDecision(decision);
|
|
794
|
+
} catch (classifierError) {
|
|
795
|
+
logger.warn(
|
|
796
|
+
`executeToolCall: shouldRetryError failed for callId=${args.toolCallId}, falling back to default classifier: ${
|
|
797
|
+
classifierError instanceof Error ? classifierError.message : String(classifierError)
|
|
798
|
+
}`,
|
|
799
|
+
);
|
|
800
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (retryEnabled && retryable && attempt < maxAttempts) {
|
|
808
|
+
const delayMs = computeRetryDelayMs(attempt, retryPolicy.backoff);
|
|
809
|
+
const nextRetryAt = Date.now() + delayMs;
|
|
810
|
+
await ctx.runMutation(internal.tool_calls.scheduleExecutionRetry, {
|
|
811
|
+
threadId: args.threadId,
|
|
812
|
+
toolCallId: args.toolCallId,
|
|
813
|
+
handler,
|
|
814
|
+
executionAttempt: attempt,
|
|
815
|
+
executionLastError: error,
|
|
816
|
+
nextRetryAt,
|
|
817
|
+
});
|
|
818
|
+
logger.warn(
|
|
819
|
+
`executeToolCall: scheduled retry for callId=${args.toolCallId} in ${delayMs}ms (attempt ${
|
|
820
|
+
attempt + 1
|
|
821
|
+
}/${maxAttempts})`,
|
|
822
|
+
);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
827
|
+
threadId: args.threadId,
|
|
828
|
+
toolCallId: args.toolCallId,
|
|
829
|
+
executionAttempt: attempt,
|
|
830
|
+
executionLastError: error,
|
|
831
|
+
clearNextRetryAt: true,
|
|
832
|
+
clearExecutionRetryFnId: true,
|
|
833
|
+
});
|
|
457
834
|
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
458
835
|
threadId: args.threadId,
|
|
459
836
|
error,
|
|
460
837
|
toolCallId: toolCall.toolCallId,
|
|
461
838
|
});
|
|
839
|
+
return null;
|
|
462
840
|
}
|
|
463
|
-
|
|
464
|
-
return null;
|
|
465
841
|
},
|
|
466
842
|
});
|
|
467
843
|
|
|
@@ -623,6 +999,7 @@ export const onToolComplete = internalMutation({
|
|
|
623
999
|
status: "stopped",
|
|
624
1000
|
activeStream: null,
|
|
625
1001
|
continue: false,
|
|
1002
|
+
retryState: undefined,
|
|
626
1003
|
});
|
|
627
1004
|
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
628
1005
|
await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
|
|
@@ -695,7 +1072,7 @@ export function createToolOutcomePart(
|
|
|
695
1072
|
};
|
|
696
1073
|
|
|
697
1074
|
if (pendingPart?.callProviderMetadata != null) {
|
|
698
|
-
part.callProviderMetadata = pendingPart.callProviderMetadata as
|
|
1075
|
+
part.callProviderMetadata = pendingPart.callProviderMetadata as ProviderMetadata;
|
|
699
1076
|
}
|
|
700
1077
|
return part;
|
|
701
1078
|
}
|