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
|
@@ -1,11 +1,13 @@
|
|
|
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";
|
|
8
9
|
import { enqueueAction } from "./agent.js";
|
|
10
|
+
import { isAlive } from "./streams.js";
|
|
9
11
|
|
|
10
12
|
const logger = new Logger("tool_calls");
|
|
11
13
|
const SECOND = 1000;
|
|
@@ -13,6 +15,72 @@ const MINUTE = 60 * SECOND;
|
|
|
13
15
|
const TOOL_CALL_TIMEOUT_MS = 30 * MINUTE;
|
|
14
16
|
const ASYNC_CALLBACK_MAX_ATTEMPTS = 3;
|
|
15
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
|
+
}
|
|
16
84
|
|
|
17
85
|
function normalizeToolCallTimeoutMs(timeoutMs: number): number {
|
|
18
86
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -43,16 +111,29 @@ async function cleanupTimeoutFn(ctx: MutationCtx, toolCall: Doc<"tool_calls">):
|
|
|
43
111
|
}
|
|
44
112
|
}
|
|
45
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
|
+
|
|
46
124
|
async function failToolCallIfPending(ctx: MutationCtx, toolCall: Doc<"tool_calls">, error: string): Promise<boolean> {
|
|
47
125
|
const latest = await ctx.db.get(toolCall._id);
|
|
48
126
|
if (!latest || latest.status !== "pending") {
|
|
49
127
|
return false;
|
|
50
128
|
}
|
|
51
129
|
await cleanupTimeoutFn(ctx, latest);
|
|
130
|
+
await cleanupExecutionRetryFn(ctx, latest);
|
|
52
131
|
await ctx.db.patch(latest._id, {
|
|
53
132
|
error,
|
|
54
133
|
status: "failed",
|
|
55
134
|
callbackLastError: error,
|
|
135
|
+
executionRetryFnId: undefined,
|
|
136
|
+
nextRetryAt: undefined,
|
|
56
137
|
});
|
|
57
138
|
|
|
58
139
|
if (latest.saveDelta) {
|
|
@@ -95,6 +176,15 @@ export type ToolCallDoc = {
|
|
|
95
176
|
args: unknown;
|
|
96
177
|
result?: unknown;
|
|
97
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;
|
|
98
188
|
};
|
|
99
189
|
|
|
100
190
|
function publicToolCall(toolCall: Doc<"tool_calls">): ToolCallDoc {
|
|
@@ -108,6 +198,15 @@ function publicToolCall(toolCall: Doc<"tool_calls">): ToolCallDoc {
|
|
|
108
198
|
args: toolCall.args,
|
|
109
199
|
result: toolCall.result,
|
|
110
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,
|
|
111
210
|
};
|
|
112
211
|
}
|
|
113
212
|
|
|
@@ -122,8 +221,71 @@ export const vToolCallDoc = v.object({
|
|
|
122
221
|
args: v.any(),
|
|
123
222
|
result: v.optional(v.any()),
|
|
124
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()),
|
|
125
233
|
});
|
|
126
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
|
+
|
|
127
289
|
export const create = mutation({
|
|
128
290
|
args: {
|
|
129
291
|
threadId: v.id("threads"),
|
|
@@ -131,43 +293,25 @@ export const create = mutation({
|
|
|
131
293
|
toolCallId: v.string(),
|
|
132
294
|
toolName: v.string(),
|
|
133
295
|
callback: v.optional(v.string()),
|
|
296
|
+
handler: v.optional(v.string()),
|
|
297
|
+
retry: v.optional(v.any()),
|
|
134
298
|
args: v.any(),
|
|
135
299
|
saveDelta: v.boolean(),
|
|
136
300
|
},
|
|
137
301
|
returns: vToolCallDoc,
|
|
138
302
|
handler: async (ctx, args) => {
|
|
139
|
-
const
|
|
140
|
-
.query("tool_calls")
|
|
141
|
-
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
142
|
-
.first();
|
|
143
|
-
if (existingToolCall) {
|
|
144
|
-
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
145
|
-
}
|
|
146
|
-
logger.debug(
|
|
147
|
-
`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`,
|
|
148
|
-
);
|
|
149
|
-
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
150
|
-
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
303
|
+
const toolCall = await createToolCallRecord(ctx, {
|
|
151
304
|
threadId: args.threadId,
|
|
152
305
|
msgId: args.msgId,
|
|
153
306
|
toolCallId: args.toolCallId,
|
|
154
307
|
toolName: args.toolName,
|
|
155
308
|
callback: args.callback,
|
|
156
|
-
|
|
309
|
+
handler: args.handler,
|
|
310
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
157
311
|
args: args.args,
|
|
158
312
|
saveDelta: args.saveDelta,
|
|
159
|
-
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
160
|
-
expiresAt,
|
|
161
|
-
status: "pending",
|
|
162
|
-
});
|
|
163
|
-
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
164
|
-
threadId: args.threadId,
|
|
165
|
-
toolCallId: args.toolCallId,
|
|
166
313
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const toolCall = await ctx.db.get(toolCallId);
|
|
170
|
-
return publicToolCall(toolCall!);
|
|
314
|
+
return publicToolCall(toolCall);
|
|
171
315
|
},
|
|
172
316
|
});
|
|
173
317
|
|
|
@@ -188,7 +332,13 @@ export const setResult = mutation({
|
|
|
188
332
|
}
|
|
189
333
|
logger.debug(`setResult: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}`);
|
|
190
334
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
191
|
-
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
|
+
});
|
|
192
342
|
return true;
|
|
193
343
|
},
|
|
194
344
|
});
|
|
@@ -210,7 +360,14 @@ export const setError = mutation({
|
|
|
210
360
|
}
|
|
211
361
|
logger.debug(`setError: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}, error=${args.error}`);
|
|
212
362
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
213
|
-
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
|
+
});
|
|
214
371
|
return true;
|
|
215
372
|
},
|
|
216
373
|
});
|
|
@@ -319,6 +476,7 @@ export const scheduleToolCall = mutation({
|
|
|
319
476
|
toolName: v.string(),
|
|
320
477
|
args: v.any(),
|
|
321
478
|
handler: v.string(),
|
|
479
|
+
retry: v.optional(v.any()),
|
|
322
480
|
saveDelta: v.boolean(),
|
|
323
481
|
},
|
|
324
482
|
returns: v.null(),
|
|
@@ -332,11 +490,13 @@ export const scheduleToolCall = mutation({
|
|
|
332
490
|
logger.debug(`scheduleToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
333
491
|
|
|
334
492
|
// Create the tool call record
|
|
335
|
-
await ctx
|
|
493
|
+
await createToolCallRecord(ctx, {
|
|
336
494
|
threadId: args.threadId,
|
|
337
495
|
msgId: args.msgId,
|
|
338
496
|
toolCallId: args.toolCallId,
|
|
339
497
|
toolName: args.toolName,
|
|
498
|
+
handler: args.handler,
|
|
499
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
340
500
|
args: args.args,
|
|
341
501
|
saveDelta: args.saveDelta,
|
|
342
502
|
});
|
|
@@ -381,7 +541,7 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
381
541
|
logger.debug(`scheduleAsyncToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
382
542
|
|
|
383
543
|
// Create the tool call record (will remain pending until addToolResult is called)
|
|
384
|
-
await ctx
|
|
544
|
+
await createToolCallRecord(ctx, {
|
|
385
545
|
threadId: args.threadId,
|
|
386
546
|
toolCallId: args.toolCallId,
|
|
387
547
|
msgId: args.msgId,
|
|
@@ -411,6 +571,139 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
411
571
|
},
|
|
412
572
|
});
|
|
413
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
|
+
|
|
414
707
|
/**
|
|
415
708
|
* Execute a tool call
|
|
416
709
|
*/
|
|
@@ -422,8 +715,6 @@ export const executeToolCall = internalAction({
|
|
|
422
715
|
},
|
|
423
716
|
returns: v.null(),
|
|
424
717
|
handler: async (ctx, args) => {
|
|
425
|
-
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
426
|
-
|
|
427
718
|
// Get the tool call record
|
|
428
719
|
const toolCall = await ctx.runQuery(api.tool_calls.getByToolCallId, {
|
|
429
720
|
threadId: args.threadId,
|
|
@@ -433,34 +724,120 @@ export const executeToolCall = internalAction({
|
|
|
433
724
|
if (!toolCall) {
|
|
434
725
|
throw new Error(`Tool call ${args.toolCallId} not found`);
|
|
435
726
|
}
|
|
727
|
+
if (toolCall.status !== "pending") {
|
|
728
|
+
logger.debug(`executeToolCall: skipping callId=${args.toolCallId}, status already terminal (${toolCall.status})`);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
436
731
|
logger.debug(`executeToolCall: tool=${toolCall.toolName}`);
|
|
437
732
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
}
|
|
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
|
+
);
|
|
444
764
|
|
|
765
|
+
try {
|
|
445
766
|
logger.debug(`executeToolCall: invoking handler for callId=${args.toolCallId}`);
|
|
446
|
-
const result = await ctx.runAction(
|
|
767
|
+
const result = await ctx.runAction(handler as FunctionHandle<"action">, toolArgs as Record<string, unknown>);
|
|
447
768
|
logger.debug(`executeToolCall: handler succeeded for callId=${args.toolCallId}`);
|
|
448
769
|
await ctx.runMutation(api.tool_calls.addToolResult, {
|
|
449
770
|
threadId: args.threadId,
|
|
450
771
|
result,
|
|
451
772
|
toolCallId: toolCall.toolCallId,
|
|
452
773
|
});
|
|
774
|
+
return null;
|
|
453
775
|
} catch (e) {
|
|
454
|
-
const
|
|
455
|
-
|
|
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
|
+
});
|
|
456
834
|
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
457
835
|
threadId: args.threadId,
|
|
458
836
|
error,
|
|
459
837
|
toolCallId: toolCall.toolCallId,
|
|
460
838
|
});
|
|
839
|
+
return null;
|
|
461
840
|
}
|
|
462
|
-
|
|
463
|
-
return null;
|
|
464
841
|
},
|
|
465
842
|
});
|
|
466
843
|
|
|
@@ -622,6 +999,7 @@ export const onToolComplete = internalMutation({
|
|
|
622
999
|
status: "stopped",
|
|
623
1000
|
activeStream: null,
|
|
624
1001
|
continue: false,
|
|
1002
|
+
retryState: undefined,
|
|
625
1003
|
});
|
|
626
1004
|
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
627
1005
|
await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
|
|
@@ -648,11 +1026,17 @@ export const onToolComplete = internalMutation({
|
|
|
648
1026
|
});
|
|
649
1027
|
|
|
650
1028
|
if (pending.length === 0) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
1029
|
+
const activeStream = thread.activeStream ? await ctx.db.get(thread.activeStream) : null;
|
|
1030
|
+
if (isAlive(activeStream)) {
|
|
1031
|
+
logger.debug("onToolComplete: all tool calls complete, active stream still alive, setting continue flag");
|
|
1032
|
+
await ctx.db.patch(args.threadId, { continue: true });
|
|
1033
|
+
} else {
|
|
1034
|
+
logger.debug("onToolComplete: all tool calls complete, scheduling continueStream");
|
|
1035
|
+
// All tool calls complete - schedule continuation
|
|
1036
|
+
await ctx.scheduler.runAfter(0, api.agent.continueStream, {
|
|
1037
|
+
threadId: args.threadId,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
656
1040
|
} else {
|
|
657
1041
|
logger.debug(`onToolComplete: ${pending.length} tool calls still pending`);
|
|
658
1042
|
}
|
|
@@ -688,7 +1072,7 @@ export function createToolOutcomePart(
|
|
|
688
1072
|
};
|
|
689
1073
|
|
|
690
1074
|
if (pendingPart?.callProviderMetadata != null) {
|
|
691
|
-
part.callProviderMetadata = pendingPart.callProviderMetadata as
|
|
1075
|
+
part.callProviderMetadata = pendingPart.callProviderMetadata as ProviderMetadata;
|
|
692
1076
|
}
|
|
693
1077
|
return part;
|
|
694
1078
|
}
|