Skip to content

feat: replace tool routers with searchTools + callTool tool routing#991

Merged
omeraplak merged 4 commits into
mainfrom
feat/improvement-tool-routing
Jan 28, 2026
Merged

feat: replace tool routers with searchTools + callTool tool routing#991
omeraplak merged 4 commits into
mainfrom
feat/improvement-tool-routing

Conversation

@omeraplak

@omeraplak omeraplak commented Jan 27, 2026

Copy link
Copy Markdown
Member

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

Replaces router tools with searchTools and callTool for tool routing. Simplifies the model surface, adds embedding-based search, validates tool calls, and updates docs and examples. Fixes #989.

  • New Features

    • searchTools: ranks tools from a hidden pool (embedding-based when configured) and returns metadata + schemas.
    • callTool: executes by name with schema-validated args; runs approvals and hooks; supports provider/MCP tools.
    • Config: toolRouting now supports embedding, pool, expose, topK, enforceSearchBeforeCall (default true).
    • Observability: new tool.search.* spans and selection metadata; API state reports search/call/expose/pool.
  • Migration

    • Remove createToolRouter and toolRouting.routers.
    • Remove router-specific config and APIs (mode, executionModel, parallel, resolver).
    • Update agent instructions to use searchTools then callTool.
    • Configure toolRouting.embedding/pool/topK as needed; set enforceSearchBeforeCall: false to relax the flow.
    • Ensure no tool name conflicts with reserved names: searchTools, callTool.

Written for commit 02aa85a. Summary will update on new commits.

Summary by CodeRabbit

  • Breaking Changes

    • Router-based tool APIs removed; routing configuration shape changed to a search+call model.
  • New Features

    • Model-facing two-step flow: searchTools to discover tools, then callTool to invoke by exact name with schema-validated args.
    • Optional enforceSearchBeforeCall toggle and embedding-based search ranking; tool pool hidden by default unless exposed.
  • Documentation

    • Changelogs, guides, examples, and blog updated with migration examples and the new search-first workflow.

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

@changeset-bot

changeset-bot Bot commented Jan 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 02aa85a

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

This PR includes changesets to release 24 packages
Name Type
@voltagent/core Major
@voltagent/a2a-server Major
@voltagent/cloudflare-d1 Major
@voltagent/langfuse-exporter Major
@voltagent/libsql Major
@voltagent/mcp-server Major
@voltagent/postgres Major
@voltagent/resumable-streams Major
@voltagent/scorers Major
@voltagent/sdk Patch
@voltagent/server-core Major
@voltagent/server-elysia Major
@voltagent/server-hono Major
@voltagent/serverless-hono Major
@voltagent/supabase Major
@voltagent/voice Major
@voltagent/voltagent-memory Major
ai-ad-generator Patch
assistant-ui-starter Patch
with-jwt-auth Patch
example-with-live-evals Patch
voltagent-with-copilotkit-server Patch
@voltagent/evals Major
@voltagent/cli 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

@ghost

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jan 27, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR replaces router-based tool routing with a search-first workflow: agents expose two system tools—searchTools (discover tools + schemas) and callTool (invoke a named tool with validated args). Routing config now uses embedding/topK/pool/expose and an optional enforceSearchBeforeCall flag; types/exports renamed from ToolRouter* → ToolSearch*.

Changes

Cohort / File(s) Summary
Core Agent Implementation
packages/core/src/agent/agent.ts
Adds searchTools/callTool system tools, embedding-based search strategy wiring, candidate ranking/selection, search-result persistence, argument/schema validation on call, provider tool call paths, tool execution factory enhancements, and tracing/observability updates.
Routing Types & Constants
packages/core/src/tool/routing/types.ts, packages/core/src/tool/routing/constants.ts, packages/core/src/tool/routing/index.ts
Renames ToolRouter* → ToolSearch* types, replaces TOOL_ROUTER_SYMBOL with TOOL_ROUTING_SEARCH_TOOL_NAME/TOOL_ROUTING_CALL_TOOL_NAME and TOOL_ROUTING_INTERNAL_TOOL_SYMBOL, removes router factory/types/exports, and publishes new search-related types.
Embedding Strategy
packages/core/src/tool/routing/embedding.ts
Renames createEmbeddingToolRouterStrategycreateEmbeddingToolSearchStrategy, switches types to search variants, normalizes parameter text generation, and updates embedding metric/trace keys from "router" → "search".
Tool Public Surface & Exports
packages/core/src/tool/index.ts
Removes createToolRouter and router-related exports/types; exports embedding search strategy and new ToolSearch* types.
Agent Types & Context
packages/core/src/agent/types.ts, packages/core/src/agent/context-keys.ts
Changes AgentToolRoutingState shape (routerssearch/call, adds enforceSearchBeforeCall, topK), and adds context keys for routing config and searched-tools tracking.
Docs, Examples & Changelog
website/docs/tools/tool-routing.md, website/recipes/tool-routing.md, website/blog/2026-01-23-tool-routing/index.md, examples/with-tool-routing/src/index.ts, packages/core/CHANGELOG.md, .changeset/rare-papayas-bake.md, docs/tool-routing-plan.md (removed)
Rewrites documentation/examples to the search-first pattern (searchTools then callTool), documents enforceSearchBeforeCall, removes the old routing plan doc, and updates blog/changelog to reflect breaking API changes.
Public Types / Config Docs
packages/core/src/types.ts
Updates VoltAgentOptions.toolRouting docs to describe the search + call workflow and visibility rules.

