Skip to content

feat: add tool routing for agents with router tools, pool/expose controls, and embedding routing#980

Merged
omeraplak merged 9 commits intomainfrom
feat/tool-routing
Jan 23, 2026
Merged

feat: add tool routing for agents with router tools, pool/expose controls, and embedding routing#980
omeraplak merged 9 commits intomainfrom
feat/tool-routing

Conversation

@omeraplak
Copy link
Copy Markdown
Member

@omeraplak omeraplak commented Jan 23, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

What is the new behavior?

fixes (issue)

Notes for reviewers


Summary by cubic

Adds tool routing with router tools, pool/expose controls, and embedding-based selection so agents can route large tool pools without bloating prompts. Also adds provider-qualified embedding model strings (e.g., openai/text-embedding-3-small) and tool hooks with onToolEnd output overrides; fixes #969.

  • New Features

    • Agent toolRouting config: define routers, pool, expose, topK, parallel, and agent/resolver modes (with optional executionModel).
    • Embedding router: enable via toolRouting.embedding or createToolRouter; ranks tools by description/tags and runs selected tools.
    • VoltAgent global defaults: set toolRouting once and apply to registered agents.
    • Model registry: embedding model routing and provider-qualified IDs; new embedding model factory support.
    • Memory: embedding accepts a string or config object in addition to AiSdkEmbeddingAdapter.
    • PlanAgent: toolRouting supported for agents and subagents.
    • Tool hooks: per-tool hooks and agent onToolEnd can return { output } to replace results (revalidated if an output schema exists).
    • Docs and example: new Tool Routing docs and example app.
  • Migration

    • Optional: switch Memory embedding to a model string (e.g., "openai/text-embedding-3-small"); existing adapters still work.
    • To enable routing, add toolRouting to Agent or VoltAgent; only routers and tools listed in expose are visible to the model.

Written for commit efc8815. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Intelligent tool routing for agents: embedding-based selection, tool pools, expose controls, resolver/agent modes, and custom router strategies.
    • Embedding configuration now accepts provider-qualified model strings (e.g., "openai/text-embedding-3-small") and router-aware embedding selection.
    • Public types and APIs for tool routers and embedding model references.
  • Documentation

    • Comprehensive tool routing guide, recipes, and a new example project with setup and usage examples.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Jan 23, 2026

🦋 Changeset detected

Latest commit: efc8815

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 23, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a new tool routing subsystem to VoltAgent: router tools, embedding-based router strategy, pool/expose controls, resolver mode, and per-call/global routing config. Enables provider-qualified embedding model strings via the model provider registry, updates Memory to accept string/config embedding specs, and includes examples and documentation.

Changes

