Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
946a489
Fix tool approval continuation streaming to separate message
sethconvex Jan 25, 2026
c3cddfd
Remove debug logging from approval example
sethconvex Jan 25, 2026
ddce09b
Add approveToolCall and denyToolCall helper methods to Agent
sethconvex Jan 26, 2026
05a5f19
Remove noisy warning for paginated tool results
sethconvex Jan 26, 2026
78dcc30
Add specific types for AI SDK v6 usage token details
sethconvex Jan 26, 2026
3a29dd1
Enhance CLAUDE.md with comprehensive AI upgrade guidance
sethconvex Jan 26, 2026
820fdea
Add compile-time errors for AI SDK v5 patterns
sethconvex Jan 26, 2026
cc2bdd8
Fix code review issues from CodeRabbit
sethconvex Jan 26, 2026
9fc04f3
Include MIGRATION.md in npm package and update error references
sethconvex Jan 26, 2026
74d91a6
Fix CI: update example tools to AI SDK v6 syntax and fix lint errors
sethconvex Jan 26, 2026
65fe1c2
Add comprehensive tests for tool approval workflow
sethconvex Jan 26, 2026
27fba6f
Add bug-finding tests for tool approval workflow
sethconvex Jan 26, 2026
25b70cb
Fix TOCTOU race condition with atomic idempotency check
sethconvex Jan 26, 2026
c391865
Improve upgrade experience with better docs and tooling
sethconvex Jan 26, 2026
70481a6
Upgrade convex to 1.31.6
sethconvex Jan 26, 2026
de5763d
Remove unused check-upgrade script
sethconvex Jan 26, 2026
8383396
Simplify TOCTOU handling: just throw if already handled
sethconvex Jan 26, 2026
353112f
Fix UI flashing during tool approval continuation
sethconvex Jan 28, 2026
6506402
Refactor approval flow: save-only approve/deny, defer tool execution
sethconvex Jan 30, 2026
cac50e3
Fix race condition: Atomic stream finish with message save
sethconvex Feb 4, 2026
06e5305
Optimize approval API: eliminate database lookup
sethconvex Feb 4, 2026
9ae2e58
Optimize approval workflow with indexed fields (O(1) lookup)
sethconvex Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions .cursor/rules/convex_rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const listWithExtraArg = query({
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("author"), args.author))
.withIndex("by_author", (q) => q.eq("author", args.author))
.order("desc")
.paginate(args.paginationOpts);
},
Expand All @@ -180,7 +180,7 @@ Note: `paginationOpts` is an object with the following properties:

