Skip to content

Comments

AI SDK v6 Support#216

Merged
sethconvex merged 49 commits intomainfrom
rc/ai-sdk-v6
Feb 20, 2026
Merged

AI SDK v6 Support#216
sethconvex merged 49 commits intomainfrom
rc/ai-sdk-v6

Conversation

@sethconvex
Copy link
Contributor

@sethconvex sethconvex commented Jan 25, 2026

Summary

Upgrades @convex-dev/agent to support AI SDK v6, dropping support for AI SDK v5.
Fixes #202

Based on @zboyles's work in #208, merged with latest main and lint/typecheck fixes.

Breaking Changes

  • Requires AI SDK v6 (ai@^6.0.35)
  • Drops support for AI SDK v5
  • Package version jumps to 0.6.0 to align with AI SDK major version

Key Changes

  • Updates message and tool invocation types to align with AI SDK v6
  • Adds tool approval workflow support (tool-approval-request, tool-approval-response)
  • inputSchema replaces parameters in tool definitions
  • input replaces args in tool calls (with backwards compat)
  • mediaType replaces mimeType (with backwards compat)
  • New embeddingModel config option (deprecates textEmbeddingModel)

Migration

See the AI SDK v6 migration guide for details on AI SDK changes.

Test plan

  • npm run typecheck passes
  • npm run lint passes
  • All 212 tests pass
  • Manual testing with real providers (OpenAI, Anthropic)
  • Test tool approval workflow

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Tool approval workflow (request/response/denial), richer tool hooks, inputSchema/input support, and needsApproval.
    • Unified embeddingModel support and LanguageModel V3 mock behavior.
  • Breaking Changes

    • Bumped to 0.6.0; AI SDK v6 required.
    • textEmbeddingModel → embeddingModel; args → input (deprecated).
    • mimeType → mediaType for file metadata; stopWhen/stepCountIs replaces maxSteps.
  • Bug Fixes

    • Improved streaming, async message handling, error suppression, and orphaned tool-message handling.
  • Documentation

    • Added MIGRATION.md, updated usage and agent/docs.

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Bump to v0.6.0 with AI SDK v6 migration: introduce Output interface and LanguageModelV3 surfaces, add embeddingModel (deprecated textEmbeddingModel fallback), async UI message processing, tool approval workflow, mediaType replacing mimeType, expanded validators/content shapes, numerous test and docs updates, and a migration guide.

Changes

Cohort / File(s) Summary
Package & Docs
CHANGELOG.md, package.json, MIGRATION.md, CLAUDE.md, TYPE_FIX_SUMMARY.md
Release bumped to v0.6.0, dependency updates to AI SDK v6, add migration guide and changelog/type-fix notes.
Core Types & API Surface
src/client/types.ts, src/client/streamText.ts, src/client/index.ts, src/client/mockModel.ts, src/client/saveInputMessages.ts
Add Output interface; constrain OUTPUT generics; migrate public APIs and mock model to v3 types; add embeddingModel with deprecated textEmbeddingModel fallback and adjust related signatures.
Tool Creation & Lifecycle
src/client/createTool.ts, src/client/index.test.ts
Introduce ctx-aware tool types, lifecycle hooks, needsApproval wiring, deprecation warnings for args/handler, and test updates to include input fields.
Tool Approval & Validators
src/validators.ts, src/shared.ts, src/mapping.ts, src/component/_generated/component.ts
Add tool-approval-request/tool-approval-response types; support input (with deprecated args) on tool-call parts; add execution-denied variant and require/propagate providerOptions.
Message Mapping & Async Processing
src/UIMessages.ts, src/component/streams.ts, src/fromUIMessages.test.ts, src/toUIMessages.test.ts
Convert fromUIMessages to async; await stream conversions; extract and propagate providerOptions; add handling for approval/denied flows; update tests for async and shape changes.
Streaming & Delta Handling
src/deltas.ts, src/deltas.test.ts
Suppress specific streaming errors (e.g., "no tool invocation found"); handle streaming tool-approval-request deltas and related state transitions; add continuation/approval tests.
Search, Embeddings & Storage
src/client/search.ts, src/client/saveInputMessages.ts
Accept embeddingModel (preferred) with fallback to textEmbeddingModel; propagate effective embedding model through embedding generation, searches, and storage flows.
Files & Media Types
src/component/schema.ts, src/component/files.ts, src/client/files.ts, src/component/messages.test.ts
Introduce mediaType (preferred) and deprecate mimeType; runtime reads both for compatibility; update schema, handlers, and tests.
Mapping & Serialization
src/mapping.ts, src/mapping.test.ts
(De)serialization updated for expanded file/image/custom content types, approvals, legacy args/input compatibility, providerOptions, and added usage token detail fields.
React Hooks & Consumers
src/react/useThreadMessages.ts
Convert UI→doc mapping to async effect using useState/useEffect to avoid treating Promises as arrays; preserve skip behavior.
Examples & Docs
docs/*, example/convex/*, example/tsconfig.json, docs/agent-usage.mdx, docs/tools.mdx, docs/getting-started.mdx
Docs/examples updated: stopWhen: stepCountIs(...) replacing maxSteps, tool options use inputSchema, embeddingModel naming, LanguageModelV3 examples, and tsconfig normalization.
Tests & Coverage
src/client/search.test.ts, src/component/messages.test.ts, src/fromUIMessages.test.ts, src/deltas.test.ts, src/mapping.test.ts, src/toUIMessages.test.ts
Extensive test additions/updates covering approval flows, orphaned tool-message filtering, async conversions, mediaType/input shape changes, and streaming deltas.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Agent as Agent
    participant Tool as Tool
    participant Approval as ApprovalHandler

    User->>Agent: Trigger tool call (with `input`)
    Agent->>Tool: Emit tool-call (toolCallId, input)
    Agent->>Approval: needsApproval?(input, ctx)
    alt approval required
        Approval->>Agent: Emit tool-approval-request (approvalId)
        Agent->>User: Present approval request
        User->>Agent: Send approval response
        Agent->>Approval: Deliver tool-approval-response (approved?/reason)
        alt approved
            Agent->>Tool: Execute tool (authorized)
            Tool-->>Agent: Return tool-result
        else denied
            Agent-->>User: Emit execution-denied output
        end
    else no approval required
        Agent->>Tool: Execute tool
        Tool-->>Agent: Return tool-result
    end
    Agent->>User: Deliver final result (tool-result / execution-denied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 I hopped through types and docs tonight,

swapped mime for media, set async right.
Approvals bloom, embeddings now new,
V6 gates opened — carrots for the crew. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'AI SDK v6 Support' accurately reflects the primary objective of upgrading the codebase to be compatible with AI SDK v6, which is the main change across multiple files.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #202: upgrades to AI SDK v6 compatibility, resolves the 53 build errors through type updates and API alignment changes, and handles breaking changes from v6 (e.g., tool parameters → inputSchema, args → input).
Out of Scope Changes check ✅ Passed All changes are aligned with AI SDK v6 migration objectives: dependency updates (package.json), type signature migrations (types.ts, validators.ts, mockModel.ts), API surface changes (createTool.ts, streaming.ts), documentation updates, and new tool approval workflow support [#202].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rc/ai-sdk-v6

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 25, 2026

Open in StackBlitz

npm i https://pkg.pr.new/get-convex/agent/@convex-dev/agent@216

commit: bb44255

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/client/createTool.ts (1)

287-298: Potential issue: lifecycle hooks bound with possibly undefined ctx.

The hooks onInputStart, onInputDelta, onInputAvailable, and toModelOutput are bound with getCtx(t) immediately after tool creation. At this point, if def.ctx was not provided, the ctx will be undefined. When wrapTools later sets the ctx, these already-bound hooks won't receive the updated context.

This differs from the execute and needsApproval handlers which call getCtx(this) at invocation time, correctly picking up the injected ctx.

🔧 Proposed fix to defer ctx resolution
-  if (def.onInputStart) {
-    t.onInputStart = def.onInputStart.bind(t, getCtx(t));
-  }
-  if (def.onInputDelta) {
-    t.onInputDelta = def.onInputDelta.bind(t, getCtx(t));
-  }
-  if (def.onInputAvailable) {
-    t.onInputAvailable = def.onInputAvailable.bind(t, getCtx(t));
-  }
-  if (def.toModelOutput) {
-    t.toModelOutput = def.toModelOutput.bind(t, getCtx(t));
-  }
+  if (def.onInputStart) {
+    const originalOnInputStart = def.onInputStart;
+    t.onInputStart = function (this: Tool<INPUT, OUTPUT>, options) {
+      return originalOnInputStart(getCtx(this), options);
+    };
+  }
+  if (def.onInputDelta) {
+    const originalOnInputDelta = def.onInputDelta;
+    t.onInputDelta = function (this: Tool<INPUT, OUTPUT>, options) {
+      return originalOnInputDelta(getCtx(this), options);
+    };
+  }
+  if (def.onInputAvailable) {
+    const originalOnInputAvailable = def.onInputAvailable;
+    t.onInputAvailable = function (this: Tool<INPUT, OUTPUT>, options) {
+      return originalOnInputAvailable(getCtx(this), options);
+    };
+  }
+  if (def.toModelOutput) {
+    const originalToModelOutput = def.toModelOutput;
+    t.toModelOutput = function (this: Tool<INPUT, OUTPUT>, options) {
+      return originalToModelOutput(getCtx(this), options);
+    };
+  }
src/client/mockModel.ts (1)

240-243: Duplicate assignment of _supportedUrls.

This assignment is a duplicate of lines 205-208. The second assignment is redundant and should be removed.

🧹 Proposed fix
         return {
           stream,
           request: { body: {} },
           response: { headers: {} },
         };
       } else if (args.doStream) {
         return args.doStream;
       } else {
         throw new Error("Provide either content or doStream");
       }
     };
-    this._supportedUrls =
-      typeof supportedUrls === "function"
-        ? supportedUrls
-        : async () => supportedUrls;
   }
src/client/search.ts (1)

238-317: Keep tool-calls when approval requests exist.

When a tool-approval-request is present but no tool-approval-response yet, tool-calls are currently filtered out. Since tool-approval-request only contains approvalId and not the tool details, this removes critical context (tool name/args) needed during the approval phase. AI SDK v6 guidance emphasizes retaining tool calls as structured parts within the message stream so the model can see what happened. Consider keeping tool-calls whenever an approval request exists by adding a hasApprovalRequest check alongside the existing hasApprovalResponse condition.

🤖 Fix all issues with AI agents
In `@docs/agent-usage.mdx`:
- Line 46: The docs have inconsistent import sources for stepCountIs (imported
from "@convex-dev/agent" on one example and from "ai" in another); update all
examples to use the same import path (pick one and apply consistently) — e.g.,
change the import that currently pulls stepCountIs from "@convex-dev/agent" so
it imports stepCountIs (and Agent if applicable) from "ai", and ensure every
other example in the file uses that same source for stepCountIs to avoid
confusion.

In `@example/convex/modelsForDemo.ts`:
- Around line 11-25: The exported variable textEmbeddingModel can be undefined
because it’s only set inside the OPENAI_API_KEY branch; update the
initialization so consumers always get a valid value: either assign a fallback
embedding model (e.g., use mockModel({}) or an embedding adapter) when
ANTHROPIC_API_KEY or GROQ_API_KEY is used, or explicitly type/export
textEmbeddingModel as optional and ensure callers handle undefined. Locate
textEmbeddingModel and languageModel in the file and initialize
textEmbeddingModel outside the conditional (or add a conditional export) so it’s
never unintentionally undefined at runtime.

In `@package.json`:
- Around line 72-73: Update the dependency version constraints for the two AI
SDK packages: change "ai" from ^6.0.35 to ^6.0.49 and "@ai-sdk/provider-utils"
from ^4.0.6 to ^4.0.9 in package.json, then run your package manager
(npm/yarn/pnpm) to install the updated versions and run the test/build pipeline
to ensure compatibility; target the dependency entries "ai" and
"@ai-sdk/provider-utils" when making the edits.

In `@src/deltas.ts`:
- Around line 521-526: Restore the compile-time exhaustiveness check for the
TextStreamPart switch by uncommenting the `const _: never = part` line in the
default branch (or, if you intentionally want to disable it, add a clear comment
explaining why and under what conditions that will remain safe); if uncommenting
produces TypeScript errors, fix them by adding any missing case handlers for the
new TextStreamPart variants or narrowing the switch's discriminant so the
assignment to `never` type succeeds, ensuring future ai SDK additions produce
compile-time failures rather than only runtime warnings.

In `@src/mapping.ts`:
- Around line 161-163: Replace the placeholder empty-object assignments for
token detail fields with undefined to match the SDK v6 optional pattern: change
any occurrences where inputTokenDetails: {} as any and outputTokenDetails: {} as
any are used (e.g., the object in mapping.ts and the similar spots in search.ts
around the search result mapping) so they initialize to undefined instead;
ensure the surrounding type annotations accept undefined (adjust union types to
include undefined if necessary) and run type checks to confirm no downstream
code assumes an object.

In `@src/react/useThreadMessages.ts`:
- Around line 244-261: The effect using useEffect with uiMessages, args,
setMessages and fromUIMessages has a race where an earlier async result can
overwrite a later one; replace the isMounted guard with a generation counter
stored in a useRef (import useRef) or an abort token: increment the ref at the
start of each effect run, capture the current generation in the async closure,
and only call setMessages(nested.flat()) if the captured generation matches the
ref.current; ensure you still clean up by incrementing or invalidating the ref
in the return function so stale promises never apply their results.
🧹 Nitpick comments (5)
src/component/files.ts (1)

49-58: Consider storing resolved value in mimeType for full backwards compatibility.

Currently, mimeType is only stored if the caller explicitly passes it. If a caller uses only mediaType, existing code that reads file.mimeType will get undefined. Consider storing the resolved value in both fields:

♻️ Proposed fix
   const fileId = await ctx.db.insert("files", {
     storageId: args.storageId,
     hash: args.hash,
     filename: args.filename,
     mediaType,
-    mimeType: args.mimeType, // Keep for backwards compatibility
+    mimeType: mediaType, // Keep for backwards compatibility
     // We start out with it unused - when it's saved in a message we increment.
     refcount: 0,
     lastTouchedAt: Date.now(),
   });
src/client/files.ts (1)

86-89: Replace ctx.storage.getMetadata() with ctx.db.system.get("_storage", storageId).

ctx.storage.getMetadata is deprecated. Use ctx.db.system.get("_storage", newStorageId) to retrieve file metadata (which includes sha256). This returns the same metadata structure and works in both action and mutation contexts.

src/deltas.ts (1)

493-511: Consider defining a proper type for the approval object.

The type assertions here are understandable given SDK type constraints, but the approval property is typed loosely as object. Consider defining an interface for better type safety:

interface ToolApproval {
  id: string;
}

This would make the approval structure explicit and catch any mismatches at compile time.

src/react/useThreadMessages.ts (1)

261-261: Conditional expression in dependency array may trigger linter warnings.

The expression args === "skip" ? undefined : args.threadId in the dependency array, while functional, is an unusual pattern that may confuse readers and potentially trigger ESLint's react-hooks/exhaustive-deps rule.

Consider extracting to a variable for clarity:

const threadId = args === "skip" ? undefined : args.threadId;
// ... then use [uiMessages, threadId] in deps
src/UIMessages.ts (1)

520-523: Consider consolidating the error text fallback.

The errorText assignment has a subtle inconsistency: when hasError is true but message.error is falsy and output is a non-string value, String(output) could produce "[object Object]" for object outputs.

💡 Suggested improvement
-          const errorText =
-            message.error || (hasError ? String(output) : undefined);
+          const errorText =
+            message.error ||
+            (hasError
+              ? typeof output === 'string'
+                ? output
+                : JSON.stringify(output) ?? 'Unknown error'
+              : undefined);


```ts
import { Agent } from "@convex-dev/agent";
import { Agent, stepCountIs } from "@convex-dev/agent";
Copy link

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:

# First, let's look at the specific lines in the documentation file
head -n 210 docs/agent-usage.mdx | tail -n 170

Repository: get-convex/agent

Length of output: 6651


🏁 Script executed:

# Search for where stepCountIs is exported from in the codebase
rg -n "export.*stepCountIs" --type ts --type js

Repository: get-convex/agent

Length of output: 116


🏁 Script executed:

# Also search for stepCountIs imports and usage throughout the codebase
rg "stepCountIs" -A 2 -B 2 --type ts --type js --type mdx | head -100

Repository: get-convex/agent

Length of output: 87


Inconsistent import source for stepCountIs across code examples.

Line 46 imports stepCountIs from @convex-dev/agent, while the "Customizing the agent" section imports it from "ai". Although @convex-dev/agent re-exports the function from "ai", the documentation should consistently import from the same source across all examples to avoid confusing users about which import path to use.

🤖 Prompt for AI Agents
In `@docs/agent-usage.mdx` at line 46, The docs have inconsistent import sources
for stepCountIs (imported from "@convex-dev/agent" on one example and from "ai"
in another); update all examples to use the same import path (pick one and apply
consistently) — e.g., change the import that currently pulls stepCountIs from
"@convex-dev/agent" so it imports stepCountIs (and Agent if applicable) from
"ai", and ensure every other example in the file uses that same source for
stepCountIs to avoid confusion.

Comment on lines 11 to 25
if (process.env.ANTHROPIC_API_KEY) {
languageModel = anthropic.chat("claude-opus-4-20250514");
} else if (process.env.OPENAI_API_KEY) {
languageModel = openai.chat("gpt-4o-mini");
textEmbeddingModel = openai.textEmbeddingModel("text-embedding-3-small");
textEmbeddingModel = openai.embedding("text-embedding-3-small");
} else if (process.env.GROQ_API_KEY) {
languageModel = groq.languageModel(
"meta-llama/llama-4-scout-17b-16e-instruct",
);
} else if (process.env.OPENROUTER_API_KEY) {
languageModel = openrouter.chat("openai/gpt-4o-mini") as LanguageModelV2;
} else {
languageModel = mockModel({});
console.warn(
"Run `npx convex env set GROQ_API_KEY=<your-api-key>` or `npx convex env set OPENAI_API_KEY=<your-api-key>` or `npx convex env set OPENROUTER_API_KEY=<your-api-key>` from the example directory to set the API key.",
"Run `npx convex env set GROQ_API_KEY=<your-api-key>` or `npx convex env set OPENAI_API_KEY=<your-api-key>` from the example directory to set the API key.",
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

textEmbeddingModel may be undefined at runtime.

The textEmbeddingModel variable is only assigned when OPENAI_API_KEY is set, but it's exported unconditionally. If ANTHROPIC_API_KEY or GROQ_API_KEY is used instead, consumers importing textEmbeddingModel will get undefined.

Consider one of these approaches:

  1. Initialize a fallback/mock embedding model for non-OpenAI providers
  2. Export it conditionally or make it explicitly optional in the type
🔧 Suggested fix: Add explicit undefined handling
-let textEmbeddingModel: EmbeddingModel;
+let textEmbeddingModel: EmbeddingModel | undefined;

Or provide a fallback for other providers if embedding functionality is needed.

🤖 Prompt for AI Agents
In `@example/convex/modelsForDemo.ts` around lines 11 - 25, The exported variable
textEmbeddingModel can be undefined because it’s only set inside the
OPENAI_API_KEY branch; update the initialization so consumers always get a valid
value: either assign a fallback embedding model (e.g., use mockModel({}) or an
embedding adapter) when ANTHROPIC_API_KEY or GROQ_API_KEY is used, or explicitly
type/export textEmbeddingModel as optional and ensure callers handle undefined.
Locate textEmbeddingModel and languageModel in the file and initialize
textEmbeddingModel outside the conditional (or add a conditional export) so it’s
never unintentionally undefined at runtime.

Comment on lines +72 to +73
"@ai-sdk/provider-utils": "^4.0.6",
"ai": "^6.0.35",
Copy link

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
# Check latest versions of AI SDK packages
echo "Checking ai package:"
curl -s https://registry.npmjs.org/ai | jq '.["dist-tags"].latest'

echo ""
echo "Checking `@ai-sdk/provider-utils` package:"
curl -s https://registry.npmjs.org/@ai-sdk/provider-utils | jq '.["dist-tags"].latest'

Repository: get-convex/agent

Length of output: 139


Update AI SDK packages to current patch versions for latest improvements and bug fixes.

The specified versions (ai@^6.0.35 and @ai-sdk/provider-utils@^4.0.6) are stable but outdated. Latest available versions are ai@6.0.49 and @ai-sdk/provider-utils@4.0.9. Consider updating the version constraints to use current releases.

🤖 Prompt for AI Agents
In `@package.json` around lines 72 - 73, Update the dependency version constraints
for the two AI SDK packages: change "ai" from ^6.0.35 to ^6.0.49 and
"@ai-sdk/provider-utils" from ^4.0.6 to ^4.0.9 in package.json, then run your
package manager (npm/yarn/pnpm) to install the updated versions and run the
test/build pipeline to ensure compatibility; target the dependency entries "ai"
and "@ai-sdk/provider-utils" when making the edits.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/createTool.ts (1)

309-320: Hooks are bound with undefined ctx when tools are created without context.

When a tool is created without ctx at definition time (relying on wrapTools to inject it later), the hooks (onInputStart, onInputDelta, onInputAvailable, toModelOutput) are bound immediately with getCtx(t) which returns undefined:

t.onInputStart = def.onInputStart.bind(t, getCtx(t)); // undefined is captured here

Unlike execute and needsApproval which are inline methods that check getCtx(this) at call time (and throw if missing), the hooks are bound functions that permanently capture the undefined value. When wrapTools later spreads the tool object and sets ctx, it only updates the ctx property—the bound hooks still have undefined captured from creation time.

The hooks should either be re-bound in wrapTools with the injected context, or they should include the same defensive checks as execute and needsApproval to ensure ctx is available at call time.

🤖 Fix all issues with AI agents
In `@CLAUDE.md`:
- Line 9: Replace the bare URL "https://docs.convex.dev/agents" with a proper
Markdown link in the CLAUDE.md content (e.g., [Convex Agents
Documentation](https://docs.convex.dev/agents)) so the URL is not bare and
matches project markdown style; update the line that currently contains the bare
URL accordingly.
🧹 Nitpick comments (5)
CLAUDE.md (1)

90-168: Consider referencing MIGRATION.md instead of duplicating.

The upgrade section largely duplicates content from MIGRATION.md. Consider referencing it to reduce maintenance burden:

## Upgrading to v0.6.0 (AI SDK v6)

See [MIGRATION.md](./MIGRATION.md) for detailed upgrade steps.

This keeps the authoritative migration content in one place.

src/client/createTool.ts (2)

14-21: Duplicated deprecation utility.

This warnDeprecation function and MIGRATION_URL constant are duplicated in src/client/index.ts (lines 22-29). Consider extracting to a shared utility module to avoid duplication.

Suggested approach

Create a shared utility file:

// src/client/deprecation.ts
export const MIGRATION_URL = "https://github.com/get-convex/agent/blob/main/MIGRATION.md";
const warnedDeprecations = new Set<string>();

export function warnDeprecation(key: string, message: string) {
  if (!warnedDeprecations.has(key)) {
    warnedDeprecations.add(key);
    console.warn(`[`@convex-dev/agent`] ${message}\n  See: ${MIGRATION_URL}`);
  }
}

Then import in both files.


69-73: Add a brief comment explaining the type's purpose.

This conditional type is non-trivial. A short comment would aid future maintainers:

Suggested comment
+/**
+ * Makes T's properties optional when N is `any` or `never`,
+ * enabling tools without explicit output types to omit execute/outputSchema.
+ */
 type NeverOptional<N, T> = 0 extends 1 & N
   ? Partial<T>
   : [N] extends [never]
     ? Partial<Record<keyof T, undefined>>
     : T;
