@convex-dev/agent 0.6.0 → 0.6.2
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 +2093 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +933 -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 +145 -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 +344 -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 +12 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +35 -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 +44 -50
- package/src/UIMessages.ts +0 -9
- package/src/client/approval.test.ts +144 -0
- package/src/client/index.ts +26 -19
- package/src/client/search.ts +0 -29
- package/src/client/setup.test.ts +3 -5
- package/src/client/start.ts +2 -2
- package/src/client/streamText.test.ts +102 -0
- package/src/client/streamText.ts +35 -9
- package/src/client/streaming.ts +44 -23
- package/src/client/utils.ts +22 -1
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +0 -5
- package/src/mapping.ts +38 -22
- package/src/test.ts +3 -4
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.2",
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"keywords": [
|
|
13
13
|
"convex",
|
|
@@ -18,26 +18,23 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"scripts": {
|
|
20
20
|
"setup": "node setup.cjs --init",
|
|
21
|
-
"dev": "
|
|
22
|
-
"dev:backend": "convex dev --typecheck-components",
|
|
21
|
+
"dev": "convex dev --start 'npm run dev:build'",
|
|
23
22
|
"dev:frontend": "cd example && vite --clearScreen false",
|
|
24
23
|
"dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial",
|
|
25
|
-
"predev": "
|
|
24
|
+
"predev": "convex init && npm run build:codegen",
|
|
26
25
|
"build": "tsc --project ./tsconfig.build.json",
|
|
27
26
|
"build:codegen": "npx convex codegen --component-dir ./src/component && npm run build",
|
|
28
27
|
"build:clean": "rm -rf dist *.tsbuildinfo && npm run build:codegen",
|
|
29
28
|
"typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex",
|
|
30
29
|
"lint": "eslint .",
|
|
31
|
-
"all": "run-p -r 'dev:*' 'test:watch'",
|
|
32
30
|
"test": "vitest run --typecheck",
|
|
33
31
|
"test:watch": "vitest --typecheck --clearScreen false",
|
|
34
32
|
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
|
35
33
|
"test:coverage": "vitest run --coverage --coverage.reporter=text",
|
|
36
|
-
"preversion": "npm ci && npm run build:clean && run
|
|
37
|
-
"prepublishOnly": "npm whoami || npm login",
|
|
34
|
+
"preversion": "npm ci && npm run build:clean && npm run test && npm run lint && npm run typecheck",
|
|
38
35
|
"alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags",
|
|
39
36
|
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
40
|
-
"version": "vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
|
|
37
|
+
"version": "(npm whoami || npm login) && vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
|
|
41
38
|
},
|
|
42
39
|
"files": [
|
|
43
40
|
"dist",
|
|
@@ -84,67 +81,64 @@
|
|
|
84
81
|
}
|
|
85
82
|
},
|
|
86
83
|
"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.
|
|
84
|
+
"@ai-sdk/anthropic": "3.0.13",
|
|
85
|
+
"@ai-sdk/google": "3.0.30",
|
|
86
|
+
"@ai-sdk/groq": "3.0.8",
|
|
87
|
+
"@ai-sdk/openai": "3.0.10",
|
|
88
|
+
"@ai-sdk/provider": "3.0.3",
|
|
89
|
+
"@ai-sdk/provider-utils": "4.0.6",
|
|
90
|
+
"@convex-dev/rag": "0.7.2",
|
|
91
|
+
"@convex-dev/rate-limiter": "0.3.2",
|
|
92
|
+
"@convex-dev/workflow": "0.3.6",
|
|
96
93
|
"@edge-runtime/vm": "5.0.0",
|
|
97
|
-
"@eslint/js": "9.
|
|
94
|
+
"@eslint/js": "9.39.4",
|
|
98
95
|
"@hookform/resolvers": "5.2.2",
|
|
99
|
-
"@langchain/textsplitters": "0.1
|
|
96
|
+
"@langchain/textsplitters": "1.0.1",
|
|
100
97
|
"@radix-ui/react-accordion": "1.2.12",
|
|
101
98
|
"@radix-ui/react-checkbox": "1.3.3",
|
|
102
|
-
"@radix-ui/react-label": "2.1.
|
|
99
|
+
"@radix-ui/react-label": "2.1.8",
|
|
103
100
|
"@radix-ui/react-select": "2.2.6",
|
|
104
|
-
"@radix-ui/react-slot": "1.2.
|
|
101
|
+
"@radix-ui/react-slot": "1.2.4",
|
|
105
102
|
"@radix-ui/react-toast": "1.2.15",
|
|
106
103
|
"@tailwindcss/typography": "0.5.19",
|
|
107
|
-
"@types/node": "20.19.
|
|
108
|
-
"@types/react": "19.2.
|
|
109
|
-
"@types/react-dom": "19.2.
|
|
104
|
+
"@types/node": "20.19.37",
|
|
105
|
+
"@types/react": "19.2.14",
|
|
106
|
+
"@types/react-dom": "19.2.3",
|
|
110
107
|
"@vitejs/plugin-react": "4.7.0",
|
|
111
|
-
"ai": "
|
|
112
|
-
"autoprefixer": "10.4.
|
|
108
|
+
"ai": "6.0.35",
|
|
109
|
+
"autoprefixer": "10.4.27",
|
|
113
110
|
"chokidar-cli": "3.0.0",
|
|
114
111
|
"class-variance-authority": "0.7.1",
|
|
115
112
|
"clsx": "2.1.1",
|
|
116
|
-
"convex": "1.
|
|
117
|
-
"convex-helpers": "0.1.
|
|
118
|
-
"convex-test": "0.0.
|
|
119
|
-
"
|
|
120
|
-
"dayjs": "1.11.18",
|
|
113
|
+
"convex": "1.35.1",
|
|
114
|
+
"convex-helpers": "0.1.114",
|
|
115
|
+
"convex-test": "0.0.44",
|
|
116
|
+
"dayjs": "1.11.20",
|
|
121
117
|
"dotenv": "16.6.1",
|
|
122
|
-
"eslint": "9.
|
|
118
|
+
"eslint": "9.39.4",
|
|
123
119
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
124
120
|
"eslint-plugin-react": "7.37.5",
|
|
125
|
-
"eslint-plugin-react-hooks": "
|
|
126
|
-
"eslint-plugin-react-refresh": "0.
|
|
127
|
-
"globals": "16.
|
|
128
|
-
"lucide-react": "0.
|
|
129
|
-
"
|
|
130
|
-
"ollama-ai-provider": "^1.2.0",
|
|
121
|
+
"eslint-plugin-react-hooks": "6.1.1",
|
|
122
|
+
"eslint-plugin-react-refresh": "0.5.2",
|
|
123
|
+
"globals": "16.5.0",
|
|
124
|
+
"lucide-react": "0.577.0",
|
|
125
|
+
"ollama-ai-provider": "1.2.0",
|
|
131
126
|
"openai": "5.23.2",
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"react": "19.2.
|
|
137
|
-
"react-
|
|
138
|
-
"react-hook-form": "7.65.0",
|
|
127
|
+
"pkg-pr-new": "0.0.66",
|
|
128
|
+
"postcss": "8.5.10",
|
|
129
|
+
"prettier": "3.8.1",
|
|
130
|
+
"react": "19.2.4",
|
|
131
|
+
"react-dom": "19.2.4",
|
|
132
|
+
"react-hook-form": "7.71.2",
|
|
139
133
|
"react-markdown": "10.1.0",
|
|
140
|
-
"react-router-dom": "7.
|
|
134
|
+
"react-router-dom": "7.13.1",
|
|
141
135
|
"readline": "1.3.0",
|
|
142
|
-
"tailwind-merge": "3.
|
|
143
|
-
"tailwindcss": "3.4.
|
|
136
|
+
"tailwind-merge": "3.5.0",
|
|
137
|
+
"tailwindcss": "3.4.19",
|
|
144
138
|
"tailwindcss-animate": "1.0.7",
|
|
145
139
|
"typescript": "5.9.3",
|
|
146
|
-
"typescript-eslint": "8.
|
|
147
|
-
"vite": "6.4.
|
|
140
|
+
"typescript-eslint": "8.57.1",
|
|
141
|
+
"vite": "6.4.2",
|
|
148
142
|
"vitest": "3.2.4",
|
|
149
143
|
"zod": "3.25.76"
|
|
150
144
|
},
|
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
|
@@ -99,9 +99,10 @@ import type {
|
|
|
99
99
|
Output,
|
|
100
100
|
} from "./types.js";
|
|
101
101
|
import { streamText } from "./streamText.js";
|
|
102
|
-
import { errorToString, willContinue } from "./utils.js";
|
|
102
|
+
import { errorToString, hasSuccessfulToolCall, willContinue } from "./utils.js";
|
|
103
103
|
|
|
104
104
|
export { stepCountIs } from "ai";
|
|
105
|
+
export { hasSuccessfulToolCall };
|
|
105
106
|
export {
|
|
106
107
|
docsToModelMessages,
|
|
107
108
|
toModelMessage,
|
|
@@ -476,8 +477,7 @@ export class Agent<
|
|
|
476
477
|
* The arguments to the generateText function, similar to the ai sdk's
|
|
477
478
|
* {@link generateText} function, along with Agent prompt options.
|
|
478
479
|
*/
|
|
479
|
-
generateTextArgs: AgentPrompt &
|
|
480
|
-
TextArgs<AgentTools, TOOLS, OUTPUT>,
|
|
480
|
+
generateTextArgs: AgentPrompt & TextArgs<AgentTools, TOOLS, OUTPUT>,
|
|
481
481
|
options?: Options,
|
|
482
482
|
): Promise<
|
|
483
483
|
GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
|
|
@@ -535,8 +535,7 @@ export class Agent<
|
|
|
535
535
|
* The arguments to the streamText function, similar to the ai sdk's
|
|
536
536
|
* {@link streamText} function, along with Agent prompt options.
|
|
537
537
|
*/
|
|
538
|
-
streamTextArgs: AgentPrompt &
|
|
539
|
-
StreamingTextArgs<AgentTools, TOOLS, OUTPUT>,
|
|
538
|
+
streamTextArgs: AgentPrompt & StreamingTextArgs<AgentTools, TOOLS, OUTPUT>,
|
|
540
539
|
/**
|
|
541
540
|
* The {@link ContextOptions} and {@link StorageOptions}
|
|
542
541
|
* options to use for fetching contextual messages and saving input/output messages.
|
|
@@ -555,10 +554,7 @@ export class Agent<
|
|
|
555
554
|
saveStreamDeltas?: boolean | StreamingOptions;
|
|
556
555
|
},
|
|
557
556
|
): Promise<
|
|
558
|
-
StreamTextResult<
|
|
559
|
-
TOOLS extends undefined ? AgentTools : TOOLS,
|
|
560
|
-
OUTPUT
|
|
561
|
-
> &
|
|
557
|
+
StreamTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
|
|
562
558
|
GenerationOutputMetadata
|
|
563
559
|
> {
|
|
564
560
|
type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
|
|
@@ -1123,25 +1119,34 @@ export class Agent<
|
|
|
1123
1119
|
// The "already handled" check (tool-approval-response) relies on seeing
|
|
1124
1120
|
// responses before their corresponding requests. If the pagination order
|
|
1125
1121
|
// changes, this logic will need to be updated.
|
|
1126
|
-
let cursor: string | null = null;
|
|
1127
1122
|
let existingResponseMessage: MessageDoc | undefined;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1123
|
+
// Limit the search to the most recent messages. Approvals should always
|
|
1124
|
+
// be near the end of the thread.
|
|
1125
|
+
const page = await this.listMessages(ctx, {
|
|
1126
|
+
threadId: args.threadId,
|
|
1127
|
+
paginationOpts: { cursor: null, numItems: 100 },
|
|
1128
|
+
});
|
|
1129
|
+
{
|
|
1133
1130
|
for (const message of page.page) {
|
|
1134
1131
|
const content = message.message?.content;
|
|
1135
1132
|
if (!Array.isArray(content)) continue;
|
|
1136
1133
|
// Check if this assistant message starts a different approval step.
|
|
1137
1134
|
// If so, any response message we've seen so far belongs to a newer
|
|
1138
1135
|
// step — reset it so we don't merge across step boundaries.
|
|
1136
|
+
// Only reset if the target approval is NOT in this message (i.e.,
|
|
1137
|
+
// this is a genuinely different step, not the same step with
|
|
1138
|
+
// multiple tool calls).
|
|
1139
1139
|
if (
|
|
1140
1140
|
message.message?.role === "assistant" &&
|
|
1141
1141
|
content.some(
|
|
1142
1142
|
(p: any) =>
|
|
1143
1143
|
p.type === "tool-approval-request" &&
|
|
1144
1144
|
p.approvalId !== args.approvalId,
|
|
1145
|
+
) &&
|
|
1146
|
+
!content.some(
|
|
1147
|
+
(p: any) =>
|
|
1148
|
+
p.type === "tool-approval-request" &&
|
|
1149
|
+
p.approvalId === args.approvalId,
|
|
1145
1150
|
)
|
|
1146
1151
|
) {
|
|
1147
1152
|
existingResponseMessage = undefined;
|
|
@@ -1169,11 +1174,10 @@ export class Agent<
|
|
|
1169
1174
|
}
|
|
1170
1175
|
}
|
|
1171
1176
|
}
|
|
1172
|
-
|
|
1173
|
-
} while (cursor !== null);
|
|
1177
|
+
}
|
|
1174
1178
|
|
|
1175
1179
|
throw new Error(
|
|
1176
|
-
`Approval request ${args.approvalId} was not found in thread ${args.threadId}`,
|
|
1180
|
+
`Approval request ${args.approvalId} was not found in the last 100 messages of thread ${args.threadId}`,
|
|
1177
1181
|
);
|
|
1178
1182
|
}
|
|
1179
1183
|
|
|
@@ -1622,7 +1626,10 @@ export class Agent<
|
|
|
1622
1626
|
} as GenerateObjectArgs<FlexibleSchema<T>>;
|
|
1623
1627
|
const ctx = (
|
|
1624
1628
|
options?.customCtx
|
|
1625
|
-
? {
|
|
1629
|
+
? {
|
|
1630
|
+
...ctx_,
|
|
1631
|
+
...options.customCtx(ctx_, targetArgs, llmArgs as any),
|
|
1632
|
+
}
|
|
1626
1633
|
: ctx_
|
|
1627
1634
|
) as GenericActionCtx<GenericDataModel> & CustomCtx;
|
|
1628
1635
|
const value = await this.generateObject(ctx, targetArgs, llmArgs, {
|
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/setup.test.ts
CHANGED
|
@@ -10,15 +10,13 @@ import {
|
|
|
10
10
|
} from "convex/server";
|
|
11
11
|
import { type AgentComponent } from "./index.js";
|
|
12
12
|
import { componentsGeneric } from "convex/server";
|
|
13
|
-
|
|
14
|
-
import componentSchema from "../component/schema.js";
|
|
15
|
-
export const componentModules = import.meta.glob("../component/**/*.ts");
|
|
13
|
+
import component from "../test.js";
|
|
16
14
|
|
|
17
15
|
export function initConvexTest<
|
|
18
16
|
Schema extends SchemaDefinition<GenericSchema, boolean>,
|
|
19
17
|
>(schema?: Schema) {
|
|
20
|
-
const t = convexTest(schema ?? defineSchema({}), modules);
|
|
21
|
-
|
|
18
|
+
const t = convexTest((schema ?? defineSchema({})) as Schema, modules);
|
|
19
|
+
component.register(t);
|
|
22
20
|
return t;
|
|
23
21
|
}
|
|
24
22
|
export const components = componentsGeneric() as unknown as {
|
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,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { Agent, createThread } from "./index.js";
|
|
3
|
+
import {
|
|
4
|
+
defineSchema,
|
|
5
|
+
type DataModelFromSchemaDefinition,
|
|
6
|
+
type ApiFromModules,
|
|
7
|
+
type ActionBuilder,
|
|
8
|
+
actionGeneric,
|
|
9
|
+
anyApi,
|
|
10
|
+
} from "convex/server";
|
|
11
|
+
import { v } from "convex/values";
|
|
12
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
13
|
+
import { mockModel } from "./mockModel.js";
|
|
14
|
+
|
|
15
|
+
const schema = defineSchema({});
|
|
16
|
+
type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
17
|
+
const action = actionGeneric as ActionBuilder<DataModel, "public">;
|
|
18
|
+
|
|
19
|
+
const FINAL_TEXT = "Hello from the model";
|
|
20
|
+
|
|
21
|
+
const agent = new Agent(components.agent, {
|
|
22
|
+
name: "stream-test",
|
|
23
|
+
languageModel: mockModel({
|
|
24
|
+
content: [{ type: "text", text: FINAL_TEXT }],
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Action that exercises streamText with saveStreamDeltas.returnImmediately=true.
|
|
29
|
+
// It consumes the stream after streamText returns, simulating the HTTP response
|
|
30
|
+
// path described in issue #265.
|
|
31
|
+
export const streamTextReturnImmediately = action({
|
|
32
|
+
args: { threadId: v.string() },
|
|
33
|
+
handler: async (ctx, { threadId }) => {
|
|
34
|
+
const result = await agent.streamText(
|
|
35
|
+
ctx,
|
|
36
|
+
{ threadId },
|
|
37
|
+
{ prompt: "Test" },
|
|
38
|
+
{
|
|
39
|
+
saveStreamDeltas: {
|
|
40
|
+
returnImmediately: true,
|
|
41
|
+
chunking: "word",
|
|
42
|
+
throttleMs: 0,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
// Drain the stream the way an HTTP response would. This triggers
|
|
47
|
+
// onStepFinish for every step, including the final one.
|
|
48
|
+
await result.consumeStream();
|
|
49
|
+
return { ok: true };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const testApi: ApiFromModules<{
|
|
54
|
+
fns: { streamTextReturnImmediately: typeof streamTextReturnImmediately };
|
|
55
|
+
}>["fns"] = anyApi["streamText.test"] as any;
|
|
56
|
+
|
|
57
|
+
describe("streamText with saveStreamDeltas.returnImmediately (issue #265)", () => {
|
|
58
|
+
test("persists the final assistant text to the messages table", async () => {
|
|
59
|
+
const t = initConvexTest(schema);
|
|
60
|
+
const threadId = await t.run(async (ctx) =>
|
|
61
|
+
createThread(ctx, components.agent, { userId: "u1" }),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await t.action(testApi.streamTextReturnImmediately, { threadId });
|
|
65
|
+
|
|
66
|
+
// Allow any background work scheduled by consumeStream to settle.
|
|
67
|
+
await t.finishAllScheduledFunctions(() => {});
|
|
68
|
+
|
|
69
|
+
const messages = await t.run(async (ctx) =>
|
|
70
|
+
agent.listMessages(ctx, {
|
|
71
|
+
threadId,
|
|
72
|
+
paginationOpts: { cursor: null, numItems: 50 },
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const assistantTextMessages = messages.page.filter(
|
|
77
|
+
(m) =>
|
|
78
|
+
m.message?.role === "assistant" &&
|
|
79
|
+
typeof m.text === "string" &&
|
|
80
|
+
m.text.length > 0,
|
|
81
|
+
);
|
|
82
|
+
expect(
|
|
83
|
+
assistantTextMessages.length,
|
|
84
|
+
"expected at least one persisted assistant message with text",
|
|
85
|
+
).toBeGreaterThan(0);
|
|
86
|
+
|
|
87
|
+
const combined = assistantTextMessages.map((m) => m.text).join("");
|
|
88
|
+
expect(combined).toContain(FINAL_TEXT);
|
|
89
|
+
|
|
90
|
+
// The stream should be marked finished, not stuck in "streaming".
|
|
91
|
+
const stillStreaming = await t.run(async (ctx) =>
|
|
92
|
+
ctx.runQuery(components.agent.streams.list, {
|
|
93
|
+
threadId,
|
|
94
|
+
statuses: ["streaming"],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(
|
|
98
|
+
stillStreaming,
|
|
99
|
+
"stream should not be stuck in 'streaming' status",
|
|
100
|
+
).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
});
|