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.
Files changed (75) hide show
  1. package/README.md +81 -7
  2. package/dist/client/api.d.ts.map +1 -1
  3. package/dist/client/api.js +23 -4
  4. package/dist/client/api.js.map +1 -1
  5. package/dist/client/handler.d.ts +22 -0
  6. package/dist/client/handler.d.ts.map +1 -1
  7. package/dist/client/handler.js +261 -118
  8. package/dist/client/handler.js.map +1 -1
  9. package/dist/client/streamer.js +1 -1
  10. package/dist/client/streamer.js.map +1 -1
  11. package/dist/client/tools.d.ts +17 -0
  12. package/dist/client/tools.d.ts.map +1 -1
  13. package/dist/client/tools.js +16 -0
  14. package/dist/client/tools.js.map +1 -1
  15. package/dist/client/types.d.ts +75 -1
  16. package/dist/client/types.d.ts.map +1 -1
  17. package/dist/client/types.js +11 -0
  18. package/dist/client/types.js.map +1 -1
  19. package/dist/component/_generated/component.d.ts +89 -0
  20. package/dist/component/_generated/component.d.ts.map +1 -1
  21. package/dist/component/agent.d.ts.map +1 -1
  22. package/dist/component/agent.js +21 -2
  23. package/dist/component/agent.js.map +1 -1
  24. package/dist/component/schema.d.ts +70 -2
  25. package/dist/component/schema.d.ts.map +1 -1
  26. package/dist/component/schema.js +21 -0
  27. package/dist/component/schema.js.map +1 -1
  28. package/dist/component/streams.js +2 -2
  29. package/dist/component/streams.js.map +1 -1
  30. package/dist/component/threads.d.ts +92 -2
  31. package/dist/component/threads.d.ts.map +1 -1
  32. package/dist/component/threads.js +83 -2
  33. package/dist/component/threads.js.map +1 -1
  34. package/dist/component/tool_calls.d.ts +55 -3
  35. package/dist/component/tool_calls.d.ts.map +1 -1
  36. package/dist/component/tool_calls.js +352 -35
  37. package/dist/component/tool_calls.js.map +1 -1
  38. package/dist/utils/logger.d.ts.map +1 -0
  39. package/dist/utils/logger.js.map +1 -0
  40. package/dist/utils/msg.d.ts +3 -0
  41. package/dist/utils/msg.d.ts.map +1 -0
  42. package/dist/utils/msg.js +7 -0
  43. package/dist/utils/msg.js.map +1 -0
  44. package/dist/utils/retry.d.ts +69 -0
  45. package/dist/utils/retry.d.ts.map +1 -0
  46. package/dist/utils/retry.js +404 -0
  47. package/dist/utils/retry.js.map +1 -0
  48. package/dist/utils/streaming.d.ts +4 -0
  49. package/dist/utils/streaming.d.ts.map +1 -0
  50. package/dist/utils/streaming.js +4 -0
  51. package/dist/utils/streaming.js.map +1 -0
  52. package/package.json +1 -1
  53. package/src/client/api.ts +24 -4
  54. package/src/client/handler.ts +337 -134
  55. package/src/client/streamer.ts +1 -1
  56. package/src/client/tools.ts +43 -1
  57. package/src/client/types.ts +60 -0
  58. package/src/component/_generated/component.ts +103 -0
  59. package/src/component/agent.ts +24 -2
  60. package/src/component/schema.ts +22 -0
  61. package/src/component/streams.ts +2 -2
  62. package/src/component/threads.ts +92 -3
  63. package/src/component/tool_calls.ts +430 -44
  64. package/src/utils/msg.ts +8 -0
  65. package/src/utils/retry.ts +528 -0
  66. package/src/{streaming.ts → utils/streaming.ts} +2 -3
  67. package/dist/logger.d.ts.map +0 -1
  68. package/dist/logger.js.map +0 -1
  69. package/dist/streaming.d.ts +0 -3
  70. package/dist/streaming.d.ts.map +0 -1
  71. package/dist/streaming.js +0 -4
  72. package/dist/streaming.js.map +0 -1
  73. /package/dist/{logger.d.ts → utils/logger.d.ts} +0 -0
  74. /package/dist/{logger.js → utils/logger.js} +0 -0
  75. /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
- }, "required", "threadId" | "msgId" | "error" | "toolCallId" | "result" | "args" | "toolName" | "_creationTime" | `result.${string}` | `args.${string}` | "_id">;
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,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAKxC,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,2BAA2B,CAAC;AAmFzD,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;CAChB,CAAC;AAiBF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;gKAUvB,CAAC;AAEH,eAAO,MAAM,MAAM;;;;;;;;wBA6CjB,CAAC;AAEH,eAAO,MAAM,SAAS;;;oBAoBpB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;oBAoBnB,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;;;;;;;;iBA2C3B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;iBAiDhC,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,eAAe;;;;iBAgD1B,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;;iBAqEzB,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
+ {"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 existingToolCall = await ctx.db
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
- callbackAttempt: args.callback ? 0 : undefined,
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.db.patch(args.id, { result: args.result, status: "completed" });
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.db.patch(args.id, { error: args.error, status: "failed", callbackLastError: args.error });
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.runMutation(api.tool_calls.create, {
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.runMutation(api.tool_calls.create, {
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(args.handler, toolArgs);
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 error = e instanceof Error ? e.message : String(e);
395
- logger.debug(`executeToolCall: handler failed for callId=${args.toolCallId}: ${error}`);
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,