src/client/index.ts (2)

445-445: Type assertion as any bypasses type safety.

The stopWhen property is cast to any here and at line 573. While this may be necessary for complex generic interactions with AI SDK v6, it could mask type errors. Consider adding a comment explaining why the cast is needed, or using a more specific type assertion if possible.


1462-1466: Type assertion for customCtx invocation.

The as any cast on llmArgs is consistent with other type handling patterns in this file. Consider documenting why these casts are necessary (likely due to complex generic interactions with AI SDK v6 types).

Copy link
Contributor Author

sethconvex commented Jan 25, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@sethconvex
Copy link
Contributor Author

@zboyles I got tool approvals working, I'd love your thoughts. It was tricky given the translation required between AI SDK 6's api, and what anthropic wants to see in the messages with regards to tool ids.

@zboyles
Copy link
Contributor

zboyles commented Jan 27, 2026

@sethconvex Excellent, I appreciate you pulling that through! I saw someone was having an issue, vercel/ai#11966, with Google's parallel calls hitting a race condition with AI SDK generating the same tool call id for multiple calls. I also think the AI SDK added the tool call id into those approval response types if I read their change log correctly, or maybe the data was there and they added the type definition. In any case, I'll be able to install this and run tests later this afternoon and will let you know how it goes. Thanks again.

@thehorse2000
Copy link

Hey team, what's the update here?

This is critical to run "modern" models like gemini 2.5/3 flash.
Hope you can prioritize it!

Thanks.

@mainpingdream
Copy link

Hi everyone, is there an update on how long it will take to get this merged?

There are a few features we need from v6, and it would be super helpful for us!

Thanks so much.

@sethconvex
Copy link
Contributor Author

sethconvex commented Feb 8, 2026 via email

@redveil-ben
Copy link

Just a bit of external validation, we refactored to AI SDK v6 and have been running this branch in our prod for about a week and haven't run across any issues. (I can't speak to the tool approval bugs as not using tool approval)

@schilffarth
Copy link

Any ETA?

@eliotgevers
Copy link

I've already got the beta version of the agent component + tool approvals working in my app: tools marked needsApproval: true pause and emit tool-approval-request, the UI persists a tool-approval-response into the thread history (keyed off promptMessageId), then I resume the original stream in an authenticated action.

Would love to see updated docs on the “recommended” approval pattern so I can sanity-check that I'm doing it the intended way.

@emadabdulrahim
Copy link

Can't wait for this to land

Copy link
Member

@ianmacartney ianmacartney left a comment

Choose a reason for hiding this comment

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

overall lgtm - I suspect keeping mimeType will make our lives simpler for now - though the mapping is annoying I will admit

}),
// Standard AI SDK tool
myTool: tool({ description, parameters, execute: () => {}}),
myTool: tool({ description, inputSchema: parameters, execute: () => {}}),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
myTool: tool({ description, inputSchema: parameters, execute: () => {}}),
myTool: tool({ description, inputSchema: z.object(...), execute: () => {}}),

Comment on lines +158 to +173
/**
* An optional list of input examples that show the language
* model what the input should look like.
*/
inputExamples?: Array<{
input: NoInfer<INPUT>;
}>;
/**
* Whether the tool needs approval before it can be executed.
*/
needsApproval?:
| boolean
| ToolNeedsApprovalFunctionCtx<
[INPUT] extends [never] ? unknown : INPUT,
Ctx
>;
Copy link
Member

Choose a reason for hiding this comment

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

is there a reason we're copying over so many types here? Is this b/c of complex type inference on INPUT/OUTPUT/Ctx?
This seems pretty fragile, would be nice to lean on the AI SDK types whenever possible

Comment on lines +297 to +301
throw new Error(
"To use a Convex tool, you must either provide the ctx" +
" at definition time (dynamically in an action), or use the Agent to" +
" call it (which injects the ctx, userId and threadId)",
);
Copy link
Member

Choose a reason for hiding this comment

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

would be nice to unify these checks

Comment on lines +152 to +154
mediaType: v.optional(v.string()),
/** @deprecated Use `mediaType` instead. */
mimeType: v.optional(v.string()),
Copy link
Member

Choose a reason for hiding this comment

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

this is ok, but it might be simpler for the component world to just use mimeType and translate- since we're going to need to do dual-reads ~indefinitely with this approach anyways (or ask users to do a migration flow)

