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,5 +1,6 @@
|
|
|
1
1
|
import type { UIMessagePart } from "ai";
|
|
2
2
|
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
3
|
+
export declare function shouldContinueAfterToolCompletion(status: Doc<"threads">["status"]): boolean;
|
|
3
4
|
export type ToolCallDoc = {
|
|
4
5
|
_id: Id<"tool_calls">;
|
|
5
6
|
_creationTime: number;
|
|
@@ -10,12 +11,30 @@ export type ToolCallDoc = {
|
|
|
10
11
|
args: unknown;
|
|
11
12
|
result?: unknown;
|
|
12
13
|
error?: string;
|
|
14
|
+
status: "pending" | "completed" | "failed";
|
|
15
|
+
callbackAttempt?: number;
|
|
16
|
+
callbackLastError?: string;
|
|
17
|
+
handler?: string;
|
|
18
|
+
executionAttempt?: number;
|
|
19
|
+
executionMaxAttempts?: number;
|
|
20
|
+
executionLastError?: string;
|
|
21
|
+
executionRetryPolicy?: unknown;
|
|
22
|
+
nextRetryAt?: number;
|
|
13
23
|
};
|
|
14
24
|
export declare const vToolCallDoc: import("convex/values").VObject<{
|
|
15
25
|
error?: string | undefined;
|
|
26
|
+
nextRetryAt?: number | undefined;
|
|
16
27
|
result?: any;
|
|
28
|
+
handler?: string | undefined;
|
|
29
|
+
callbackAttempt?: number | undefined;
|
|
30
|
+
callbackLastError?: string | undefined;
|
|
31
|
+
executionAttempt?: number | undefined;
|
|
32
|
+
executionMaxAttempts?: number | undefined;
|
|
33
|
+
executionLastError?: string | undefined;
|
|
34
|
+
executionRetryPolicy?: any;
|
|
17
35
|
threadId: import("convex/values").GenericId<"threads">;
|
|
18
36
|
msgId: string;
|
|
37
|
+
status: "completed" | "failed" | "pending";
|
|
19
38
|
toolCallId: string;
|
|
20
39
|
args: any;
|
|
21
40
|
toolName: string;
|
|
@@ -31,9 +50,20 @@ export declare const vToolCallDoc: import("convex/values").VObject<{
|
|
|
31
50
|
args: import("convex/values").VAny<any, "required", string>;
|
|
32
51
|
result: import("convex/values").VAny<any, "optional", string>;
|
|
33
52
|
error: import("convex/values").VString<string | undefined, "optional">;
|
|
34
|
-
|
|
53
|
+
status: import("convex/values").VUnion<"completed" | "failed" | "pending", [import("convex/values").VLiteral<"pending", "required">, import("convex/values").VLiteral<"completed", "required">, import("convex/values").VLiteral<"failed", "required">], "required", never>;
|
|
54
|
+
callbackAttempt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
55
|
+
callbackLastError: import("convex/values").VString<string | undefined, "optional">;
|
|
56
|
+
handler: import("convex/values").VString<string | undefined, "optional">;
|
|
57
|
+
executionAttempt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
58
|
+
executionMaxAttempts: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
59
|
+
executionLastError: import("convex/values").VString<string | undefined, "optional">;
|
|
60
|
+
executionRetryPolicy: import("convex/values").VAny<any, "optional", string>;
|
|
61
|
+
nextRetryAt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
62
|
+
}, "required", "threadId" | "msgId" | "status" | "error" | "nextRetryAt" | "toolCallId" | "result" | "args" | "handler" | "toolName" | "_creationTime" | "callbackAttempt" | "callbackLastError" | "executionAttempt" | "executionMaxAttempts" | "executionLastError" | "executionRetryPolicy" | `result.${string}` | `args.${string}` | `executionRetryPolicy.${string}` | "_id">;
|
|
35
63
|
export declare const create: import("convex/server").RegisteredMutation<"public", {
|
|
36
64
|
callback?: string | undefined;
|
|
65
|
+
handler?: string | undefined;
|
|
66
|
+
retry?: any;
|
|
37
67
|
threadId: import("convex/values").GenericId<"threads">;
|
|
38
68
|
msgId: string;
|
|
39
69
|
toolCallId: string;
|
|
@@ -68,13 +98,14 @@ export declare const list: import("convex/server").RegisteredQuery<"public", {
|
|
|
68
98
|
* Schedule a tool call for execution
|
|
69
99
|
*/
|
|
70
100
|
export declare const scheduleToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
101
|
+
retry?: any;
|
|
71
102
|
threadId: import("convex/values").GenericId<"threads">;
|
|
72
103
|
msgId: string;
|
|
73
104
|
toolCallId: string;
|
|
74
105
|
args: any;
|
|
106
|
+
handler: string;
|
|
75
107
|
saveDelta: boolean;
|
|
76
108
|
toolName: string;
|
|
77
|
-
handler: string;
|
|
78
109
|
}, Promise<null>>;
|
|
79
110
|
/**
|
|
80
111
|
* Schedule an async tool call - creates the record and notifies the callback,
|
|
@@ -89,6 +120,27 @@ export declare const scheduleAsyncToolCall: import("convex/server").RegisteredMu
|
|
|
89
120
|
saveDelta: boolean;
|
|
90
121
|
toolName: string;
|
|
91
122
|
}, Promise<null>>;
|
|
123
|
+
export declare const updateExecutionRetryState: import("convex/server").RegisteredMutation<"internal", {
|
|
124
|
+
nextRetryAt?: number | undefined;
|
|
125
|
+
executionLastError?: string | undefined;
|
|
126
|
+
executionRetryFnId?: import("convex/values").GenericId<"_scheduled_functions"> | undefined;
|
|
127
|
+
clearNextRetryAt?: boolean | undefined;
|
|
128
|
+
clearExecutionRetryFnId?: boolean | undefined;
|
|
129
|
+
threadId: import("convex/values").GenericId<"threads">;
|
|
130
|
+
toolCallId: string;
|
|
131
|
+
executionAttempt: number;
|
|
132
|
+
}, Promise<null>>;
|
|
133
|
+
export declare const scheduleExecutionRetry: import("convex/server").RegisteredMutation<"internal", {
|
|
134
|
+
threadId: import("convex/values").GenericId<"threads">;
|
|
135
|
+
nextRetryAt: number;
|
|
136
|
+
toolCallId: string;
|
|
137
|
+
handler: string;
|
|
138
|
+
executionAttempt: number;
|
|
139
|
+
executionLastError: string;
|
|
140
|
+
}, Promise<null>>;
|
|
141
|
+
export declare const resumePendingSyncToolExecutions: import("convex/server").RegisteredMutation<"public", {
|
|
142
|
+
limit?: number | undefined;
|
|
143
|
+
}, Promise<number>>;
|
|
92
144
|
/**
|
|
93
145
|
* Execute a tool call
|
|
94
146
|
*/
|
|
@@ -111,9 +163,9 @@ export declare const executeAsyncToolCallback: import("convex/server").Registere
|
|
|
111
163
|
}, Promise<null>>;
|
|
112
164
|
export declare const onAsyncCallbackFailure: import("convex/server").RegisteredMutation<"internal", {
|
|
113
165
|
threadId: import("convex/values").GenericId<"threads">;
|
|
166
|
+
attempt: number;
|
|
114
167
|
error: string;
|
|
115
168
|
toolCallId: string;
|
|
116
|
-
attempt: number;
|
|
117
169
|
}, Promise<null>>;
|
|
118
170
|
export declare const failPendingToolCall: import("convex/server").RegisteredMutation<"internal", {
|
|
119
171
|
threadId: import("convex/values").GenericId<"threads">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool_calls.d.ts","sourceRoot":"","sources":["../../src/component/tool_calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"tool_calls.d.ts","sourceRoot":"","sources":["../../src/component/tool_calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,aAAa,EAAE,MAAM,IAAI,CAAC;AAM1D,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,2BAA2B,CAAC;AAiGzD,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAE3F;AAmED,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA0BF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kXAmBvB,CAAC;AAwDH,eAAO,MAAM,MAAM;;;;;;;;;;wBA2BjB,CAAC;AAEH,eAAO,MAAM,SAAS;;;oBA0BpB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;oBA2BnB,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;iBA6C7B,CAAC;AAEH,eAAO,MAAM,WAAW;;0BAYtB,CAAC;AAEH,eAAO,MAAM,eAAe;;;+BAgB1B,CAAC;AAEH,eAAO,MAAM,IAAI;;0BAYf,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;iBA8C3B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;iBAiDhC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;iBA+CpC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;iBAmCjC,CAAC;AAEH,eAAO,MAAM,+BAA+B;;mBA6C1C,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;;iBAoI1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,wBAAwB;;;;;;;iBAqCnC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;iBAqDjC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;iBA6B9B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,cAAc;;iBA2EzB,CAAC;AAEH,KAAK,iBAAiB,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;AAE3C,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,CAAC,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAAE,GACnE,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,IAAI,CAyBhC;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;iBAuDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;iBA2DvB,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
@@ -10,6 +11,51 @@ const MINUTE = 60 * SECOND;
|
|
|
10
11
|
const TOOL_CALL_TIMEOUT_MS = 30 * MINUTE;
|
|
11
12
|
const ASYNC_CALLBACK_MAX_ATTEMPTS = 3;
|
|
12
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
|
+
}
|
|
13
59
|
function normalizeToolCallTimeoutMs(timeoutMs) {
|
|
14
60
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
15
61
|
throw new Error(`Invalid tool call timeout: ${timeoutMs}`);
|
|
@@ -27,6 +73,9 @@ function formatTimeoutMs(timeoutMs) {
|
|
|
27
73
|
}
|
|
28
74
|
return `${timeoutMs}ms`;
|
|
29
75
|
}
|
|
76
|
+
export function shouldContinueAfterToolCompletion(status) {
|
|
77
|
+
return status === "streaming" || status === "awaiting_tool_results";
|
|
78
|
+
}
|
|
30
79
|
async function cleanupTimeoutFn(ctx, toolCall) {
|
|
31
80
|
if (!toolCall.timeoutFnId) {
|
|
32
81
|
return;
|
|
@@ -36,16 +85,28 @@ async function cleanupTimeoutFn(ctx, toolCall) {
|
|
|
36
85
|
await ctx.scheduler.cancel(toolCall.timeoutFnId);
|
|
37
86
|
}
|
|
38
87
|
}
|
|
88
|
+
async function cleanupExecutionRetryFn(ctx, toolCall) {
|
|
89
|
+
if (!toolCall.executionRetryFnId) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
93
|
+
if (retryFn?.state.kind === "pending") {
|
|
94
|
+
await ctx.scheduler.cancel(toolCall.executionRetryFnId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
39
97
|
async function failToolCallIfPending(ctx, toolCall, error) {
|
|
40
98
|
const latest = await ctx.db.get(toolCall._id);
|
|
41
99
|
if (!latest || latest.status !== "pending") {
|
|
42
100
|
return false;
|
|
43
101
|
}
|
|
44
102
|
await cleanupTimeoutFn(ctx, latest);
|
|
103
|
+
await cleanupExecutionRetryFn(ctx, latest);
|
|
45
104
|
await ctx.db.patch(latest._id, {
|
|
46
105
|
error,
|
|
47
106
|
status: "failed",
|
|
48
107
|
callbackLastError: error,
|
|
108
|
+
executionRetryFnId: undefined,
|
|
109
|
+
nextRetryAt: undefined,
|
|
49
110
|
});
|
|
50
111
|
if (latest.saveDelta) {
|
|
51
112
|
logger.debug(`failPendingToolCall: inserting tool outcome delta for callId=${latest.toolCallId}`);
|
|
@@ -81,6 +142,15 @@ function publicToolCall(toolCall) {
|
|
|
81
142
|
args: toolCall.args,
|
|
82
143
|
result: toolCall.result,
|
|
83
144
|
error: toolCall.error,
|
|
145
|
+
status: toolCall.status,
|
|
146
|
+
callbackAttempt: toolCall.callbackAttempt,
|
|
147
|
+
callbackLastError: toolCall.callbackLastError,
|
|
148
|
+
handler: toolCall.handler,
|
|
149
|
+
executionAttempt: toolCall.executionAttempt,
|
|
150
|
+
executionMaxAttempts: toolCall.executionMaxAttempts,
|
|
151
|
+
executionLastError: toolCall.executionLastError,
|
|
152
|
+
executionRetryPolicy: toolCall.executionRetryPolicy,
|
|
153
|
+
nextRetryAt: toolCall.nextRetryAt,
|
|
84
154
|
};
|
|
85
155
|
}
|
|
86
156
|
// Tool call doc validator for return types
|
|
@@ -94,7 +164,54 @@ export const vToolCallDoc = v.object({
|
|
|
94
164
|
args: v.any(),
|
|
95
165
|
result: v.optional(v.any()),
|
|
96
166
|
error: v.optional(v.string()),
|
|
167
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed")),
|
|
168
|
+
callbackAttempt: v.optional(v.number()),
|
|
169
|
+
callbackLastError: v.optional(v.string()),
|
|
170
|
+
handler: v.optional(v.string()),
|
|
171
|
+
executionAttempt: v.optional(v.number()),
|
|
172
|
+
executionMaxAttempts: v.optional(v.number()),
|
|
173
|
+
executionLastError: v.optional(v.string()),
|
|
174
|
+
executionRetryPolicy: v.optional(v.any()),
|
|
175
|
+
nextRetryAt: v.optional(v.number()),
|
|
97
176
|
});
|
|
177
|
+
async function createToolCallRecord(ctx, args) {
|
|
178
|
+
const existingToolCall = await ctx.db
|
|
179
|
+
.query("tool_calls")
|
|
180
|
+
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
181
|
+
.first();
|
|
182
|
+
if (existingToolCall) {
|
|
183
|
+
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
184
|
+
}
|
|
185
|
+
logger.debug(`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`);
|
|
186
|
+
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
187
|
+
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
188
|
+
threadId: args.threadId,
|
|
189
|
+
msgId: args.msgId,
|
|
190
|
+
toolCallId: args.toolCallId,
|
|
191
|
+
toolName: args.toolName,
|
|
192
|
+
callback: args.callback,
|
|
193
|
+
handler: args.handler,
|
|
194
|
+
callbackAttempt: args.callback ? 0 : undefined,
|
|
195
|
+
executionAttempt: args.retry ? 0 : undefined,
|
|
196
|
+
executionMaxAttempts: args.retry?.maxAttempts,
|
|
197
|
+
executionRetryPolicy: args.retry,
|
|
198
|
+
args: args.args,
|
|
199
|
+
saveDelta: args.saveDelta,
|
|
200
|
+
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
201
|
+
expiresAt,
|
|
202
|
+
status: "pending",
|
|
203
|
+
});
|
|
204
|
+
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
205
|
+
threadId: args.threadId,
|
|
206
|
+
toolCallId: args.toolCallId,
|
|
207
|
+
});
|
|
208
|
+
await ctx.db.patch(toolCallId, { timeoutFnId });
|
|
209
|
+
const toolCall = await ctx.db.get(toolCallId);
|
|
210
|
+
if (!toolCall) {
|
|
211
|
+
throw new Error(`Tool call ${toolCallId} not found after creation`);
|
|
212
|
+
}
|
|
213
|
+
return toolCall;
|
|
214
|
+
}
|
|
98
215
|
export const create = mutation({
|
|
99
216
|
args: {
|
|
100
217
|
threadId: v.id("threads"),
|
|
@@ -102,39 +219,24 @@ export const create = mutation({
|
|
|
102
219
|
toolCallId: v.string(),
|
|
103
220
|
toolName: v.string(),
|
|
104
221
|
callback: v.optional(v.string()),
|
|
222
|
+
handler: v.optional(v.string()),
|
|
223
|
+
retry: v.optional(v.any()),
|
|
105
224
|
args: v.any(),
|
|
106
225
|
saveDelta: v.boolean(),
|
|
107
226
|
},
|
|
108
227
|
returns: vToolCallDoc,
|
|
109
228
|
handler: async (ctx, args) => {
|
|
110
|
-
const
|
|
111
|
-
.query("tool_calls")
|
|
112
|
-
.withIndex("by_thread_tool_call_id", (q) => q.eq("threadId", args.threadId).eq("toolCallId", args.toolCallId))
|
|
113
|
-
.first();
|
|
114
|
-
if (existingToolCall) {
|
|
115
|
-
throw new Error(`Tool call ${args.toolCallId} already exists`);
|
|
116
|
-
}
|
|
117
|
-
logger.debug(`create: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}, msgId=${args.msgId}`);
|
|
118
|
-
const expiresAt = Date.now() + TOOL_CALL_TIMEOUT_MS;
|
|
119
|
-
const toolCallId = await ctx.db.insert("tool_calls", {
|
|
229
|
+
const toolCall = await createToolCallRecord(ctx, {
|
|
120
230
|
threadId: args.threadId,
|
|
121
231
|
msgId: args.msgId,
|
|
122
232
|
toolCallId: args.toolCallId,
|
|
123
233
|
toolName: args.toolName,
|
|
124
234
|
callback: args.callback,
|
|
125
|
-
|
|
235
|
+
handler: args.handler,
|
|
236
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
126
237
|
args: args.args,
|
|
127
238
|
saveDelta: args.saveDelta,
|
|
128
|
-
timeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
129
|
-
expiresAt,
|
|
130
|
-
status: "pending",
|
|
131
|
-
});
|
|
132
|
-
const timeoutFnId = await ctx.scheduler.runAfter(TOOL_CALL_TIMEOUT_MS, internal.tool_calls.failPendingToolCall, {
|
|
133
|
-
threadId: args.threadId,
|
|
134
|
-
toolCallId: args.toolCallId,
|
|
135
239
|
});
|
|
136
|
-
await ctx.db.patch(toolCallId, { timeoutFnId });
|
|
137
|
-
const toolCall = await ctx.db.get(toolCallId);
|
|
138
240
|
return publicToolCall(toolCall);
|
|
139
241
|
},
|
|
140
242
|
});
|
|
@@ -155,7 +257,13 @@ export const setResult = mutation({
|
|
|
155
257
|
}
|
|
156
258
|
logger.debug(`setResult: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}`);
|
|
157
259
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
158
|
-
await ctx
|
|
260
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
261
|
+
await ctx.db.patch(args.id, {
|
|
262
|
+
result: args.result,
|
|
263
|
+
status: "completed",
|
|
264
|
+
executionRetryFnId: undefined,
|
|
265
|
+
nextRetryAt: undefined,
|
|
266
|
+
});
|
|
159
267
|
return true;
|
|
160
268
|
},
|
|
161
269
|
});
|
|
@@ -176,7 +284,14 @@ export const setError = mutation({
|
|
|
176
284
|
}
|
|
177
285
|
logger.debug(`setError: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}, error=${args.error}`);
|
|
178
286
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
179
|
-
await ctx
|
|
287
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
288
|
+
await ctx.db.patch(args.id, {
|
|
289
|
+
error: args.error,
|
|
290
|
+
status: "failed",
|
|
291
|
+
callbackLastError: args.error,
|
|
292
|
+
executionRetryFnId: undefined,
|
|
293
|
+
nextRetryAt: undefined,
|
|
294
|
+
});
|
|
180
295
|
return true;
|
|
181
296
|
},
|
|
182
297
|
});
|
|
@@ -276,6 +391,7 @@ export const scheduleToolCall = mutation({
|
|
|
276
391
|
toolName: v.string(),
|
|
277
392
|
args: v.any(),
|
|
278
393
|
handler: v.string(),
|
|
394
|
+
retry: v.optional(v.any()),
|
|
279
395
|
saveDelta: v.boolean(),
|
|
280
396
|
},
|
|
281
397
|
returns: v.null(),
|
|
@@ -287,11 +403,13 @@ export const scheduleToolCall = mutation({
|
|
|
287
403
|
}
|
|
288
404
|
logger.debug(`scheduleToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
289
405
|
// Create the tool call record
|
|
290
|
-
await ctx
|
|
406
|
+
await createToolCallRecord(ctx, {
|
|
291
407
|
threadId: args.threadId,
|
|
292
408
|
msgId: args.msgId,
|
|
293
409
|
toolCallId: args.toolCallId,
|
|
294
410
|
toolName: args.toolName,
|
|
411
|
+
handler: args.handler,
|
|
412
|
+
retry: normalizeSyncToolRetryPolicy(args.retry),
|
|
295
413
|
args: args.args,
|
|
296
414
|
saveDelta: args.saveDelta,
|
|
297
415
|
});
|
|
@@ -330,7 +448,7 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
330
448
|
}
|
|
331
449
|
logger.debug(`scheduleAsyncToolCall: tool=${args.toolName}, callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
332
450
|
// Create the tool call record (will remain pending until addToolResult is called)
|
|
333
|
-
await ctx
|
|
451
|
+
await createToolCallRecord(ctx, {
|
|
334
452
|
threadId: args.threadId,
|
|
335
453
|
toolCallId: args.toolCallId,
|
|
336
454
|
msgId: args.msgId,
|
|
@@ -354,6 +472,126 @@ export const scheduleAsyncToolCall = mutation({
|
|
|
354
472
|
return null;
|
|
355
473
|
},
|
|
356
474
|
});
|
|
475
|
+
export const updateExecutionRetryState = internalMutation({
|
|
476
|
+
args: {
|
|
477
|
+
threadId: v.id("threads"),
|
|
478
|
+
toolCallId: v.string(),
|
|
479
|
+
executionAttempt: v.number(),
|
|
480
|
+
executionLastError: v.optional(v.string()),
|
|
481
|
+
nextRetryAt: v.optional(v.number()),
|
|
482
|
+
executionRetryFnId: v.optional(v.id("_scheduled_functions")),
|
|
483
|
+
clearNextRetryAt: v.optional(v.boolean()),
|
|
484
|
+
clearExecutionRetryFnId: v.optional(v.boolean()),
|
|
485
|
+
},
|
|
486
|
+
returns: v.null(),
|
|
487
|
+
handler: async (ctx, args) => {
|
|
488
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
489
|
+
threadId: args.threadId,
|
|
490
|
+
toolCallId: args.toolCallId,
|
|
491
|
+
});
|
|
492
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const patch = {
|
|
496
|
+
executionAttempt: args.executionAttempt,
|
|
497
|
+
};
|
|
498
|
+
if (args.executionLastError !== undefined) {
|
|
499
|
+
patch.executionLastError = args.executionLastError;
|
|
500
|
+
}
|
|
501
|
+
if (args.nextRetryAt !== undefined) {
|
|
502
|
+
patch.nextRetryAt = args.nextRetryAt;
|
|
503
|
+
}
|
|
504
|
+
if (args.executionRetryFnId !== undefined) {
|
|
505
|
+
patch.executionRetryFnId = args.executionRetryFnId;
|
|
506
|
+
}
|
|
507
|
+
if (args.clearNextRetryAt) {
|
|
508
|
+
patch.nextRetryAt = undefined;
|
|
509
|
+
}
|
|
510
|
+
if (args.clearExecutionRetryFnId) {
|
|
511
|
+
patch.executionRetryFnId = undefined;
|
|
512
|
+
}
|
|
513
|
+
await ctx.db.patch(toolCall._id, patch);
|
|
514
|
+
return null;
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
export const scheduleExecutionRetry = internalMutation({
|
|
518
|
+
args: {
|
|
519
|
+
threadId: v.id("threads"),
|
|
520
|
+
toolCallId: v.string(),
|
|
521
|
+
handler: v.string(),
|
|
522
|
+
executionAttempt: v.number(),
|
|
523
|
+
executionLastError: v.string(),
|
|
524
|
+
nextRetryAt: v.number(),
|
|
525
|
+
},
|
|
526
|
+
returns: v.null(),
|
|
527
|
+
handler: async (ctx, args) => {
|
|
528
|
+
const toolCall = await getToolCallByScope(ctx, {
|
|
529
|
+
threadId: args.threadId,
|
|
530
|
+
toolCallId: args.toolCallId,
|
|
531
|
+
});
|
|
532
|
+
if (!toolCall || toolCall.status !== "pending") {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
await cleanupExecutionRetryFn(ctx, toolCall);
|
|
536
|
+
const delayMs = Math.max(0, args.nextRetryAt - Date.now());
|
|
537
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
538
|
+
threadId: args.threadId,
|
|
539
|
+
toolCallId: args.toolCallId,
|
|
540
|
+
handler: args.handler,
|
|
541
|
+
});
|
|
542
|
+
await ctx.db.patch(toolCall._id, {
|
|
543
|
+
executionAttempt: args.executionAttempt,
|
|
544
|
+
executionLastError: args.executionLastError,
|
|
545
|
+
nextRetryAt: args.nextRetryAt,
|
|
546
|
+
executionRetryFnId,
|
|
547
|
+
});
|
|
548
|
+
return null;
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
export const resumePendingSyncToolExecutions = mutation({
|
|
552
|
+
args: {
|
|
553
|
+
limit: v.optional(v.number()),
|
|
554
|
+
},
|
|
555
|
+
returns: v.number(),
|
|
556
|
+
handler: async (ctx, args) => {
|
|
557
|
+
const limit = Math.max(1, Math.floor(args.limit ?? 100));
|
|
558
|
+
const pending = await ctx.db
|
|
559
|
+
.query("tool_calls")
|
|
560
|
+
.withIndex("by_status_only", (q) => q.eq("status", "pending"))
|
|
561
|
+
.take(limit * 2);
|
|
562
|
+
let resumed = 0;
|
|
563
|
+
const now = Date.now();
|
|
564
|
+
for (const toolCall of pending) {
|
|
565
|
+
if (resumed >= limit) {
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
if (!toolCall.handler) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (toolCall.executionRetryFnId) {
|
|
572
|
+
const retryFn = await ctx.db.system.get(toolCall.executionRetryFnId);
|
|
573
|
+
if (retryFn?.state.kind === "pending") {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const nextRetryAt = toolCall.nextRetryAt ?? now;
|
|
578
|
+
const delayMs = Math.max(0, nextRetryAt - now);
|
|
579
|
+
const executionRetryFnId = await ctx.scheduler.runAfter(delayMs, internal.tool_calls.executeToolCall, {
|
|
580
|
+
threadId: toolCall.threadId,
|
|
581
|
+
toolCallId: toolCall.toolCallId,
|
|
582
|
+
handler: toolCall.handler,
|
|
583
|
+
});
|
|
584
|
+
await ctx.db.patch(toolCall._id, {
|
|
585
|
+
executionRetryFnId,
|
|
586
|
+
});
|
|
587
|
+
resumed += 1;
|
|
588
|
+
}
|
|
589
|
+
if (resumed > 0) {
|
|
590
|
+
logger.warn(`resumePendingSyncToolExecutions: resumed ${resumed} pending sync tool call(s)`);
|
|
591
|
+
}
|
|
592
|
+
return resumed;
|
|
593
|
+
},
|
|
594
|
+
});
|
|
357
595
|
/**
|
|
358
596
|
* Execute a tool call
|
|
359
597
|
*/
|
|
@@ -365,7 +603,6 @@ export const executeToolCall = internalAction({
|
|
|
365
603
|
},
|
|
366
604
|
returns: v.null(),
|
|
367
605
|
handler: async (ctx, args) => {
|
|
368
|
-
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}`);
|
|
369
606
|
// Get the tool call record
|
|
370
607
|
const toolCall = await ctx.runQuery(api.tool_calls.getByToolCallId, {
|
|
371
608
|
threadId: args.threadId,
|
|
@@ -374,32 +611,107 @@ export const executeToolCall = internalAction({
|
|
|
374
611
|
if (!toolCall) {
|
|
375
612
|
throw new Error(`Tool call ${args.toolCallId} not found`);
|
|
376
613
|
}
|
|
614
|
+
if (toolCall.status !== "pending") {
|
|
615
|
+
logger.debug(`executeToolCall: skipping callId=${args.toolCallId}, status already terminal (${toolCall.status})`);
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
377
618
|
logger.debug(`executeToolCall: tool=${toolCall.toolName}`);
|
|
619
|
+
const thread = await ctx.runQuery(api.threads.get, {
|
|
620
|
+
threadId: args.threadId,
|
|
621
|
+
});
|
|
622
|
+
if (!thread) {
|
|
623
|
+
throw new Error(`Thread ${args.threadId} not found`);
|
|
624
|
+
}
|
|
625
|
+
if (thread.stopSignal || thread.status === "stopped") {
|
|
626
|
+
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
627
|
+
threadId: args.threadId,
|
|
628
|
+
toolCallId: toolCall.toolCallId,
|
|
629
|
+
error: "Tool execution cancelled because the thread was stopped",
|
|
630
|
+
});
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const handler = toolCall.handler ?? args.handler;
|
|
634
|
+
const retryPolicy = normalizeSyncToolRetryPolicy(toolCall.executionRetryPolicy);
|
|
635
|
+
const retryEnabled = retryPolicy?.enabled === true;
|
|
636
|
+
const maxAttempts = retryEnabled ? Math.max(1, retryPolicy.maxAttempts ?? SYNC_TOOL_MAX_ATTEMPTS) : 1;
|
|
637
|
+
const toolArgs = typeof toolCall.args === "object" && toolCall.args !== null ? toolCall.args : {};
|
|
638
|
+
const attempt = Math.max(1, (toolCall.executionAttempt ?? 0) + 1);
|
|
639
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
640
|
+
threadId: args.threadId,
|
|
641
|
+
toolCallId: args.toolCallId,
|
|
642
|
+
executionAttempt: attempt,
|
|
643
|
+
clearNextRetryAt: true,
|
|
644
|
+
clearExecutionRetryFnId: true,
|
|
645
|
+
});
|
|
646
|
+
logger.debug(`executeToolCall: callId=${args.toolCallId}, thread=${args.threadId}, attempt=${attempt}/${maxAttempts}`);
|
|
378
647
|
try {
|
|
379
|
-
// Execute the tool handler
|
|
380
|
-
// The handler string is passed from the client and we need to resolve it
|
|
381
|
-
// For now, we'll use ctx.runAction with a dynamic reference
|
|
382
|
-
// This requires the handler to be a proper function reference string
|
|
383
|
-
const toolArgs = typeof toolCall.args === "object" && toolCall.args !== null ? toolCall.args : {};
|
|
384
648
|
logger.debug(`executeToolCall: invoking handler for callId=${args.toolCallId}`);
|
|
385
|
-
const result = await ctx.runAction(
|
|
649
|
+
const result = await ctx.runAction(handler, toolArgs);
|
|
386
650
|
logger.debug(`executeToolCall: handler succeeded for callId=${args.toolCallId}`);
|
|
387
651
|
await ctx.runMutation(api.tool_calls.addToolResult, {
|
|
388
652
|
threadId: args.threadId,
|
|
389
653
|
result,
|
|
390
654
|
toolCallId: toolCall.toolCallId,
|
|
391
655
|
});
|
|
656
|
+
return null;
|
|
392
657
|
}
|
|
393
658
|
catch (e) {
|
|
394
|
-
const
|
|
395
|
-
|
|
659
|
+
const errorInfo = extractToolErrorInfo(e);
|
|
660
|
+
const error = errorInfo.message;
|
|
661
|
+
logger.debug(`executeToolCall: handler failed for callId=${args.toolCallId} (attempt=${attempt}): ${error}`);
|
|
662
|
+
let retryable = false;
|
|
663
|
+
if (retryEnabled) {
|
|
664
|
+
if (retryPolicy.shouldRetryError) {
|
|
665
|
+
try {
|
|
666
|
+
const decision = await ctx.runAction(retryPolicy.shouldRetryError, {
|
|
667
|
+
threadId: args.threadId,
|
|
668
|
+
toolCallId: args.toolCallId,
|
|
669
|
+
toolName: toolCall.toolName,
|
|
670
|
+
args: toolCall.args,
|
|
671
|
+
error,
|
|
672
|
+
attempt,
|
|
673
|
+
maxAttempts,
|
|
674
|
+
});
|
|
675
|
+
retryable = isRetryableDecision(decision);
|
|
676
|
+
}
|
|
677
|
+
catch (classifierError) {
|
|
678
|
+
logger.warn(`executeToolCall: shouldRetryError failed for callId=${args.toolCallId}, falling back to default classifier: ${classifierError instanceof Error ? classifierError.message : String(classifierError)}`);
|
|
679
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
retryable = isRetryableToolErrorDefault(errorInfo);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (retryEnabled && retryable && attempt < maxAttempts) {
|
|
687
|
+
const delayMs = computeRetryDelayMs(attempt, retryPolicy.backoff);
|
|
688
|
+
const nextRetryAt = Date.now() + delayMs;
|
|
689
|
+
await ctx.runMutation(internal.tool_calls.scheduleExecutionRetry, {
|
|
690
|
+
threadId: args.threadId,
|
|
691
|
+
toolCallId: args.toolCallId,
|
|
692
|
+
handler,
|
|
693
|
+
executionAttempt: attempt,
|
|
694
|
+
executionLastError: error,
|
|
695
|
+
nextRetryAt,
|
|
696
|
+
});
|
|
697
|
+
logger.warn(`executeToolCall: scheduled retry for callId=${args.toolCallId} in ${delayMs}ms (attempt ${attempt + 1}/${maxAttempts})`);
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
await ctx.runMutation(internal.tool_calls.updateExecutionRetryState, {
|
|
701
|
+
threadId: args.threadId,
|
|
702
|
+
toolCallId: args.toolCallId,
|
|
703
|
+
executionAttempt: attempt,
|
|
704
|
+
executionLastError: error,
|
|
705
|
+
clearNextRetryAt: true,
|
|
706
|
+
clearExecutionRetryFnId: true,
|
|
707
|
+
});
|
|
396
708
|
await ctx.runMutation(api.tool_calls.addToolError, {
|
|
397
709
|
threadId: args.threadId,
|
|
398
710
|
error,
|
|
399
711
|
toolCallId: toolCall.toolCallId,
|
|
400
712
|
});
|
|
713
|
+
return null;
|
|
401
714
|
}
|
|
402
|
-
return null;
|
|
403
715
|
},
|
|
404
716
|
});
|
|
405
717
|
/**
|
|
@@ -543,6 +855,7 @@ export const onToolComplete = internalMutation({
|
|
|
543
855
|
status: "stopped",
|
|
544
856
|
activeStream: null,
|
|
545
857
|
continue: false,
|
|
858
|
+
retryState: undefined,
|
|
546
859
|
});
|
|
547
860
|
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
548
861
|
await ctx.runMutation(thread.onStatusChangeHandle, {
|
|
@@ -562,6 +875,10 @@ export const onToolComplete = internalMutation({
|
|
|
562
875
|
}
|
|
563
876
|
return null;
|
|
564
877
|
}
|
|
878
|
+
if (!shouldContinueAfterToolCompletion(thread.status)) {
|
|
879
|
+
logger.debug(`onToolComplete: thread status=${thread.status}, skipping continuation`);
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
565
882
|
// Check for pending tool calls
|
|
566
883
|
const pending = await ctx.runQuery(api.tool_calls.listPending, {
|
|
567
884
|
threadId: args.threadId,
|