@convex-dev/agent 0.6.0-alpha.0 → 0.6.0
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/MIGRATION.md +153 -0
- package/package.json +4 -2
- package/src/UIMessages.ts +135 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +50 -52
- package/src/client/index.ts +162 -1
- package/src/client/mockModel.ts +9 -2
- package/src/client/search.test.ts +4 -5
- package/src/client/search.ts +47 -2
- package/src/client/start.ts +41 -24
- package/src/client/streamText.ts +27 -3
- package/src/client/streaming.integration.test.ts +1206 -0
- package/src/client/streaming.ts +41 -12
- package/src/client/types.ts +14 -12
- package/src/component/_generated/component.ts +53 -64
- package/src/component/messages.ts +12 -2
- package/src/deltas.ts +0 -1
- package/src/mapping.test.ts +143 -1
- package/src/mapping.ts +115 -6
- package/src/react/useDeltaStreams.ts +6 -0
- package/dist/UIMessages.d.ts +0 -46
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js +0 -546
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts +0 -170
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js +0 -115
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/defaultComponent.d.ts +0 -11
- package/dist/client/defaultComponent.d.ts.map +0 -1
- package/dist/client/defaultComponent.js +0 -7
- package/dist/client/defaultComponent.js.map +0 -1
- package/dist/client/definePlaygroundAPI.d.ts +0 -1725
- package/dist/client/definePlaygroundAPI.d.ts.map +0 -1
- package/dist/client/definePlaygroundAPI.js +0 -271
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts +0 -69
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js +0 -181
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts +0 -2044
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js +0 -804
- package/dist/client/index.js.map +0 -1
- package/dist/client/messages.d.ts +0 -461
- package/dist/client/messages.d.ts.map +0 -1
- package/dist/client/messages.js +0 -106
- package/dist/client/messages.js.map +0 -1
- package/dist/client/mockModel.d.ts +0 -42
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js +0 -175
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts +0 -20
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js +0 -58
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts +0 -493
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js +0 -425
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts +0 -84
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js +0 -171
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts +0 -46
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js +0 -93
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts +0 -3767
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js +0 -294
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/threads.d.ts +0 -46
- package/dist/client/threads.d.ts.map +0 -1
- package/dist/client/threads.js +0 -49
- package/dist/client/threads.js.map +0 -1
- package/dist/client/types.d.ts +0 -460
- package/dist/client/types.d.ts.map +0 -1
- package/dist/client/types.js +0 -2
- package/dist/client/types.js.map +0 -1
- package/dist/client/utils.d.ts +0 -4
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js +0 -21
- package/dist/client/utils.js.map +0 -1
- package/dist/component/_generated/api.d.ts +0 -48
- package/dist/component/_generated/api.d.ts.map +0 -1
- package/dist/component/_generated/api.js +0 -31
- package/dist/component/_generated/api.js.map +0 -1
- package/dist/component/_generated/component.d.ts +0 -3119
- package/dist/component/_generated/component.d.ts.map +0 -1
- package/dist/component/_generated/component.js +0 -11
- package/dist/component/_generated/component.js.map +0 -1
- package/dist/component/_generated/dataModel.d.ts +0 -46
- package/dist/component/_generated/dataModel.d.ts.map +0 -1
- package/dist/component/_generated/dataModel.js +0 -11
- package/dist/component/_generated/dataModel.js.map +0 -1
- package/dist/component/_generated/server.d.ts +0 -121
- package/dist/component/_generated/server.d.ts.map +0 -1
- package/dist/component/_generated/server.js +0 -78
- package/dist/component/_generated/server.js.map +0 -1
- package/dist/component/apiKeys.d.ts +0 -11
- package/dist/component/apiKeys.d.ts.map +0 -1
- package/dist/component/apiKeys.js +0 -69
- package/dist/component/apiKeys.js.map +0 -1
- package/dist/component/convex.config.d.ts +0 -3
- package/dist/component/convex.config.d.ts.map +0 -1
- package/dist/component/convex.config.js +0 -3
- package/dist/component/convex.config.js.map +0 -1
- package/dist/component/files.d.ts +0 -97
- package/dist/component/files.d.ts.map +0 -1
- package/dist/component/files.js +0 -190
- package/dist/component/files.js.map +0 -1
- package/dist/component/messages.d.ts +0 -3850
- package/dist/component/messages.d.ts.map +0 -1
- package/dist/component/messages.js +0 -747
- package/dist/component/messages.js.map +0 -1
- package/dist/component/schema.d.ts +0 -8026
- package/dist/component/schema.d.ts.map +0 -1
- package/dist/component/schema.js +0 -147
- package/dist/component/schema.js.map +0 -1
- package/dist/component/streams.d.ts +0 -128
- package/dist/component/streams.d.ts.map +0 -1
- package/dist/component/streams.js +0 -413
- package/dist/component/streams.js.map +0 -1
- package/dist/component/threads.d.ts +0 -115
- package/dist/component/threads.d.ts.map +0 -1
- package/dist/component/threads.js +0 -208
- package/dist/component/threads.js.map +0 -1
- package/dist/component/users.d.ts +0 -52
- package/dist/component/users.d.ts.map +0 -1
- package/dist/component/users.js +0 -229
- package/dist/component/users.js.map +0 -1
- package/dist/component/vector/index.d.ts +0 -61
- package/dist/component/vector/index.d.ts.map +0 -1
- package/dist/component/vector/index.js +0 -146
- package/dist/component/vector/index.js.map +0 -1
- package/dist/component/vector/tables.d.ts +0 -58
- package/dist/component/vector/tables.d.ts.map +0 -1
- package/dist/component/vector/tables.js +0 -56
- package/dist/component/vector/tables.js.map +0 -1
- package/dist/deltas.d.ts +0 -43
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js +0 -447
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts +0 -54
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js +0 -626
- package/dist/mapping.js.map +0 -1
- package/dist/react/SmoothText.d.ts +0 -5
- package/dist/react/SmoothText.d.ts.map +0 -1
- package/dist/react/SmoothText.js +0 -6
- package/dist/react/SmoothText.js.map +0 -1
- package/dist/react/index.d.ts +0 -25
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -70
- package/dist/react/index.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts +0 -42
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js +0 -74
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts +0 -12
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/types.js +0 -2
- package/dist/react/types.js.map +0 -1
- package/dist/react/useDeltaStreams.d.ts +0 -10
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js +0 -101
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useSmoothText.d.ts +0 -27
- package/dist/react/useSmoothText.d.ts.map +0 -1
- package/dist/react/useSmoothText.js +0 -68
- package/dist/react/useSmoothText.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts +0 -22
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js +0 -92
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts +0 -104
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js +0 -148
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts +0 -96
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js +0 -108
- package/dist/react/useUIMessages.js.map +0 -1
- package/dist/shared.d.ts +0 -26
- package/dist/shared.d.ts.map +0 -1
- package/dist/shared.js +0 -67
- package/dist/shared.js.map +0 -1
- package/dist/validators.d.ts +0 -24516
- package/dist/validators.d.ts.map +0 -1
- package/dist/validators.js +0 -475
- package/dist/validators.js.map +0 -1
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Migration Guide: v0.3.x to v0.6.0 (AI SDK v6)
|
|
2
|
+
|
|
3
|
+
This guide helps you upgrade from @convex-dev/agent v0.3.x to v0.6.0.
|
|
4
|
+
|
|
5
|
+
## Step 1: Update dependencies
|
|
6
|
+
|
|
7
|
+
Update all AI SDK packages **together** to avoid peer dependency conflicts:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @convex-dev/agent@^0.6.0 ai@^6.0.35 @ai-sdk/provider-utils@^4.0.6
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Official AI SDK providers
|
|
14
|
+
|
|
15
|
+
Update your AI SDK provider packages to v3.x:
|
|
16
|
+
```bash
|
|
17
|
+
# For OpenAI
|
|
18
|
+
npm install @ai-sdk/openai@^3.0.10
|
|
19
|
+
|
|
20
|
+
# For Anthropic
|
|
21
|
+
npm install @ai-sdk/anthropic@^3.0.13
|
|
22
|
+
|
|
23
|
+
# For Groq
|
|
24
|
+
npm install @ai-sdk/groq@^3.0.8
|
|
25
|
+
|
|
26
|
+
# For Google (Gemini)
|
|
27
|
+
npm install @ai-sdk/google@^3.0.8
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Third-party providers
|
|
31
|
+
|
|
32
|
+
Third-party providers also need updates to be compatible with AI SDK v6:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# For OpenRouter
|
|
36
|
+
npm install @openrouter/ai-sdk-provider@^2.0.0
|
|
37
|
+
|
|
38
|
+
# For other providers, check their documentation for AI SDK v6 compatibility
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Handling dependency conflicts
|
|
42
|
+
|
|
43
|
+
If you see peer dependency warnings or errors, try updating all packages at once:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install @convex-dev/agent@^0.6.0 ai@^6.0.35 @ai-sdk/openai@^3.0.10 @openrouter/ai-sdk-provider@^2.0.0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you still have conflicts, you can use `--force` as a last resort:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @convex-dev/agent@^0.6.0 --force
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **Note**: Using `--force` can lead to inconsistent dependency trees. After using it, verify your app works correctly and consider running `npm dedupe` to clean up.
|
|
56
|
+
|
|
57
|
+
## Step 2: Update tool definitions
|
|
58
|
+
|
|
59
|
+
Replace `parameters` with `inputSchema`:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Before (v5)
|
|
63
|
+
const myTool = createTool({
|
|
64
|
+
description: "My tool",
|
|
65
|
+
parameters: z.object({ query: z.string() }),
|
|
66
|
+
execute: async (ctx, args) => { ... }
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// After (v6)
|
|
70
|
+
const myTool = createTool({
|
|
71
|
+
description: "My tool",
|
|
72
|
+
inputSchema: z.object({ query: z.string() }),
|
|
73
|
+
execute: async (ctx, input, options) => { ... }
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Step 3: Update Agent config (if using embeddings)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Before
|
|
81
|
+
new Agent(components.agent, {
|
|
82
|
+
textEmbeddingModel: openai.embedding("text-embedding-3-small")
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// After
|
|
86
|
+
new Agent(components.agent, {
|
|
87
|
+
embeddingModel: openai.embedding("text-embedding-3-small")
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Step 4: Update maxSteps (optional)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Before
|
|
95
|
+
await agent.generateText(ctx, { threadId }, {
|
|
96
|
+
prompt: "...",
|
|
97
|
+
maxSteps: 5
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// After (maxSteps still works, but stopWhen is preferred)
|
|
101
|
+
import { stepCountIs } from "ai"
|
|
102
|
+
await agent.generateText(ctx, { threadId }, {
|
|
103
|
+
prompt: "...",
|
|
104
|
+
stopWhen: stepCountIs(5)
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Step 5: Verify
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm run typecheck
|
|
112
|
+
npm test
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Common Issues
|
|
116
|
+
|
|
117
|
+
### EmbeddingModelV2 vs EmbeddingModelV3 type errors
|
|
118
|
+
Ensure all `@ai-sdk/*` packages are updated to v3.x. Older versions use AI SDK v5 types.
|
|
119
|
+
|
|
120
|
+
### Tool `args` vs `input`
|
|
121
|
+
AI SDK v6 renamed `args` to `input` in tool calls. The library maintains backwards compatibility, but you may see this in types.
|
|
122
|
+
|
|
123
|
+
### `mimeType` vs `mediaType`
|
|
124
|
+
AI SDK v6 renamed `mimeType` to `mediaType`. Backwards compatibility is maintained.
|
|
125
|
+
|
|
126
|
+
### Peer dependency conflicts
|
|
127
|
+
|
|
128
|
+
If you see errors like:
|
|
129
|
+
```
|
|
130
|
+
npm error ERESOLVE unable to resolve dependency tree
|
|
131
|
+
npm error peer ai@"^5.0.0" from @openrouter/ai-sdk-provider@1.0.3
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This means a third-party provider needs updating. Common solutions:
|
|
135
|
+
|
|
136
|
+
1. **Update the provider** to a version compatible with AI SDK v6
|
|
137
|
+
2. **Check npm** for the latest version: `npm view @openrouter/ai-sdk-provider versions`
|
|
138
|
+
3. **Use `--force`** if a compatible version isn't available yet (temporary workaround)
|
|
139
|
+
|
|
140
|
+
### Third-party provider compatibility
|
|
141
|
+
|
|
142
|
+
| Provider | AI SDK v5 (ai@5.x) | AI SDK v6 (ai@6.x) |
|
|
143
|
+
|----------|-------------------|-------------------|
|
|
144
|
+
| @openrouter/ai-sdk-provider | v1.x | v2.x |
|
|
145
|
+
| @ai-sdk/openai | v1.x-v2.x | v3.x |
|
|
146
|
+
| @ai-sdk/anthropic | v1.x-v2.x | v3.x |
|
|
147
|
+
| @ai-sdk/groq | v1.x-v2.x | v3.x |
|
|
148
|
+
| @ai-sdk/google | v1.x-v2.x | v3.x |
|
|
149
|
+
|
|
150
|
+
## More Information
|
|
151
|
+
|
|
152
|
+
- [AI SDK v6 Migration Guide](https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0)
|
|
153
|
+
- [Convex Agent Documentation](https://docs.convex.dev/agents)
|
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.0
|
|
10
|
+
"version": "0.6.0",
|
|
11
11
|
"license": "Apache-2.0",
|
|
12
12
|
"keywords": [
|
|
13
13
|
"convex",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|
|
44
|
-
"src"
|
|
44
|
+
"src",
|
|
45
|
+
"MIGRATION.md"
|
|
45
46
|
],
|
|
46
47
|
"exports": {
|
|
47
48
|
"./package.json": "./package.json",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
},
|
|
85
86
|
"devDependencies": {
|
|
86
87
|
"@ai-sdk/anthropic": "^3.0.13",
|
|
88
|
+
"@ai-sdk/google": "^3.0.30",
|
|
87
89
|
"@ai-sdk/groq": "^3.0.8",
|
|
88
90
|
"@ai-sdk/openai": "^3.0.10",
|
|
89
91
|
"@ai-sdk/provider": "^3.0.3",
|
package/src/UIMessages.ts
CHANGED
|
@@ -396,6 +396,54 @@ function createAssistantUIMessage<
|
|
|
396
396
|
? ("streaming" as const)
|
|
397
397
|
: lastMessage.status;
|
|
398
398
|
|
|
399
|
+
<<<<<<< Updated upstream
|
|
400
|
+
=======
|
|
401
|
+
// Extract approval parts from raw message content for UI rendering
|
|
402
|
+
type ApprovalPart =
|
|
403
|
+
| { type: "tool-approval-request"; approvalId: string; toolCallId: string }
|
|
404
|
+
| {
|
|
405
|
+
type: "tool-approval-response";
|
|
406
|
+
approvalId: string;
|
|
407
|
+
approved: boolean;
|
|
408
|
+
reason?: string;
|
|
409
|
+
};
|
|
410
|
+
const approvalParts: ApprovalPart[] = [];
|
|
411
|
+
|
|
412
|
+
// Extract execution-denied tool results from raw content for UI rendering
|
|
413
|
+
// (these are converted to text format for provider compatibility in start.ts)
|
|
414
|
+
type ExecutionDeniedInfo = {
|
|
415
|
+
toolCallId: string;
|
|
416
|
+
reason?: string;
|
|
417
|
+
};
|
|
418
|
+
const executionDeniedResults: ExecutionDeniedInfo[] = [];
|
|
419
|
+
|
|
420
|
+
for (const message of group) {
|
|
421
|
+
const rawContent = message.message?.content;
|
|
422
|
+
if (Array.isArray(rawContent)) {
|
|
423
|
+
for (const part of rawContent) {
|
|
424
|
+
if (
|
|
425
|
+
part.type === "tool-approval-request" ||
|
|
426
|
+
part.type === "tool-approval-response"
|
|
427
|
+
) {
|
|
428
|
+
approvalParts.push(part as ApprovalPart);
|
|
429
|
+
}
|
|
430
|
+
// Check for execution-denied in tool-result outputs
|
|
431
|
+
if (
|
|
432
|
+
part.type === "tool-result" &&
|
|
433
|
+
typeof part.output === "object" &&
|
|
434
|
+
part.output !== null &&
|
|
435
|
+
(part.output as { type?: string }).type === "execution-denied"
|
|
436
|
+
) {
|
|
437
|
+
executionDeniedResults.push({
|
|
438
|
+
toolCallId: part.toolCallId as string,
|
|
439
|
+
reason: (part.output as { reason?: string }).reason,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
>>>>>>> Stashed changes
|
|
399
447
|
// Collect all parts from all messages
|
|
400
448
|
const allParts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
|
|
401
449
|
|
|
@@ -477,6 +525,12 @@ function createAssistantUIMessage<
|
|
|
477
525
|
break;
|
|
478
526
|
}
|
|
479
527
|
case "tool-result": {
|
|
528
|
+
<<<<<<< Updated upstream
|
|
529
|
+
=======
|
|
530
|
+
// Note: execution-denied outputs are handled separately via pre-extraction
|
|
531
|
+
// from raw content (converted to text format for providers in start.ts).
|
|
532
|
+
// See executionDeniedResults processing at the end of this function.
|
|
533
|
+
>>>>>>> Stashed changes
|
|
480
534
|
const typedPart = contentPart as unknown as ToolResultPart & {
|
|
481
535
|
output: { type: string; value?: unknown; reason?: string };
|
|
482
536
|
};
|
|
@@ -642,6 +696,87 @@ function createAssistantUIMessage<
|
|
|
642
696
|
}
|
|
643
697
|
}
|
|
644
698
|
|
|
699
|
+
<<<<<<< Updated upstream
|
|
700
|
+
=======
|
|
701
|
+
// Final output states that should not be overwritten by approval processing
|
|
702
|
+
const finalStates = new Set([
|
|
703
|
+
"output-available",
|
|
704
|
+
"output-error",
|
|
705
|
+
"output-denied",
|
|
706
|
+
]);
|
|
707
|
+
|
|
708
|
+
// Process approval parts to update tool call states
|
|
709
|
+
for (const approvalPart of approvalParts) {
|
|
710
|
+
if (approvalPart.type === "tool-approval-request") {
|
|
711
|
+
const toolCallPart = allParts.find(
|
|
712
|
+
(part) =>
|
|
713
|
+
"toolCallId" in part && part.toolCallId === approvalPart.toolCallId,
|
|
714
|
+
) as ToolUIPart | undefined;
|
|
715
|
+
|
|
716
|
+
if (toolCallPart) {
|
|
717
|
+
// Always set approval info (needed for response matching), but only
|
|
718
|
+
// update state if not in a final state
|
|
719
|
+
(toolCallPart as ToolUIPart & { approval?: object }).approval = {
|
|
720
|
+
id: approvalPart.approvalId,
|
|
721
|
+
};
|
|
722
|
+
if (!finalStates.has(toolCallPart.state)) {
|
|
723
|
+
toolCallPart.state = "approval-requested";
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} else if (approvalPart.type === "tool-approval-response") {
|
|
727
|
+
const toolCallPart = allParts.find(
|
|
728
|
+
(part) =>
|
|
729
|
+
"approval" in part &&
|
|
730
|
+
(part as ToolUIPart & { approval?: { id: string } }).approval?.id ===
|
|
731
|
+
approvalPart.approvalId,
|
|
732
|
+
) as ToolUIPart | undefined;
|
|
733
|
+
|
|
734
|
+
if (toolCallPart) {
|
|
735
|
+
// Always update approval info, but only update state if not in a final state
|
|
736
|
+
(toolCallPart as ToolUIPart & { approval?: object }).approval = {
|
|
737
|
+
id: approvalPart.approvalId,
|
|
738
|
+
approved: approvalPart.approved,
|
|
739
|
+
reason: approvalPart.reason,
|
|
740
|
+
};
|
|
741
|
+
if (!finalStates.has(toolCallPart.state)) {
|
|
742
|
+
if (approvalPart.approved) {
|
|
743
|
+
toolCallPart.state = "approval-responded";
|
|
744
|
+
} else {
|
|
745
|
+
toolCallPart.state = "output-denied";
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Process execution-denied results to update tool call states
|
|
753
|
+
for (const denied of executionDeniedResults) {
|
|
754
|
+
const toolCallPart = allParts.find(
|
|
755
|
+
(part) =>
|
|
756
|
+
"toolCallId" in part && part.toolCallId === denied.toolCallId,
|
|
757
|
+
) as ToolUIPart | undefined;
|
|
758
|
+
|
|
759
|
+
if (toolCallPart) {
|
|
760
|
+
toolCallPart.state = "output-denied";
|
|
761
|
+
if (!("approval" in toolCallPart) || !toolCallPart.approval) {
|
|
762
|
+
(toolCallPart as ToolUIPart & { approval?: object }).approval = {
|
|
763
|
+
id: "",
|
|
764
|
+
approved: false,
|
|
765
|
+
reason: denied.reason,
|
|
766
|
+
};
|
|
767
|
+
} else {
|
|
768
|
+
const approval = (
|
|
769
|
+
toolCallPart as ToolUIPart & {
|
|
770
|
+
approval: { approved?: boolean; reason?: string };
|
|
771
|
+
}
|
|
772
|
+
).approval;
|
|
773
|
+
approval.approved = false;
|
|
774
|
+
approval.reason = denied.reason;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
>>>>>>> Stashed changes
|
|
645
780
|
return {
|
|
646
781
|
...common,
|
|
647
782
|
role: "assistant",
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { Agent, createTool } from "./index.js";
|
|
3
|
+
import type {
|
|
4
|
+
DataModelFromSchemaDefinition,
|
|
5
|
+
ApiFromModules,
|
|
6
|
+
ActionBuilder,
|
|
7
|
+
MutationBuilder,
|
|
8
|
+
} from "convex/server";
|
|
9
|
+
import { anyApi, actionGeneric, mutationGeneric } from "convex/server";
|
|
10
|
+
import { v } from "convex/values";
|
|
11
|
+
import { defineSchema } from "convex/server";
|
|
12
|
+
import { stepCountIs, type LanguageModelUsage } from "ai";
|
|
13
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
14
|
+
import { z } from "zod/v4";
|
|
15
|
+
import { mockModel } from "./mockModel.js";
|
|
16
|
+
import type { UsageHandler } from "./types.js";
|
|
17
|
+
|
|
18
|
+
const schema = defineSchema({});
|
|
19
|
+
type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
20
|
+
const action = actionGeneric as ActionBuilder<DataModel, "public">;
|
|
21
|
+
const mutation = mutationGeneric as MutationBuilder<DataModel, "public">;
|
|
22
|
+
|
|
23
|
+
// Tool that always requires approval
|
|
24
|
+
const deleteFileTool = createTool({
|
|
25
|
+
description: "Delete a file",
|
|
26
|
+
inputSchema: z.object({ filename: z.string() }),
|
|
27
|
+
needsApproval: () => true,
|
|
28
|
+
execute: async (_ctx, input) => `Deleted: ${input.filename}`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Track usage handler calls to verify the full flow is exercised
|
|
32
|
+
const usageCalls: LanguageModelUsage[] = [];
|
|
33
|
+
const testUsageHandler: UsageHandler = async (_ctx, args) => {
|
|
34
|
+
usageCalls.push(args.usage);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getApprovalIdFromSavedMessages(
|
|
38
|
+
savedMessages:
|
|
39
|
+
| Array<{
|
|
40
|
+
message?: { content: unknown };
|
|
41
|
+
}>
|
|
42
|
+
| undefined,
|
|
43
|
+
): string {
|
|
44
|
+
const approvalRequest = savedMessages
|
|
45
|
+
?.flatMap((savedMessage) =>
|
|
46
|
+
Array.isArray(savedMessage.message?.content)
|
|
47
|
+
? savedMessage.message.content
|
|
48
|
+
: [],
|
|
49
|
+
)
|
|
50
|
+
.find((part) => {
|
|
51
|
+
const maybeApproval = part as { type?: unknown };
|
|
52
|
+
return maybeApproval.type === "tool-approval-request";
|
|
53
|
+
}) as { approvalId?: unknown } | undefined;
|
|
54
|
+
if (typeof approvalRequest?.approvalId !== "string") {
|
|
55
|
+
throw new Error("No approval request found in saved messages");
|
|
56
|
+
}
|
|
57
|
+
return approvalRequest.approvalId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Agents (separate mock model instances to avoid shared callIndex) ---
|
|
61
|
+
|
|
62
|
+
const approvalAgent = new Agent(components.agent, {
|
|
63
|
+
name: "approval-test",
|
|
64
|
+
instructions: "You delete files when asked.",
|
|
65
|
+
tools: { deleteFile: deleteFileTool },
|
|
66
|
+
languageModel: mockModel({
|
|
67
|
+
contentSteps: [
|
|
68
|
+
// Step 1: model makes a tool call (LanguageModelV3 uses `input` as JSON string)
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
type: "tool-call",
|
|
72
|
+
toolCallId: "tc-approve",
|
|
73
|
+
toolName: "deleteFile",
|
|
74
|
+
input: JSON.stringify({ filename: "test.txt" }),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
// Step 2: after tool execution, model responds with text
|
|
78
|
+
[{ type: "text", text: "Done! I deleted test.txt." }],
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
stopWhen: stepCountIs(5),
|
|
82
|
+
usageHandler: testUsageHandler,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const denialAgent = new Agent(components.agent, {
|
|
86
|
+
name: "denial-test",
|
|
87
|
+
instructions: "You delete files when asked.",
|
|
88
|
+
tools: { deleteFile: deleteFileTool },
|
|
89
|
+
languageModel: mockModel({
|
|
90
|
+
contentSteps: [
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
type: "tool-call",
|
|
94
|
+
toolCallId: "tc-deny",
|
|
95
|
+
toolName: "deleteFile",
|
|
96
|
+
input: JSON.stringify({ filename: "secret.txt" }),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
[{ type: "text", text: "OK, I won't delete that file." }],
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
stopWhen: stepCountIs(5),
|
|
103
|
+
usageHandler: testUsageHandler,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// --- Test helpers ---
|
|
107
|
+
|
|
108
|
+
export const testApproveFlow = action({
|
|
109
|
+
args: {},
|
|
110
|
+
handler: async (ctx) => {
|
|
111
|
+
const { thread } = await approvalAgent.createThread(ctx, { userId: "u1" });
|
|
112
|
+
|
|
113
|
+
// Step 1: Generate text — model returns tool call, SDK sees needsApproval → stops
|
|
114
|
+
const result1 = await thread.generateText({
|
|
115
|
+
prompt: "Delete test.txt",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
|
|
119
|
+
|
|
120
|
+
// Step 2: Approve the tool call
|
|
121
|
+
const { messageId } = await ctx.runMutation(
|
|
122
|
+
anyApi["approval.test"].submitApprovalForApprovalAgent,
|
|
123
|
+
{ threadId: thread.threadId, approvalId },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Step 3: Continue generation — SDK executes tool, model responds
|
|
127
|
+
const result2 = await thread.generateText({
|
|
128
|
+
promptMessageId: messageId,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Verify thread has all messages persisted
|
|
132
|
+
const allMessages = await approvalAgent.listMessages(ctx, {
|
|
133
|
+
threadId: thread.threadId,
|
|
134
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
approvalId,
|
|
139
|
+
firstText: result1.text,
|
|
140
|
+
secondText: result2.text,
|
|
141
|
+
firstSavedCount: result1.savedMessages?.length ?? 0,
|
|
142
|
+
secondSavedCount: result2.savedMessages?.length ?? 0,
|
|
143
|
+
totalThreadMessages: allMessages.page.length,
|
|
144
|
+
threadMessageRoles: allMessages.page.map((m) => m.message?.role),
|
|
145
|
+
usageCallCount: usageCalls.length,
|
|
146
|
+
// Verify usage data includes detail fields (AI SDK v6)
|
|
147
|
+
lastUsage: usageCalls.at(-1),
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const testDenyFlow = action({
|
|
153
|
+
args: {},
|
|
154
|
+
handler: async (ctx) => {
|
|
155
|
+
const { thread } = await denialAgent.createThread(ctx, { userId: "u2" });
|
|
156
|
+
|
|
157
|
+
// Step 1: Generate — model returns tool call, approval requested
|
|
158
|
+
const result1 = await thread.generateText({
|
|
159
|
+
prompt: "Delete secret.txt",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
|
|
163
|
+
|
|
164
|
+
// Step 2: Deny the tool call
|
|
165
|
+
const { messageId } = await ctx.runMutation(
|
|
166
|
+
anyApi["approval.test"].submitDenialForDenialAgent,
|
|
167
|
+
{
|
|
168
|
+
threadId: thread.threadId,
|
|
169
|
+
approvalId,
|
|
170
|
+
reason: "This file is important",
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Step 3: Continue generation — SDK creates execution-denied, model responds
|
|
175
|
+
const result2 = await thread.generateText({
|
|
176
|
+
promptMessageId: messageId,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Verify thread state
|
|
180
|
+
const allMessages = await denialAgent.listMessages(ctx, {
|
|
181
|
+
threadId: thread.threadId,
|
|
182
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
approvalId,
|
|
187
|
+
firstText: result1.text,
|
|
188
|
+
secondText: result2.text,
|
|
189
|
+
totalThreadMessages: allMessages.page.length,
|
|
190
|
+
threadMessageRoles: allMessages.page.map((m) => m.message?.role),
|
|
191
|
+
usageCallCount: usageCalls.length,
|
|
192
|
+
lastUsage: usageCalls.at(-1),
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
export const testApproveFlowWithInterveningMessage = action({
|
|
198
|
+
args: {},
|
|
199
|
+
handler: async (ctx) => {
|
|
200
|
+
const { thread } = await approvalAgent.createThread(ctx, { userId: "u3" });
|
|
201
|
+
|
|
202
|
+
const result1 = await thread.generateText({
|
|
203
|
+
prompt: "Delete test.txt",
|
|
204
|
+
});
|
|
205
|
+
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
|
|
206
|
+
|
|
207
|
+
const approvalRequest = (
|
|
208
|
+
await approvalAgent.listMessages(ctx, {
|
|
209
|
+
threadId: thread.threadId,
|
|
210
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
211
|
+
})
|
|
212
|
+
).page.find((m) => {
|
|
213
|
+
const content = m.message?.content;
|
|
214
|
+
return (
|
|
215
|
+
Array.isArray(content) &&
|
|
216
|
+
content.some(
|
|
217
|
+
(p) =>
|
|
218
|
+
p.type === "tool-approval-request" && p.approvalId === approvalId,
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
if (!approvalRequest) {
|
|
223
|
+
throw new Error("Approval request message not found");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const intervening = await approvalAgent.saveMessage(ctx, {
|
|
227
|
+
threadId: thread.threadId,
|
|
228
|
+
prompt: "Intervening user message",
|
|
229
|
+
skipEmbeddings: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const { messageId } = await ctx.runMutation(
|
|
233
|
+
anyApi["approval.test"].submitApprovalForApprovalAgent,
|
|
234
|
+
{ threadId: thread.threadId, approvalId },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const result2 = await thread.generateText({
|
|
238
|
+
promptMessageId: messageId,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const allMessages = await approvalAgent.listMessages(ctx, {
|
|
242
|
+
threadId: thread.threadId,
|
|
243
|
+
paginationOpts: { cursor: null, numItems: 40 },
|
|
244
|
+
});
|
|
245
|
+
const approvalResponse = allMessages.page.find((m) => m._id === messageId);
|
|
246
|
+
if (!approvalResponse) {
|
|
247
|
+
throw new Error("Saved approval response message not found");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
secondText: result2.text,
|
|
252
|
+
approvalResponseOrder: approvalResponse.order,
|
|
253
|
+
approvalRequestId: approvalRequest._id,
|
|
254
|
+
approvalRequestOrder: approvalRequest.order,
|
|
255
|
+
interveningOrder: intervening.message.order,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
export const submitApprovalForApprovalAgent = mutation({
|
|
261
|
+
args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
|
|
262
|
+
handler: async (ctx, { threadId, approvalId, reason }) => {
|
|
263
|
+
return approvalAgent.approveToolCall(ctx, { threadId, approvalId, reason });
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
export const submitDenialForDenialAgent = mutation({
|
|
268
|
+
args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
|
|
269
|
+
handler: async (ctx, { threadId, approvalId, reason }) => {
|
|
270
|
+
return denialAgent.denyToolCall(ctx, { threadId, approvalId, reason });
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const testApi: ApiFromModules<{
|
|
275
|
+
fns: {
|
|
276
|
+
testApproveFlow: typeof testApproveFlow;
|
|
277
|
+
testDenyFlow: typeof testDenyFlow;
|
|
278
|
+
testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
|
|
279
|
+
submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
|
|
280
|
+
submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
|
|
281
|
+
};
|
|
282
|
+
}>["fns"] = anyApi["approval.test"] as any;
|
|
283
|
+
|
|
284
|
+
describe("Tool Approval Workflow", () => {
|
|
285
|
+
test("approve: generate → approval request → approve → tool executes → final text", async () => {
|
|
286
|
+
usageCalls.length = 0;
|
|
287
|
+
const t = initConvexTest(schema);
|
|
288
|
+
const result = await t.action(testApi.testApproveFlow, {});
|
|
289
|
+
|
|
290
|
+
expect(result.approvalId).toBeDefined();
|
|
291
|
+
// First call produces no text (just a tool call)
|
|
292
|
+
expect(result.firstText).toBe("");
|
|
293
|
+
// Second call produces the final text
|
|
294
|
+
expect(result.secondText).toBe("Done! I deleted test.txt.");
|
|
295
|
+
// First call: user message + assistant (tool-call + approval-request)
|
|
296
|
+
expect(result.firstSavedCount).toBeGreaterThanOrEqual(2);
|
|
297
|
+
// Second call: tool-result + assistant text
|
|
298
|
+
expect(result.secondSavedCount).toBeGreaterThanOrEqual(1);
|
|
299
|
+
// Thread should have (ascending): user, assistant(tool-call+approval),
|
|
300
|
+
// tool(approval-response), tool(tool-result), assistant(text)
|
|
301
|
+
// listMessages returns descending order:
|
|
302
|
+
expect(result.threadMessageRoles).toEqual([
|
|
303
|
+
"assistant", // final text
|
|
304
|
+
"tool", // tool-result
|
|
305
|
+
"tool", // approval-response
|
|
306
|
+
"assistant", // tool-call + approval-request
|
|
307
|
+
"user", // prompt
|
|
308
|
+
]);
|
|
309
|
+
// Usage handler should be called for each generateText call
|
|
310
|
+
expect(result.usageCallCount).toBeGreaterThanOrEqual(2);
|
|
311
|
+
// Usage data should include AI SDK v6 detail fields
|
|
312
|
+
expect(result.lastUsage).toBeDefined();
|
|
313
|
+
expect(result.lastUsage!.inputTokenDetails).toBeDefined();
|
|
314
|
+
expect(result.lastUsage!.outputTokenDetails).toBeDefined();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("deny: generate → approval request → deny → model acknowledges denial", async () => {
|
|
318
|
+
usageCalls.length = 0;
|
|
319
|
+
const t = initConvexTest(schema);
|
|
320
|
+
const result = await t.action(testApi.testDenyFlow, {});
|
|
321
|
+
|
|
322
|
+
expect(result.approvalId).toBeDefined();
|
|
323
|
+
expect(result.firstText).toBe("");
|
|
324
|
+
expect(result.secondText).toBe("OK, I won't delete that file.");
|
|
325
|
+
// Same message ordering as approve flow:
|
|
326
|
+
// user, assistant(tool-call+approval), tool(denial-response),
|
|
327
|
+
// tool(execution-denied result), assistant(text)
|
|
328
|
+
expect(result.threadMessageRoles).toEqual([
|
|
329
|
+
"assistant",
|
|
330
|
+
"tool",
|
|
331
|
+
"tool",
|
|
332
|
+
"assistant",
|
|
333
|
+
"user",
|
|
334
|
+
]);
|
|
335
|
+
// Usage handler exercised
|
|
336
|
+
expect(result.usageCallCount).toBeGreaterThanOrEqual(2);
|
|
337
|
+
expect(result.lastUsage!.inputTokenDetails).toBeDefined();
|
|
338
|
+
expect(result.lastUsage!.outputTokenDetails).toBeDefined();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("approve remains valid with an intervening thread message", async () => {
|
|
342
|
+
usageCalls.length = 0;
|
|
343
|
+
const t = initConvexTest(schema);
|
|
344
|
+
const result = await t.action(testApi.testApproveFlowWithInterveningMessage, {});
|
|
345
|
+
|
|
346
|
+
expect(result.secondText).toBe("Done! I deleted test.txt.");
|
|
347
|
+
expect(result.approvalResponseOrder).toBe(result.approvalRequestOrder);
|
|
348
|
+
expect(result.interveningOrder).toBeGreaterThan(result.approvalResponseOrder);
|
|
349
|
+
});
|
|
350
|
+
});
|