@convex-dev/agent 0.6.0 → 0.6.1
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/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +634 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +167 -0
- package/dist/client/createTool.d.ts.map +1 -0
- package/dist/client/createTool.js +116 -0
- package/dist/client/createTool.js.map +1 -0
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1725 -0
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -0
- package/dist/client/definePlaygroundAPI.js +271 -0
- package/dist/client/definePlaygroundAPI.js.map +1 -0
- package/dist/client/files.d.ts +69 -0
- package/dist/client/files.d.ts.map +1 -0
- package/dist/client/files.js +181 -0
- package/dist/client/files.js.map +1 -0
- package/dist/client/index.d.ts +2091 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +929 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +182 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +493 -0
- package/dist/client/search.d.ts.map +1 -0
- package/dist/client/search.js +435 -0
- package/dist/client/search.js.map +1 -0
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +185 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +127 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3778 -0
- package/dist/client/streaming.d.ts.map +1 -0
- package/dist/client/streaming.js +330 -0
- package/dist/client/streaming.js.map +1 -0
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +461 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +48 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +3120 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/apiKeys.d.ts +11 -0
- package/dist/component/apiKeys.d.ts.map +1 -0
- package/dist/component/apiKeys.js +69 -0
- package/dist/component/apiKeys.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/files.d.ts +97 -0
- package/dist/component/files.d.ts.map +1 -0
- package/dist/component/files.js +190 -0
- package/dist/component/files.js.map +1 -0
- package/dist/component/messages.d.ts +3851 -0
- package/dist/component/messages.d.ts.map +1 -0
- package/dist/component/messages.js +757 -0
- package/dist/component/messages.js.map +1 -0
- package/dist/component/schema.d.ts +8029 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +147 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/streams.d.ts +128 -0
- package/dist/component/streams.d.ts.map +1 -0
- package/dist/component/streams.js +413 -0
- package/dist/component/streams.js.map +1 -0
- package/dist/component/threads.d.ts +115 -0
- package/dist/component/threads.d.ts.map +1 -0
- package/dist/component/threads.js +208 -0
- package/dist/component/threads.js.map +1 -0
- package/dist/component/users.d.ts +52 -0
- package/dist/component/users.d.ts.map +1 -0
- package/dist/component/users.js +229 -0
- package/dist/component/users.js.map +1 -0
- package/dist/component/vector/index.d.ts +61 -0
- package/dist/component/vector/index.d.ts.map +1 -0
- package/dist/component/vector/index.js +146 -0
- package/dist/component/vector/index.js.map +1 -0
- package/dist/component/vector/tables.d.ts +58 -0
- package/dist/component/vector/tables.d.ts.map +1 -0
- package/dist/component/vector/tables.js +56 -0
- package/dist/component/vector/tables.js.map +1 -0
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +446 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +76 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/mapping.js +720 -0
- package/dist/mapping.js.map +1 -0
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/optimisticallySendMessage.d.ts +42 -0
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/react/optimisticallySendMessage.js +74 -0
- package/dist/react/optimisticallySendMessage.js.map +1 -0
- package/dist/react/types.d.ts +12 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/react/types.js +2 -0
- package/dist/react/types.js.map +1 -0
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +106 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +27 -0
- package/dist/react/useSmoothText.d.ts.map +1 -0
- package/dist/react/useSmoothText.js +68 -0
- package/dist/react/useSmoothText.js.map +1 -0
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +26 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +67 -0
- package/dist/shared.js.map +1 -0
- package/dist/validators.d.ts +24516 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +475 -0
- package/dist/validators.js.map +1 -0
- package/package.json +39 -39
- package/src/UIMessages.ts +0 -9
- package/src/client/approval.test.ts +144 -0
- package/src/client/index.ts +17 -9
- package/src/client/search.ts +0 -29
- package/src/client/start.ts +2 -2
- package/src/client/streamText.ts +9 -0
- package/src/client/streaming.ts +30 -23
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +0 -5
- package/src/mapping.ts +38 -22
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"email": "support@convex.dev",
|
|
8
8
|
"url": "https://github.com/get-convex/agent/issues"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.6.
|
|
10
|
+
"version": "0.6.1",
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"keywords": [
|
|
13
13
|
"convex",
|
|
@@ -84,66 +84,66 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@ai-sdk/anthropic": "
|
|
88
|
-
"@ai-sdk/google": "
|
|
89
|
-
"@ai-sdk/groq": "
|
|
90
|
-
"@ai-sdk/openai": "
|
|
91
|
-
"@ai-sdk/provider": "
|
|
92
|
-
"@ai-sdk/provider-utils": "
|
|
93
|
-
"@convex-dev/rag": "0.7.
|
|
94
|
-
"@convex-dev/rate-limiter": "0.3.
|
|
95
|
-
"@convex-dev/workflow": "0.3.
|
|
87
|
+
"@ai-sdk/anthropic": "3.0.13",
|
|
88
|
+
"@ai-sdk/google": "3.0.30",
|
|
89
|
+
"@ai-sdk/groq": "3.0.8",
|
|
90
|
+
"@ai-sdk/openai": "3.0.10",
|
|
91
|
+
"@ai-sdk/provider": "3.0.3",
|
|
92
|
+
"@ai-sdk/provider-utils": "4.0.6",
|
|
93
|
+
"@convex-dev/rag": "0.7.2",
|
|
94
|
+
"@convex-dev/rate-limiter": "0.3.2",
|
|
95
|
+
"@convex-dev/workflow": "0.3.6",
|
|
96
96
|
"@edge-runtime/vm": "5.0.0",
|
|
97
|
-
"@eslint/js": "9.
|
|
97
|
+
"@eslint/js": "9.39.4",
|
|
98
98
|
"@hookform/resolvers": "5.2.2",
|
|
99
99
|
"@langchain/textsplitters": "0.1.0",
|
|
100
100
|
"@radix-ui/react-accordion": "1.2.12",
|
|
101
101
|
"@radix-ui/react-checkbox": "1.3.3",
|
|
102
|
-
"@radix-ui/react-label": "2.1.
|
|
102
|
+
"@radix-ui/react-label": "2.1.8",
|
|
103
103
|
"@radix-ui/react-select": "2.2.6",
|
|
104
|
-
"@radix-ui/react-slot": "1.2.
|
|
104
|
+
"@radix-ui/react-slot": "1.2.4",
|
|
105
105
|
"@radix-ui/react-toast": "1.2.15",
|
|
106
106
|
"@tailwindcss/typography": "0.5.19",
|
|
107
|
-
"@types/node": "20.19.
|
|
108
|
-
"@types/react": "19.2.
|
|
109
|
-
"@types/react-dom": "19.2.
|
|
107
|
+
"@types/node": "20.19.37",
|
|
108
|
+
"@types/react": "19.2.14",
|
|
109
|
+
"@types/react-dom": "19.2.3",
|
|
110
110
|
"@vitejs/plugin-react": "4.7.0",
|
|
111
|
-
"ai": "
|
|
112
|
-
"autoprefixer": "10.4.
|
|
111
|
+
"ai": "6.0.35",
|
|
112
|
+
"autoprefixer": "10.4.27",
|
|
113
113
|
"chokidar-cli": "3.0.0",
|
|
114
114
|
"class-variance-authority": "0.7.1",
|
|
115
115
|
"clsx": "2.1.1",
|
|
116
|
-
"convex": "1.
|
|
117
|
-
"convex-helpers": "0.1.
|
|
118
|
-
"convex-test": "0.0.
|
|
116
|
+
"convex": "1.33.1",
|
|
117
|
+
"convex-helpers": "0.1.114",
|
|
118
|
+
"convex-test": "0.0.41",
|
|
119
119
|
"cpy-cli": "5.0.0",
|
|
120
|
-
"dayjs": "1.11.
|
|
120
|
+
"dayjs": "1.11.20",
|
|
121
121
|
"dotenv": "16.6.1",
|
|
122
|
-
"eslint": "9.
|
|
122
|
+
"eslint": "9.39.4",
|
|
123
123
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
124
124
|
"eslint-plugin-react": "7.37.5",
|
|
125
|
-
"eslint-plugin-react-hooks": "
|
|
126
|
-
"eslint-plugin-react-refresh": "0.
|
|
127
|
-
"globals": "16.
|
|
128
|
-
"lucide-react": "0.
|
|
125
|
+
"eslint-plugin-react-hooks": "6.1.1",
|
|
126
|
+
"eslint-plugin-react-refresh": "0.5.2",
|
|
127
|
+
"globals": "16.5.0",
|
|
128
|
+
"lucide-react": "0.577.0",
|
|
129
129
|
"npm-run-all2": "8.0.4",
|
|
130
|
-
"ollama-ai-provider": "
|
|
130
|
+
"ollama-ai-provider": "1.2.0",
|
|
131
131
|
"openai": "5.23.2",
|
|
132
|
-
"path-exists-cli": "
|
|
133
|
-
"pkg-pr-new": "0.0.
|
|
134
|
-
"postcss": "8.5.
|
|
135
|
-
"prettier": "3.
|
|
136
|
-
"react": "19.2.
|
|
137
|
-
"react-dom": "19.2.
|
|
138
|
-
"react-hook-form": "7.
|
|
132
|
+
"path-exists-cli": "2.0.0",
|
|
133
|
+
"pkg-pr-new": "0.0.66",
|
|
134
|
+
"postcss": "8.5.8",
|
|
135
|
+
"prettier": "3.8.1",
|
|
136
|
+
"react": "19.2.4",
|
|
137
|
+
"react-dom": "19.2.4",
|
|
138
|
+
"react-hook-form": "7.71.2",
|
|
139
139
|
"react-markdown": "10.1.0",
|
|
140
|
-
"react-router-dom": "7.
|
|
140
|
+
"react-router-dom": "7.13.1",
|
|
141
141
|
"readline": "1.3.0",
|
|
142
|
-
"tailwind-merge": "3.
|
|
143
|
-
"tailwindcss": "3.4.
|
|
142
|
+
"tailwind-merge": "3.5.0",
|
|
143
|
+
"tailwindcss": "3.4.19",
|
|
144
144
|
"tailwindcss-animate": "1.0.7",
|
|
145
145
|
"typescript": "5.9.3",
|
|
146
|
-
"typescript-eslint": "8.
|
|
146
|
+
"typescript-eslint": "8.57.1",
|
|
147
147
|
"vite": "6.4.1",
|
|
148
148
|
"vitest": "3.2.4",
|
|
149
149
|
"zod": "3.25.76"
|
package/src/UIMessages.ts
CHANGED
|
@@ -396,8 +396,6 @@ function createAssistantUIMessage<
|
|
|
396
396
|
? ("streaming" as const)
|
|
397
397
|
: lastMessage.status;
|
|
398
398
|
|
|
399
|
-
<<<<<<< Updated upstream
|
|
400
|
-
=======
|
|
401
399
|
// Extract approval parts from raw message content for UI rendering
|
|
402
400
|
type ApprovalPart =
|
|
403
401
|
| { type: "tool-approval-request"; approvalId: string; toolCallId: string }
|
|
@@ -443,7 +441,6 @@ function createAssistantUIMessage<
|
|
|
443
441
|
}
|
|
444
442
|
}
|
|
445
443
|
|
|
446
|
-
>>>>>>> Stashed changes
|
|
447
444
|
// Collect all parts from all messages
|
|
448
445
|
const allParts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
|
|
449
446
|
|
|
@@ -525,12 +522,9 @@ function createAssistantUIMessage<
|
|
|
525
522
|
break;
|
|
526
523
|
}
|
|
527
524
|
case "tool-result": {
|
|
528
|
-
<<<<<<< Updated upstream
|
|
529
|
-
=======
|
|
530
525
|
// Note: execution-denied outputs are handled separately via pre-extraction
|
|
531
526
|
// from raw content (converted to text format for providers in start.ts).
|
|
532
527
|
// See executionDeniedResults processing at the end of this function.
|
|
533
|
-
>>>>>>> Stashed changes
|
|
534
528
|
const typedPart = contentPart as unknown as ToolResultPart & {
|
|
535
529
|
output: { type: string; value?: unknown; reason?: string };
|
|
536
530
|
};
|
|
@@ -696,8 +690,6 @@ function createAssistantUIMessage<
|
|
|
696
690
|
}
|
|
697
691
|
}
|
|
698
692
|
|
|
699
|
-
<<<<<<< Updated upstream
|
|
700
|
-
=======
|
|
701
693
|
// Final output states that should not be overwritten by approval processing
|
|
702
694
|
const finalStates = new Set([
|
|
703
695
|
"output-available",
|
|
@@ -776,7 +768,6 @@ function createAssistantUIMessage<
|
|
|
776
768
|
}
|
|
777
769
|
}
|
|
778
770
|
|
|
779
|
-
>>>>>>> Stashed changes
|
|
780
771
|
return {
|
|
781
772
|
...common,
|
|
782
773
|
role: "assistant",
|
|
@@ -57,6 +57,17 @@ function getApprovalIdFromSavedMessages(
|
|
|
57
57
|
return approvalRequest.approvalId;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Second tool that also requires approval
|
|
61
|
+
const renameFileTool = createTool({
|
|
62
|
+
description: "Rename a file",
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
oldName: z.string(),
|
|
65
|
+
newName: z.string(),
|
|
66
|
+
}),
|
|
67
|
+
needsApproval: () => true,
|
|
68
|
+
execute: async (_ctx, input) => `Renamed: ${input.oldName} → ${input.newName}`,
|
|
69
|
+
});
|
|
70
|
+
|
|
60
71
|
// --- Agents (separate mock model instances to avoid shared callIndex) ---
|
|
61
72
|
|
|
62
73
|
const approvalAgent = new Agent(components.agent, {
|
|
@@ -257,6 +268,114 @@ export const testApproveFlowWithInterveningMessage = action({
|
|
|
257
268
|
},
|
|
258
269
|
});
|
|
259
270
|
|
|
271
|
+
// Agent that calls two tools in one step, both needing approval
|
|
272
|
+
const multiToolAgent = new Agent(components.agent, {
|
|
273
|
+
name: "multi-tool-test",
|
|
274
|
+
instructions: "You manage files.",
|
|
275
|
+
tools: { deleteFile: deleteFileTool, renameFile: renameFileTool },
|
|
276
|
+
languageModel: mockModel({
|
|
277
|
+
contentSteps: [
|
|
278
|
+
// Step 1: model calls two tools at once
|
|
279
|
+
[
|
|
280
|
+
{
|
|
281
|
+
type: "tool-call",
|
|
282
|
+
toolCallId: "tc-multi-1",
|
|
283
|
+
toolName: "deleteFile",
|
|
284
|
+
input: JSON.stringify({ filename: "old.txt" }),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: "tool-call",
|
|
288
|
+
toolCallId: "tc-multi-2",
|
|
289
|
+
toolName: "renameFile",
|
|
290
|
+
input: JSON.stringify({ oldName: "a.txt", newName: "b.txt" }),
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
// Step 2: after both tools execute, model responds
|
|
294
|
+
[{ type: "text", text: "Done! Deleted old.txt and renamed a.txt to b.txt." }],
|
|
295
|
+
],
|
|
296
|
+
}),
|
|
297
|
+
stopWhen: stepCountIs(5),
|
|
298
|
+
usageHandler: testUsageHandler,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
export const testMultiToolApproveFlow = action({
|
|
302
|
+
args: {},
|
|
303
|
+
handler: async (ctx) => {
|
|
304
|
+
const { thread } = await multiToolAgent.createThread(ctx, {
|
|
305
|
+
userId: "u-multi",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Step 1: Generate — model calls two tools, both need approval
|
|
309
|
+
const result1 = await thread.generateText({
|
|
310
|
+
prompt: "Delete old.txt and rename a.txt to b.txt",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Extract both approval IDs
|
|
314
|
+
const approvalParts = result1.savedMessages
|
|
315
|
+
?.flatMap((m) =>
|
|
316
|
+
Array.isArray(m.message?.content)
|
|
317
|
+
? (m.message.content as unknown[])
|
|
318
|
+
: [],
|
|
319
|
+
)
|
|
320
|
+
.filter(
|
|
321
|
+
(
|
|
322
|
+
p,
|
|
323
|
+
): p is {
|
|
324
|
+
type: "tool-approval-request";
|
|
325
|
+
approvalId: string;
|
|
326
|
+
toolCallId: string;
|
|
327
|
+
} => (p as { type?: string }).type === "tool-approval-request",
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (!approvalParts || approvalParts.length !== 2) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Expected 2 approval requests, got ${approvalParts?.length ?? 0}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Approve both tool calls
|
|
337
|
+
const { messageId: _msgId1 } = await ctx.runMutation(
|
|
338
|
+
anyApi["approval.test"].submitApprovalForMultiToolAgent,
|
|
339
|
+
{ threadId: thread.threadId, approvalId: approvalParts[0].approvalId },
|
|
340
|
+
);
|
|
341
|
+
const { messageId: msgId2 } = await ctx.runMutation(
|
|
342
|
+
anyApi["approval.test"].submitApprovalForMultiToolAgent,
|
|
343
|
+
{ threadId: thread.threadId, approvalId: approvalParts[1].approvalId },
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Continue generation with the last approval message
|
|
347
|
+
const result2 = await thread.generateText({
|
|
348
|
+
promptMessageId: msgId2,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const allMessages = await multiToolAgent.listMessages(ctx, {
|
|
352
|
+
threadId: thread.threadId,
|
|
353
|
+
paginationOpts: { cursor: null, numItems: 40 },
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
approvalCount: approvalParts.length,
|
|
358
|
+
firstText: result1.text,
|
|
359
|
+
secondText: result2.text,
|
|
360
|
+
threadMessageRoles: allMessages.page.map((m) => m.message?.role),
|
|
361
|
+
// Check that both approvals were merged into one tool message
|
|
362
|
+
toolMessageCount: allMessages.page.filter(
|
|
363
|
+
(m) => m.message?.role === "tool",
|
|
364
|
+
).length,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
export const submitApprovalForMultiToolAgent = mutation({
|
|
370
|
+
args: {
|
|
371
|
+
threadId: v.string(),
|
|
372
|
+
approvalId: v.string(),
|
|
373
|
+
},
|
|
374
|
+
handler: async (ctx, { threadId, approvalId }) => {
|
|
375
|
+
return multiToolAgent.approveToolCall(ctx, { threadId, approvalId });
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
260
379
|
export const submitApprovalForApprovalAgent = mutation({
|
|
261
380
|
args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
|
|
262
381
|
handler: async (ctx, { threadId, approvalId, reason }) => {
|
|
@@ -276,7 +395,9 @@ const testApi: ApiFromModules<{
|
|
|
276
395
|
testApproveFlow: typeof testApproveFlow;
|
|
277
396
|
testDenyFlow: typeof testDenyFlow;
|
|
278
397
|
testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
|
|
398
|
+
testMultiToolApproveFlow: typeof testMultiToolApproveFlow;
|
|
279
399
|
submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
|
|
400
|
+
submitApprovalForMultiToolAgent: typeof submitApprovalForMultiToolAgent;
|
|
280
401
|
submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
|
|
281
402
|
};
|
|
282
403
|
}>["fns"] = anyApi["approval.test"] as any;
|
|
@@ -338,6 +459,29 @@ describe("Tool Approval Workflow", () => {
|
|
|
338
459
|
expect(result.lastUsage!.outputTokenDetails).toBeDefined();
|
|
339
460
|
});
|
|
340
461
|
|
|
462
|
+
test("multi-tool: approve two tool calls from the same step", async () => {
|
|
463
|
+
usageCalls.length = 0;
|
|
464
|
+
const t = initConvexTest(schema);
|
|
465
|
+
const result = await t.action(testApi.testMultiToolApproveFlow, {});
|
|
466
|
+
|
|
467
|
+
expect(result.approvalCount).toBe(2);
|
|
468
|
+
expect(result.firstText).toBe("");
|
|
469
|
+
expect(result.secondText).toBe(
|
|
470
|
+
"Done! Deleted old.txt and renamed a.txt to b.txt.",
|
|
471
|
+
);
|
|
472
|
+
// Both approval responses should be merged into one tool message
|
|
473
|
+
// (write-time merge in respondToToolCallApproval via findApprovalContext)
|
|
474
|
+
// Thread: user, assistant(2 tool-calls + 2 approvals),
|
|
475
|
+
// tool(2 approval-responses merged), tool(2 tool-results), assistant(text)
|
|
476
|
+
expect(result.threadMessageRoles).toEqual([
|
|
477
|
+
"assistant", // final text
|
|
478
|
+
"tool", // tool-results
|
|
479
|
+
"tool", // approval-responses (merged)
|
|
480
|
+
"assistant", // tool-calls + approval-requests
|
|
481
|
+
"user", // prompt
|
|
482
|
+
]);
|
|
483
|
+
});
|
|
484
|
+
|
|
341
485
|
test("approve remains valid with an intervening thread message", async () => {
|
|
342
486
|
usageCalls.length = 0;
|
|
343
487
|
const t = initConvexTest(schema);
|
package/src/client/index.ts
CHANGED
|
@@ -1123,25 +1123,34 @@ export class Agent<
|
|
|
1123
1123
|
// The "already handled" check (tool-approval-response) relies on seeing
|
|
1124
1124
|
// responses before their corresponding requests. If the pagination order
|
|
1125
1125
|
// changes, this logic will need to be updated.
|
|
1126
|
-
let cursor: string | null = null;
|
|
1127
1126
|
let existingResponseMessage: MessageDoc | undefined;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1127
|
+
// Limit the search to the most recent messages. Approvals should always
|
|
1128
|
+
// be near the end of the thread.
|
|
1129
|
+
const page = await this.listMessages(ctx, {
|
|
1130
|
+
threadId: args.threadId,
|
|
1131
|
+
paginationOpts: { cursor: null, numItems: 100 },
|
|
1132
|
+
});
|
|
1133
|
+
{
|
|
1133
1134
|
for (const message of page.page) {
|
|
1134
1135
|
const content = message.message?.content;
|
|
1135
1136
|
if (!Array.isArray(content)) continue;
|
|
1136
1137
|
// Check if this assistant message starts a different approval step.
|
|
1137
1138
|
// If so, any response message we've seen so far belongs to a newer
|
|
1138
1139
|
// step — reset it so we don't merge across step boundaries.
|
|
1140
|
+
// Only reset if the target approval is NOT in this message (i.e.,
|
|
1141
|
+
// this is a genuinely different step, not the same step with
|
|
1142
|
+
// multiple tool calls).
|
|
1139
1143
|
if (
|
|
1140
1144
|
message.message?.role === "assistant" &&
|
|
1141
1145
|
content.some(
|
|
1142
1146
|
(p: any) =>
|
|
1143
1147
|
p.type === "tool-approval-request" &&
|
|
1144
1148
|
p.approvalId !== args.approvalId,
|
|
1149
|
+
) &&
|
|
1150
|
+
!content.some(
|
|
1151
|
+
(p: any) =>
|
|
1152
|
+
p.type === "tool-approval-request" &&
|
|
1153
|
+
p.approvalId === args.approvalId,
|
|
1145
1154
|
)
|
|
1146
1155
|
) {
|
|
1147
1156
|
existingResponseMessage = undefined;
|
|
@@ -1169,11 +1178,10 @@ export class Agent<
|
|
|
1169
1178
|
}
|
|
1170
1179
|
}
|
|
1171
1180
|
}
|
|
1172
|
-
|
|
1173
|
-
} while (cursor !== null);
|
|
1181
|
+
}
|
|
1174
1182
|
|
|
1175
1183
|
throw new Error(
|
|
1176
|
-
`Approval request ${args.approvalId} was not found in thread ${args.threadId}`,
|
|
1184
|
+
`Approval request ${args.approvalId} was not found in the last 100 messages of thread ${args.threadId}`,
|
|
1177
1185
|
);
|
|
1178
1186
|
}
|
|
1179
1187
|
|
package/src/client/search.ts
CHANGED
|
@@ -35,28 +35,6 @@ import {
|
|
|
35
35
|
toModelMessage,
|
|
36
36
|
} from "../mapping.js";
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Filter out approval parts from messages before sending to AI providers.
|
|
40
|
-
* Providers like Anthropic don't understand tool-approval-request and
|
|
41
|
-
* tool-approval-response parts and will error if they are included.
|
|
42
|
-
*/
|
|
43
|
-
function filterApprovalPartsForProvider(messages: ModelMessage[]): ModelMessage[] {
|
|
44
|
-
return messages.map((message) => {
|
|
45
|
-
if (!Array.isArray(message.content)) {
|
|
46
|
-
return message;
|
|
47
|
-
}
|
|
48
|
-
const filteredContent = message.content.filter(
|
|
49
|
-
(part) =>
|
|
50
|
-
(part as { type: string }).type !== "tool-approval-request" &&
|
|
51
|
-
(part as { type: string }).type !== "tool-approval-response",
|
|
52
|
-
);
|
|
53
|
-
return {
|
|
54
|
-
...message,
|
|
55
|
-
content: filteredContent,
|
|
56
|
-
} as typeof message;
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
38
|
const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
|
|
61
39
|
// 10k characters should be more than enough for most cases, and stays under
|
|
62
40
|
// the 8k token limit for some models.
|
|
@@ -256,11 +234,7 @@ export async function fetchRecentAndSearchMessages(
|
|
|
256
234
|
searchMessages = filterOutOrphanedToolMessages(
|
|
257
235
|
sorted(searchResults.filter((m) => !included?.has(m._id))),
|
|
258
236
|
);
|
|
259
|
-
searchMessages = filterApprovalPartsForProvider(searchMessages);
|
|
260
237
|
}
|
|
261
|
-
|
|
262
|
-
// filter out approval parts before returning
|
|
263
|
-
searchMessages = filterApprovalPartsForProvider(searchMessages);
|
|
264
238
|
// Ensure we don't include tool messages without a corresponding tool call
|
|
265
239
|
return { recentMessages, searchMessages };
|
|
266
240
|
}
|
|
@@ -704,9 +678,6 @@ export async function fetchContextWithPrompt(
|
|
|
704
678
|
processedMessages = await inlineMessagesFiles(processedMessages);
|
|
705
679
|
}
|
|
706
680
|
|
|
707
|
-
// Filter out approval parts before sending to AI providers
|
|
708
|
-
processedMessages = filterApprovalPartsForProvider(processedMessages);
|
|
709
|
-
|
|
710
681
|
return {
|
|
711
682
|
messages: processedMessages,
|
|
712
683
|
order: promptMessage?.order,
|
package/src/client/start.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type ToolSet,
|
|
11
11
|
} from "ai";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
serializeResponseMessages,
|
|
14
14
|
serializeObjectResult,
|
|
15
15
|
} from "../mapping.js";
|
|
16
16
|
import { embedMessages, fetchContextWithPrompt } from "./search.js";
|
|
@@ -252,7 +252,7 @@ export async function startGeneration<
|
|
|
252
252
|
previousResponseMessageCount,
|
|
253
253
|
);
|
|
254
254
|
previousResponseMessageCount = allResponseMessages.length;
|
|
255
|
-
serialized = await
|
|
255
|
+
serialized = await serializeResponseMessages(
|
|
256
256
|
ctx,
|
|
257
257
|
component,
|
|
258
258
|
toSave.step,
|
package/src/client/streamText.ts
CHANGED
|
@@ -168,6 +168,15 @@ export async function streamText<
|
|
|
168
168
|
// finish() was never called, leaving the streaming message stuck in
|
|
169
169
|
// "streaming" state. Clean it up by marking it as aborted.
|
|
170
170
|
await streamer?.fail(e instanceof Error ? e.message : String(e));
|
|
171
|
+
// Save the deferred final step if it was already generated but not yet persisted
|
|
172
|
+
if (pendingFinalStep) {
|
|
173
|
+
try {
|
|
174
|
+
await call.save({ step: pendingFinalStep }, false);
|
|
175
|
+
} catch (saveError) {
|
|
176
|
+
console.error("Failed to save deferred final step:", saveError);
|
|
177
|
+
}
|
|
178
|
+
pendingFinalStep = undefined;
|
|
179
|
+
}
|
|
171
180
|
throw e;
|
|
172
181
|
}
|
|
173
182
|
}
|
package/src/client/streaming.ts
CHANGED
|
@@ -244,16 +244,25 @@ export class DeltaStreamer<T> {
|
|
|
244
244
|
this.abortController = new AbortController();
|
|
245
245
|
if (config.abortSignal) {
|
|
246
246
|
config.abortSignal.addEventListener("abort", async () => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
try {
|
|
248
|
+
if (this.abortController.signal.aborted) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
251
|
this.abortController.abort();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
// Wait for in-flight stream creation before trying to abort it
|
|
253
|
+
if (this.#creatingStreamIdPromise) {
|
|
254
|
+
await this.#creatingStreamIdPromise;
|
|
255
|
+
}
|
|
256
|
+
if (this.streamId) {
|
|
257
|
+
await this.#ongoingWrite;
|
|
258
|
+
await this.ctx.runMutation(this.component.streams.abort, {
|
|
259
|
+
streamId: this.streamId,
|
|
260
|
+
reason: "abortSignal",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Best-effort cleanup — the stream will be garbage-collected
|
|
265
|
+
// by the 10-minute timeout if this fails.
|
|
257
266
|
}
|
|
258
267
|
});
|
|
259
268
|
}
|
|
@@ -293,7 +302,9 @@ export class DeltaStreamer<T> {
|
|
|
293
302
|
await this.addParts([chunk]);
|
|
294
303
|
}
|
|
295
304
|
// Skip finish if it will be handled externally (atomically with message save)
|
|
296
|
-
// or if the stream was aborted (e.g., due to a failed delta write)
|
|
305
|
+
// or if the stream was aborted (e.g., due to a failed delta write).
|
|
306
|
+
// Aborted streams are cleaned up via streams.abort (called by the abort
|
|
307
|
+
// signal handler), so we don't need to call finish() for them.
|
|
297
308
|
if (!this.#finishedExternally && !this.abortController.signal.aborted) {
|
|
298
309
|
await this.finish();
|
|
299
310
|
}
|
|
@@ -376,10 +387,7 @@ export class DeltaStreamer<T> {
|
|
|
376
387
|
return;
|
|
377
388
|
}
|
|
378
389
|
await this.#ongoingWrite;
|
|
379
|
-
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
await this.#sendDelta();
|
|
390
|
+
await this.#sendDelta(); // #sendDelta checks aborted internally
|
|
383
391
|
if (this.abortController.signal.aborted) {
|
|
384
392
|
return;
|
|
385
393
|
}
|
|
@@ -439,16 +447,15 @@ export function compressTextStreamParts(
|
|
|
439
447
|
} else {
|
|
440
448
|
compressed.push(part);
|
|
441
449
|
}
|
|
450
|
+
} else if (part.type === "file") {
|
|
451
|
+
compressed.push({
|
|
452
|
+
type: "file",
|
|
453
|
+
file: {
|
|
454
|
+
...part.file,
|
|
455
|
+
uint8Array: undefined as unknown as Uint8Array,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
442
458
|
} else {
|
|
443
|
-
if (part.type === "file") {
|
|
444
|
-
compressed.push({
|
|
445
|
-
type: "file",
|
|
446
|
-
file: {
|
|
447
|
-
...part.file,
|
|
448
|
-
uint8Array: undefined as unknown as Uint8Array,
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
459
|
compressed.push(part);
|
|
453
460
|
}
|
|
454
461
|
}
|
|
@@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
|
|
|
38
38
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
|
39
39
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
|
40
40
|
*
|
|
41
|
-
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
|
41
|
+
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
|
42
42
|
*
|
|
43
43
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
|
44
44
|
* strings when type checking.
|
|
@@ -107,11 +107,6 @@ export const internalAction: ActionBuilder<DataModel, "internal"> =
|
|
|
107
107
|
*/
|
|
108
108
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
|
109
109
|
|
|
110
|
-
type GenericCtx =
|
|
111
|
-
| GenericActionCtx<DataModel>
|
|
112
|
-
| GenericMutationCtx<DataModel>
|
|
113
|
-
| GenericQueryCtx<DataModel>;
|
|
114
|
-
|
|
115
110
|
/**
|
|
116
111
|
* A set of services for use within Convex query functions.
|
|
117
112
|
*
|
package/src/mapping.ts
CHANGED
|
@@ -297,21 +297,49 @@ export function toModelMessageWarnings(
|
|
|
297
297
|
}) as any;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Serialize explicitly provided response messages for a step.
|
|
302
|
+
* Used by the streaming/generation loop where the caller tracks which
|
|
303
|
+
* messages are new via slicing.
|
|
304
|
+
*/
|
|
305
|
+
export async function serializeResponseMessages<TOOLS extends ToolSet>(
|
|
306
|
+
ctx: ActionCtx,
|
|
307
|
+
component: AgentComponent,
|
|
308
|
+
step: StepResult<TOOLS>,
|
|
309
|
+
model: ModelOrMetadata | undefined,
|
|
310
|
+
responseMessages: ModelMessage[],
|
|
311
|
+
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
312
|
+
return serializeStepMessages(ctx, component, step, model, responseMessages);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Serialize the new messages from a step using a heuristic to determine
|
|
317
|
+
* which response messages are new (last 1-2 messages).
|
|
318
|
+
*/
|
|
300
319
|
export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
301
320
|
ctx: ActionCtx,
|
|
302
321
|
component: AgentComponent,
|
|
303
322
|
step: StepResult<TOOLS>,
|
|
304
323
|
model: ModelOrMetadata | undefined,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
324
|
+
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
325
|
+
const hasToolMessage = step.response.messages.at(-1)?.role === "tool";
|
|
326
|
+
let messagesToSerialize: ModelMessage[];
|
|
327
|
+
if (hasToolMessage) {
|
|
328
|
+
messagesToSerialize = step.response.messages.slice(-2);
|
|
329
|
+
} else if (step.content.length) {
|
|
330
|
+
messagesToSerialize = step.response.messages.slice(-1);
|
|
331
|
+
} else {
|
|
332
|
+
messagesToSerialize = [{ role: "assistant" as const, content: [] }];
|
|
333
|
+
}
|
|
334
|
+
return serializeStepMessages(ctx, component, step, model, messagesToSerialize);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function serializeStepMessages<TOOLS extends ToolSet>(
|
|
338
|
+
ctx: ActionCtx,
|
|
339
|
+
component: AgentComponent,
|
|
340
|
+
step: StepResult<TOOLS>,
|
|
341
|
+
model: ModelOrMetadata | undefined,
|
|
342
|
+
messagesToSerialize: ModelMessage[],
|
|
315
343
|
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
316
344
|
// If there are tool results, there's another message with the tool results
|
|
317
345
|
// ref: https://github.com/vercel/ai/blob/main/packages/ai/src/generate-text/to-response-messages.ts#L120
|
|
@@ -330,18 +358,6 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
330
358
|
} satisfies Omit<MessageWithMetadata, "message" | "text" | "fileIds">;
|
|
331
359
|
const toolFields = { sources: step.sources };
|
|
332
360
|
|
|
333
|
-
// Determine which messages to serialize for this step
|
|
334
|
-
let messagesToSerialize: ModelMessage[];
|
|
335
|
-
if (newResponseMessages) {
|
|
336
|
-
messagesToSerialize = newResponseMessages;
|
|
337
|
-
} else if (hasToolMessage) {
|
|
338
|
-
messagesToSerialize = step.response.messages.slice(-2);
|
|
339
|
-
} else if (step.content.length) {
|
|
340
|
-
messagesToSerialize = step.response.messages.slice(-1);
|
|
341
|
-
} else {
|
|
342
|
-
messagesToSerialize = [{ role: "assistant" as const, content: [] }];
|
|
343
|
-
}
|
|
344
|
-
|
|
345
361
|
const messages: MessageWithMetadata[] = await Promise.all(
|
|
346
362
|
messagesToSerialize.map(async (msg): Promise<MessageWithMetadata> => {
|
|
347
363
|
const { message, fileIds } = await serializeMessage(ctx, component, msg);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"3.2.4","results":[[":client/index.test.ts",{"duration":129.9694999999997,"failed":false}],[":component/messages.test.ts",{"duration":191.73391600000014,"failed":false}],[":component/users.test.ts",{"duration":545.7272500000001,"failed":false}],[":mapping.test.ts",{"duration":28.127082999999402,"failed":false}],[":react/deltas.test.ts",{"duration":5.8497500000000855,"failed":false}],[":react/toUIMessages.test.ts",{"duration":6.495208000000048,"failed":false}],[":validators.test.ts",{"duration":1.0037499999999682,"failed":false}],[":deltas.test.ts",{"duration":0,"failed":false}],[":client/streaming.test.ts",{"duration":177.87866699695587,"failed":false}]]}
|