Sequence Diagram(s)

sequenceDiagram
    participant Model as Model/Agent
    participant SearchTool as searchTools
    participant CallTool as callTool
    participant ToolPool as Tool Pool
    participant Tool as Target Tool

    Model->>SearchTool: searchTools(user query)
    activate SearchTool
    Note over SearchTool: embedding-based ranking (topK)\nreturns ToolSearchResult with schemas
    SearchTool-->>Model: ranked results
    deactivate SearchTool

    Note over Model: record searched tools in context

    Model->>CallTool: callTool(toolName, validatedArgs)
    activate CallTool
    Note over CallTool: validate args against schema
    opt enforceSearchBeforeCall
        CallTool->>CallTool: assert tool was in prior search
    end
    CallTool->>ToolPool: locate tool by name
    ToolPool-->>CallTool: tool reference
    CallTool->>Tool: execute(args)
    Tool-->>CallTool: result
    CallTool-->>Model: execution result
    deactivate CallTool
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code to make tools sing,

First I sniff the carrots, then I pull the string.
Search finds the spells, schemas set them right,
I call with care and trace the flight.
A joyful hop — tools found and bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replacing tool routers with searchTools and callTool, which is the primary objective of this PR.
Description check ✅ Passed The PR description includes all required checklist items marked complete, clearly articulates current vs new behavior, links related issue #989, and provides comprehensive migration notes and feature summary.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #989: embedding-based tool search (configurable topK), robust ranking for ambiguous inputs, schema-validated tool calls, parameter mapping correctness, and tool metadata exposure in model-friendly form.
Out of Scope Changes check ✅ Passed All changes are directly scoped to tool routing refactoring: searchTools/callTool implementation, embedding integration, config restructuring, observability updates, documentation, and examples—all aligned with issue #989 objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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.

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

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.

No issues found across 16 files

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jan 27, 2026

Copy link
Copy Markdown

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: 02aa85a
Status: ✅  Deploy successful!
Preview URL: https://da90182d.voltagent.pages.dev
Branch Preview URL: https://feat-improvement-tool-routin.voltagent.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

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.

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 (1)
packages/core/src/tool/routing/embedding.ts (1)

40-65: Add a safe fallback if schema normalization throws.

normalizeParametersForText assumes schema conversion will always succeed; a thrown error here would break tool routing. A small try/catch keeps routing resilient.

🛠️ Proposed fix
 const normalizeParametersForText = (value: unknown): unknown => {
   if (!value || typeof value !== "object") {
     return value;
   }

   const candidate = value as { _def?: unknown; def?: unknown };
   if (candidate._def || candidate.def) {
-    return zodSchemaToJsonUI(value);
+    try {
+      return zodSchemaToJsonUI(value);
+    } catch {
+      return value;
+    }
   }

   return value;
 };