Cohort / File(s) Summary
Routing Types & Factories
packages/core/src/tool/routing/types.ts, packages/core/src/tool/routing/index.ts, packages/core/src/tool/routing/constants.ts
New routing type system, ToolRouter factory (createToolRouter), metadata symbol, and public exports for routing utilities and types.
Embedding Router Strategy
packages/core/src/tool/routing/embedding.ts
New embedding-based router strategy (createEmbeddingToolRouterStrategy) with caching, batch embedding, scoring, tracing hooks, and topK selection.
Agent Integration & Runtime
packages/core/src/agent/agent.ts, packages/core/src/agent/types.ts, packages/core/src/agent/context-keys.ts, packages/core/src/agent/hooks/index.ts
Major agent changes: per-call/default toolRouting options, toolPoolManager wiring, __executeToolRouter, routing state in full state, API tool merging, and new exported routing-related types and AGENT_REF_CONTEXT_KEY.
Model Provider + Embedding Types
packages/core/src/registries/model-provider-registry.ts, packages/core/src/registries/embedding-model-router-types.generated.ts, packages/core/src/registries/embedding-model-router-types.ts, packages/core/scripts/generate-model-provider-registry.js
Registry extended to discover/resolve embedding models, new EmbeddingModelFactory/instance types, generated EmbeddingModelsMap and EmbeddingRouterModelId types, and resolveEmbeddingModel API.
Memory / Embedding Adaptation
packages/core/src/memory/types.ts, packages/core/src/memory/index.ts, packages/core/src/memory/adapters/embedding/types.ts, packages/core/src/memory/adapters/embedding/ai-sdk.ts, packages/core/src/memory/adapters/embedding/ai-sdk.spec.ts, packages/core/src/memory/index.spec-d.ts
Memory accepts embedding as string/model-ref/config or adapter (EmbeddingAdapterInput). AiSdkEmbeddingAdapter gains lazy resolution of provider-qualified strings via ModelProviderRegistry; resolveEmbeddingAdapter added. Tests updated.
Registry & VoltAgent Global Routing
packages/core/src/registries/agent-registry.ts, packages/core/src/voltagent.ts, packages/core/src/types.ts, packages/core/src/index.ts
AgentRegistry gains global toolRouting getter/setter; VoltAgent propagates global routing to agents on registration; new exports for embedding/routing types added.
Tool Manager & Tool Lookup
packages/core/src/tool/manager/BaseToolManager.ts, packages/core/src/tool/index.ts
Added getToolByName (recursive lookup) and exported routing helpers (createToolRouter, createEmbeddingToolRouterStrategy, isToolRouter) and routing types from the tool module.
PlanAgent Propagation
packages/core/src/planagent/plan-agent.ts
PlanAgent now accepts/forwards toolRouting to subagents and normalizes default routing for subagent creation.
Examples: embedding model spec changes
examples/*/src/index.ts, examples/*/src/retriever/*, examples/*/README.md (multiple example files)
Updated many examples to use provider-qualified embedding model strings (e.g., "openai/text-embedding-3-small") instead of AiSdkEmbeddingAdapter/openai imports.
New Example: Tool Routing
examples/with-tool-routing/package.json, examples/with-tool-routing/src/index.ts, examples/with-tool-routing/README.md, examples/with-tool-routing/tsconfig.json
New example project demonstrating tool routing with pools, routers, and Agent wiring.
Docs, Recipes & Site Navigation
website/docs/tools/tool-routing.md, website/recipes/tool-routing.md, .changeset/*, docs/tool-routing-plan.md, website/**/memory/*.md, website/sidebars.ts, website/sidebarsRecipes.ts, examples/README.md, changelogs
Extensive documentation additions/updates: tool routing guide, plan, recipes, memory docs updated to show string-based embedding model usage, and sidebar entries added.

Sequence Diagram(s)

sequenceDiagram
    participant Agent
    participant ToolRouter as ToolRouter (Embedding)
    participant Embedding as EmbeddingAdapter
    participant Candidates as ToolCandidates
    participant Provider as ProviderTools

    Agent->>ToolRouter: execute({ query })
    activate ToolRouter
    ToolRouter->>Embedding: embed(query)
    Embedding->>Embedding: resolve model if provider string
    Embedding-->>ToolRouter: queryEmbedding

    ToolRouter->>Candidates: embedBatch(toolTexts)
    Candidates->>Embedding: embedBatch(toolTexts)
    Embedding-->>Candidates: toolEmbeddings (cacheable)

    ToolRouter->>ToolRouter: score & select topK
    ToolRouter-->>Agent: selections
    deactivate ToolRouter

    Agent->>Provider: resolve args & execute selected tool(s)
    Provider-->>Agent: results
Loading
sequenceDiagram
    participant VoltAgent
    participant Registry as AgentRegistry
    participant AgentA as Agent
    participant AgentB as Subagent

    VoltAgent->>Registry: setGlobalToolRouting(config)
    Registry-->>VoltAgent: ack

    VoltAgent->>AgentA: register(agent)
    AgentA->>AgentA: __setDefaultToolRouting(global)
    AgentA->>AgentA: applyToolRoutingConfig()
    AgentA->>AgentB: addSubAgent(def)
    AgentB->>AgentB: inherit toolRouting & init
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through docs and code with cheer,

Routed tools by embedding near,
Pools and routers, topK delight,
Agents lighter, choices tight,
A tiny rabbit cheers this new flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The PR title accurately and concisely summarizes the main change: adding tool routing for agents with router tools, pool/expose controls, and embedding routing.
Description check ✅ Passed The PR description includes all required checklist items marked complete, clearly explains new features, migration notes, and links to fixed issue #969.
Linked Issues check ✅ Passed The changes comprehensively address issue #969's objectives: tool routing with routers, pool/expose controls, embedding-based selection, subagent support, and framework-level integration.
Out of Scope Changes check ✅ Passed All changes are directly related to tool routing objectives. Embedding configuration updates support provider-qualified model strings, documentation reflects the new feature, and examples demonstrate its usage.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@joggrbot

This comment has been minimized.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Jan 23, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: efc8815
Status: ✅  Deploy successful!
Preview URL: https://c1edbff6.voltagent.pages.dev
Branch Preview URL: https://feat-tool-routing.voltagent.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 66 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/src/agent/agent.ts">

<violation number="1" location="packages/core/src/agent/agent.ts:4602">
P2: `toolRouting: false` should disable router tools for a call, but the current fallback returns all static tools (including routers). This means per-call routing disablement still exposes router tools to the model.</violation>

<violation number="2" location="packages/core/src/agent/agent.ts:5114">
P1: Provider tools can execute before approval is enforced. `runInternalGenerateText` triggers the tool call, but `ensureToolApproval` runs only after the tool has already executed, so tools requiring approval may still run and produce side effects. Approval needs to be enforced before invoking the provider tool.</violation>

<violation number="3" location="packages/core/src/agent/agent.ts:5159">
P2: Provider tools that require approval throw and abort the entire tool-router execution instead of returning a per-tool error result. This makes one denied provider tool fail the full router request.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Base automatically changed from feat/tool-hooks to main January 23, 2026 03:01
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/src/agent/agent.ts">

<violation number="1" location="packages/core/src/agent/agent.ts:5256">
P1: Tool approval can be bypassed: approved args are only injected as a prompt instruction, but the actual toolCall.input is never revalidated. A model that ignores the instruction can execute the tool with unapproved arguments. Re-run ensureToolApproval on toolCall.input (or verify it matches approvedArgs) before executing/hooks.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

[tool.name]: tool,
};

const approvedArgsInstruction = approvedArgs
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 23, 2026

Choose a reason for hiding this comment

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

P1: Tool approval can be bypassed: approved args are only injected as a prompt instruction, but the actual toolCall.input is never revalidated. A model that ignores the instruction can execute the tool with unapproved arguments. Re-run ensureToolApproval on toolCall.input (or verify it matches approvedArgs) before executing/hooks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/agent/agent.ts, line 5256:

<comment>Tool approval can be bypassed: approved args are only injected as a prompt instruction, but the actual toolCall.input is never revalidated. A model that ignores the instruction can execute the tool with unapproved arguments. Re-run ensureToolApproval on toolCall.input (or verify it matches approvedArgs) before executing/hooks.</comment>

<file context>
@@ -5159,25 +5216,58 @@ export class Agent {
       [tool.name]: tool,
     };
 
+    const approvedArgsInstruction = approvedArgs
+      ? `Use these tool arguments: ${safeStringify(approvedArgs)}`
+      : "";
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@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 (4)
packages/voltagent-memory/CHANGELOG.md (1)

231-241: Don't modify historical changelog entries; document API changes in a new version entry.

The embedding configuration in this example (version 0.1.1, released in PR #641) is being updated to show the new string-based API (embedding: "openai/text-embedding-3-small"), but this API pattern is introduced in the current PR #980, not in v0.1.1.

Changelogs should be append-only and historically accurate. Retroactively updating old version examples creates confusion:

  • Users who installed v0.1.1 would have used AiSdkEmbeddingAdapter(openai.embedding("text-embedding-3-small")), not the string format
  • Developers checking historical releases won't see the actual API that was available then
  • Migration guidance is unclear - when exactly did this API change?

Instead, document this embedding model string support as a new feature or migration note in a new changelog entry for the current version (likely 1.0.4 or 2.1.0 depending on semantic versioning strategy). The new entry should:

  1. Explain the new string-based embedding model format
  2. Show both old and new patterns for migration
  3. Indicate whether the old AiSdkEmbeddingAdapter pattern is still supported or deprecated
📝 Recommended changelog structure

Add a new version entry at the top of the changelog:

# `@voltagent/voltagent-memory`

+## 1.0.4 (or next version)
+
+### Patch Changes
+
+- [`#980`](https://github.com/VoltAgent/voltagent/pull/980) feat: support provider-qualified embedding model strings
+
+  Memory now accepts embedding models as plain strings in addition to adapter instances:
+
+  ```typescript
+  // New: provider-qualified model string (recommended)
+  const memory = new Memory({
+    storage: new ManagedMemoryAdapter({ databaseName: "production-memory" }),
+    embedding: "openai/text-embedding-3-small",
+  });
+
+  // Still supported: adapter instance
+  const memory = new Memory({
+    storage: new ManagedMemoryAdapter({ databaseName: "production-memory" }),
+    embedding: new AiSdkEmbeddingAdapter(openai.embedding("text-embedding-3-small")),
+  });
+  ```
+
 ## 1.0.3

And revert lines 231-241 to show the original v0.1.1 API (with AiSdkEmbeddingAdapter).

examples/with-lancedb/src/retriever/index.ts (1)

40-46: Use AiSdkEmbeddingAdapter from @voltagent/core or resolve the string model identifier via ModelProviderRegistry before passing to embed().

The embed function from the ai package expects an EmbeddingModel object, not a string. Passing "openai/text-embedding-3-small" directly will cause a runtime error. The codebase's AiSdkEmbeddingAdapter demonstrates the correct pattern: string identifiers with provider prefixes (e.g., openai/...) must be resolved via ModelProviderRegistry.getInstance().resolveEmbeddingModel() first, then passed to embed(). Either refactor getEmbedding() to use the adapter, or resolve the model string before the embed() call.

website/docs/getting-started/migration-guide.md (1)

331-331: Fix double slash in documentation link.

There's a typo in the link path with a double slash: ../observability//developer-console.md

📝 Proposed fix
-[Learn more](../observability//developer-console.md)
+[Learn more](../observability/developer-console.md)
packages/core/src/agent/agent.ts (1)

4801-4815: Remove the duplicated tool.hooks.onEnd call on the error path.

Line 4809 repeats the exact hook invocation from Line 4801, which can double side-effects/logging when a tool fails.

🩹 Proposed fix
-        await tool.hooks?.onEnd?.({
-          tool,
-          args,
-          output: undefined,
-          error: voltAgentError,
-          options: executionOptions,
-        });
-
-        await tool.hooks?.onEnd?.({
-          tool,
-          args,
-          output: undefined,
-          error: voltAgentError,
-          options: executionOptions,
-        });
+        await tool.hooks?.onEnd?.({
+          tool,
+          args,
+          output: undefined,
+          error: voltAgentError,
+          options: executionOptions,
+        });
🤖 Fix all issues with AI agents
In `@packages/core/src/agent/agent.ts`:
- Around line 5763-5767: The onToolEnd handler currently always returns
undefined, discarding any output override from hooks; change it to await both
options.hooks?.onToolEnd and this.hooks.onToolEnd, capture their return values
(e.g., resOptions and resThis), and return the appropriate non-undefined result
so overrides propagate—prefer the later hook's return (this.hooks.onToolEnd) if
it returns a value, otherwise fall back to the options.hooks.onToolEnd result;
update the function around onToolEnd to return that captured value instead of
always returning undefined.

In `@packages/core/src/memory/adapters/embedding/ai-sdk.spec.ts`:
- Around line 198-213: Replace the ad-hoc resolveSpy.mockRestore() at the end of
the test with a proper per-test cleanup by adding an afterEach hook that calls
vi.restoreAllMocks(), so the spy on
ModelProviderRegistry.getInstance().resolveEmbeddingModel (resolveSpy) is always
restored even if the test fails; locate the test referencing
ModelProviderRegistry, resolveEmbeddingModel, resolveSpy and
AiSdkEmbeddingAdapter and add the afterEach(() => vi.restoreAllMocks()) hook (or
enable restoreMocks in Vitest config) to ensure no spy leakage between tests.

In `@packages/core/src/memory/adapters/embedding/ai-sdk.ts`:
- Around line 39-48: Trim and validate model strings early: in the constructor
(where this.model is set) call this.model = this.model.trim() and if the result
is an empty string throw an error to prevent resolveModel() from returning ""
which later gets passed to embed(...). For non-prefixed model names detected in
resolveModel() (the branch checking hasProviderPrefix), either validate the raw
name format against your acceptable model-name regex/list or route it through
the registry resolution logic (e.g., call the same registry lookup used for
provider-qualified names) before assigning this.modelName and returning; ensure
resolveModel(), constructor, this.model and this.modelName are updated
accordingly so empty/whitespace models are rejected and non-prefixed names are
validated or resolved.

In `@packages/core/src/tool/routing/embedding.ts`:
- Around line 49-53: The parameter normalization logic in embedding.ts is
inverted: change the conditional for tool.parameters so Zod schema objects are
converted with zodSchemaToJsonUI and non-object values are used directly;
specifically, in the block that builds `normalized` (referencing
`tool.parameters`, `zodSchemaToJsonUI`, and `safeStringify`) invert the ternary
so it calls `zodSchemaToJsonUI(tool.parameters)` when `typeof tool.parameters
=== "object"` and otherwise uses `tool.parameters`, then push the stringified
result as before.

In `@website/docs/tools/tool-routing.md`:
- Around line 238-243: Remove the duplicate embedding model bullet in the list
under the embedding router description by deleting the repeated
"openai/text-embedding-3-small" entry so the list contains unique items
("openai/text-embedding-3-small", "text-embedding-3-small"); update the
surrounding text if necessary to keep punctuation/formatting consistent.

In `@website/recipes/tool-routing.md`:
- Around line 124-133: The example references mcpToolkit but never defines or
imports it, causing confusion; update the toolRouting.pool example by either
adding a short MCP toolkit import/initialization (create an MCPToolkit instance
and assign it to mcpToolkit) or replace mcpToolkit with a concrete,
self-contained tool instance, and/or add a comment linking to the MCP docs;
specifically modify the snippet that defines toolRouting.pool and introduce
MCPToolkit (or a concrete alternative) so mcpToolkit is a valid symbol.
♻️ Duplicate comments (1)
packages/core/src/agent/agent.ts (1)

5277-5318: Approval can still be bypassed for provider tools.

Line 5277 only injects approvedArgs as prompt text; the actual toolCall.input is never validated against those approved args before the tool executes, so a model can call the tool with unapproved inputs. This was flagged earlier and still appears present.

Suggested direction: re-validate toolCall.input against approvedArgs (or re-run ensureToolApproval on the actual input) before acting on results, and hard-fail if they diverge.

🧹 Nitpick comments (8)
examples/with-auth/src/index.ts (1)

34-36: Consider adding a comment clarifying this is a placeholder secret.

The hardcoded "super-secret" JWT secret is fine for an example, but a brief inline comment would help users understand they must replace it in production.

💡 Suggested improvement
       provider: jwtAuth({
-        secret: "super-secret",
+        secret: "super-secret", // TODO: Replace with a secure secret in production
       }),
examples/with-tool-routing/README.md (1)

45-53: Consider expanding the README with usage details and code snippets.

Compared to other example READMEs (e.g., with-working-memory), this README lacks:

  • A "Highlights" section explaining what tool routing provides
  • A code snippet demonstrating the tool routing configuration
  • "Run Locally" instructions

Since tool routing is a new feature, users would benefit from a brief explanation and example code directly in the README.

docs/tool-routing-plan.md (1)

21-24: Clarify embedding strategy wording for model references.

Since routing can now be enabled via model reference strings, consider updating the scope bullet so it doesn’t imply adapters are required.

✏️ Suggested wording tweak
- - Embedding strategy: optional selector using EmbeddingAdapter / AiSdkEmbeddingAdapter.
+ - Embedding strategy: optional selector using EmbeddingModelReference (model string) or EmbeddingAdapter.
packages/core/src/memory/types.ts (1)

229-277: Align embedding docs and adapter type surface.

MemoryConfig.embedding now accepts config objects (model + options), so the doc comment should reflect that. Also, EmbeddingAdapterInput now mixes EmbeddingAdapter with EmbeddingModelReference from the adapters module—worth aligning the EmbeddingAdapter signature across modules to avoid drift (notably getDimensions optionality).

✏️ Doc comment update
-  * Optional embedding adapter or model reference for semantic operations
+  * Optional embedding adapter, model reference, or config (model + options) for semantic operations
packages/core/src/memory/adapters/embedding/ai-sdk.ts (2)

11-19: Type safety issue: model field typed as EmbeddingModel but assigned a string.

The model field is declared as EmbeddingModel (line 11), but the constructor assigns normalizedModel which can be a string (line 19). This creates a type inconsistency that TypeScript may not catch due to the any-like nature of some AI SDK types.

Consider using a union type or separate fields:

♻️ Suggested type fix
 export class AiSdkEmbeddingAdapter implements EmbeddingAdapter {
-  private model: EmbeddingModel;
+  private model: EmbeddingModel | string;
   private dimensions: number;
   private modelName: string;
   private options: EmbeddingOptions;
   private modelResolvePromise?: Promise<EmbeddingModel>;

51-63: Consider not clearing modelResolvePromise to enable caching.

Currently, modelResolvePromise is cleared in finally(). While this works correctly (subsequent calls check typeof this.model !== "string" first), keeping the resolved promise cached would be slightly more efficient and clearer in intent.

♻️ Optional: Keep promise for caching
     this.modelResolvePromise = ModelProviderRegistry.getInstance()
       .resolveEmbeddingModel(trimmed)
       .then((resolved) => {
         this.model = resolved;
         this.modelName = trimmed;
         return resolved;
-      })
-      .finally(() => {
-        this.modelResolvePromise = undefined;
       });

     return this.modelResolvePromise;
packages/core/src/memory/index.ts (1)

47-68: Unreachable fallback case at line 67.

The resolveEmbeddingAdapter function handles all expected input types (undefined, EmbeddingAdapter, string, EmbeddingAdapterConfig), but line 67 appears unreachable. If EmbeddingAdapterInput is a union of these types, this fallback should never execute. If it can execute, passing an unknown type to AiSdkEmbeddingAdapter may cause unexpected behavior.

Consider either removing the unreachable fallback or adding a type assertion to satisfy the exhaustiveness check:

♻️ Suggested improvement
  if (isEmbeddingAdapterConfig(embedding)) {
    const { model, ...options } = embedding;
    return new AiSdkEmbeddingAdapter(model, options);
  }

-  return new AiSdkEmbeddingAdapter(embedding);
+  // Exhaustive check - this should be unreachable if EmbeddingAdapterInput is properly typed
+  const _exhaustive: never = embedding;
+  return _exhaustive;
packages/core/src/tool/routing/embedding.ts (1)

100-109: Defensive fallback returns empty embedding.

At line 103, if a cache entry is missing, an empty array is returned. This results in a 0 similarity score (line 165). While this is safe, it could silently hide bugs where the cache wasn't populated correctly. Consider logging a warning for this unexpected case.

@omeraplak
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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)
packages/core/src/agent/agent.ts (1)

4801-4815: Avoid calling tool.hooks.onEnd twice on errors.

This can duplicate side effects (metrics, cleanup, state changes) and is likely unintended.

🛠️ Proposed fix
         await tool.hooks?.onEnd?.({
           tool,
           args,
           output: undefined,
           error: voltAgentError,
           options: executionOptions,
         });
-
-        await tool.hooks?.onEnd?.({
-          tool,
-          args,
-          output: undefined,
-          error: voltAgentError,
-          options: executionOptions,
-        });
🤖 Fix all issues with AI agents
In `@website/recipes/tool-routing.md`:
- Around line 95-97: The resolver currently returns metadata ({ query, tool:
tool.name }) instead of the tool's argument object expected by the tool's
parameter schema; update the resolver (function named resolver) to produce and
return an arguments object that matches the tool's expected params (e.g., for
get_weather return { location: <derived-from-query> } or call the tool's
argument-construction/validation helper if available) rather than only
tool.name, ensuring the returned shape matches get_weather's parameter schema.
🧹 Nitpick comments (6)
packages/core/src/memory/adapters/embedding/ai-sdk.spec.ts (1)

186-216: Consider adding test coverage for edge cases in model resolution.

The test validates the happy path for provider-qualified strings, but the implementation has several branches that lack test coverage:

  1. Non-prefixed bare model names (e.g., "text-embedding-ada-002")
  2. Invalid bare model names that should throw
  3. Empty/whitespace-only strings passed to the constructor
  4. Resolution failure scenarios from the registry
♻️ Suggested additional tests
it("should accept valid bare model names without registry resolution", async () => {
  const { embed } = await import("ai");
  vi.mocked(embed).mockResolvedValue({
    value: "test text",
    embedding: [0.1, 0.2],
    usage: { tokens: 10 },
  } as EmbedResult<string>);

  const bareAdapter = new AiSdkEmbeddingAdapter("text-embedding-ada-002");
  await bareAdapter.embed("test text");

  expect(embed).toHaveBeenCalledWith({
    model: "text-embedding-ada-002",
    value: "test text",
  });
});

it("should throw for invalid bare model names", async () => {
  const invalidAdapter = new AiSdkEmbeddingAdapter("invalid model name!");
  await expect(invalidAdapter.embed("test")).rejects.toThrow('Invalid embedding model id');
});

it("should throw for empty model string", () => {
  expect(() => new AiSdkEmbeddingAdapter("")).toThrow("Embedding model is required.");
  expect(() => new AiSdkEmbeddingAdapter("   ")).toThrow("Embedding model is required.");
});
packages/core/src/memory/adapters/embedding/ai-sdk.ts (2)

41-76: Race condition: concurrent callers may trigger redundant registry lookups.

When modelResolvePromise is cleared in .finally(), subsequent concurrent calls that arrive after resolution completes but before this.model is checked will start a new registry lookup, even though the model was already resolved.

The current flow:

  1. Call A starts resolution, sets modelResolvePromise
  2. Call B waits on modelResolvePromise
  3. Resolution completes, this.model is updated, .finally() clears modelResolvePromise
  4. Call C arrives, sees typeof this.model !== "string" → returns immediately ✓

This is actually safe because this.model is updated in .then() before .finally() clears the promise. However, if resolution fails, .finally() still clears the promise, allowing retries on subsequent calls—which may be intentional for transient failures.

One minor robustness improvement: avoid the redundant trim() call on line 49 since the constructor already trims.

♻️ Minor cleanup
  private async resolveModel(): Promise<EmbeddingModel> {
    if (typeof this.model !== "string") {
      return this.model;
    }
    if (this.modelResolvePromise) {
      return this.modelResolvePromise;
    }

-   const trimmed = this.model.trim();
-   if (!trimmed) {
-     throw new Error("Embedding model is required.");
-   }
+   // Constructor guarantees non-empty trimmed string
+   const trimmed = this.model;

    const hasProviderPrefix = trimmed.includes("/") || trimmed.includes(":");

64-76: Error from registry resolution is not propagated with context.

If resolveEmbeddingModel throws (e.g., provider not registered), the error bubbles up directly. Unlike embed() and embedBatch() which wrap errors with context messages, resolution failures will surface raw registry errors.

Consider wrapping the registry call for consistent error messaging:

♻️ Suggested improvement
    this.modelResolvePromise = ModelProviderRegistry.getInstance()
      .resolveEmbeddingModel(trimmed)
      .then((resolved) => {
        this.model = resolved;
        this.modelName = trimmed;
        return resolved;
      })
+     .catch((error) => {
+       throw new Error(
+         `Failed to resolve embedding model "${trimmed}": ${error instanceof Error ? error.message : String(error)}`,
+       );
+     })
      .finally(() => {
        this.modelResolvePromise = undefined;
      });
website/recipes/tool-routing.md (3)

57-65: Incomplete code snippet may confuse readers.

The toolRouting: snippet is shown as a standalone object literal without the surrounding Agent or VoltAgent context. Consider wrapping it in a complete example or adding a comment indicating where this config belongs.

📝 Suggested improvement
 ## Explicit Pools (Two Categories)
 
 ```typescript
 const weatherPool = [weatherTool];
 const financePool = [convertCurrencyTool];
 
+const agent = new Agent({
+  name: "Pool Agent",
+  model: openai("gpt-4o-mini"),
-toolRouting: {
-  embedding: "openai/text-embedding-3-small",
-  pool: [...weatherPool, ...financePool],
-}
+  toolRouting: {
+    embedding: "openai/text-embedding-3-small",
+    pool: [...weatherPool, ...financePool],
+  },
+});
</details>

---

`71-76`: **Same issue: standalone `toolRouting` config.**

This snippet has the same problem—readers may not immediately understand where to place this configuration.

---

`137-142`: **Standalone `toolRouting` block without surrounding context.**

After properly importing and initializing `mcp`, the final `toolRouting:` block is still a fragment. Consider wrapping it in a complete `Agent` or `VoltAgent` instantiation for clarity.

<details>
<summary>📝 Suggested improvement</summary>

```diff
 const mcpTools = await mcp.getTools();
 
-toolRouting: {
-  pool: [
-    openai.tools.webSearch(),
-    ...mcpTools,
-  ],
-}
+const agent = new Agent({
+  name: "MCP Agent",
+  model: openai("gpt-4o-mini"),
+  toolRouting: {
+    embedding: "openai/text-embedding-3-small",
+    pool: [
+      openai.tools.webSearch(),
+      ...mcpTools,
+    ],
+  },
+});

Comment on lines +95 to +97
resolver: async ({ query, tool }) => {
return { query, tool: tool.name };
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Resolver returns metadata instead of tool arguments.

The resolver function returns { query, tool: tool.name } which appears to be metadata rather than arguments that match the tool's parameter schema. For example, if get_weather expects { location: string }, this resolver wouldn't provide valid arguments.

Consider updating to demonstrate proper argument resolution:

📝 Suggested improvement
   resolver: async ({ query, tool }) => {
-    return { query, tool: tool.name };
+    // Extract location from query for weather tool, etc.
+    if (tool.name === "get_weather") {
+      return { location: query };
+    }
+    return { query };
   },
📝 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
resolver: async ({ query, tool }) => {
return { query, tool: tool.name };
},
resolver: async ({ query, tool }) => {
// Extract location from query for weather tool, etc.
if (tool.name === "get_weather") {
return { location: query };
}
return { query };
},
🤖 Prompt for AI Agents
In `@website/recipes/tool-routing.md` around lines 95 - 97, The resolver currently
returns metadata ({ query, tool: tool.name }) instead of the tool's argument
object expected by the tool's parameter schema; update the resolver (function
named resolver) to produce and return an arguments object that matches the
tool's expected params (e.g., for get_weather return { location:
<derived-from-query> } or call the tool's argument-construction/validation
helper if available) rather than only tool.name, ensuring the returned shape
matches get_weather's parameter schema.

@omeraplak omeraplak merged commit b65715e into main Jan 23, 2026
23 checks passed
@omeraplak omeraplak deleted the feat/tool-routing branch January 23, 2026 04:46
@coderabbitai coderabbitai bot mentioned this pull request Apr 1, 2026
5 tasks
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.

[Feature Request] Layered/Meta Tool Routing to Reduce Agent Context Token Usage

1 participant