* @returns
*/
export function fromUIMessages<METADATA = unknown>(
export async function fromUIMessages<METADATA = unknown>(
Copy link
Member

Choose a reason for hiding this comment

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

I would love to make this synchronous if at all possible - simplifies frontend a lot.. but I guess AI SDK made the call to make this async..

Comment on lines +56 to +58
mediaType: v.optional(v.string()),
/** @deprecated Use `mediaType` instead. */
mimeType: v.optional(v.string()),
Copy link
Member

Choose a reason for hiding this comment

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

if we revert the schema to only use mimeType (which is what we did for v5 support already) we should revert this too

type: v.literal("image"),
image: v.union(v.string(), v.bytes()),
mediaType: v.optional(v.string()),
/** @deprecated Use `mediaType` instead. */
Copy link
Member

Choose a reason for hiding this comment

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

we can live with the dual-read world, but worth considering if having both of these greatly simplifies something to make it worth it - we generally need to do some wrangling with images to turn the buffers into the right kind, save to file storage, etc.

Comment on lines +199 to +203
v.object({
type: v.literal("file-url"),
url: v.string(),
providerOptions,
}),
Copy link
Member

Choose a reason for hiding this comment

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

bummer all of these can't be shared with existing validators. In general the validator / types for all agent-related functions is going to be pretty gnarly for any function that accepts a vMessage.. not that re-using validators helps us with that (yet - would be nice to leverage shared types to simplify in our code generation but that's just a proposal rn)

args: Infer<typeof addFileArgs>,
) {
// Support both mediaType (preferred) and mimeType (deprecated)
const mediaType = args.mediaType ?? args.mimeType;
Copy link
Member

Choose a reason for hiding this comment

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

I don't see the equivalent of this for images - which also is trying to rename

reason?: string;
type: "tool-approval-response";
}
>;
Copy link
Member

Choose a reason for hiding this comment

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

wowza this is a big file - hopfully we can flip the legacyComponentApi default to false so folks don't get this in their api.d.ts when they upgrade

Copilot AI and others added 22 commits February 19, 2026 22:33
- Updated component schema to use optional mediaType and mimeType
- Updated files.ts addFile mutation to accept both mediaType and mimeType
- Updated client files.ts to use mediaType when calling component
- Updated getFile to support both mediaType and mimeType from database
- Updated mapping.ts to return mediaType instead of mimeType
- Maintained backwards compatibility throughout

Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
- Updated all file part definitions in generated component.ts
- Changed mimeType from required to optional
- Added mediaType as optional field
- Resolved all TypeScript type errors related to mimeType/mediaType

Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
…t types

Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
…in generated types. Ran `convex codegen --component-dir ./src/component`.
Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
…esult output types

Co-authored-by: zboyles <2215540+zboyles@users.noreply.github.com>
- Implemented handling of `tool-approval-request` and `tool-approval-response` in `deltas.ts` to update tool state accordingly.
- Enhanced tests in `deltas.test.ts` and `toUIMessages.test.ts` to verify the approval workflow, including state transitions for approval requests and responses.
- Updated `UIMessages.ts` to manage tool approval states and responses, ensuring correct state updates for tool calls based on approval outcomes.
…tToolApprovals`. Without the fix, the tool call was being filtered out because it had no tool-result, even though it had an approval response - which caused the SDK to fail at line 91 in `ai/src/generate-text/collect-tool-approvals.ts`.
…result errors since stored messages have the complete data.
… tool approval, as stored messages contain complete context.
- Upgrade @convex-dev/rag from 0.6.1 to 0.7.0
  RAG 0.7.0 uses ai@^6.0.0 (compatible with v6), while 0.6.x used
  AI SDK v5 types (EmbeddingModelV2) causing type conflicts

- Fix unused type params in Output interface (src/client/types.ts)
  Prefix with _ to indicate intentionally unused for type signature
  compatibility

- Remove unused vContent import (src/mapping.ts)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add MIGRATION.md with step-by-step upgrade instructions
- Add CLAUDE.md for AI agent guidance
- Add runtime deprecation warnings that fire once per session:
  - textEmbeddingModel → embeddingModel
  - createTool args → inputSchema
  - createTool handler → execute
- Warnings link to migration guide on GitHub

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix race condition in useThreadMessages.ts using generation counter
- Replace {} as any with properly typed token detail objects
- Convert bare URL to markdown link in CLAUDE.md
- Add warning comment about textEmbeddingModel potentially being undefined
- Document why exhaustiveness check is disabled in deltas.ts (forwards compat)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
docs/agent-usage.mdx (1)

196-214: ⚠️ Potential issue | 🟡 Minor

Documentation references deprecated textEmbeddingModel instead of embeddingModel.

Line 199 mentions "you'll need a textEmbeddingModel model" and Line 214 uses textEmbeddingModel in the code example. According to the migration guide (Step 3), textEmbeddingModel is deprecated in favor of embeddingModel. This doc section should be updated to match.

Proposed fix
 The agent by default only needs a `chat` model to be configured. However, for
-vector search, you'll need a `textEmbeddingModel` model. A `name` is helpful to
+vector search, you'll need an `embeddingModel` model. A `name` is helpful to
 attribute each message to a specific agent. Other options are defaults that can
 be over-ridden at each LLM call-site.
   // Embedding model to power vector search of message history (RAG).
-  textEmbeddingModel: openai.embedding("text-embedding-3-small"),
+  embeddingModel: openai.embedding("text-embedding-3-small"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/agent-usage.mdx` around lines 196 - 214, The docs and example use the
deprecated textEmbeddingModel property; update the prose and the sharedDefaults
example to use embeddingModel instead (replace mentions of textEmbeddingModel
with embeddingModel and set sharedDefaults.embeddingModel =
openai.embedding("text-embedding-3-small")); ensure any explanatory text
referencing "textEmbeddingModel" is changed to "embeddingModel" so the Agent
config (e.g., languageModel and embeddingModel in sharedDefaults) matches the
current API.
src/toUIMessages.test.ts (1)

442-442: ⚠️ Potential issue | 🟡 Minor

Duplicate test description — rename the second test.

Both tests at Line 442 and Line 549 share the exact name "sets text field correctly when message has many parts with text part as final part". While vitest runs both, they appear identically in reports, making failures ambiguous. The second test exercises a multi-step flow with a dateTime tool call, so something like "sets text field correctly in multi-step tool-call + final-text flow" would distinguish them.

