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,14 +1,61 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { Logger } from "../logger.js";
|
|
2
|
+
import { Logger } from "../utils/logger.js";
|
|
3
|
+
import { extractToolErrorInfo, isRetryableDecision, isRetryableToolErrorDefault } from "../utils/retry.js";
|
|
3
4
|
import { api, internal } from "./_generated/api.js";
|
|
4
5
|
import { internalAction, internalMutation, mutation, query } from "./_generated/server.js";
|
|
5
6
|
import { enqueueAction } from "./agent.js";
|
|
7
|
+
import { isAlive } from "./streams.js";
|
|
6
8
|
const logger = new Logger("tool_calls");
|
|
7
9
|
const SECOND = 1000;
|
|
8
10
|
const MINUTE = 60 * SECOND;
|
|
9
11
|
const TOOL_CALL_TIMEOUT_MS = 30 * MINUTE;
|
|
10
12
|
const ASYNC_CALLBACK_MAX_ATTEMPTS = 3;
|
|
11
13
|
const ASYNC_CALLBACK_RETRY_BASE_MS = 5 * SECOND;
|
|
14
|
+
const SYNC_TOOL_MAX_ATTEMPTS = 3;
|
|
15
|
+
const SYNC_TOOL_RETRY_INITIAL_BACKOFF_MS = 500;
|
|
16
|
+
function normalizeSyncToolRetryPolicy(value) {
|
|
17
|
+
if (value == null || typeof value !== "object") {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const obj = value;
|
|
21
|
+
if (obj.enabled !== true) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
enabled: true,
|
|
26
|
+
maxAttempts: typeof obj.maxAttempts === "number" ? obj.maxAttempts : undefined,
|
|
27
|
+
backoff: obj.backoff ?? undefined,
|
|
28
|
+
shouldRetryError: typeof obj.shouldRetryError === "string" ? obj.shouldRetryError : undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function clampDelayMs(value) {
|
|
32
|
+
if (!Number.isFinite(value) || value < 0)
|
|
33
|
+
return 0;
|
|
34
|
+
return Math.floor(value);
|
|
35
|
+
}
|
|
36
|
+
function computeRetryDelayMs(attempt, backoff) {
|
|
37
|
+
const policy = backoff ?? {
|
|
38
|
+
strategy: "exponential",
|
|
39
|
+
initialDelayMs: SYNC_TOOL_RETRY_INITIAL_BACKOFF_MS,
|
|
40
|
+
multiplier: 2,
|
|
41
|
+
maxDelayMs: 10_000,
|
|
42
|
+
jitter: true,
|
|
43
|
+
};
|
|
44
|
+
if ("delayMs" in policy) {
|
|
45
|
+
const delayMs = clampDelayMs(policy.delayMs);
|
|
46
|
+
if (!policy.jitter)
|
|
47
|
+
return delayMs;
|
|
48
|
+
return Math.floor(Math.random() * (delayMs + 1));
|
|
49
|
+
}
|
|
50
|
+
const initialDelayMs = clampDelayMs(policy.initialDelayMs);
|
|
51
|
+
const multiplier = Number.isFinite(policy.multiplier ?? 2) ? (policy.multiplier ?? 2) : 2;
|
|
52
|
+
const unbounded = initialDelayMs * multiplier ** Math.max(0, attempt - 1);
|
|
53
|
+
const maxDelayMs = policy.maxDelayMs == null ? unbounded : clampDelayMs(policy.maxDelayMs);
|
|
54
|
+
const delayMs = Math.min(unbounded, maxDelayMs);
|
|
55
|
+
if (!policy.jitter)
|
|
56
|
+
return delayMs;
|
|
57
|
+
return Math.floor(Math.random() * (delayMs + 1));
|
|
58
|
+
}
|
|
12
59
|
function normalizeToolCallTimeoutMs(timeoutMs) {
|
|
13
60
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
14
61
|
throw new Error(`Invalid tool call timeout: ${timeoutMs}`);
|
|
@@ -35,16 +82,28 @@ async function cleanupTimeoutFn(ctx, toolCall) {
|
|
|
35
82
|
await ctx.scheduler.cancel(toolCall.timeoutFnId);
|
|
36
83
|
}
|
|
37
84
|
}
|
|
85
|
+
async function cleanupExecutionRetryFn(ctx, toolCall) {
|
|
86
|
+
if (!toolCall.executionRetryFnId) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
90
|
+
if (retryFn?.state.kind === "pending") {
|
|
91
|
+
await ctx.scheduler.cancel(toolCall.executionRetryFnId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
38
94
|
async function failToolCallIfPending(ctx, toolCall, error) {
|
|
39
95
|
const latest = await ctx.db.get(toolCall._id);
|
|
40
96
|
if (!latest || latest.status !== "pending") {
|
|
41
97
|
return false;
|
|
42
98
|
}
|
|
43
99
|
await cleanupTimeoutFn(ctx, latest);
|
|
100
|
+
await cleanupExecutionRetryFn(ctx, latest);
|
|
44
101
|
await ctx.db.patch(latest._id, {
|
|
45
102
|
error,
|
|
46
103
|
status: "failed",
|
|
47
104
|
callbackLastError: error,
|
|
105
|
+
executionRetryFnId: undefined,
|
|
106
|
+
nextRetryAt: undefined,
|
|
48
107
|
});
|
|
49
108
|
if (latest.saveDelta) {
|
|
50
109
|
logger.debug(`failPendingToolCall: inserting tool outcome delta for callId=${latest.toolCallId}`);
|
|
@@ -80,6 +139,15 @@ function publicToolCall(toolCall) {
|
|
|
80
139
|
args: toolCall.args,
|
|
81
140
|
result: toolCall.result,
|
|
82
141
|
error: toolCall.error,
|
|
142
|
+
status: toolCall.status,
|
|
143
|
+
callbackAttempt: toolCall.callbackAttempt,
|
|
144
|
+
callbackLastError: toolCall.callbackLastError,
|
|
145
|
+
handler: toolCall.handler,
|
|
146
|
+
executionAttempt: toolCall.executionAttempt,
|
|
147
|
+
executionMaxAttempts: toolCall.executionMaxAttempts,
|
|
148
|
+
executionLastError: toolCall.executionLastError,
|
|
149
|
+
executionRetryPolicy: toolCall.executionRetryPolicy,
|
|
150
|
+
nextRetryAt: toolCall.nextRetryAt,
|
|
83
151
|
};
|
|
84
152
|
}
|
|
85
153
|
// Tool call doc validator for return types
|
|
@@ -93,7 +161,54 @@ export const vToolCallDoc = v.object({
|
|
|
93
161
|
args: v.any(),
|
|
94
162
|
result: v.optional(v.any()),
|
|
95
163
|
error: v.optional(v.string()),
|
|
164
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed")),
|
|
165
|
+
callbackAttempt: v.optional(v.number()),
|
|
166
|
+
callbackLastError: v.optional(v.string()),
|
|
167
|
+
handler: v.optional(v.string()),
|
|
168
|
+
executionAttempt: v.optional(v.number()),
|
|
169
|
+
executionMaxAttempts: v.optional(v.number()),
|
|
170
|
+
executionLastError: v.optional(v.string()),
|
|
171
|
+
executionRetryPolicy: v.optional(v.any()),
|
|
172
|
+
nextRetryAt: v.optional(v.number()),
|
|
96
173
|
});
|
|
174
|
+
async function createToolCallRecord(ctx, args) {
|
|
175
|
+
const existingToolCall = await ctx.db
|
|
176
|
+
.query("tool_calls")
|
|
177
|
+
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
178
|
+
.first();
|
|
179
|
+
if (existingToolCall) {
|
|
180
|
+
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
181
|
+
}
|
|
182
|
+
logger.debug(`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`);
|
|
183
|
+
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
184
|
+
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
185
|
+
threadId: args.threadId,
|
|
186
|
+
msgId: args.msgId,
|
|
187
|
+
toolCallId: args.toolCallId,
|
|
188
|
+
toolName: args.toolName,
|
|
189
|
+
callback: args.callback,
|
|
190
|
+
handler: args.handler,
|
|
191
|
+
callbackAttempt: args.callback ? 0 : undefined,
|
|
192
|
+
executionAttempt: args.retry ? 0 : undefined,
|
|
193
|
+
executionMaxAttempts: args.retry?.maxAttempts,
|
|
194
|
+
executionRetryPolicy: args.retry,
|
|
195
|
+
args: args.args,
|
|
196
|
+
saveDelta: args.saveDelta,
|
|
197
|
+
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
198
|
+
expiresAt,
|
|
199
|
+
status: "pending",
|
|
200
|
+
});
|
|
201
|
+
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
202
|
+
threadId: args.threadId,
|
|
203
|
+
toolCallId: args.toolCallId,
|
|
204
|
+
});
|
|
205
|
+
await ctx.db.patch(toolCallId, { timeoutFnId });
|
|
206
|
+
const toolCall = await ctx.db.get(toolCallId);
|
|
207
|
+
if (!toolCall) {
|
|
208
|
+
throw new Error(`Tool call ${toolCallId} not found after creation`);
|
|
209
|
+
}
|
|
210
|
+
return toolCall;
|
|
211
|
+
}
|
|
97
212
|
export const create = mutation({
|
|
98
213
|
args: {
|
|
99
214
|
threadId: v.id("threads"),
|
|
@@ -101,39 +216,24 @@ export const create = mutation({
|
|
|
101
216
|
toolCallId: v.string(),
|
|
102
217
|
toolName: v.string(),
|
|
103
218
|
callback: v.optional(v.string()),
|
|
219
|
+
handler: v.optional(v.string()),
|
|
220
|
+
retry: v.optional(v.any()),
|
|
104
221
|
args: v.any(),
|
|
105
222
|
saveDelta: v.boolean(),
|
|
106
223
|
},
|
|
107
224
|
returns: vToolCallDoc,
|
|
108
225
|
handler: async (ctx, args) => {
|
|
109
|
-
const
|
|
110
|
-
.query("tool_calls")
|
|
111
|
-
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
112
|
-
.first();
|
|
113
|
-
if (existingToolCall) {
|
|
114
|
-
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
115
|
-
}
|
|
116
|
-
logger.debug(`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`);
|
|
117
|
-
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
118
|
-
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
226
|
+
const toolCall = await createToolCallRecord(ctx, {
|
|
119
227
|
threadId: args.threadId,
|
|
120
228
|
msgId: args.msgId,
|
|
121
229
|
toolCallId: args.toolCallId,
|
|
122
230
|
toolName: args.toolName,
|
|
123
231
|
callback: args.callback,
|
|
124
|
-
|
|
232
|
+
handler: args.handler,
|
|
233
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
125
234
|
args: args.args,
|
|
126
235
|
saveDelta: args.saveDelta,
|
|
127
|
-
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
128
|
-
expiresAt,
|
|
129
|
-
status: "pending",
|
|
130
236
|
});
|
|
131
|
-
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
132
|
-
threadId: args.threadId,
|
|
133
|
-
toolCallId: args.toolCallId,
|
|
134
|
-
});
|
|
135
|
-
await ctx.db.patch(toolCallId, { timeoutFnId });
|
|
136
|
-
const toolCall = await ctx.db.get(toolCallId);
|
|
137
237
|
return publicToolCall(toolCall);
|
|
138
238
|
},
|
|
139
239
|
});
|
|
@@ -154,7 +254,13 @@ export const setResult = mutation({
|
|
|
154
254
|
}
|
|
155
255
|
logger.debug(`setResult: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}`);
|
|
156
256
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
157
|
-
await ctx
|
|
257
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
258
|
+
await ctx.db.patch(args.id, {
|
|
259
|
+
result: args.result,
|
|
260
|
+
status: "completed",
|
|
261
|
+
executionRetryFnId: undefined,
|
|
262
|
+
nextRetryAt: undefined,
|
|
263
|
+
});
|
|
158
264
|
return true;
|
|
159
265
|
},
|
|
160
266
|
});
|
|
@@ -175,7 +281,14 @@ export const setError = mutation({
|
|
|
175
281
|
}
|
|
176
282
|
logger.debug(`setError: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}, error=${args.error}`);
|
|
177
283
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
178
|
-
await ctx
|
|
284
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
285
|
+
await ctx.db.patch(args.id, {
|
|
286
|
+
error: args.error,
|
|
287
|
+
status: "failed",
|
|
288
|
+
callbackLastError: args.error,
|
|
289
|
+
executionRetryFnId: undefined,
|
|
290
|
+
nextRetryAt: undefined,
|
|
291
|
+
});
|
|
179
292
|
return true;
|
|
180
293
|
},
|
|
181
294
|
});
|
|
@@ -275,6 +388,7 @@ export const scheduleToolCall = mutation({
|
|
|
275
388
|
toolName: v.string(),
|
|
276
389
|
args: v.any(),
|
|
277
390
|
handler: v.string(),
|
|
391
|
+
retry: v.optional(v.any()),
|
|
278
392
|
saveDelta: v.boolean(),
|
|
279
393
|
},
|
|
280
394
|
returns: v.null(),
|
|
@@ -286,11 +400,13 @@ export const scheduleToolCall = mutation({
|
|
|
286
400
|
}
|
|
287
401
|
logger.debug(`scheduleToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
288
402
|
// Create the tool call record
|
|
289
|
-
await ctx
|
|
403
|
+
await createToolCallRecord(ctx, {
|
|
290
404
|
threadId: args.threadId,
|
|
291
405
|
msgId: args.msgId,
|
|
292
406
|
toolCallId: args.toolCallId,
|
|
293
407
|
toolName: args.toolName,
|
|
408
|
+
handler: args.handler,
|
|
409
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
294
410
|
args: args.args,
|
|
295
411
|
saveDelta: args.saveDelta,
|
|
296
412
|
});
|
|
@@ -329,7 +445,7 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
329
445
|
}
|
|
330
446
|
logger.debug(`scheduleAsyncToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
331
447
|
// Create the tool call record (will remain pending until addToolResult is called)
|
|
332
|
-
await ctx
|
|
448
|
+
await createToolCallRecord(ctx, {
|
|
333
449
|
threadId: args.threadId,
|
|
334
450
|
toolCallId: args.toolCallId,
|
|
335
451
|
msgId: args.msgId,
|
|
@@ -353,6 +469,126 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
353
469
|
return null;
|
|
354
470
|
},
|
|
355
471
|
});
|
|
472
|
+
export const updateExecutionRetryState = internalMutation({
|
|
473
|
+
args: {
|
|
474
|
+
threadId: v.id("threads"),
|
|
475
|
+
toolCallId: v.string(),
|
|
476
|
+
executionAttempt: v.number(),
|
|
477
|
+
executionLastError: v.optional(v.string()),
|
|
478
|
+
nextRetryAt: v.optional(v.number()),
|
|
479
|
+
executionRetryFnId: v.optional(v.id("_scheduled_functions")),
|
|
480
|
+
clearNextRetryAt: v.optional(v.boolean()),
|
|
481
|
+
clearExecutionRetryFnId: v.optional(v.boolean()),
|
|
482
|
+
},
|
|
483
|
+
returns: v.null(),
|
|
484
|
+
handler: async (ctx, args) => {
|
|
485
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
486
|
+
threadId: args.threadId,
|
|
487
|
+
toolCallId: args.toolCallId,
|
|
488
|
+
});
|
|
489
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const patch = {
|
|
493
|
+
executionAttempt: args.executionAttempt,
|
|
494
|
+
};
|
|
495
|
+
if (args.executionLastError !== undefined) {
|
|
496
|
+
patch.executionLastError = args.executionLastError;
|
|
497
|
+
}
|
|
498
|
+
if (args.nextRetryAt !== undefined) {
|
|
499
|
+
patch.nextRetryAt = args.nextRetryAt;
|
|
500
|
+
}
|
|
501
|
+
if (args.executionRetryFnId !== undefined) {
|
|
502
|
+
patch.executionRetryFnId = args.executionRetryFnId;
|
|
503
|
+
}
|
|
504
|
+
if (args.clearNextRetryAt) {
|
|
505
|
+
patch.nextRetryAt = undefined;
|
|
506
|
+
}
|
|
507
|
+
if (args.clearExecutionRetryFnId) {
|
|
508
|
+
patch.executionRetryFnId = undefined;
|
|
509
|
+
}
|
|
510
|
+
await ctx.db.patch(toolCall._id, patch);
|
|
511
|
+
return null;
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
export const scheduleExecutionRetry = internalMutation({
|
|
515
|
+
args: {
|
|
516
|
+
threadId: v.id("threads"),
|
|
517
|
+
toolCallId: v.string(),
|
|
518
|
+
handler: v.string(),
|
|
519
|
+
executionAttempt: v.number(),
|
|
520
|
+
executionLastError: v.string(),
|
|
521
|
+
nextRetryAt: v.number(),
|
|
522
|
+
},
|
|
523
|
+
returns: v.null(),
|
|
524
|
+
handler: async (ctx, args) => {
|
|
525
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
526
|
+
threadId: args.threadId,
|
|
527
|
+
toolCallId: args.toolCallId,
|
|
528
|
+
});
|
|
529
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
533
|
+
const delayMs = Math.max(0, args.nextRetryAt - Date.now());
|
|
534
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
535
|
+
threadId: args.threadId,
|
|
536
|
+
toolCallId: args.toolCallId,
|
|
537
|
+
handler: args.handler,
|
|
538
|
+
});
|
|
539
|
+
await ctx.db.patch(toolCall._id, {
|
|
540
|
+
executionAttempt: args.executionAttempt,
|
|
541
|
+
executionLastError: args.executionLastError,
|
|
542
|
+
nextRetryAt: args.nextRetryAt,
|
|
543
|
+
executionRetryFnId,
|
|
544
|
+
});
|
|
545
|
+
return null;
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
export const resumePendingSyncToolExecutions = mutation({
|
|
549
|
+
args: {
|
|
550
|
+
limit: v.optional(v.number()),
|
|
551
|
+
},
|
|
552
|
+
returns: v.number(),
|
|
553
|
+
handler: async (ctx, args) => {
|
|
554
|
+
const limit = Math.max(1, Math.floor(args.limit ?? 100));
|
|
555
|
+
const pending = await ctx.db
|
|
556
|
+
.query("tool_calls")
|
|
557
|
+
.withIndex("by_status_only", (q) => q.eq("status", "pending"))
|
|
558
|
+
.take(limit * 2);
|
|
559
|
+
let resumed = 0;
|
|
560
|
+
const now = Date.now();
|
|
561
|
+
for (const toolCall of pending) {
|
|
562
|
+
if (resumed >= limit) {
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
if (!toolCall.handler) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (toolCall.executionRetryFnId) {
|
|
569
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
570
|
+
if (retryFn?.state.kind === "pending") {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const nextRetryAt = toolCall.nextRetryAt ?? now;
|
|
575
|
+
const delayMs = Math.max(0, nextRetryAt - now);
|
|
576
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
577
|
+
threadId: toolCall.threadId,
|
|
578
|
+
toolCallId: toolCall.toolCallId,
|
|
579
|
+
handler: toolCall.handler,
|
|
580
|
+
});
|
|
581
|
+
await ctx.db.patch(toolCall._id, {
|
|
582
|
+
executionRetryFnId,
|
|
583
|
+
});
|
|
584
|
+
resumed += 1;
|
|
585
|
+
}
|
|
586
|
+
if (resumed > 0) {
|
|
587
|
+
logger.warn(`resumePendingSyncToolExecutions: resumed ${resumed} pending sync tool call(s)`);
|
|
588
|
+
}
|
|
589
|
+
return resumed;
|
|
590
|
+
},
|
|
591
|
+
});
|
|
356
592
|
/**
|
|
357
593
|
* Execute a tool call
|
|
358
594
|
*/
|
|
@@ -364,7 +600,6 @@ export const executeToolCall = internalAction({
|
|
|
364
600
|
},
|
|
365
601
|
returns: v.null(),
|
|
366
602
|
handler: async (ctx, args) => {
|
|
367
|
-
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
368
603
|
// Get the tool call record
|
|
369
604
|
const toolCall = await ctx.runQuery(api.tool_calls.getByToolCallId, {
|
|
370
605
|
threadId: args.threadId,
|
|
@@ -373,32 +608,107 @@ export const executeToolCall = internalAction({
|
|
|
373
608
|
if (!toolCall) {
|
|
374
609
|
throw new Error(`Tool call ${args.toolCallId} not found`);
|
|
375
610
|
}
|
|
611
|
+
if (toolCall.status !== "pending") {
|
|
612
|
+
logger.debug(`executeToolCall: skipping callId=${args.toolCallId}, status already terminal (${toolCall.status})`);
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
376
615
|
logger.debug(`executeToolCall: tool=${toolCall.toolName}`);
|
|
616
|
+
const thread = await ctx.runQuery(api.threads.get, {
|
|
617
|
+
threadId: args.threadId,
|
|
618
|
+
});
|
|
619
|
+
if (!thread) {
|
|
620
|
+
throw new Error(`Thread ${args.threadId} not found`);
|
|
621
|
+
}
|
|
622
|
+
if (thread.stopSignal || thread.status === "stopped") {
|
|
623
|
+
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
624
|
+
threadId: args.threadId,
|
|
625
|
+
toolCallId: toolCall.toolCallId,
|
|
626
|
+
error: "Tool execution cancelled because the thread was stopped",
|
|
627
|
+
});
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const handler = toolCall.handler ?? args.handler;
|
|
631
|
+
const retryPolicy = normalizeSyncToolRetryPolicy(toolCall.executionRetryPolicy);
|
|
632
|
+
const retryEnabled = retryPolicy?.enabled === true;
|
|
633
|
+
const maxAttempts = retryEnabled ? Math.max(1, retryPolicy.maxAttempts ?? SYNC_TOOL_MAX_ATTEMPTS) : 1;
|
|
634
|
+
const toolArgs = typeof toolCall.args === "object" && toolCall.args !== null ? toolCall.args : {};
|
|
635
|
+
const attempt = Math.max(1, (toolCall.executionAttempt ?? 0) + 1);
|
|
636
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
637
|
+
threadId: args.threadId,
|
|
638
|
+
toolCallId: args.toolCallId,
|
|
639
|
+
executionAttempt: attempt,
|
|
640
|
+
clearNextRetryAt: true,
|
|
641
|
+
clearExecutionRetryFnId: true,
|
|
642
|
+
});
|
|
643
|
+
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}, attempt=${attempt}/${maxAttempts}`);
|
|
377
644
|
try {
|
|
378
|
-
// Execute the tool handler
|
|
379
|
-
// The handler string is passed from the client and we need to resolve it
|
|
380
|
-
// For now, we'll use ctx.runAction with a dynamic reference
|
|
381
|
-
// This requires the handler to be a proper function reference string
|
|
382
|
-
const toolArgs = typeof toolCall.args === "object" && toolCall.args !== null ? toolCall.args : {};
|
|
383
645
|
logger.debug(`executeToolCall: invoking handler for callId=${args.toolCallId}`);
|
|
384
|
-
const result = await ctx.runAction(
|
|
646
|
+
const result = await ctx.runAction(handler, toolArgs);
|
|
385
647
|
logger.debug(`executeToolCall: handler succeeded for callId=${args.toolCallId}`);
|
|
386
648
|
await ctx.runMutation(api.tool_calls.addToolResult, {
|
|
387
649
|
threadId: args.threadId,
|
|
388
650
|
result,
|
|
389
651
|
toolCallId: toolCall.toolCallId,
|
|
390
652
|
});
|
|
653
|
+
return null;
|
|
391
654
|
}
|
|
392
655
|
catch (e) {
|
|
393
|
-
const
|
|
394
|
-
|
|
656
|
+
const errorInfo = extractToolErrorInfo(e);
|
|
657
|
+
const error = errorInfo.message;
|
|
658
|
+
logger.debug(`executeToolCall: handler failed for callId=${args.toolCallId} (attempt=${attempt}): ${error}`);
|
|
659
|
+
let retryable = false;
|
|
660
|
+
if (retryEnabled) {
|
|
661
|
+
if (retryPolicy.shouldRetryError) {
|
|
662
|
+
try {
|
|
663
|
+
const decision = await ctx.runAction(retryPolicy.shouldRetryError, {
|
|
664
|
+
threadId: args.threadId,
|
|
665
|
+
toolCallId: args.toolCallId,
|
|
666
|
+
toolName: toolCall.toolName,
|
|
667
|
+
args: toolCall.args,
|
|
668
|
+
error,
|
|
669
|
+
attempt,
|
|
670
|
+
maxAttempts,
|
|
671
|
+
});
|
|
672
|
+
retryable = isRetryableDecision(decision);
|
|
673
|
+
}
|
|
674
|
+
catch (classifierError) {
|
|
675
|
+
logger.warn(`executeToolCall: shouldRetryError failed for callId=${args.toolCallId}, falling back to default classifier: ${classifierError instanceof Error ? classifierError.message : String(classifierError)}`);
|
|
676
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (retryEnabled && retryable && attempt < maxAttempts) {
|
|
684
|
+
const delayMs = computeRetryDelayMs(attempt, retryPolicy.backoff);
|
|
685
|
+
const nextRetryAt = Date.now() + delayMs;
|
|
686
|
+
await ctx.runMutation(internal.tool_calls.scheduleExecutionRetry, {
|
|
687
|
+
threadId: args.threadId,
|
|
688
|
+
toolCallId: args.toolCallId,
|
|
689
|
+
handler,
|
|
690
|
+
executionAttempt: attempt,
|
|
691
|
+
executionLastError: error,
|
|
692
|
+
nextRetryAt,
|
|
693
|
+
});
|
|
694
|
+
logger.warn(`executeToolCall: scheduled retry for callId=${args.toolCallId} in ${delayMs}ms (attempt ${attempt + 1}/${maxAttempts})`);
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
698
|
+
threadId: args.threadId,
|
|
699
|
+
toolCallId: args.toolCallId,
|
|
700
|
+
executionAttempt: attempt,
|
|
701
|
+
executionLastError: error,
|
|
702
|
+
clearNextRetryAt: true,
|
|
703
|
+
clearExecutionRetryFnId: true,
|
|
704
|
+
});
|
|
395
705
|
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
396
706
|
threadId: args.threadId,
|
|
397
707
|
error,
|
|
398
708
|
toolCallId: toolCall.toolCallId,
|
|
399
709
|
});
|
|
710
|
+
return null;
|
|
400
711
|
}
|
|
401
|
-
return null;
|
|
402
712
|
},
|
|
403
713
|
});
|
|
404
714
|
/**
|
|
@@ -542,6 +852,7 @@ export const onToolComplete = internalMutation({
|
|
|
542
852
|
status: "stopped",
|
|
543
853
|
activeStream: null,
|
|
544
854
|
continue: false,
|
|
855
|
+
retryState: undefined,
|
|
545
856
|
});
|
|
546
857
|
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
547
858
|
await ctx.runMutation(thread.onStatusChangeHandle, {
|
|
@@ -566,11 +877,18 @@ export const onToolComplete = internalMutation({
|
|
|
566
877
|
threadId: args.threadId,
|
|
567
878
|
});
|
|
568
879
|
if (pending.length === 0) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
880
|
+
const activeStream = thread.activeStream ? await ctx.db.get(thread.activeStream) : null;
|
|
881
|
+
if (isAlive(activeStream)) {
|
|
882
|
+
logger.debug("onToolComplete: all tool calls complete, active stream still alive, setting continue flag");
|
|
883
|
+
await ctx.db.patch(args.threadId, { continue: true });
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
logger.debug("onToolComplete: all tool calls complete, scheduling continueStream");
|
|
887
|
+
// All tool calls complete - schedule continuation
|
|
888
|
+
await ctx.scheduler.runAfter(0, api.agent.continueStream, {
|
|
889
|
+
threadId: args.threadId,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
574
892
|
}
|
|
575
893
|
else {
|
|
576
894
|
logger.debug(`onToolComplete: ${pending.length} tool calls still pending`);
|