convex-durable-agents 0.2.3 → 0.2.5
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 +22 -0
- package/dist/client/handler.d.ts.map +1 -1
- package/dist/client/handler.js +261 -118
- 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 +55 -3
- package/dist/component/tool_calls.d.ts.map +1 -1
- package/dist/component/tool_calls.js +352 -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/msg.d.ts +3 -0
- package/dist/utils/msg.d.ts.map +1 -0
- package/dist/utils/msg.js +7 -0
- package/dist/utils/msg.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 +337 -134
- 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 +430 -44
- package/src/utils/msg.ts +8 -0
- 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) {
|
|
@@ -34,6 +101,10 @@ function formatTimeoutMs(timeoutMs: number): string {
|
|
|
34
101
|
return `${timeoutMs}ms`;
|
|
35
102
|
}
|
|
36
103
|
|
|
104
|
+
export function shouldContinueAfterToolCompletion(status: Doc<"threads">["status"]): boolean {
|
|
105
|
+
return status === "streaming" || status === "awaiting_tool_results";
|
|
106
|
+
}
|
|
107
|
+
|
|
37
108
|
async function cleanupTimeoutFn(ctx: MutationCtx, toolCall: Doc<"tool_calls">): Promise<void> {
|
|
38
109
|
if (!toolCall.timeoutFnId) {
|
|
39
110
|
return;
|
|
@@ -44,16 +115,29 @@ async function cleanupTimeoutFn(ctx: MutationCtx, toolCall: Doc<"tool_calls">):
|
|
|
44
115
|
}
|
|
45
116
|
}
|
|
46
117
|
|
|
118
|
+
async function cleanupExecutionRetryFn(ctx: MutationCtx, toolCall: Doc<"tool_calls">): Promise<void> {
|
|
119
|
+
if (!toolCall.executionRetryFnId) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
123
|
+
if (retryFn?.state.kind === "pending") {
|
|
124
|
+
await ctx.scheduler.cancel(toolCall.executionRetryFnId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
47
128
|
async function failToolCallIfPending(ctx: MutationCtx, toolCall: Doc<"tool_calls">, error: string): Promise<boolean> {
|
|
48
129
|
const latest = await ctx.db.get(toolCall._id);
|
|
49
130
|
if (!latest || latest.status !== "pending") {
|
|
50
131
|
return false;
|
|
51
132
|
}
|
|
52
133
|
await cleanupTimeoutFn(ctx, latest);
|
|
134
|
+
await cleanupExecutionRetryFn(ctx, latest);
|
|
53
135
|
await ctx.db.patch(latest._id, {
|
|
54
136
|
error,
|
|
55
137
|
status: "failed",
|
|
56
138
|
callbackLastError: error,
|
|
139
|
+
executionRetryFnId: undefined,
|
|
140
|
+
nextRetryAt: undefined,
|
|
57
141
|
});
|
|
58
142
|
|
|
59
143
|
if (latest.saveDelta) {
|
|
@@ -96,6 +180,15 @@ export type ToolCallDoc = {
|
|
|
96
180
|
args: unknown;
|
|
97
181
|
result?: unknown;
|
|
98
182
|
error?: string;
|
|
183
|
+
status: "pending" | "completed" | "failed";
|
|
184
|
+
callbackAttempt?: number;
|
|
185
|
+
callbackLastError?: string;
|
|
186
|
+
handler?: string;
|
|
187
|
+
executionAttempt?: number;
|
|
188
|
+
executionMaxAttempts?: number;
|
|
189
|
+
executionLastError?: string;
|
|
190
|
+
executionRetryPolicy?: unknown;
|
|
191
|
+
nextRetryAt?: number;
|
|
99
192
|
};
|
|
100
193
|
|
|
101
194
|
function publicToolCall(toolCall: Doc<"tool_calls">): ToolCallDoc {
|
|
@@ -109,6 +202,15 @@ function publicToolCall(toolCall: Doc<"tool_calls">): ToolCallDoc {
|
|
|
109
202
|
args: toolCall.args,
|
|
110
203
|
result: toolCall.result,
|
|
111
204
|
error: toolCall.error,
|
|
205
|
+
status: toolCall.status,
|
|
206
|
+
callbackAttempt: toolCall.callbackAttempt,
|
|
207
|
+
callbackLastError: toolCall.callbackLastError,
|
|
208
|
+
handler: toolCall.handler,
|
|
209
|
+
executionAttempt: toolCall.executionAttempt,
|
|
210
|
+
executionMaxAttempts: toolCall.executionMaxAttempts,
|
|
211
|
+
executionLastError: toolCall.executionLastError,
|
|
212
|
+
executionRetryPolicy: toolCall.executionRetryPolicy,
|
|
213
|
+
nextRetryAt: toolCall.nextRetryAt,
|
|
112
214
|
};
|
|
113
215
|
}
|
|
114
216
|
|
|
@@ -123,8 +225,71 @@ export const vToolCallDoc = v.object({
|
|
|
123
225
|
args: v.any(),
|
|
124
226
|
result: v.optional(v.any()),
|
|
125
227
|
error: v.optional(v.string()),
|
|
228
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed")),
|
|
229
|
+
callbackAttempt: v.optional(v.number()),
|
|
230
|
+
callbackLastError: v.optional(v.string()),
|
|
231
|
+
handler: v.optional(v.string()),
|
|
232
|
+
executionAttempt: v.optional(v.number()),
|
|
233
|
+
executionMaxAttempts: v.optional(v.number()),
|
|
234
|
+
executionLastError: v.optional(v.string()),
|
|
235
|
+
executionRetryPolicy: v.optional(v.any()),
|
|
236
|
+
nextRetryAt: v.optional(v.number()),
|
|
126
237
|
});
|
|
127
238
|
|
|
239
|
+
type CreateToolCallArgs = {
|
|
240
|
+
threadId: Id<"threads">;
|
|
241
|
+
msgId: string;
|
|
242
|
+
toolCallId: string;
|
|
243
|
+
toolName: string;
|
|
244
|
+
callback?: string;
|
|
245
|
+
handler?: string;
|
|
246
|
+
retry?: SyncToolRetryPolicy;
|
|
247
|
+
args: unknown;
|
|
248
|
+
saveDelta: boolean;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
async function createToolCallRecord(ctx: MutationCtx, args: CreateToolCallArgs): Promise<Doc<"tool_calls">> {
|
|
252
|
+
const existingToolCall = await ctx.db
|
|
253
|
+
.query("tool_calls")
|
|
254
|
+
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
255
|
+
.first();
|
|
256
|
+
if (existingToolCall) {
|
|
257
|
+
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
258
|
+
}
|
|
259
|
+
logger.debug(
|
|
260
|
+
`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`,
|
|
261
|
+
);
|
|
262
|
+
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
263
|
+
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
264
|
+
threadId: args.threadId,
|
|
265
|
+
msgId: args.msgId,
|
|
266
|
+
toolCallId: args.toolCallId,
|
|
267
|
+
toolName: args.toolName,
|
|
268
|
+
callback: args.callback,
|
|
269
|
+
handler: args.handler,
|
|
270
|
+
callbackAttempt: args.callback ? 0 : undefined,
|
|
271
|
+
executionAttempt: args.retry ? 0 : undefined,
|
|
272
|
+
executionMaxAttempts: args.retry?.maxAttempts,
|
|
273
|
+
executionRetryPolicy: args.retry,
|
|
274
|
+
args: args.args,
|
|
275
|
+
saveDelta: args.saveDelta,
|
|
276
|
+
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
277
|
+
expiresAt,
|
|
278
|
+
status: "pending",
|
|
279
|
+
});
|
|
280
|
+
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
281
|
+
threadId: args.threadId,
|
|
282
|
+
toolCallId: args.toolCallId,
|
|
283
|
+
});
|
|
284
|
+
await ctx.db.patch(toolCallId, { timeoutFnId });
|
|
285
|
+
|
|
286
|
+
const toolCall = await ctx.db.get(toolCallId);
|
|
287
|
+
if (!toolCall) {
|
|
288
|
+
throw new Error(`Tool call ${toolCallId} not found after creation`);
|
|
289
|
+
}
|
|
290
|
+
return toolCall;
|
|
291
|
+
}
|
|
292
|
+
|
|
128
293
|
export const create = mutation({
|
|
129
294
|
args: {
|
|
130
295
|
threadId: v.id("threads"),
|
|
@@ -132,43 +297,25 @@ export const create = mutation({
|
|
|
132
297
|
toolCallId: v.string(),
|
|
133
298
|
toolName: v.string(),
|
|
134
299
|
callback: v.optional(v.string()),
|
|
300
|
+
handler: v.optional(v.string()),
|
|
301
|
+
retry: v.optional(v.any()),
|
|
135
302
|
args: v.any(),
|
|
136
303
|
saveDelta: v.boolean(),
|
|
137
304
|
},
|
|
138
305
|
returns: vToolCallDoc,
|
|
139
306
|
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", {
|
|
307
|
+
const toolCall = await createToolCallRecord(ctx, {
|
|
152
308
|
threadId: args.threadId,
|
|
153
309
|
msgId: args.msgId,
|
|
154
310
|
toolCallId: args.toolCallId,
|
|
155
311
|
toolName: args.toolName,
|
|
156
312
|
callback: args.callback,
|
|
157
|
-
|
|
313
|
+
handler: args.handler,
|
|
314
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
158
315
|
args: args.args,
|
|
159
316
|
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
317
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const toolCall = await ctx.db.get(toolCallId);
|
|
171
|
-
return publicToolCall(toolCall!);
|
|
318
|
+
return publicToolCall(toolCall);
|
|
172
319
|
},
|
|
173
320
|
});
|
|
174
321
|
|
|
@@ -189,7 +336,13 @@ export const setResult = mutation({
|
|
|
189
336
|
}
|
|
190
337
|
logger.debug(`setResult: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}`);
|
|
191
338
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
192
|
-
await ctx
|
|
339
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
340
|
+
await ctx.db.patch(args.id, {
|
|
341
|
+
result: args.result,
|
|
342
|
+
status: "completed",
|
|
343
|
+
executionRetryFnId: undefined,
|
|
344
|
+
nextRetryAt: undefined,
|
|
345
|
+
});
|
|
193
346
|
return true;
|
|
194
347
|
},
|
|
195
348
|
});
|
|
@@ -211,7 +364,14 @@ export const setError = mutation({
|
|
|
211
364
|
}
|
|
212
365
|
logger.debug(`setError: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}, error=${args.error}`);
|
|
213
366
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
214
|
-
await ctx
|
|
367
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
368
|
+
await ctx.db.patch(args.id, {
|
|
369
|
+
error: args.error,
|
|
370
|
+
status: "failed",
|
|
371
|
+
callbackLastError: args.error,
|
|
372
|
+
executionRetryFnId: undefined,
|
|
373
|
+
nextRetryAt: undefined,
|
|
374
|
+
});
|
|
215
375
|
return true;
|
|
216
376
|
},
|
|
217
377
|
});
|
|
@@ -320,6 +480,7 @@ export const scheduleToolCall = mutation({
|
|
|
320
480
|
toolName: v.string(),
|
|
321
481
|
args: v.any(),
|
|
322
482
|
handler: v.string(),
|
|
483
|
+
retry: v.optional(v.any()),
|
|
323
484
|
saveDelta: v.boolean(),
|
|
324
485
|
},
|
|
325
486
|
returns: v.null(),
|
|
@@ -333,11 +494,13 @@ export const scheduleToolCall = mutation({
|
|
|
333
494
|
logger.debug(`scheduleToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
334
495
|
|
|
335
496
|
// Create the tool call record
|
|
336
|
-
await ctx
|
|
497
|
+
await createToolCallRecord(ctx, {
|
|
337
498
|
threadId: args.threadId,
|
|
338
499
|
msgId: args.msgId,
|
|
339
500
|
toolCallId: args.toolCallId,
|
|
340
501
|
toolName: args.toolName,
|
|
502
|
+
handler: args.handler,
|
|
503
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
341
504
|
args: args.args,
|
|
342
505
|
saveDelta: args.saveDelta,
|
|
343
506
|
});
|
|
@@ -382,7 +545,7 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
382
545
|
logger.debug(`scheduleAsyncToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
383
546
|
|
|
384
547
|
// Create the tool call record (will remain pending until addToolResult is called)
|
|
385
|
-
await ctx
|
|
548
|
+
await createToolCallRecord(ctx, {
|
|
386
549
|
threadId: args.threadId,
|
|
387
550
|
toolCallId: args.toolCallId,
|
|
388
551
|
msgId: args.msgId,
|
|
@@ -412,6 +575,139 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
412
575
|
},
|
|
413
576
|
});
|
|
414
577
|
|
|
578
|
+
export const updateExecutionRetryState = internalMutation({
|
|
579
|
+
args: {
|
|
580
|
+
threadId: v.id("threads"),
|
|
581
|
+
toolCallId: v.string(),
|
|
582
|
+
executionAttempt: v.number(),
|
|
583
|
+
executionLastError: v.optional(v.string()),
|
|
584
|
+
nextRetryAt: v.optional(v.number()),
|
|
585
|
+
executionRetryFnId: v.optional(v.id("_scheduled_functions")),
|
|
586
|
+
clearNextRetryAt: v.optional(v.boolean()),
|
|
587
|
+
clearExecutionRetryFnId: v.optional(v.boolean()),
|
|
588
|
+
},
|
|
589
|
+
returns: v.null(),
|
|
590
|
+
handler: async (ctx, args) => {
|
|
591
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
592
|
+
threadId: args.threadId,
|
|
593
|
+
toolCallId: args.toolCallId,
|
|
594
|
+
});
|
|
595
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const patch: {
|
|
600
|
+
executionAttempt: number;
|
|
601
|
+
executionLastError?: string;
|
|
602
|
+
nextRetryAt?: number | undefined;
|
|
603
|
+
executionRetryFnId?: Id<"_scheduled_functions"> | undefined;
|
|
604
|
+
} = {
|
|
605
|
+
executionAttempt: args.executionAttempt,
|
|
606
|
+
};
|
|
607
|
+
if (args.executionLastError !== undefined) {
|
|
608
|
+
patch.executionLastError = args.executionLastError;
|
|
609
|
+
}
|
|
610
|
+
if (args.nextRetryAt !== undefined) {
|
|
611
|
+
patch.nextRetryAt = args.nextRetryAt;
|
|
612
|
+
}
|
|
613
|
+
if (args.executionRetryFnId !== undefined) {
|
|
614
|
+
patch.executionRetryFnId = args.executionRetryFnId;
|
|
615
|
+
}
|
|
616
|
+
if (args.clearNextRetryAt) {
|
|
617
|
+
patch.nextRetryAt = undefined;
|
|
618
|
+
}
|
|
619
|
+
if (args.clearExecutionRetryFnId) {
|
|
620
|
+
patch.executionRetryFnId = undefined;
|
|
621
|
+
}
|
|
622
|
+
await ctx.db.patch(toolCall._id, patch);
|
|
623
|
+
return null;
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
export const scheduleExecutionRetry = internalMutation({
|
|
628
|
+
args: {
|
|
629
|
+
threadId: v.id("threads"),
|
|
630
|
+
toolCallId: v.string(),
|
|
631
|
+
handler: v.string(),
|
|
632
|
+
executionAttempt: v.number(),
|
|
633
|
+
executionLastError: v.string(),
|
|
634
|
+
nextRetryAt: v.number(),
|
|
635
|
+
},
|
|
636
|
+
returns: v.null(),
|
|
637
|
+
handler: async (ctx, args) => {
|
|
638
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
639
|
+
threadId: args.threadId,
|
|
640
|
+
toolCallId: args.toolCallId,
|
|
641
|
+
});
|
|
642
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
647
|
+
const delayMs = Math.max(0, args.nextRetryAt - Date.now());
|
|
648
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
649
|
+
threadId: args.threadId,
|
|
650
|
+
toolCallId: args.toolCallId,
|
|
651
|
+
handler: args.handler,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
await ctx.db.patch(toolCall._id, {
|
|
655
|
+
executionAttempt: args.executionAttempt,
|
|
656
|
+
executionLastError: args.executionLastError,
|
|
657
|
+
nextRetryAt: args.nextRetryAt,
|
|
658
|
+
executionRetryFnId,
|
|
659
|
+
});
|
|
660
|
+
return null;
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
export const resumePendingSyncToolExecutions = mutation({
|
|
665
|
+
args: {
|
|
666
|
+
limit: v.optional(v.number()),
|
|
667
|
+
},
|
|
668
|
+
returns: v.number(),
|
|
669
|
+
handler: async (ctx, args) => {
|
|
670
|
+
const limit = Math.max(1, Math.floor(args.limit ?? 100));
|
|
671
|
+
const pending = await ctx.db
|
|
672
|
+
.query("tool_calls")
|
|
673
|
+
.withIndex("by_status_only", (q) => q.eq("status", "pending"))
|
|
674
|
+
.take(limit * 2);
|
|
675
|
+
|
|
676
|
+
let resumed = 0;
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
for (const toolCall of pending) {
|
|
679
|
+
if (resumed >= limit) {
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
if (!toolCall.handler) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (toolCall.executionRetryFnId) {
|
|
686
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
687
|
+
if (retryFn?.state.kind === "pending") {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const nextRetryAt = toolCall.nextRetryAt ?? now;
|
|
692
|
+
const delayMs = Math.max(0, nextRetryAt - now);
|
|
693
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
694
|
+
threadId: toolCall.threadId,
|
|
695
|
+
toolCallId: toolCall.toolCallId,
|
|
696
|
+
handler: toolCall.handler,
|
|
697
|
+
});
|
|
698
|
+
await ctx.db.patch(toolCall._id, {
|
|
699
|
+
executionRetryFnId,
|
|
700
|
+
});
|
|
701
|
+
resumed += 1;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (resumed > 0) {
|
|
705
|
+
logger.warn(`resumePendingSyncToolExecutions: resumed ${resumed} pending sync tool call(s)`);
|
|
706
|
+
}
|
|
707
|
+
return resumed;
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
|
|
415
711
|
/**
|
|
416
712
|
* Execute a tool call
|
|
417
713
|
*/
|
|
@@ -423,8 +719,6 @@ export const executeToolCall = internalAction({
|
|
|
423
719
|
},
|
|
424
720
|
returns: v.null(),
|
|
425
721
|
handler: async (ctx, args) => {
|
|
426
|
-
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
427
|
-
|
|
428
722
|
// Get the tool call record
|
|
429
723
|
const toolCall = await ctx.runQuery(api.tool_calls.getByToolCallId, {
|
|
430
724
|
threadId: args.threadId,
|
|
@@ -434,34 +728,120 @@ export const executeToolCall = internalAction({
|
|
|
434
728
|
if (!toolCall) {
|
|
435
729
|
throw new Error(`Tool call ${args.toolCallId} not found`);
|
|
436
730
|
}
|
|
731
|
+
if (toolCall.status !== "pending") {
|
|
732
|
+
logger.debug(`executeToolCall: skipping callId=${args.toolCallId}, status already terminal (${toolCall.status})`);
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
437
735
|
logger.debug(`executeToolCall: tool=${toolCall.toolName}`);
|
|
438
736
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
737
|
+
const thread = await ctx.runQuery(api.threads.get, {
|
|
738
|
+
threadId: args.threadId,
|
|
739
|
+
});
|
|
740
|
+
if (!thread) {
|
|
741
|
+
throw new Error(`Thread ${args.threadId} not found`);
|
|
742
|
+
}
|
|
743
|
+
if (thread.stopSignal || thread.status === "stopped") {
|
|
744
|
+
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
745
|
+
threadId: args.threadId,
|
|
746
|
+
toolCallId: toolCall.toolCallId,
|
|
747
|
+
error: "Tool execution cancelled because the thread was stopped",
|
|
748
|
+
});
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const handler = toolCall.handler ?? args.handler;
|
|
753
|
+
const retryPolicy = normalizeSyncToolRetryPolicy(toolCall.executionRetryPolicy);
|
|
754
|
+
const retryEnabled = retryPolicy?.enabled === true;
|
|
755
|
+
const maxAttempts = retryEnabled ? Math.max(1, retryPolicy.maxAttempts ?? SYNC_TOOL_MAX_ATTEMPTS) : 1;
|
|
756
|
+
const toolArgs = typeof toolCall.args === "object" && toolCall.args !== null ? toolCall.args : {};
|
|
757
|
+
const attempt = Math.max(1, (toolCall.executionAttempt ?? 0) + 1);
|
|
758
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
759
|
+
threadId: args.threadId,
|
|
760
|
+
toolCallId: args.toolCallId,
|
|
761
|
+
executionAttempt: attempt,
|
|
762
|
+
clearNextRetryAt: true,
|
|
763
|
+
clearExecutionRetryFnId: true,
|
|
764
|
+
});
|
|
765
|
+
logger.debug(
|
|
766
|
+
`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}, attempt=${attempt}/${maxAttempts}`,
|
|
767
|
+
);
|
|
445
768
|
|
|
769
|
+
try {
|
|
446
770
|
logger.debug(`executeToolCall: invoking handler for callId=${args.toolCallId}`);
|
|
447
|
-
const result = await ctx.runAction(
|
|
771
|
+
const result = await ctx.runAction(handler as FunctionHandle<"action">, toolArgs as Record<string, unknown>);
|
|
448
772
|
logger.debug(`executeToolCall: handler succeeded for callId=${args.toolCallId}`);
|
|
449
773
|
await ctx.runMutation(api.tool_calls.addToolResult, {
|
|
450
774
|
threadId: args.threadId,
|
|
451
775
|
result,
|
|
452
776
|
toolCallId: toolCall.toolCallId,
|
|
453
777
|
});
|
|
778
|
+
return null;
|
|
454
779
|
} catch (e) {
|
|
455
|
-
const
|
|
456
|
-
|
|
780
|
+
const errorInfo = extractToolErrorInfo(e);
|
|
781
|
+
const error = errorInfo.message;
|
|
782
|
+
logger.debug(`executeToolCall: handler failed for callId=${args.toolCallId} (attempt=${attempt}): ${error}`);
|
|
783
|
+
|
|
784
|
+
let retryable = false;
|
|
785
|
+
if (retryEnabled) {
|
|
786
|
+
if (retryPolicy.shouldRetryError) {
|
|
787
|
+
try {
|
|
788
|
+
const decision = await ctx.runAction(retryPolicy.shouldRetryError as FunctionHandle<"action">, {
|
|
789
|
+
threadId: args.threadId,
|
|
790
|
+
toolCallId: args.toolCallId,
|
|
791
|
+
toolName: toolCall.toolName,
|
|
792
|
+
args: toolCall.args,
|
|
793
|
+
error,
|
|
794
|
+
attempt,
|
|
795
|
+
maxAttempts,
|
|
796
|
+
});
|
|
797
|
+
retryable = isRetryableDecision(decision);
|
|
798
|
+
} catch (classifierError) {
|
|
799
|
+
logger.warn(
|
|
800
|
+
`executeToolCall: shouldRetryError failed for callId=${args.toolCallId}, falling back to default classifier: ${
|
|
801
|
+
classifierError instanceof Error ? classifierError.message : String(classifierError)
|
|
802
|
+
}`,
|
|
803
|
+
);
|
|
804
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (retryEnabled && retryable && attempt < maxAttempts) {
|
|
812
|
+
const delayMs = computeRetryDelayMs(attempt, retryPolicy.backoff);
|
|
813
|
+
const nextRetryAt = Date.now() + delayMs;
|
|
814
|
+
await ctx.runMutation(internal.tool_calls.scheduleExecutionRetry, {
|
|
815
|
+
threadId: args.threadId,
|
|
816
|
+
toolCallId: args.toolCallId,
|
|
817
|
+
handler,
|
|
818
|
+
executionAttempt: attempt,
|
|
819
|
+
executionLastError: error,
|
|
820
|
+
nextRetryAt,
|
|
821
|
+
});
|
|
822
|
+
logger.warn(
|
|
823
|
+
`executeToolCall: scheduled retry for callId=${args.toolCallId} in ${delayMs}ms (attempt ${
|
|
824
|
+
attempt + 1
|
|
825
|
+
}/${maxAttempts})`,
|
|
826
|
+
);
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
831
|
+
threadId: args.threadId,
|
|
832
|
+
toolCallId: args.toolCallId,
|
|
833
|
+
executionAttempt: attempt,
|
|
834
|
+
executionLastError: error,
|
|
835
|
+
clearNextRetryAt: true,
|
|
836
|
+
clearExecutionRetryFnId: true,
|
|
837
|
+
});
|
|
457
838
|
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
458
839
|
threadId: args.threadId,
|
|
459
840
|
error,
|
|
460
841
|
toolCallId: toolCall.toolCallId,
|
|
461
842
|
});
|
|
843
|
+
return null;
|
|
462
844
|
}
|
|
463
|
-
|
|
464
|
-
return null;
|
|
465
845
|
},
|
|
466
846
|
});
|
|
467
847
|
|
|
@@ -623,6 +1003,7 @@ export const onToolComplete = internalMutation({
|
|
|
623
1003
|
status: "stopped",
|
|
624
1004
|
activeStream: null,
|
|
625
1005
|
continue: false,
|
|
1006
|
+
retryState: undefined,
|
|
626
1007
|
});
|
|
627
1008
|
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
628
1009
|
await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
|
|
@@ -643,6 +1024,11 @@ export const onToolComplete = internalMutation({
|
|
|
643
1024
|
return null;
|
|
644
1025
|
}
|
|
645
1026
|
|
|
1027
|
+
if (!shouldContinueAfterToolCompletion(thread.status)) {
|
|
1028
|
+
logger.debug(`onToolComplete: thread status=${thread.status}, skipping continuation`);
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
646
1032
|
// Check for pending tool calls
|
|
647
1033
|
const pending = await ctx.runQuery(api.tool_calls.listPending, {
|
|
648
1034
|
threadId: args.threadId,
|
|
@@ -695,7 +1081,7 @@ export function createToolOutcomePart(
|
|
|
695
1081
|
};
|
|
696
1082
|
|
|
697
1083
|
if (pendingPart?.callProviderMetadata != null) {
|
|
698
|
-
part.callProviderMetadata = pendingPart.callProviderMetadata as
|
|
1084
|
+
part.callProviderMetadata = pendingPart.callProviderMetadata as ProviderMetadata;
|
|
699
1085
|
}
|
|
700
1086
|
return part;
|
|
701
1087
|
}
|
package/src/utils/msg.ts
ADDED