## Schema guidelines
- Always define your schema in `convex/schema.ts`.
- Always import the schema definition functions from `convex/server`:
- Always import the schema definition functions from `convex/server`.
- System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are `_creationTime` which has the validator `v.number()` and `_id` which has the validator `v.id(tableName)`.
- Always include all index fields in the index name. For example, if an index is defined as `["field1", "field2"]`, the index name should be "by_field1_and_field2".
- Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes.
Expand All @@ -198,7 +198,7 @@ export const exampleQuery = query({
handler: async (ctx, args) => {
const idToUsername: Record<Id<"users">, string> = {};
for (const userId of args.userIds) {
const user = await ctx.db.get(userId);
const user = await ctx.db.get("users", userId);
if (user) {
idToUsername[user._id] = user.username;
}
Expand Down Expand Up @@ -236,8 +236,8 @@ const messages = await ctx.db


## Mutation guidelines
- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist.
- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist.
- Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })`
- Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist. Syntax: `await ctx.db.patch('tasks', taskId, { completed: true })`

## Action guidelines
- Always add `"use node";` to the top of files containing actions that use Node.js built-in modules.
Expand Down Expand Up @@ -307,7 +307,7 @@ export const exampleQuery = query({
args: { fileId: v.id("_storage") },
returns: v.null(),
handler: async (ctx, args) => {
const metadata: FileMetadata | null = await ctx.db.system.get(args.fileId);
const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);
console.log(metadata);
return null;
},
Expand Down Expand Up @@ -434,7 +434,7 @@ Internal Functions:
"description": "This example shows how to build a chat app without authentication.",
"version": "1.0.0",
"dependencies": {
"convex": "^1.17.4",
"convex": "^1.31.2",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find the convex_rules.mdc file and see the current state
find . -name "convex_rules.mdc" -type f

Repository: get-convex/agent

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Find all ctx.db calls in the codebase to assess migration needs
rg -n --type=ts --type=tsx --type=js --type=jsx 'ctx\.db\.(get|patch|replace|delete)\(' \
  --glob '!node_modules/**' \
  --glob '!.cursor/**' \
  -A 2

Repository: get-convex/agent

Length of output: 87


🌐 Web query:

Convex ctx.db API changes version 1.31.0 table name parameter

💡 Result:

In convex npm package v1.31.0, Convex changed several ctx.db methods so they accept the table name as the first argument (the old “infer table from the ID” form is still supported for now, but the table name is now recommended and is expected to be required for future features like custom/bring-your-own IDs). [1] [2]

What changed (table name parameter added)

These methods now take tableName first: db.get, db.patch, db.replace, db.delete. [1]

Old (still works for now):

  • await ctx.db.get(id)
  • await ctx.db.patch(id, value)
  • await ctx.db.replace(id, value)
  • await ctx.db.delete(id)

New (v1.31.0+):

  • await ctx.db.get("messages", id)
  • await ctx.db.patch("messages", id, value)
  • await ctx.db.replace("messages", id, value)
  • await ctx.db.delete("messages", id) [1] [2]

Migration help

  • ESLint rule: @convex-dev/explicit-table-ids (autofix via npx eslint . --fix) [1]
  • Codemod: npx @convex-dev/codemod@latest explicit-ids [1]

🏁 Script executed:

#!/bin/bash
# Search for ctx.db calls without specifying file types
rg -n 'ctx\.db\.(get|patch|replace|delete)\(' \
  --glob '!node_modules/**' \
  --glob '!.cursor/**' \
  -A 2

Repository: get-convex/agent

Length of output: 13897


🏁 Script executed:

#!/bin/bash
# Check the current convex version in package.json
cat package.json | grep -A 2 '"convex"'

Repository: get-convex/agent

Length of output: 281


Version bump to ^1.31.2 aligns with Convex's v1.31.0 API changes requiring table names as the first argument to ctx.db methods.

Since version 1.31.0, ctx.db.get(), ctx.db.patch(), ctx.db.replace(), and ctx.db.delete() accept the table name as the first argument. The old API (e.g., ctx.db.get(id)) remains functional for now but will be required for custom ID generation in future versions.

The codebase has 70+ instances of old-style calls throughout src/component/ that need migration to the new format (e.g., ctx.db.get("tableName", id)). Consider using the automated migration tool: npx @convex-dev/codemod@latest explicit-ids or enable the ESLint rule @convex-dev/explicit-table-ids with --fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.cursor/rules/convex_rules.mdc at line 437, The project is using Convex
v1.31.2 which requires passing the table name as the first argument to ctx.db
methods; update all old-style calls like ctx.db.get(id), ctx.db.patch(id, ...),
ctx.db.replace(id, ...), and ctx.db.delete(id) to the new form
ctx.db.get("tableName", id), ctx.db.patch("tableName", id, ...),
ctx.db.replace("tableName", id, ...), and ctx.db.delete("tableName", id). Locate
usages of ctx.db.get / ctx.db.patch / ctx.db.replace / ctx.db.delete across
src/component/ and either run the automated codemod npx
`@convex-dev/codemod`@latest explicit-ids or enable and run ESLint rule
`@convex-dev/explicit-table-ids` --fix to perform the migration consistently, then
run tests to confirm no regressions.

"openai": "^4.79.0"
},
"devDependencies": {
Expand Down Expand Up @@ -667,6 +667,35 @@ export default defineSchema({
});
```

#### convex/tsconfig.json
```typescript
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,

/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}
```

#### src/App.tsx
```typescript
export default function App() {
Expand Down
117 changes: 87 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ npm run typecheck # Full TypeScript validation including example/convex
- **Message-centric design** - All operations revolve around message persistence/retrieval
- **Component-based encapsulation** - Uses Convex Components pattern
- **Multi-user support** - Threads have optional `userId` for ownership
- **Tool approval flow** - Tools can require human approval via `needsApproval`, with `agent.approveToolCall()` and `agent.denyToolCall()` methods

### Database Tables (Convex Component)

Expand Down Expand Up @@ -87,82 +88,138 @@ Follow the rules in `.cursor/rules/convex_rules.mdc`:
- `@convex-dev/agent/validators` - Convex validators for integration
- `@convex-dev/agent/test` - Testing utilities

## Upgrading to v0.6.0 (AI SDK v6)
## AI Guidance: Helping Users Upgrade to v0.6.0

When helping users upgrade from @convex-dev/agent v0.3.x to v0.6.0, follow these steps:
**IMPORTANT: v0.6.0 requires AI SDK v6 (ai@^6.0.0)**

### Step 1: Update dependencies in package.json
When helping users upgrade from @convex-dev/agent v0.3.x (AI SDK v5) to v0.6.0 (AI SDK v6):

### Step 1: Update Dependencies First

Update all AI SDK packages together to avoid peer dependency conflicts:

```bash
npm install @convex-dev/agent@^0.6.0 ai@^6.0.35 @ai-sdk/provider-utils@^4.0.6
npm install @ai-sdk/openai@^3.0.10 # or whichever provider
```

Also update any AI SDK provider packages:
```bash
npm install @ai-sdk/openai@^3.0.10 @ai-sdk/anthropic@^3.0.13
```
**Compatible sibling packages:**
- `@convex-dev/rag@^0.7.0` (v0.6.0 has type conflicts with AI SDK v6)
- `@convex-dev/workflow@^0.3.2`

### Step 2: Detect v5 Patterns

### Step 2: Update tool definitions
Search for these patterns indicating v5 usage:
- `createTool({ args:` - should be `inputSchema`
- `createTool({ handler:` - should be `execute`
- `textEmbeddingModel:` - should be `embeddingModel`
- `maxSteps:` in generateText/streamText - should be `stopWhen: stepCountIs(N)`
- `mode: "json"` in generateObject - removed in v6
- `@ai-sdk/*` packages at v1.x or v2.x - should be v3.x
- Type imports: `LanguageModelV2` → `LanguageModelV3`, `EmbeddingModel<string>` → `EmbeddingModelV3`

Replace `parameters` with `inputSchema`:
### Step 3: Apply Transformations

**Tool definitions:**
```typescript
// Before (v5)
// BEFORE (v5)
const myTool = createTool({
description: "...",
parameters: z.object({ query: z.string() }),
execute: async (ctx, args) => { ... }
handler: async (ctx, args) => {
return args.query.toUpperCase();
}
})

// After (v6)
// AFTER (v6)
const myTool = createTool({
description: "...",
inputSchema: z.object({ query: z.string() }),
execute: async (ctx, input, options) => { ... }
execute: async (ctx, input, options) => {
return input.query.toUpperCase();
}
})
```

### Step 3: Update maxSteps usage (if applicable)
**Agent embedding config:**
```typescript
// BEFORE
new Agent(components.agent, {
textEmbeddingModel: openai.embedding("text-embedding-3-small")
})

// AFTER
new Agent(components.agent, {
embeddingModel: openai.embedding("text-embedding-3-small")
})
```

**Step limits:**
```typescript
// Before (v5)
// BEFORE
await agent.generateText(ctx, { threadId }, {
prompt: "...",
maxSteps: 5
})

// After (v6) - maxSteps still works but stopWhen is preferred
import { stepCountIs } from "ai"
// AFTER
import { stepCountIs } from "@convex-dev/agent"
await agent.generateText(ctx, { threadId }, {
prompt: "...",
stopWhen: stepCountIs(5)
})
```

### Step 4: Update embedding model config (optional)
**Type imports:**
```typescript
// BEFORE (v5)
import type { LanguageModelV2 } from "@ai-sdk/provider";
import type { EmbeddingModel } from "ai";
let model: LanguageModelV2;
let embedder: EmbeddingModel<string>;

// AFTER (v6)
import type { LanguageModelV3, EmbeddingModelV3 } from "@ai-sdk/provider";
let model: LanguageModelV3;
let embedder: EmbeddingModelV3;
```

**generateObject (remove mode: "json"):**
```typescript
// Before
new Agent(components.agent, {
textEmbeddingModel: openai.embedding("text-embedding-3-small")
// BEFORE (v5)
await generateObject({
model,
mode: "json",
schema: z.object({ ... }),
prompt: "..."
})

// After (textEmbeddingModel still works but embeddingModel is preferred)
new Agent(components.agent, {
embeddingModel: openai.embedding("text-embedding-3-small")
// AFTER (v6) - mode: "json" removed, just use schema
await generateObject({
model,
schema: z.object({ ... }),
prompt: "..."
})
```

### Step 5: Verify the upgrade
### Step 4: Verify

```bash
npm run typecheck
npm run lint
npm test
```

### Common issues
### Common Issues

- **EmbeddingModelV2 vs V3 errors**: Ensure all `@ai-sdk/*` packages are v3.x
- **Tool `args` vs `input`**: v6 uses `input` in execute signature (2nd param)
- **`mimeType` vs `mediaType`**: v6 prefers `mediaType` (backwards compat maintained)
- **Type import errors**: `LanguageModelV2` is now `LanguageModelV3`, `EmbeddingModel<string>` is now `EmbeddingModelV3` (no longer generic)
- **generateObject mode errors**: `mode: "json"` was removed in v6 - just remove the mode option

### New v6 Features to Mention

- **EmbeddingModelV2 vs V3 errors**: Ensure all @ai-sdk/* packages are updated to v3.x
- **Tool input/args**: v6 uses `input` instead of `args` in tool calls (backwards compat maintained)
- **mimeType vs mediaType**: v6 uses `mediaType` (backwards compat maintained)
After upgrade, users can now use:
- **Tool approval**: `needsApproval` in createTool, `agent.approveToolCall()`, `agent.denyToolCall()`
- **Reasoning streaming**: Works with models like Groq that support reasoning
- **Detailed token usage**: `inputTokenDetails`, `outputTokenDetails` in usage tracking
58 changes: 58 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ This guide helps you upgrade from @convex-dev/agent v0.3.x to v0.6.0.

## Step 1: Update dependencies

Update all AI SDK packages **together** to avoid peer dependency conflicts:

```bash
npm install @convex-dev/agent@^0.6.0 ai@^6.0.35 @ai-sdk/provider-utils@^4.0.6
```

### Official AI SDK providers

Update your AI SDK provider packages to v3.x:
```bash
# For OpenAI
Expand All @@ -18,8 +22,38 @@ npm install @ai-sdk/anthropic@^3.0.13

# For Groq
npm install @ai-sdk/groq@^3.0.8

# For Google (Gemini)
npm install @ai-sdk/google@^3.0.8
```

### Third-party providers

Third-party providers also need updates to be compatible with AI SDK v6:

```bash
# For OpenRouter
npm install @openrouter/ai-sdk-provider@^2.0.0

# For other providers, check their documentation for AI SDK v6 compatibility
```

### Handling dependency conflicts

If you see peer dependency warnings or errors, try updating all packages at once:

```bash
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
```

If you still have conflicts, you can use `--force` as a last resort:

```bash
npm install @convex-dev/agent@^0.6.0 --force
```

> **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.

## Step 2: Update tool definitions

Replace `parameters` with `inputSchema`:
Expand Down Expand Up @@ -89,6 +123,30 @@ AI SDK v6 renamed `args` to `input` in tool calls. The library maintains backwar
### `mimeType` vs `mediaType`
AI SDK v6 renamed `mimeType` to `mediaType`. Backwards compatibility is maintained.

### Peer dependency conflicts

If you see errors like:
```
npm error ERESOLVE unable to resolve dependency tree
npm error peer ai@"^5.0.0" from @openrouter/ai-sdk-provider@1.0.3
```

This means a third-party provider needs updating. Common solutions:

1. **Update the provider** to a version compatible with AI SDK v6
2. **Check npm** for the latest version: `npm view @openrouter/ai-sdk-provider versions`
3. **Use `--force`** if a compatible version isn't available yet (temporary workaround)

### Third-party provider compatibility

| Provider | AI SDK v5 (ai@5.x) | AI SDK v6 (ai@6.x) |
|----------|-------------------|-------------------|
| @openrouter/ai-sdk-provider | v1.x | v2.x |
| @ai-sdk/openai | v1.x-v2.x | v3.x |
| @ai-sdk/anthropic | v1.x-v2.x | v3.x |
| @ai-sdk/groq | v1.x-v2.x | v3.x |
| @ai-sdk/google | v1.x-v2.x | v3.x |

## More Information

- [AI SDK v6 Migration Guide](https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0)
Expand Down
4 changes: 4 additions & 0 deletions example/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
* @module
*/

import type * as agents_approval from "../agents/approval.js";
import type * as agents_config from "../agents/config.js";
import type * as agents_fashion from "../agents/fashion.js";
import type * as agents_simple from "../agents/simple.js";
import type * as agents_story from "../agents/story.js";
import type * as agents_weather from "../agents/weather.js";
import type * as chat_approval from "../chat/approval.js";
import type * as chat_basic from "../chat/basic.js";
import type * as chat_human from "../chat/human.js";
import type * as chat_streamAbort from "../chat/streamAbort.js";
Expand Down Expand Up @@ -55,11 +57,13 @@ import type {
} from "convex/server";

declare const fullApi: ApiFromModules<{
"agents/approval": typeof agents_approval;
"agents/config": typeof agents_config;
"agents/fashion": typeof agents_fashion;
"agents/simple": typeof agents_simple;
"agents/story": typeof agents_story;
"agents/weather": typeof agents_weather;
"chat/approval": typeof chat_approval;
"chat/basic": typeof chat_basic;
"chat/human": typeof chat_human;
"chat/streamAbort": typeof chat_streamAbort;
Expand Down
2 changes: 1 addition & 1 deletion example/convex/_generated/dataModel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
* Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
*
* Documents can be loaded using `db.get(id)` in query and mutation functions.
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
*
* IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking.
Expand Down
Loading
Loading