🤖 Fix all issues with AI agents
In `@docs/tool-routing-plan.md`:
- Around line 54-59: Update the "### 6) Tests" checklist to reflect the current
implementation status: for the header "### 6) Tests" edit the four items
("Search selection tests (embedding + fallback)", "callTool validation + error
path tests", "Provider tool fallback tests", "Embedding cache tests") to mark
completed tests with checked boxes or remove/annotate items that are not
applicable, and add brief references to the corresponding test files or test
suites (e.g., unit/integration test names) if available so the checklist is no
longer stale.

In `@packages/core/src/agent/agent.ts`:
- Around line 4791-4800: When toolRouting is false, do not silently filter out
tools named by getToolRoutingSupportToolNames; instead detect conflicts between
supportNames and keys of preparedStaticTools/preparedDynamicTools and surface a
clear error. In the block after resolving toolRouting (resolveToolRouting and
setting TOOL_ROUTING_CONTEXT_KEY), compute the intersection of supportNames
(from getToolRoutingSupportToolNames) with Object.keys(preparedStaticTools) and
Object.keys(preparedDynamicTools); if any conflicts exist, throw an Error (or
use the agent’s logger) listing the conflicting tool names and referencing
toolRouting so callers can fix naming, rather than silently removing entries.
- Around line 5549-5562: The code currently throws a generic Error when
needsApproval === true which bypasses the ToolDeniedError handling; remove the
throw and always delegate to ensureToolApproval so approval failures follow the
existing abort flow. Replace the throw in the needsApproval === true branch with
a call to this.ensureToolApproval(tool, args, executionOptions,
executionOptions.toolContext?.callId ?? randomUUID()), ensuring
ensureToolApproval (and ToolDeniedError) handles denials for provider tools and
uses tool.name/args/executionOptions for context.
- Around line 5597-5604: The current comparison uses safeStringify(requested)
!== safeStringify(received) which can fail for equivalent objects with different
key orders; replace this stringified check with a deep object equality check
(e.g., use a reliable utility like deepEqual or lodash.isEqual) to compare args
and toolCall.input (callInput) directly and keep the existing error throw
referencing tool.name if the deep equality fails; update imports and tests
accordingly so the equality check uses the chosen deep comparer rather than
safeStringify.

Comment thread docs/tool-routing-plan.md Outdated
Comment thread packages/core/src/agent/agent.ts Outdated
Comment thread packages/core/src/agent/agent.ts Outdated
Comment thread packages/core/src/agent/agent.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/agent/agent.ts`:
- Around line 5160-5176: The search candidates should exclude tools that cannot
be executed by callTool: in buildToolSearchCandidates, after the existing
isToolRoutingSupportTool filter, also filter out tools that are non-executable
by checking for an executable signature (e.g., typeof (tool as any).call !==
"function" or an explicit flag like (tool as any).callable === false) so only
runnable tools reach the map; reference buildToolSearchCandidates,
toolPoolManager.getAllTools, isToolRoutingSupportTool, isProviderTool and
callTool when making the change.

Comment thread packages/core/src/agent/agent.ts
toolCallId,
error: `Tool ${tool.name} requires approval.`,
};
private recordSearchedTools(options: ToolExecuteOptions | undefined, toolNames: string[]): void {

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.

There's a minor issue here: the context only exists in a single agent call. This means that multiple calls to the same conversation will require the agent to repeatedly use the search tool, even if the context has already been searched. A better approach would be to store the records in the conversation.

what about do you think? @omeraplak

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

hmmm the conversation table might not be a perfect fit for this. We could consider creating a separate table instead.

As a temporary workaround, something like this could work. You keep a per-conversation context in memory and pass the same context object across calls so the agent can maintain state between messages.

// simple in-memory store (replace with Redis/DB in production)
const conversationContexts = new Map<string, Map<string | symbol, unknown>>();

function getConversationContext(conversationId: string) {
  let ctx = conversationContexts.get(conversationId);
  if (!ctx) {
    ctx = new Map();
    conversationContexts.set(conversationId, ctx);
  }
  return ctx;
}

// usage
const conversationId = "conv_123";
const context = getConversationContext(conversationId);

await agent.streamText("first message", { conversationId, context });
await agent.streamText("second message", { conversationId, context });

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 0

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)

5567-5659: Force deterministic temperature for provider-tool proxy calls.

Using the agent’s temperature can make the proxy call non-deterministic and trigger false arg-mismatch errors. This should be forced to 0 to keep callTool stable.

🔧 Proposed fix
-      temperature: this.temperature ?? 0,
+      temperature: 0,

@omeraplak
omeraplak merged commit e0b6693 into main Jan 28, 2026
21 checks passed
@omeraplak
omeraplak deleted the feat/improvement-tool-routing branch January 28, 2026 04:02
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.

This routing tool doesn't work very well. [Feature Request] Layered/Meta Tool Routing to Reduce Agent Context Token Usage

2 participants