✏️ Suggested rename
-  it("sets text field correctly when message has many parts with text part as final part", () => {
+  it("sets text field correctly when multi-step tool call precedes final text part", () => {
     const messages = [
       baseMessageDoc({
         text: "what time is it in paris?",

Also applies to: 549-549

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

In `@src/toUIMessages.test.ts` at line 442, Rename the duplicate test description
string used in the second it(...) block that currently reads "sets text field
correctly when message has many parts with text part as final part" to a unique,
descriptive name (e.g. "sets text field correctly in multi-step tool-call +
final-text flow") so test reports are unambiguous; locate the second it(...)
declaration in the toUIMessages.test file (the test exercising dateTime tool
call) and replace its first argument (the description string) with the new
unique string.
src/UIMessages.ts (1)

58-68: ⚠️ Potential issue | 🟠 Major

fromUIMessages is now async — a breaking API change for external consumers.

The function signature at lines 58-68 confirms this is now export async function returning Promise<...>. Since it's exported from the public API (src/client/index.ts line 181), external downstream consumers who depend on this library will need to add await to their call sites. All internal callers in the codebase have already been updated to properly await the result.

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

In `@src/UIMessages.ts` around lines 58 - 68, The exported function fromUIMessages
was changed to an async function, introducing a breaking API change; revert it
to a synchronous export to maintain compatibility by removing the async modifier
and returning the array directly (so callers that await still work). Locate the
fromUIMessages function and its return expression, remove the async keyword and
ensure it returns (MessageDoc & { streaming: boolean; metadata?: METADATA })[]
instead of Promise<...>; keep the function name and signature generic parameters
the same so the public export used in client/index.ts is unchanged. If
asynchronous work is truly required internally, implement that as an internal
helper (e.g., fromUIMessagesAsync) and call it from fromUIMessages in a
synchronous manner or document a new async helper instead of changing the public
API.
src/client/search.ts (1)

449-473: ⚠️ Potential issue | 🟠 Major

Semantic mismatch: embedding usage padded with undefined fields to satisfy LanguageModelUsage.

The usageHandler callback signature expects LanguageModelUsage (which requires inputTokenDetails / outputTokenDetails), but EmbeddingModelUsage only exposes tokens. The objects with undefined fields satisfy TypeScript structurally, but any usage handler that destructures or accumulates these detail fields will silently get NaN/undefined without knowing why.

A cleaner solution is to adjust UsageHandler's usage parameter to a union or a shared base type that accommodates embedding-only payloads, or at minimum document the undefined semantics explicitly.

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

In `@src/client/search.ts` around lines 449 - 473, The current call to
usageHandler in search.ts passes an object shaped as LanguageModelUsage but
fills inputTokenDetails/outputTokenDetails with undefined to represent
embedding-only usage; instead, update the UsageHandler type (or the type used in
the handler signature) to accept a union or shared base between
LanguageModelUsage and EmbeddingModelUsage (e.g., UsageBase | LanguageModelUsage
| EmbeddingModelUsage) so embedding-only payloads can be passed without padded
undefined fields, then change the call site that constructs the usage to pass an
EmbeddingModelUsage instance (only tokens) or the appropriate union variant;
also add a short comment/doc on EmbeddingModelUsage semantics so consumers know
those detail fields won’t be present for embeddings.
src/client/createTool.ts (1)

309-320: ⚠️ Potential issue | 🔴 Critical

toModelOutput (and pre-existing lifecycle hooks) bind ctx at creation time, so injected runtime ctx is never seen.

bind(t, getCtx(t)) captures t.ctx (which equals def.ctx, typically undefined at definition time) as a fixed first argument. When wrapTools later creates { ...tool, ctx } and the AI SDK calls out.onInputStart(options) / out.toModelOutput(options), the bound function ignores the new out.ctx and passes the stale/undefined ctx to the user's handler.

By contrast, execute and needsApproval correctly read getCtx(this) inside a regular function, so they see the runtime-injected ctx when called as methods on the spread object. The new toModelOutput binding introduced by this PR has the same flaw, and the pattern should be fixed consistently across all four hooks.

 if (def.onInputStart) {
-  t.onInputStart = def.onInputStart.bind(t, getCtx(t));
+  t.onInputStart = function(this: Tool<INPUT, OUTPUT>, options: ToolExecutionOptions) {
+    return def.onInputStart!(getCtx(this), options);
+  };
 }
 if (def.onInputDelta) {
-  t.onInputDelta = def.onInputDelta.bind(t, getCtx(t));
+  t.onInputDelta = function(this: Tool<INPUT, OUTPUT>, options: { inputTextDelta: string } & ToolExecutionOptions) {
+    return def.onInputDelta!(getCtx(this), options);
+  };
 }
 if (def.onInputAvailable) {
-  t.onInputAvailable = def.onInputAvailable.bind(t, getCtx(t));
+  t.onInputAvailable = function(this: Tool<INPUT, OUTPUT>, options: { input: any } & ToolExecutionOptions) {
+    return def.onInputAvailable!(getCtx(this), options);
+  };
 }
 if (def.toModelOutput) {
-  t.toModelOutput = def.toModelOutput.bind(t, getCtx(t));
+  t.toModelOutput = function(this: Tool<INPUT, OUTPUT>, options: { toolCallId: string; input: any; output: any }) {
+    return def.toModelOutput!(getCtx(this), options);
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/createTool.ts` around lines 309 - 320, The lifecycle hooks
(toModelOutput, onInputStart, onInputDelta, onInputAvailable) are currently
bound with getCtx(t) at creation time, capturing a stale ctx; change each
binding to a runtime wrapper function that computes ctx via getCtx(this) when
invoked (e.g., replace t.toModelOutput = def.toModelOutput.bind(t, getCtx(t))
with a regular function that calls def.toModelOutput.call(this, getCtx(this),
...args)), mirroring how execute and needsApproval obtain ctx, so when wrapTools
spreads and injects ctx the handler sees the injected runtime ctx; update the
same pattern for onInputStart, onInputDelta and onInputAvailable and keep use of
.call/.apply to preserve original this (t) and argument forwarding.
🧹 Nitpick comments (10)
src/deltas.ts (1)

69-103: Error suppression via string matching is fragile.

The "no tool invocation found" substring check (Line 77) couples this logic to the exact error message wording in the AI SDK. If the message changes in a future SDK update, suppression silently stops working and errors will propagate instead — which is a safe failure mode, but may cause unexpected regressions in the tool-approval continuation flow.

Consider matching on error type/code if available, or adding a comment noting the specific AI SDK version/source where this message originates so it can be tracked during upgrades.

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

In `@src/deltas.ts` around lines 69 - 103, The current suppression in the onError
callback of readUIMessageStream relies on matching the literal substring "no
tool invocation found" which is fragile; update onError to detect the condition
by checking a stable identifier (e.g., an error.code property or specific Error
subclass) rather than string matching, using properties of the thrown error
(e.g., e.code, e.name, or instanceof SomeSdkError) and fall back to the string
check only if those fields are absent; reference the symbols
readUIMessageStream, onError, suppressError, and the "no tool invocation found"
check when making the change, and if the SDK doesn't expose a stable code/type
add a clear comment documenting the SDK and version/source of that message so
future upgrades can revisit this logic.
src/client/types.ts (1)

24-31: Output interface declared between two import blocks.

The Output interface (Lines 25–31) is placed between the "ai" import block (Lines 1–23) and the "convex/server" import block (Lines 32–38). While TypeScript accepts this, it deviates from the idiomatic pattern of grouping all imports at the top. Move the interface below the last import statement.

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

In `@src/client/types.ts` around lines 24 - 31, Move the exported Output interface
declaration so it appears after the last import block (i.e., below the
"convex/server" import) instead of between import groups; locate the `export
interface Output<_T = any, _P = any, _E = any>` declaration and cut/paste it
below the final import, keeping its name and export unchanged and verifying any
referenced types still resolve.
src/client/mockModel.ts (1)

205-208: _supportedUrls is initialized twice; the first assignment is dead code.

Lines 205–208 assign this._supportedUrls, then this.doStream = ... is assigned (lines 209–239), then this._supportedUrls is assigned again with the identical expression at lines 240–243. The first assignment is always overwritten and can be removed.

♻️ Proposed fix
-    this._supportedUrls =
-      typeof supportedUrls === "function"
-        ? supportedUrls
-        : async () => supportedUrls;
-    this.doStream = async (options) => {
+    this.doStream = async (options) => {

Also applies to: 240-243

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

In `@src/client/mockModel.ts` around lines 205 - 208, Remove the redundant early
initialization of this._supportedUrls (the assignment before this.doStream)
which is immediately overwritten later; keep the later identical assignment
(after this.doStream) so _supportedUrls is only set once, ensuring the property
remains a function or async wrapper as intended and leaving this.doStream and
other constructor logic untouched.
src/validators.ts (1)

381-389: MessageContentParts is missing the new approval types.

MessageContentParts (Lines 381–389) is the public union of all possible content part types, but it doesn't include Infer<typeof vToolApprovalRequest> or Infer<typeof vToolApprovalResponse>. Consumers who exhaustively switch on this type will silently miss approval parts.

♻️ Proposed fix
 export type MessageContentParts =
   | Infer<typeof vTextPart>
   | Infer<typeof vImagePart>
   | Infer<typeof vFilePart>
   | Infer<typeof vReasoningPart>
   | Infer<typeof vRedactedReasoningPart>
   | Infer<typeof vToolCallPart>
   | Infer<typeof vToolResultPart>
-  | Infer<typeof vSourcePart>;
+  | Infer<typeof vSourcePart>
+  | Infer<typeof vToolApprovalRequest>
+  | Infer<typeof vToolApprovalResponse>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validators.ts` around lines 381 - 389, The exported union type
MessageContentParts is missing the approval part types; update the union to
include Infer<typeof vToolApprovalRequest> and Infer<typeof
vToolApprovalResponse> so consumers who pattern-match on MessageContentParts
will see approval parts. Locate the MessageContentParts type definition and add
Infer<typeof vToolApprovalRequest> and Infer<typeof vToolApprovalResponse> to
the union alongside vToolCallPart and vToolResultPart.
src/mapping.ts (1)

182-209: compatibility warning round-trip is correct — but as any casts hide the type mismatch.

serializeWarnings converts CallWarning type: "compatibility" → stored type: "unsupported-setting", and toModelMessageWarnings reverses that mapping. This is correct: AI SDK v6 internally emits type: "compatibility" (with feature field) when wrapping V2 providers via asLanguageModelV3. The stored vLanguageModelCallWarning only has unsupported-setting, so the round-trip mapping is the right approach.

The as any at lines 190 and 208 is necessary because vLanguageModelCallWarning doesn't include the compatibility variant. Consider adding a comment explaining this intentional type coercion so future maintainers understand why it's needed.

📝 Suggested comment
-  }) as any;
+  // `as any`: the stored `vLanguageModelCallWarning` validator doesn't include
+  // the `compatibility` type that AI SDK v6 emits for V2 provider wrapping.
+  }) as MessageWithMetadata["warnings"];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mapping.ts` around lines 182 - 209, Add a short explanatory comment above
the intentional type coercions in serializeWarnings and toModelMessageWarnings
that documents the round-trip mapping between CallWarning type "compatibility"
(with field feature) and the stored vLanguageModelCallWarning
"unsupported-setting", and why the `as any` cast is used
(vLanguageModelCallWarning's type union omits the "compatibility" variant when
wrapping V2 providers via asLanguageModelV3); reference the functions
serializeWarnings and toModelMessageWarnings and the involved variants
("compatibility" ↔ "unsupported-setting") so future maintainers understand this
deliberate coercion.
src/UIMessages.ts (1)

480-512: Double as unknown as casts bypass type safety in tool-result handling.

The contentPart as unknown as ToolResultPart & { output: {...} } pattern at Line 480 (and similar patterns at Lines 568 and 592) bypasses TypeScript's structural checks entirely. If AI SDK v6 changes the shape of ToolResultPart.output, these casts will silently produce runtime errors.

Consider narrowing with a runtime type-guard instead:

♻️ Example type-guard approach
- const typedPart = contentPart as unknown as ToolResultPart & {
-   output: { type: string; value?: unknown; reason?: string };
- };
+ function hasOutputType(p: unknown): p is { output: { type: string; value?: unknown; reason?: string } } {
+   return typeof p === "object" && p !== null && "output" in p &&
+     typeof (p as any).output === "object" && "type" in (p as any).output;
+ }
+ if (!hasOutputType(contentPart)) break;
+ const typedPart = contentPart as ToolResultPart & { output: { type: string; value?: unknown; reason?: string } };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/UIMessages.ts` around lines 480 - 512, The double "as unknown as" cast
for typedPart bypasses TypeScript checks and should be replaced with a runtime
type guard that verifies contentPart has the expected ToolResultPart shape
(e.g., has output with a string type and optional value/reason) before treating
it as such; implement a function like isToolResultPartWithOutput(part): part is
ToolResultPart & { output: { type: string; value?: unknown; reason?: string } }
and use it in the conditional that checks typedPart.output?.type ===
"execution-denied", updating the logic that finds the matching call in allParts
and mutates call.state/approval (symbols: typedPart, ToolResultPart, output,
allParts, ToolUIPart, contentPart.toolName, contentPart.toolCallId) and replace
the other similar double-cast sites in this file with the same guard.
src/client/createTool.ts (1)

14-21: MIGRATION_URL/warnDeprecation duplicated from src/client/index.ts.

Both modules define identical utilities in isolation, so the same deprecation key (e.g. "textEmbeddingModel") can emit a warning from both modules independently. Extract to a shared src/client/deprecation.ts and import from both sites.

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

In `@src/client/createTool.ts` around lines 14 - 21, MIGRATION_URL and
warnDeprecation are duplicated in createTool.ts and index.ts leading to
duplicate warnings; remove the duplicate definitions and extract them into a
single shared module (e.g., create a new src/client/deprecation.ts exporting
MIGRATION_URL and warnDeprecation), then import warnDeprecation (and
MIGRATION_URL if needed) from that module in both createTool.ts and index.ts so
the warnedDeprecations Set is shared and a given deprecation key (like
"textEmbeddingModel") only logs once.
src/client/search.ts (1)

360-362: Confusing shadowing: rename textEmbeddingModel to effectiveEmbeddingModel.

The local variable is assigned options.embeddingModel ?? options.textEmbeddingModel but is named textEmbeddingModel, shadowing the concept it's resolving. Every other function in this file (embedMany, generateAndSaveEmbeddings, fetchContextWithPrompt) uses the name effectiveEmbeddingModel for the same pattern — this one diverges.

♻️ Proposed fix
-  const textEmbeddingModel =
-    options.embeddingModel ?? options.textEmbeddingModel;
-  if (!textEmbeddingModel) {
+  const effectiveEmbeddingModel =
+    options.embeddingModel ?? options.textEmbeddingModel;
+  if (!effectiveEmbeddingModel) {
     return undefined;
   }
   ...
-    const model = getModelName(textEmbeddingModel);
+    const model = getModelName(effectiveEmbeddingModel);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/search.ts` around lines 360 - 362, Rename the local variable
textEmbeddingModel to effectiveEmbeddingModel to match the naming used elsewhere
and avoid concept shadowing; change the declaration that assigns
options.embeddingModel ?? options.textEmbeddingModel to use
effectiveEmbeddingModel and update all subsequent references in this function
(including the null-check if (!textEmbeddingModel) and any uses later in this
scope) so they reference effectiveEmbeddingModel instead.
src/client/index.ts (2)

22-30: Import appears after module-level code — move imports to the top.

MIGRATION_URL, warnedDeprecations, and warnDeprecation were inserted at lines 22–29, pushing the convex-helpers import to line 30. While ES import declarations are hoisted and this works at runtime, mixing code and imports breaks conventional file layout and will confuse tools and readers.

♻️ Proposed fix
 import { assert, omit, pick } from "convex-helpers";
 // ... (all other imports) ...
 
+const MIGRATION_URL = "https://github.com/get-convex/agent/blob/main/MIGRATION.md";
+const warnedDeprecations = new Set<string>();
+function warnDeprecation(key: string, message: string) {
+  if (!warnedDeprecations.has(key)) {
+    warnedDeprecations.add(key);
+    console.warn(`[`@convex-dev/agent`] ${message}\n  See: ${MIGRATION_URL}`);
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/index.ts` around lines 22 - 30, Move the import block to the top
of the module so no module-level code (MIGRATION_URL, warnedDeprecations,
warnDeprecation) appears before imports; specifically, relocate the line "import
{ assert, omit, pick } from 'convex-helpers'" above the declarations of
MIGRATION_URL, warnedDeprecations, and the warnDeprecation function to restore
conventional file layout and avoid mixing executable code with import
statements.

22-29: MIGRATION_URL and warnDeprecation are duplicated between index.ts and createTool.ts.

Both files define identical MIGRATION_URL, warnedDeprecations, and warnDeprecation. Because these are separate module-level Sets, the same deprecation key in both modules will fire two separate console warnings. Extract to a shared internal utility (e.g. src/client/deprecation.ts) and import from both.

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

In `@src/client/index.ts` around lines 22 - 29, Duplicate module-level deprecation
logic (MIGRATION_URL, warnedDeprecations, warnDeprecation) causes duplicate
console warnings; extract these three symbols into a single shared internal
module (e.g., deprecation.ts) and have both modules import warnDeprecation and
MIGRATION_URL from it, replacing their local definitions; ensure the shared
warnedDeprecations Set is used by the exported warnDeprecation function so a
deprecation key only logs once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/agent-usage.mdx`:
- Line 252: The example assigns an undefined symbol `parameters` to
`inputSchema`, which is misleading; replace the undefined reference with an
inline schema object to match other examples by updating the `myTool` definition
(the `tool` call) to provide a concrete `inputSchema` (e.g., an object
describing fields and types) instead of `parameters`, so readers can see the
expected schema shape directly in the `myTool: tool({ description, inputSchema:
... , execute })` example.

In `@src/client/createTool.ts`:
- Around line 241-263: The function currently throws when inputSchema is missing
before emitting deprecation warnings, so move the warnDeprecation checks to
occur before the inputSchema existence check: check def.args and def.handler and
call warnDeprecation for "createTool.args" and "createTool.handler" (referencing
def.args, def.inputSchema, def.handler) prior to throwing for missing
inputSchema, then compute inputSchema and executeHandler (inputSchema =
def.inputSchema ?? def.args; executeHandler = def.execute ?? def.handler) and
finally throw the existing error if inputSchema is falsy; this ensures
deprecated properties are warned about even when inputSchema is absent.

In `@src/client/search.test.ts`:
- Around line 295-338: The function filterOutOrphanedToolMessages is removing
tool-call entries that lack approval responses but leaves their matching
tool-approval-request items orphaned; update the logic so that after you
identify removed tool-call IDs (use the existing approvalRequestsByToolCallId
mapping or compute a removedToolCallIds set), change the content filtering step
that currently keeps non-tool-call items unconditionally to also skip any
tool-approval-request whose toolCallId is in removedToolCallIds; ensure
comparisons use the toolCallId field and apply this when building each
MessageDoc.message.content so both the tool-call and its associated
tool-approval-request are removed together.

In `@src/client/search.ts`:
- Around line 261-286: The assistant messages can end up containing orphaned
"tool-approval-request" parts when their sibling "tool-call" was filtered out;
update the second-pass filtering logic to also drop "tool-approval-request"
parts whose toolCallId has no surviving tool-call and no approval response. Use
the existing approvalRequestsByToolCallId map and
hasApprovalResponse(toolCallId) to detect orphans: when iterating assistant
parts, if part.type === "tool-approval-request" look up its toolCallId in
approvalRequestsByToolCallId and remove the part if the corresponding tool-call
was filtered (i.e., there is no surviving tool-call entry for that toolCallId
and hasApprovalResponse(toolCallId) is false). Ensure this uses the same
identifiers (approvalRequestsByToolCallId, hasApprovalResponse) so the
approval-request is kept only when its tool-call or an approval response exists.

In `@src/deltas.ts`:
- Around line 493-510: The case handling "tool-approval-request" currently uses
a double-cast on part (part as unknown as {...}) which masks the real SDK shape;
replace the unsafe assertion by reading the SDK shape directly or adding a
runtime type guard: check whether part.toolCall?.toolCallId exists and if so use
that value (otherwise fall back to a normalized part.toolCallId), then look up
toolPartsById with the resolved id and set toolPart.state and toolPart.approval
as before; remove the double assertion and/or add a small type-guard function
(e.g., isToolApprovalRequestPart) so TypeScript can’t be bypassed and runtime
shape mismatches are handled explicitly in the "tool-approval-request" case.

In `@src/UIMessages.ts`:
- Around line 97-101: convertToModelMessages() expands a single UI message into
multiple MessageDoc records and appends a suffix to the stored _id (constructed
from uiMessage.id + `-${i}`) to avoid collisions for children; add a concise
note to the CHANGELOG and migration guide describing this ID transformation,
show the recommended consumer update pattern (e.g., query by exact _id or
implement a fallback lookup by uiMessage.id as in
playground/src/components/MessageList.tsx), and mention that consumers who
previously queried by uiMessage.id must adapt their lookups to account for
suffixed document IDs.

---

Outside diff comments:
In `@docs/agent-usage.mdx`:
- Around line 196-214: The docs and example use the deprecated
textEmbeddingModel property; update the prose and the sharedDefaults example to
use embeddingModel instead (replace mentions of textEmbeddingModel with
embeddingModel and set sharedDefaults.embeddingModel =
openai.embedding("text-embedding-3-small")); ensure any explanatory text
referencing "textEmbeddingModel" is changed to "embeddingModel" so the Agent
config (e.g., languageModel and embeddingModel in sharedDefaults) matches the
current API.

In `@src/client/createTool.ts`:
- Around line 309-320: The lifecycle hooks (toModelOutput, onInputStart,
onInputDelta, onInputAvailable) are currently bound with getCtx(t) at creation
time, capturing a stale ctx; change each binding to a runtime wrapper function
that computes ctx via getCtx(this) when invoked (e.g., replace t.toModelOutput =
def.toModelOutput.bind(t, getCtx(t)) with a regular function that calls
def.toModelOutput.call(this, getCtx(this), ...args)), mirroring how execute and
needsApproval obtain ctx, so when wrapTools spreads and injects ctx the handler
sees the injected runtime ctx; update the same pattern for onInputStart,
onInputDelta and onInputAvailable and keep use of .call/.apply to preserve
original this (t) and argument forwarding.

In `@src/client/search.ts`:
- Around line 449-473: The current call to usageHandler in search.ts passes an
object shaped as LanguageModelUsage but fills
inputTokenDetails/outputTokenDetails with undefined to represent embedding-only
usage; instead, update the UsageHandler type (or the type used in the handler
signature) to accept a union or shared base between LanguageModelUsage and
EmbeddingModelUsage (e.g., UsageBase | LanguageModelUsage | EmbeddingModelUsage)
so embedding-only payloads can be passed without padded undefined fields, then
change the call site that constructs the usage to pass an EmbeddingModelUsage
instance (only tokens) or the appropriate union variant; also add a short
comment/doc on EmbeddingModelUsage semantics so consumers know those detail
fields won’t be present for embeddings.

In `@src/toUIMessages.test.ts`:
- Line 442: Rename the duplicate test description string used in the second
it(...) block that currently reads "sets text field correctly when message has
many parts with text part as final part" to a unique, descriptive name (e.g.
"sets text field correctly in multi-step tool-call + final-text flow") so test
reports are unambiguous; locate the second it(...) declaration in the
toUIMessages.test file (the test exercising dateTime tool call) and replace its
first argument (the description string) with the new unique string.

In `@src/UIMessages.ts`:
- Around line 58-68: The exported function fromUIMessages was changed to an
async function, introducing a breaking API change; revert it to a synchronous
export to maintain compatibility by removing the async modifier and returning
the array directly (so callers that await still work). Locate the fromUIMessages
function and its return expression, remove the async keyword and ensure it
returns (MessageDoc & { streaming: boolean; metadata?: METADATA })[] instead of
Promise<...>; keep the function name and signature generic parameters the same
so the public export used in client/index.ts is unchanged. If asynchronous work
is truly required internally, implement that as an internal helper (e.g.,
fromUIMessagesAsync) and call it from fromUIMessages in a synchronous manner or
document a new async helper instead of changing the public API.

---

Duplicate comments:
In `@docs/agent-usage.mdx`:
- Line 46: The docs have inconsistent imports for stepCountIs (one example
imports it from "@convex-dev/agent" while another imports from "ai");
standardize the examples by importing stepCountIs from the same package as Agent
(use "@convex-dev/agent") and update any imports elsewhere in the document so
every code sample imports Agent and stepCountIs from the same module.

In `@example/convex/modelsForDemo.ts`:
- Around line 8-11: The declared variable textEmbeddingModel is typed as
EmbeddingModel but is only assigned when OPENAI_API_KEY is present, so change
its type to EmbeddingModel | undefined to reflect possible absence; update the
declaration of textEmbeddingModel to use the union type and audit any
uses/exports of textEmbeddingModel (and any callers of LanguageModelV3 if
related) to handle the undefined case or add runtime guards where the variable
is consumed.

In `@src/mapping.ts`:
- Around line 161-172: The previous review flagged using "{} as any" for
LanguageModelUsage token details; update the object shape to explicitly provide
fields with undefined values instead of casting, so set inputTokenDetails
(cacheReadTokens, cacheWriteTokens, noCacheTokens) and outputTokenDetails
(textTokens, reasoningTokens) to objects with those keys assigned undefined to
satisfy the LanguageModelUsage type; ensure these explicit objects replace any
"{} as any" occurrences (look for inputTokenDetails, outputTokenDetails and the
LanguageModelUsage construction) so the types are correct and no casting is
used.

---

Nitpick comments:
In `@src/client/createTool.ts`:
- Around line 14-21: MIGRATION_URL and warnDeprecation are duplicated in
createTool.ts and index.ts leading to duplicate warnings; remove the duplicate
definitions and extract them into a single shared module (e.g., create a new
src/client/deprecation.ts exporting MIGRATION_URL and warnDeprecation), then
import warnDeprecation (and MIGRATION_URL if needed) from that module in both
createTool.ts and index.ts so the warnedDeprecations Set is shared and a given
deprecation key (like "textEmbeddingModel") only logs once.

In `@src/client/index.ts`:
- Around line 22-30: Move the import block to the top of the module so no
module-level code (MIGRATION_URL, warnedDeprecations, warnDeprecation) appears
before imports; specifically, relocate the line "import { assert, omit, pick }
from 'convex-helpers'" above the declarations of MIGRATION_URL,
warnedDeprecations, and the warnDeprecation function to restore conventional
file layout and avoid mixing executable code with import statements.
- Around line 22-29: Duplicate module-level deprecation logic (MIGRATION_URL,
warnedDeprecations, warnDeprecation) causes duplicate console warnings; extract
these three symbols into a single shared internal module (e.g., deprecation.ts)
and have both modules import warnDeprecation and MIGRATION_URL from it,
replacing their local definitions; ensure the shared warnedDeprecations Set is
used by the exported warnDeprecation function so a deprecation key only logs
once.

In `@src/client/mockModel.ts`:
- Around line 205-208: Remove the redundant early initialization of
this._supportedUrls (the assignment before this.doStream) which is immediately
overwritten later; keep the later identical assignment (after this.doStream) so
_supportedUrls is only set once, ensuring the property remains a function or
async wrapper as intended and leaving this.doStream and other constructor logic
untouched.

In `@src/client/search.ts`:
- Around line 360-362: Rename the local variable textEmbeddingModel to
effectiveEmbeddingModel to match the naming used elsewhere and avoid concept
shadowing; change the declaration that assigns options.embeddingModel ??
options.textEmbeddingModel to use effectiveEmbeddingModel and update all
subsequent references in this function (including the null-check if
(!textEmbeddingModel) and any uses later in this scope) so they reference
effectiveEmbeddingModel instead.

In `@src/client/types.ts`:
- Around line 24-31: Move the exported Output interface declaration so it
appears after the last import block (i.e., below the "convex/server" import)
instead of between import groups; locate the `export interface Output<_T = any,
_P = any, _E = any>` declaration and cut/paste it below the final import,
keeping its name and export unchanged and verifying any referenced types still
resolve.

In `@src/deltas.ts`:
- Around line 69-103: The current suppression in the onError callback of
readUIMessageStream relies on matching the literal substring "no tool invocation
found" which is fragile; update onError to detect the condition by checking a
stable identifier (e.g., an error.code property or specific Error subclass)
rather than string matching, using properties of the thrown error (e.g., e.code,
e.name, or instanceof SomeSdkError) and fall back to the string check only if
those fields are absent; reference the symbols readUIMessageStream, onError,
suppressError, and the "no tool invocation found" check when making the change,
and if the SDK doesn't expose a stable code/type add a clear comment documenting
the SDK and version/source of that message so future upgrades can revisit this
logic.

In `@src/mapping.ts`:
- Around line 182-209: Add a short explanatory comment above the intentional
type coercions in serializeWarnings and toModelMessageWarnings that documents
the round-trip mapping between CallWarning type "compatibility" (with field
feature) and the stored vLanguageModelCallWarning "unsupported-setting", and why
the `as any` cast is used (vLanguageModelCallWarning's type union omits the
"compatibility" variant when wrapping V2 providers via asLanguageModelV3);
reference the functions serializeWarnings and toModelMessageWarnings and the
involved variants ("compatibility" ↔ "unsupported-setting") so future
maintainers understand this deliberate coercion.

In `@src/UIMessages.ts`:
- Around line 480-512: The double "as unknown as" cast for typedPart bypasses
TypeScript checks and should be replaced with a runtime type guard that verifies
contentPart has the expected ToolResultPart shape (e.g., has output with a
string type and optional value/reason) before treating it as such; implement a
function like isToolResultPartWithOutput(part): part is ToolResultPart & {
output: { type: string; value?: unknown; reason?: string } } and use it in the
conditional that checks typedPart.output?.type === "execution-denied", updating
the logic that finds the matching call in allParts and mutates
call.state/approval (symbols: typedPart, ToolResultPart, output, allParts,
ToolUIPart, contentPart.toolName, contentPart.toolCallId) and replace the other
similar double-cast sites in this file with the same guard.

In `@src/validators.ts`:
- Around line 381-389: The exported union type MessageContentParts is missing
the approval part types; update the union to include Infer<typeof
vToolApprovalRequest> and Infer<typeof vToolApprovalResponse> so consumers who
pattern-match on MessageContentParts will see approval parts. Locate the
MessageContentParts type definition and add Infer<typeof vToolApprovalRequest>
and Infer<typeof vToolApprovalResponse> to the union alongside vToolCallPart and
vToolResultPart.

Comment on lines +241 to +263
const inputSchema = def.inputSchema ?? def.args;
if (!inputSchema)
throw new Error("To use a Convex tool, you must provide an `inputSchema` (or `args`)");

if (def.args && !def.inputSchema) {
warnDeprecation(
"createTool.args",
"createTool: 'args' is deprecated. Use 'inputSchema' instead.",
);
}
if (def.handler && !def.execute) {
warnDeprecation(
"createTool.handler",
"createTool: 'handler' is deprecated. Use 'execute' instead.",
);
}

// Extra AI SDK pass-through options.
providerOptions?: ProviderOptions;
}): Tool<INPUT, OUTPUT> {
const t = tool({
const executeHandler = def.execute ?? def.handler;
if (!executeHandler && !def.outputSchema)
throw new Error(
"To use a Convex tool, you must either provide an execute" +
" handler function, define an outputSchema, or both",
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error thrown before deprecation warning fires when both inputSchema is missing and args/handler is present.

When !inputSchema is true (line 242), the function throws immediately — the deprecation warnings for args (line 245) and handler (line 251) are never emitted. A user who passes only the deprecated args property but makes a typo in the key will get a confusing error with no migration hint. Reversing the order (warn then throw) provides a better developer experience.

♻️ Proposed fix
   const inputSchema = def.inputSchema ?? def.args;
-  if (!inputSchema)
-    throw new Error("To use a Convex tool, you must provide an `inputSchema` (or `args`)");
-
   if (def.args && !def.inputSchema) {
     warnDeprecation("createTool.args", "createTool: 'args' is deprecated. Use 'inputSchema' instead.");
   }
   if (def.handler && !def.execute) {
     warnDeprecation("createTool.handler", "createTool: 'handler' is deprecated. Use 'execute' instead.");
   }
+  if (!inputSchema)
+    throw new Error("To use a Convex tool, you must provide an `inputSchema` (or `args`)");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/createTool.ts` around lines 241 - 263, The function currently
throws when inputSchema is missing before emitting deprecation warnings, so move
the warnDeprecation checks to occur before the inputSchema existence check:
check def.args and def.handler and call warnDeprecation for "createTool.args"
and "createTool.handler" (referencing def.args, def.inputSchema, def.handler)
prior to throwing for missing inputSchema, then compute inputSchema and
executeHandler (inputSchema = def.inputSchema ?? def.args; executeHandler =
def.execute ?? def.handler) and finally throw the existing error if inputSchema
is falsy; this ensures deprecated properties are warned about even when
inputSchema is absent.

Comment on lines +295 to +338
it("should filter out tool calls with approval request but NO approval response", () => {
const messages: MessageDoc[] = [
{
_id: "1",
message: {
role: "assistant",
content: [
{ type: "text", text: "I'll run the dangerous tool" },
{
type: "tool-call",
toolCallId: "call_123",
toolName: "dangerousTool",
input: { action: "delete" },
args: { action: "delete" },
},
{
type: "tool-approval-request",
toolCallId: "call_123",
approvalId: "approval_456",
},
],
},
order: 1,
} as MessageDoc,
// No approval response provided
];

const result = filterOutOrphanedToolMessages(messages);
expect(result).toHaveLength(1);
// The assistant message should have the tool-call filtered out
const assistantContent = result[0].message?.content;
expect(Array.isArray(assistantContent)).toBe(true);
if (Array.isArray(assistantContent)) {
// Text and approval-request should remain, but tool-call should be filtered
expect(assistantContent).toHaveLength(2);
expect(assistantContent.find((p) => p.type === "text")).toBeDefined();
expect(
assistantContent.find((p) => p.type === "tool-approval-request"),
).toBeDefined();
expect(
assistantContent.find((p) => p.type === "tool-call"),
).toBeUndefined();
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

ast-grep --pattern $'function filterOutOrphanedToolMessages($_) {
  $$$
}'

Repository: get-convex/agent

Length of output: 5001


🏁 Script executed:

# Search for other tests of filterOutOrphanedToolMessages
rg "filterOutOrphanedToolMessages" src/client/search.test.ts -A 5 -B 2

Repository: get-convex/agent

Length of output: 2221


🏁 Script executed:

# Check for any comments in the implementation explaining the approval-request handling
sed -n '245,331p' src/client/search.ts | cat -n

Repository: get-convex/agent

Length of output: 3435


Fix: Tool-approval-requests should be removed when their corresponding tool-calls are filtered.

The current implementation filters out tool-calls without an approval response (lines 49-54) but leaves their corresponding tool-approval-requests orphaned in the content. Lines 328-333 of the test assert this behavior, but there is no logic to remove these dangling approval-requests.

The filter condition keeps all non-tool-call items unconditionally, including approval-requests. To maintain data integrity, when a tool-call is removed due to lacking an approval response, its tool-approval-request should also be removed. The implementation already tracks approvalRequestsByToolCallId (lines 23-26) and could use this mapping to filter out orphaned approval-requests alongside their tool-calls.

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

In `@src/client/search.test.ts` around lines 295 - 338, The function
filterOutOrphanedToolMessages is removing tool-call entries that lack approval
responses but leaves their matching tool-approval-request items orphaned; update
the logic so that after you identify removed tool-call IDs (use the existing
approvalRequestsByToolCallId mapping or compute a removedToolCallIds set),
change the content filtering step that currently keeps non-tool-call items
unconditionally to also skip any tool-approval-request whose toolCallId is in
removedToolCallIds; ensure comparisons use the toolCallId field and apply this
when building each MessageDoc.message.content so both the tool-call and its
associated tool-approval-request are removed together.

Comment on lines +261 to +286
} else if (content.type === "tool-approval-request") {
const approvalRequest = content as {
type: "tool-approval-request";
toolCallId: string;
approvalId: string;
};
approvalRequestsByToolCallId.set(
approvalRequest.toolCallId,
approvalRequest.approvalId,
);
} else if (content.type === "tool-approval-response") {
const approvalResponse = content as {
type: "tool-approval-response";
approvalId: string;
};
approvalResponseIds.add(approvalResponse.approvalId);
}
}
}
}

// Helper: check if tool call has a corresponding approval response
const hasApprovalResponse = (toolCallId: string) => {
const approvalId = approvalRequestsByToolCallId.get(toolCallId);
return approvalId !== undefined && approvalResponseIds.has(approvalId);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Orphaned tool-approval-request items can survive when their companion tool-call is filtered out.

In the second pass (lines 293–298), only tool-call parts are subject to removal from assistant messages; all other part types pass through unconditionally. If a tool-approval-request part exists alongside a tool-call that gets filtered (no tool result, no approval response), the assistant message will contain an approval request without a corresponding tool call, which may confuse the model on the next turn.

Consider either filtering tool-approval-request alongside its sibling tool-call (matching on toolCallId), or explicitly documenting that this intermediate state is intentional and harmless.

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

In `@src/client/search.ts` around lines 261 - 286, The assistant messages can end
up containing orphaned "tool-approval-request" parts when their sibling
"tool-call" was filtered out; update the second-pass filtering logic to also
drop "tool-approval-request" parts whose toolCallId has no surviving tool-call
and no approval response. Use the existing approvalRequestsByToolCallId map and
hasApprovalResponse(toolCallId) to detect orphans: when iterating assistant
parts, if part.type === "tool-approval-request" look up its toolCallId in
approvalRequestsByToolCallId and remove the part if the corresponding tool-call
was filtered (i.e., there is no surviving tool-call entry for that toolCallId
and hasApprovalResponse(toolCallId) is false). Ensure this uses the same
identifiers (approvalRequestsByToolCallId, hasApprovalResponse) so the
approval-request is kept only when its tool-call or an approval response exists.

Comment on lines +493 to +510
case "tool-approval-request": {
const typedPart = part as unknown as {
type: "tool-approval-request";
toolCallId: string;
approvalId: string;
};
const toolPart = toolPartsById.get(typedPart.toolCallId);
if (toolPart) {
toolPart.state = "approval-requested";
(toolPart as ToolUIPart & { approval?: object }).approval = {
id: typedPart.approvalId,
};
} else {
console.warn(
`Expected tool call part ${typedPart.toolCallId} for approval request`,
);
}
break;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for tool-approval-request in the codebase
rg -n "tool-approval-request" --type ts -g '!*.test.*' -g '!node_modules'

Repository: get-convex/agent

Length of output: 1438


🏁 Script executed:

# Check what TextStreamPart type looks like and where it's imported from
rg -n "TextStreamPart" --type ts -g '!*.test.*' -g '!node_modules' | head -20

Repository: get-convex/agent

Length of output: 1568


🏁 Script executed:

# Look at the imports in deltas.ts to understand the type source
head -50 src/deltas.ts | grep -E "^import|^from"

Repository: get-convex/agent

Length of output: 280


🌐 Web query:

AI SDK v6 TextStreamPart type definition tool-approval-request

💡 Result:

In AI SDK v6, tool-approval-request is represented in the TextStreamPart union via ToolApprovalRequestOutput.

ToolApprovalRequestOutput (the tool-approval-request part)

export type ToolApprovalRequestOutput<TOOLS extends ToolSet> = {
  type: 'tool-approval-request';
  approvalId: string;
  toolCall: TypedToolCall<TOOLS>;
};

[1]

Where it appears in TextStreamPart

TextStreamPart<TOOLS> is a union of many streamed event shapes and includes:

| ToolApprovalRequestOutput<TOOLS>

as one of its variants. [2]

Sources: [1] [2]


🏁 Script executed:

# Check the AI SDK version in package.json
rg -n "@ai-sdk" package.json

Repository: get-convex/agent

Length of output: 290


🏁 Script executed:

# Check if tool-approval-request is mentioned in deltas.ts context
sed -n '490,530p' src/deltas.ts

Repository: get-convex/agent

Length of output: 1315


🏁 Script executed:

# Look at the imports and type definitions more carefully
rg -B5 -A10 "type TextStreamPart" src/deltas.ts | head -30

Repository: get-convex/agent

Length of output: 455


🏁 Script executed:

# Check src/validators.ts to see how tool-approval-request is defined
sed -n '260,275p' src/validators.ts

Repository: get-convex/agent

Length of output: 428


🏁 Script executed:

# Find the main "ai" package version
rg -n '"ai"' package.json | grep -v "@ai-sdk"

Repository: get-convex/agent

Length of output: 121


🏁 Script executed:

# Check if there's a tsconfig or type declaration that shows what ai version is being used
cat package.json | grep -A20 '"dependencies"'

Repository: get-convex/agent

Length of output: 42


🌐 Web query:

AI SDK TextStreamPart tool-approval-request type version history

💡 Result:

What changed, when (AI SDK TextStreamParttype: "tool-approval-request")

  • AI SDK v5.x: TextStreamPart did not include any "tool-approval-request" variant (string not present in ai@5.0.29 type definitions). [4]
  • AI SDK v6.0.0: Tool execution approval was introduced as part of the AI SDK 6 beta (i.e., the overall “tool approval system” feature set that enables human-in-the-loop tool execution). [1] [2]
  • AI SDK v6.0.3: Added MCP tool approval support (changelog entry: “feat: add MCP tool approval”). [2]
  • AI SDK v6.x (example: ai@6.0.41): TextStreamPart includes ToolApprovalRequestOutput:
    • type: "tool-approval-request"
    • approvalId: string
    • toolCall: TypedToolCall<TOOLS> [3]

Related docs/behavior

  • For OpenAI’s MCP integration, when approval is required, the model can return a tool-approval-request content part. [5]
  • If an approval response references an unknown approvalId, the SDK throws AI_InvalidToolApprovalError (no matching tool-approval-request in history). [6]

🏁 Script executed:

# Check where tool-approval-request parts are generated/transformed
rg -B5 -A10 "toolCallId.*approvalId\|approvalId.*toolCallId" --type ts -g '!*.test.*' -g '!node_modules' | head -50

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

# Check the readUIMessageStream function to understand the pipeline
rg -B3 -A15 "readUIMessageStream" src/deltas.ts | head -40

Repository: get-convex/agent

Length of output: 1244


🏁 Script executed:

# Look for any transformation of toolCall to toolCallId
rg -n "toolCall" src/deltas.ts

Repository: get-convex/agent

Length of output: 1256


🏁 Script executed:

# Check if the part has a toolCall property that's being used
rg -B10 "tool-approval-request" src/deltas.ts | grep -A20 "case.*tool-approval"

Repository: get-convex/agent

Length of output: 185


🏁 Script executed:

# Look at how other similar parts extract IDs from objects
rg -B2 -A5 "part\.id\|toolCall" src/deltas.ts | head -60

Repository: get-convex/agent

Length of output: 42


🌐 Web query:

AI SDK v6 ToolApprovalRequestOutput toolCall structure toolCallId

💡 Result:

In AI SDK v6, a ToolApprovalRequestOutput is an output part shaped like this (TypeScript):

type ToolApprovalRequestOutput<TOOLS extends ToolSet> = {
  type: 'tool-approval-request';
  approvalId: string;
  toolCall: TypedToolCall<TOOLS>;
};

toolCall is the tool call being gated for approval, and its ID is toolCall.toolCallId (i.e., toolCallId is nested under toolCall, not on the approval request object itself). The base tool call shape is:

type BaseToolCall = {
  type: 'tool-call';
  toolCallId: string;
  providerExecuted?: boolean;
  providerMetadata?: ProviderMetadata;
};

So, in JSON-like form you’ll typically see:

{
  "type": "tool-approval-request",
  "approvalId": "",
  "toolCall": {
    "type": "tool-call",
    "toolCallId": "",
    "toolName": "",
    "input": { }
  }
}

[1] [2]


Shape mismatch in tool-approval-request handling — double assertion masks actual SDK type.

The part as unknown as { ... } cast at line 494 works around a shape incompatibility: AI SDK v6 provides tool-approval-request parts with toolCall: TypedToolCall<TOOLS> (where toolCallId is nested as toolCall.toolCallId), but this code asserts a flattened shape with toolCallId as a direct property.

This suggests the codebase normalizes SDK parts to an internal representation. While the type is correctly included in TextStreamPart in AI SDK v6, the double assertion means the actual SDK shape is hidden from TypeScript's type checker. If the SDK's nested structure changes, no compile-time error will surface—only runtime behavior.

Either use the SDK shape directly (part.toolCall.toolCallId) or document why the normalized shape is necessary and maintain a type guard to validate it.

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

In `@src/deltas.ts` around lines 493 - 510, The case handling
"tool-approval-request" currently uses a double-cast on part (part as unknown as
{...}) which masks the real SDK shape; replace the unsafe assertion by reading
the SDK shape directly or adding a runtime type guard: check whether
part.toolCall?.toolCallId exists and if so use that value (otherwise fall back
to a normalized part.toolCallId), then look up toolPartsById with the resolved
id and set toolPart.state and toolPart.approval as before; remove the double
assertion and/or add a small type-guard function (e.g.,
isToolApprovalRequestPart) so TypeScript can’t be bypassed and runtime shape
mismatches are handled explicitly in the "tool-approval-request" case.

Comment on lines +97 to +101
const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } =
{
...commonFields,
_id: uiMessage.id + `-${i}`,
stepOrder: stepOrder + i,
Copy link

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
# Check callers of fromUIMessages
rg -n --type ts "fromUIMessages" -A 10

Repository: get-convex/agent

Length of output: 14661


🏁 Script executed:

#!/bin/bash
# Get the full context of the line in question
cat -n src/UIMessages.ts | sed -n '90,110p'

Repository: get-convex/agent

Length of output: 1090


🏁 Script executed:

#!/bin/bash
# Check if there's a git history showing what changed
git log --oneline -20 src/UIMessages.ts 2>/dev/null | head -20

Repository: get-convex/agent

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Search for any lookups or queries using _id from UIMessages
rg -n "uiMessage\.id|_id.*uiMessage" --type ts

Repository: get-convex/agent

Length of output: 535


🏁 Script executed:

cat -n src/UIMessages.ts | sed -n '70,115p'

Repository: get-convex/agent

Length of output: 2303


🏁 Script executed:

# Check the git diff to see what changed
git diff HEAD~1 src/UIMessages.ts 2>/dev/null | head -100

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

# Check if playground is a test or example directory
fd -t f "MessageList.tsx" -x cat {}

Repository: get-convex/agent

Length of output: 2005


🏁 Script executed:

# Search for any migration guides or documentation about this change
rg -n "migration|breaking" --type md --type txt --type js --type ts -i

Repository: get-convex/agent

Length of output: 1654


🏁 Script executed:

# Check if there are other places handling ID mismatches similar to MessageList.tsx
rg -n "message\._id|_id.*===" --type ts -B 2 -A 2 | head -80

Repository: get-convex/agent

Length of output: 4316


🏁 Script executed:

# Look for any comments or documentation in UIMessages.ts about the suffix
rg -n "suffix|breaking|migration|v1|v2" src/UIMessages.ts -i -B 2 -A 2

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

# Check CHANGELOG.md for any mention of the ID suffix changes
cat CHANGELOG.md | head -100

Repository: get-convex/agent

Length of output: 4567


🏁 Script executed:

# Look at the full migration guide
cat MIGRATION.md

Repository: get-convex/agent

Length of output: 2223


🏁 Script executed:

# Check git history of the specific lines to see if suffix was recent
git log -p --follow -S 'uiMessage.id + `-${i}`' src/UIMessages.ts 2>/dev/null | head -150

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

# Check the CHANGELOG for any mention of the specific _id suffix behavior
rg -n "suffix|_id.*\-\$|tool.*message" CHANGELOG.md -B 2 -A 2

Repository: get-convex/agent

Length of output: 1127


🏁 Script executed:

# Look at lines around the famous "Breaking: `.id` on `toUIMessages`" line to understand context
cat CHANGELOG.md | sed -n '230,250p'

Repository: get-convex/agent

Length of output: 1171


🏁 Script executed:

# Search for any tests that verify the _id suffix behavior
rg -n "_id.*\-0|suffix" src/fromUIMessages.test.ts -B 3 -A 3

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

# Let's check what convertToModelMessages returns to understand when multiple messages are generated
rg -n "convertToModelMessages" src/ -A 5 -B 2 | head -100

Repository: get-convex/agent

Length of output: 1797


🏁 Script executed:

# Check if there's any inline documentation about the _id suffix in the code
cat -n src/UIMessages.ts | sed -n '85,110p'

Repository: get-convex/agent

Length of output: 1343


🏁 Script executed:

# Search for any documentation or comments about ID collision prevention
rg -n "separate.*document|split.*message|tool.*message" src/UIMessages.ts -i -B 2 -A 2

Repository: get-convex/agent

Length of output: 814


🏁 Script executed:

# Let's check if there's a CHANGELOG entry about this specific _id change
rg -n "0.6.0" CHANGELOG.md -A 50 | grep -i "id\|break\|tool\|message"

Repository: get-convex/agent

Length of output: 1539


🏁 Script executed:

# Check what convertToModelMessages is (from AI SDK) and whether it returns multiple messages
rg -n "export.*convertToModelMessages|import.*convertToModelMessages" -B 2 -A 2

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

# Verify: does a single UI message ever produce multiple model messages?
cat -n src/UIMessages.ts | sed -n '58,130p'

Repository: get-convex/agent

Length of output: 3407


🏁 Script executed:

# Final verification: check test file to see if there are any assertions about _id format
cat src/fromUIMessages.test.ts | grep -A 5 -B 5 "_id" | head -50

Repository: get-convex/agent

Length of output: 966


🏁 Script executed:

# Check if there are any comments explaining the _id suffixing design
cat -n src/UIMessages.ts | sed -n '85,108p'

Repository: get-convex/agent

Length of output: 1174


🏁 Script executed:

# Look for any documentation about why the suffix is used
rg -n "separate|distinct|multiple|tool|step" src/UIMessages.ts -B 1 -A 1

Repository: get-convex/agent

Length of output: 6357


_id suffix separates expanded tool and reasoning messages.

The code at line 100 sets _id: uiMessage.id + '-${i}' because convertToModelMessages() expands a single UI message into multiple document records (one for each tool call, reasoning step, etc.). This intentional suffix prevents ID collisions between the main message and its expanded children.

However, this design is not documented in the CHANGELOG or migration guide. Consumers that query by uiMessage.id directly may need to adapt (as shown in playground/src/components/MessageList.tsx, which implements a lookup fallback pattern). Consider adding a note to the migration guide explaining this ID transformation when messages are split into multiple documents.

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

In `@src/UIMessages.ts` around lines 97 - 101, convertToModelMessages() expands a
single UI message into multiple MessageDoc records and appends a suffix to the
stored _id (constructed from uiMessage.id + `-${i}`) to avoid collisions for
children; add a concise note to the CHANGELOG and migration guide describing
this ID transformation, show the recommended consumer update pattern (e.g.,
query by exact _id or implement a fallback lookup by uiMessage.id as in
playground/src/components/MessageList.tsx), and mention that consumers who
previously queried by uiMessage.id must adapt their lookups to account for
suffixed document IDs.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/validators.ts (1)

147-156: ⚠️ Potential issue | 🟡 Minor

Add mediaType field to vToolResultContent image part for consistency.

The image object within vToolResultContent (line 150–155) currently has only mimeType: v.optional(v.string()), but vImagePart and vFilePart both include mediaType: v.optional(v.string()) as the primary field with mimeType marked as deprecated. Update vToolResultContent to match this pattern.

Suggested fix
     v.object({
       type: v.literal("image"),
       data: v.string(),
+      mediaType: v.optional(v.string()),
+      /** `@deprecated` Use `mediaType` instead. */
       mimeType: v.optional(v.string()),
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validators.ts` around lines 147 - 156, vToolResultContent's image branch
is missing the mediaType field; update the image object inside
vToolResultContent to include mediaType: v.optional(v.string()) (and keep
mimeType: v.optional(v.string()) for backward compatibility) so it matches
vImagePart and vFilePart. Modify the union branch in vToolResultContent (the
v.object that currently has data and mimeType) to add mediaType as an optional
string field.
src/client/mockModel.ts (1)

187-191: ⚠️ Potential issue | 🟡 Minor

Fix off-by-one bug in array index access for doGenerate and doStream.

When doGenerate or doStream is an array, the current code accesses the wrong index. After this.doGenerateCalls.push(options) on line 182 increments the length, the code accesses args.doGenerate[this.doGenerateCalls.length] on line 187, which skips the first element. On the first call, this.doGenerateCalls.length is 1, so it accesses index [1] instead of [0]. The same issue exists for doStream on line 209.

Proposed fix
     } else if (Array.isArray(args.doGenerate)) {
-      return args.doGenerate[this.doGenerateCalls.length];
+      return args.doGenerate[this.doGenerateCalls.length - 1];
     } else if (contentSteps.length) {

And similarly for doStream:

     } else if (Array.isArray(args.doStream)) {
-      return args.doStream[this.doStreamCalls.length];
+      return args.doStream[this.doStreamCalls.length - 1];
     } else if (contentSteps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/mockModel.ts` around lines 187 - 191, The array access for mocked
responses is off-by-one: after pushing the call into this.doGenerateCalls (and
this.doStreamCalls), the code uses args.doGenerate[this.doGenerateCalls.length]
(and args.doStream[this.doStreamCalls.length]) which skips the first element;
change those accesses to use the previous index (this.doGenerateCalls.length - 1
and this.doStreamCalls.length - 1) so the first call returns args.doGenerate[0]
(and similarly for doStream). Locate the branches handling
Array.isArray(args.doGenerate) and Array.isArray(args.doStream) in mockModel.ts
and update the index expressions accordingly while keeping the existing push
behavior.
🧹 Nitpick comments (2)
src/validators.ts (1)

261-306: Resolve @todo comments on providerMetadata/providerOptions before shipping.

Lines 274-277 and 302-305 have @todo Should we continue to include? on both vToolApprovalRequest and vToolApprovalResponse. These should be resolved before release — if they aren't needed, removing them avoids growing the stored document size; if they are needed, the @todo should be removed.

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

In `@src/validators.ts` around lines 261 - 306, Resolve the TODOs for
providerMetadata and providerOptions in vToolApprovalRequest and
vToolApprovalResponse: either remove those properties from both validator
objects if they are unused (and delete any related storage/usage) or keep them
but remove the `@todo` and explicitly define their shape/optional status (e.g.,
make providerMetadata and providerOptions optional with a concrete schema/types)
so the validators (vToolApprovalRequest and vToolApprovalResponse) are final and
documented.
src/client/createTool.ts (1)

349-356: wrapTools puts a bare { ctx } object in the output for falsy tool entries.

When tool is falsy, tool && !(tool as any).__acceptsCtx evaluates to false and execution falls into the else branch. Spreading null/undefined produces { ctx }, which is placed in the output set as a corrupt tool entry. ToolSet shouldn't carry nulls in well-typed code, but this is trivially broken in plain JavaScript or after an as any cast.

♻️ Proposed fix
-    if (tool && !(tool as any).__acceptsCtx) {
+    if (!tool || !(tool as any).__acceptsCtx) {
       output[name] = tool;
     } else {
       const out = { ...tool, ctx };
       output[name] = out;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/createTool.ts` around lines 349 - 356, The loop in wrapTools
incorrectly treats falsy tool entries as objects and places a bare { ctx } into
output; update the conditional so falsy tools are preserved (or left as-is)
instead of being spread: in the for loop over toolSet, first check if tool is
falsy (if (!tool) output[name] = tool), otherwise check the __acceptsCtx flag
(if (!(tool as any).__acceptsCtx) output[name] = tool), and only when tool is
truthy and accepts ctx create the merged out = { ...tool, ctx } and assign it to
output[name]; reference toolSet, output, wrapTools, ctx, and __acceptsCtx when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/createTool.ts`:
- Around line 309-332: The hook wrappers (onInputStart, onInputDelta,
onInputAvailable, toModelOutput) call getCtx(this) and may pass undefined to
user callbacks; mirror the existing guard used in execute and needsApproval by
checking the result of getCtx(this) and throwing the same descriptive error when
ctx is missing before calling the original hook
(origOnInputStart/origOnInputDelta/origOnInputAvailable/origToModelOutput), so
user hooks receive a validated ctx or get the clear actionable throw instead of
a cryptic runtime TypeError.

In `@src/client/mockModel.ts`:
- Around line 1-5: The array-access off-by-one bug occurs in the mock
implementation where doGenerate and doStream push a call then immediately index
it using this.doGenerateCalls.length and this.doStreamCalls.length (so first
call reads index 1); update the indexing to use this.doGenerateCalls.length - 1
and this.doStreamCalls.length - 1 when retrieving the last pushed entry
(references: doGenerate, doStream, doGenerateCalls, doStreamCalls) so the
returned/streamed response uses the correct zero-based index.
- Around line 15-21: DEFAULT_USAGE currently uses a flat token layout; update it
to match AI SDK v6’s LanguageModelV3GenerateResult usage shape by making
inputTokens and outputTokens nested objects and adjusting fields accordingly:
replace outputTokens:number and inputTokens:number with inputTokens:{ total:3,
noCache:3, cacheRead:undefined, cacheWrite:undefined } and outputTokens:{
total:10, text:10, reasoning:undefined } (keep totalTokens:13 if needed) and
remove or map inputTokenDetails/outputTokenDetails to the new
cacheRead/cacheWrite fields so DEFAULT_USAGE matches the v6 spec; modify the
DEFAULT_USAGE constant in src/client/mockModel.ts.

In `@src/validators.ts`:
- Around line 118-145: vToolCallPart currently uses the same discriminant
literal for both union variants which prevents fast-path discriminant matching;
update the two variants so the new-format object keeps type:
v.literal("tool-call") and the legacy-format object uses a distinct literal such
as type: v.literal("tool-call-legacy") (or "tool-call-v1") in the v.union
definition, and then search for any runtime/type checks that inspect the type
field and update them to accept the new legacy literal if needed; this change is
centered on the vToolCallPart validator to enable proper discriminated-union
behavior.

---

Outside diff comments:
In `@src/client/mockModel.ts`:
- Around line 187-191: The array access for mocked responses is off-by-one:
after pushing the call into this.doGenerateCalls (and this.doStreamCalls), the
code uses args.doGenerate[this.doGenerateCalls.length] (and
args.doStream[this.doStreamCalls.length]) which skips the first element; change
those accesses to use the previous index (this.doGenerateCalls.length - 1 and
this.doStreamCalls.length - 1) so the first call returns args.doGenerate[0] (and
similarly for doStream). Locate the branches handling
Array.isArray(args.doGenerate) and Array.isArray(args.doStream) in mockModel.ts
and update the index expressions accordingly while keeping the existing push
behavior.

In `@src/validators.ts`:
- Around line 147-156: vToolResultContent's image branch is missing the
mediaType field; update the image object inside vToolResultContent to include
mediaType: v.optional(v.string()) (and keep mimeType: v.optional(v.string()) for
backward compatibility) so it matches vImagePart and vFilePart. Modify the union
branch in vToolResultContent (the v.object that currently has data and mimeType)
to add mediaType as an optional string field.

---

Duplicate comments:
In `@docs/agent-usage.mdx`:
- Line 46: The docs show inconsistent imports for stepCountIs (imported once
from "@convex-dev/agent" and elsewhere from "ai"); pick the correct canonical
module (either "@convex-dev/agent" or "ai") and update every example to import
stepCountIs from that single source, adjusting the import lines that reference
stepCountIs and related symbols (e.g., Agent, stepCountIs) so all examples
consistently use the chosen module.

In `@src/client/createTool.ts`:
- Around line 241-243: The guard that throws when inputSchema is missing runs
before emitting deprecation warnings, so users passing only the deprecated
def.args never see the migration hint; move the deprecation warnings for
def.args and def.handler (calls to warnDeprecation) to run before the existence
check or perform the deprecation checks immediately before the throw, then
compute inputSchema = def.inputSchema ?? def.args and only throw after
warnDeprecation has been invoked so users get the migration message; reference
the inputSchema variable, def.inputSchema/def.args, and warnDeprecation when
making this change.

---

Nitpick comments:
In `@src/client/createTool.ts`:
- Around line 349-356: The loop in wrapTools incorrectly treats falsy tool
entries as objects and places a bare { ctx } into output; update the conditional
so falsy tools are preserved (or left as-is) instead of being spread: in the for
loop over toolSet, first check if tool is falsy (if (!tool) output[name] =
tool), otherwise check the __acceptsCtx flag (if (!(tool as any).__acceptsCtx)
output[name] = tool), and only when tool is truthy and accepts ctx create the
merged out = { ...tool, ctx } and assign it to output[name]; reference toolSet,
output, wrapTools, ctx, and __acceptsCtx when making the change.

In `@src/validators.ts`:
- Around line 261-306: Resolve the TODOs for providerMetadata and
providerOptions in vToolApprovalRequest and vToolApprovalResponse: either remove
those properties from both validator objects if they are unused (and delete any
related storage/usage) or keep them but remove the `@todo` and explicitly define
their shape/optional status (e.g., make providerMetadata and providerOptions
optional with a concrete schema/types) so the validators (vToolApprovalRequest
and vToolApprovalResponse) are final and documented.

Comment on lines 309 to 332
if (def.onInputStart) {
t.onInputStart = def.onInputStart.bind(t, getCtx(t));
const origOnInputStart = def.onInputStart;
t.onInputStart = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputStart.call(this, getCtx(this), options);
};
}
if (def.onInputDelta) {
t.onInputDelta = def.onInputDelta.bind(t, getCtx(t));
const origOnInputDelta = def.onInputDelta;
t.onInputDelta = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputDelta.call(this, getCtx(this), options);
};
}
if (def.onInputAvailable) {
t.onInputAvailable = def.onInputAvailable.bind(t, getCtx(t));
const origOnInputAvailable = def.onInputAvailable;
t.onInputAvailable = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputAvailable.call(this, getCtx(this), options);
};
}
if (def.toModelOutput) {
const origToModelOutput = def.toModelOutput;
t.toModelOutput = function (this: Tool<INPUT, OUTPUT>, options) {
return origToModelOutput.call(this, getCtx(this), options);
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hook callbacks silently pass undefined ctx, unlike execute/needsApproval which throw a clear error.

execute (lines 296–302) and needsApproval (lines 279–285) both guard against a missing ctx with a descriptive throw. All four hook wrappers — onInputStart, onInputDelta, onInputAvailable, and toModelOutput — call getCtx(this) without this guard, so if a user defines any of these hooks but neither passes ctx at definition time nor uses wrapTools, the callback receives undefined as its ctx argument. This bypasses the type constraint at runtime and produces a cryptic TypeError deep in user code rather than the standard actionable message.

🛡️ Proposed fix — add the same ctx guard to all hook wrappers
  if (def.onInputStart) {
    const origOnInputStart = def.onInputStart;
    t.onInputStart = function (this: Tool<INPUT, OUTPUT>, options) {
+     if (!getCtx(this)) {
+       throw new Error(
+         "To use a Convex tool, you must either provide the ctx" +
+           " at definition time (dynamically in an action), or use the Agent to" +
+           " call it (which injects the ctx, userId and threadId)",
+       );
+     }
      return origOnInputStart.call(this, getCtx(this), options);
    };
  }
  if (def.onInputDelta) {
    const origOnInputDelta = def.onInputDelta;
    t.onInputDelta = function (this: Tool<INPUT, OUTPUT>, options) {
+     if (!getCtx(this)) {
+       throw new Error(
+         "To use a Convex tool, you must either provide the ctx" +
+           " at definition time (dynamically in an action), or use the Agent to" +
+           " call it (which injects the ctx, userId and threadId)",
+       );
+     }
      return origOnInputDelta.call(this, getCtx(this), options);
    };
  }
  if (def.onInputAvailable) {
    const origOnInputAvailable = def.onInputAvailable;
    t.onInputAvailable = function (this: Tool<INPUT, OUTPUT>, options) {
+     if (!getCtx(this)) {
+       throw new Error(
+         "To use a Convex tool, you must either provide the ctx" +
+           " at definition time (dynamically in an action), or use the Agent to" +
+           " call it (which injects the ctx, userId and threadId)",
+       );
+     }
      return origOnInputAvailable.call(this, getCtx(this), options);
    };
  }
  if (def.toModelOutput) {
    const origToModelOutput = def.toModelOutput;
    t.toModelOutput = function (this: Tool<INPUT, OUTPUT>, options) {
+     if (!getCtx(this)) {
+       throw new Error(
+         "To use a Convex tool, you must either provide the ctx" +
+           " at definition time (dynamically in an action), or use the Agent to" +
+           " call it (which injects the ctx, userId and threadId)",
+       );
+     }
      return origToModelOutput.call(this, getCtx(this), options);
    };
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (def.onInputStart) {
t.onInputStart = def.onInputStart.bind(t, getCtx(t));
const origOnInputStart = def.onInputStart;
t.onInputStart = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputStart.call(this, getCtx(this), options);
};
}
if (def.onInputDelta) {
t.onInputDelta = def.onInputDelta.bind(t, getCtx(t));
const origOnInputDelta = def.onInputDelta;
t.onInputDelta = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputDelta.call(this, getCtx(this), options);
};
}
if (def.onInputAvailable) {
t.onInputAvailable = def.onInputAvailable.bind(t, getCtx(t));
const origOnInputAvailable = def.onInputAvailable;
t.onInputAvailable = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputAvailable.call(this, getCtx(this), options);
};
}
if (def.toModelOutput) {
const origToModelOutput = def.toModelOutput;
t.toModelOutput = function (this: Tool<INPUT, OUTPUT>, options) {
return origToModelOutput.call(this, getCtx(this), options);
};
}
if (def.onInputStart) {
const origOnInputStart = def.onInputStart;
t.onInputStart = function (this: Tool<INPUT, OUTPUT>, options) {
if (!getCtx(this)) {
throw new Error(
"To use a Convex tool, you must either provide the ctx" +
" at definition time (dynamically in an action), or use the Agent to" +
" call it (which injects the ctx, userId and threadId)",
);
}
return origOnInputStart.call(this, getCtx(this), options);
};
}
if (def.onInputDelta) {
const origOnInputDelta = def.onInputDelta;
t.onInputDelta = function (this: Tool<INPUT, OUTPUT>, options) {
if (!getCtx(this)) {
throw new Error(
"To use a Convex tool, you must either provide the ctx" +
" at definition time (dynamically in an action), or use the Agent to" +
" call it (which injects the ctx, userId and threadId)",
);
}
return origOnInputDelta.call(this, getCtx(this), options);
};
}
if (def.onInputAvailable) {
const origOnInputAvailable = def.onInputAvailable;
t.onInputAvailable = function (this: Tool<INPUT, OUTPUT>, options) {
if (!getCtx(this)) {
throw new Error(
"To use a Convex tool, you must either provide the ctx" +
" at definition time (dynamically in an action), or use the Agent to" +
" call it (which injects the ctx, userId and threadId)",
);
}
return origOnInputAvailable.call(this, getCtx(this), options);
};
}
if (def.toModelOutput) {
const origToModelOutput = def.toModelOutput;
t.toModelOutput = function (this: Tool<INPUT, OUTPUT>, options) {
if (!getCtx(this)) {
throw new Error(
"To use a Convex tool, you must either provide the ctx" +
" at definition time (dynamically in an action), or use the Agent to" +
" call it (which injects the ctx, userId and threadId)",
);
}
return origToModelOutput.call(this, getCtx(this), options);
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/createTool.ts` around lines 309 - 332, The hook wrappers
(onInputStart, onInputDelta, onInputAvailable, toModelOutput) call getCtx(this)
and may pass undefined to user callbacks; mirror the existing guard used in
execute and needsApproval by checking the result of getCtx(this) and throwing
the same descriptive error when ctx is missing before calling the original hook
(origOnInputStart/origOnInputDelta/origOnInputAvailable/origToModelOutput), so
user hooks receive a validated ctx or get the clear actionable throw instead of
a cryptic runtime TypeError.

Comment on lines 1 to 5
import type {
LanguageModelV2,
LanguageModelV2Content,
LanguageModelV2StreamPart,
LanguageModelV3,
LanguageModelV3Content,
LanguageModelV3StreamPart,
} from "@ai-sdk/provider";
Copy link

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:

# First, let's look at the file to understand the context
head -100 src/client/mockModel.ts | cat -n

Repository: get-convex/agent

Length of output: 3833


🏁 Script executed:

# Check if the file exists and get basic info
wc -l src/client/mockModel.ts

Repository: get-convex/agent

Length of output: 87


🏁 Script executed:

# Search for LanguageModelV3 imports and usage
rg "LanguageModelV3" src/client/mockModel.ts -A 2 -B 2

Repository: get-convex/agent

Length of output: 2718


🏁 Script executed:

# Check package.json to see AI SDK versions
cat package.json | grep -A 5 '"@ai-sdk/provider"'
cat package.json | grep -A 5 '"ai"'

Repository: get-convex/agent

Length of output: 675


🏁 Script executed:

# Check the simulateReadableStream import
rg "simulateReadableStream" src/client/mockModel.ts -B 2 -A 2

Repository: get-convex/agent

Length of output: 469


🏁 Script executed:

# Look at lines around 180-200 to see the doGenerate array access
sed -n '175,200p' src/client/mockModel.ts | cat -n

Repository: get-convex/agent

Length of output: 1010


🏁 Script executed:

# Check the full doStream method to see the parallel structure
sed -n '200,225p' src/client/mockModel.ts | cat -n

Repository: get-convex/agent

Length of output: 1082


🏁 Script executed:

# Search for all uses of 'as any' in the file
rg "as any" src/client/mockModel.ts -B 2 -A 2

Repository: get-convex/agent

Length of output: 350


🏁 Script executed:

# Get the complete context around the array access with line numbers
sed -n '176,230p' src/client/mockModel.ts | cat -n

Repository: get-convex/agent

Length of output: 2132


🏁 Script executed:

# Check if there are any recent commits that might show if this is pre-existing
git log --oneline -20 -- src/client/mockModel.ts

Repository: get-convex/agent

Length of output: 161


Fix off-by-one bug in array indexing for doGenerate and doStream.

The migration to LanguageModelV3 types is correct and complete. However, there's an off-by-one error in the array response path:

  • Lines 190 and 212 access this.doGenerateCalls.length and this.doStreamCalls.length respectively, but these are incremented before the access. After push(), the length is already 1 for the first call, so it accesses index [1] instead of [0].

Change both to use this.doGenerateCalls.length - 1 and this.doStreamCalls.length - 1 to get the correct zero-based index.

All imports, types, and signatures correctly use LanguageModelV3 from @ai-sdk/provider v3.0.3 and simulateReadableStream from ai v6.0.35.

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

In `@src/client/mockModel.ts` around lines 1 - 5, The array-access off-by-one bug
occurs in the mock implementation where doGenerate and doStream push a call then
immediately index it using this.doGenerateCalls.length and
this.doStreamCalls.length (so first call reads index 1); update the indexing to
use this.doGenerateCalls.length - 1 and this.doStreamCalls.length - 1 when
retrieving the last pushed entry (references: doGenerate, doStream,
doGenerateCalls, doStreamCalls) so the returned/streamed response uses the
correct zero-based index.

Comment on lines +15 to +21
const DEFAULT_USAGE = {
outputTokens: 10,
inputTokens: 3,
totalTokens: 13,
inputTokenDetails: undefined,
outputTokenDetails: undefined,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /repo && find . -type f -name "mockModel.ts" -o -name "*.ts" | head -20

Repository: get-convex/agent

Length of output: 115


🏁 Script executed:

cat -n src/client/mockModel.ts

Repository: get-convex/agent

Length of output: 9298


🏁 Script executed:

rg "LanguageModelV3" --type ts -A 3 -B 3

Repository: get-convex/agent

Length of output: 6195


🏁 Script executed:

rg "usage.*Type\|UsageType" --type ts | head -20

Repository: get-convex/agent

Length of output: 42


🌐 Web query:

AI SDK v6 LanguageModelV3 usage type doGenerate fields documentation

💡 Result:

In AI SDK v6, a LanguageModelV3 provider implements:

doGenerate(options: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult>

(See the V3 provider spec overview.) [1]

LanguageModelV3GenerateResult (what doGenerate() must return)

A minimal valid result looks like this (from the official MockLanguageModelV3 examples): [2]

{
  content: [{ type: 'text', text: 'Hello, world!' }],
  finishReason: { unified: 'stop', raw: undefined },
  usage: {
    inputTokens: {
      total: 10,
      noCache: 10,
      cacheRead: undefined,
      cacheWrite: undefined,
    },
    outputTokens: {
      total: 20,
      text: 20,
      reasoning: undefined,
    },
  },
  warnings: [],
}

Fields

  • content: array of generated content parts (e.g. text, reasoning, tool-call, file, source). [1]
  • finishReason: { unified, raw } (unified reason + provider raw reason). [2]
  • usage (nested in v6):
    • inputTokens: { total, noCache, cacheRead?, cacheWrite? }
    • outputTokens: { total, text?, reasoning? } [2]
  • warnings: provider warnings (often []). [2]
  • Optional metadata commonly returned by providers: request / response bodies (and in streaming there are response-metadata parts). [1]

Where this type is used

Middleware wrapGenerate receives doGenerate: () => PromiseLike<LanguageModelV3GenerateResult>. [3]

Sources: [1] [2] [3]


DEFAULT_USAGE structure does not match AI SDK v6 LanguageModelV3GenerateResult specification.

The mock uses a flat structure with outputTokens, inputTokens, totalTokens, and *TokenDetails fields, but AI SDK v6 expects nested objects: inputTokens: { total, noCache, cacheRead?, cacheWrite? } and outputTokens: { total, text?, reasoning? }. Update the structure at lines 15-21 to conform to the v6 usage type.

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

In `@src/client/mockModel.ts` around lines 15 - 21, DEFAULT_USAGE currently uses a
flat token layout; update it to match AI SDK v6’s LanguageModelV3GenerateResult
usage shape by making inputTokens and outputTokens nested objects and adjusting
fields accordingly: replace outputTokens:number and inputTokens:number with
inputTokens:{ total:3, noCache:3, cacheRead:undefined, cacheWrite:undefined }
and outputTokens:{ total:10, text:10, reasoning:undefined } (keep totalTokens:13
if needed) and remove or map inputTokenDetails/outputTokenDetails to the new
cacheRead/cacheWrite fields so DEFAULT_USAGE matches the v6 spec; modify the
DEFAULT_USAGE constant in src/client/mockModel.ts.

Comment on lines +118 to +145
// Union type to support both old (args) and new (input) formats
// Both include input for type hint support
export const vToolCallPart = v.union(
// New format: input is primary, args is optional for backwards compat
v.object({
type: v.literal("tool-call"),
toolCallId: v.string(),
toolName: v.string(),
input: v.any(),
/** @deprecated Use `input` instead. */
args: v.optional(v.any()),
providerExecuted: v.optional(v.boolean()),
providerOptions,
providerMetadata,
}),
// Legacy format: args is present, input is optional
v.object({
type: v.literal("tool-call"),
toolCallId: v.string(),
toolName: v.string(),
/** @deprecated Use `input` instead. */
args: v.any(),
input: v.optional(v.any()),
providerExecuted: v.optional(v.boolean()),
providerOptions,
providerMetadata,
}),
);
Copy link

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 read the actual file to see the code at lines 118-145
head -150 src/validators.ts | tail -35

Repository: get-convex/agent

Length of output: 1098


🏁 Script executed:

#!/bin/bash
# Look for all v.union patterns in the validators.ts file to understand the pattern
rg -n 'v\.union' src/validators.ts -A 3

Repository: get-convex/agent

Length of output: 4408


🏁 Script executed:

#!/bin/bash
# Check if there are tests for vToolCallPart that would validate the union behavior
fd -e test.ts -e test.js -e spec.ts -e spec.js | xargs rg -l 'vToolCallPart\|ToolCallPart' 2>/dev/null | head -5

Repository: get-convex/agent

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look for how other discriminated unions are structured in the codebase
rg -n 'v\.literal\(' src/validators.ts | head -20

Repository: get-convex/agent

Length of output: 727


vToolCallPart union should use distinct type literals for each variant, like other discriminated unions in the codebase.

Both variants currently share type: v.literal("tool-call"), forcing Convex to use sequential fallback validation instead of fast-path discriminant matching. While this works correctly (variant 1 requires input, variant 2 requires args), it's inconsistent with the codebase pattern. Every other discriminated union here uses distinct type literal values (e.g., vToolResultOutput uses "text", "json", "execution-denied"; vToolResultContent variants use distinct types). Consider using type: v.literal("tool-call") for the new format and type: v.literal("tool-call-legacy") (or a version discriminant) for backwards compatibility.

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

In `@src/validators.ts` around lines 118 - 145, vToolCallPart currently uses the
same discriminant literal for both union variants which prevents fast-path
discriminant matching; update the two variants so the new-format object keeps
type: v.literal("tool-call") and the legacy-format object uses a distinct
literal such as type: v.literal("tool-call-legacy") (or "tool-call-v1") in the
v.union definition, and then search for any runtime/type checks that inspect the
type field and update them to accept the new legacy literal if needed; this
change is centered on the vToolCallPart validator to enable proper
discriminated-union behavior.

@sethconvex sethconvex merged commit 92532ae into main Feb 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for AI SDK v6

10 participants