Skip to content

fix(core): preserve user callTool/searchTools with tool routing#1117

Merged
omeraplak merged 2 commits into
mainfrom
fix/core-tool-routing-user-tool-precedence-1116
Feb 28, 2026
Merged

fix(core): preserve user callTool/searchTools with tool routing#1117
omeraplak merged 2 commits into
mainfrom
fix/core-tool-routing-user-tool-precedence-1116

Conversation

@omeraplak

@omeraplak omeraplak commented Feb 28, 2026

Copy link
Copy Markdown
Member

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

When toolRouting is enabled, user-defined tools named callTool and searchTools are treated as internal routing tools by name and get replaced/filtered, so they are not actually registered as user tools.

What is the new behavior?

  • Internal routing tools are identified only by the internal symbol marker (not by name).
  • If a user defines callTool/searchTools, those user-defined tools take precedence and internal support tools are not inserted over them.
  • Tool routing state/pool filtering now excludes only true internal support tools.
  • toolRouting: false per-request checks true internal support tools instead of reserved names, so user-defined same-name tools are allowed.

fixes #1116

Notes for reviewers

  • Added regression tests in packages/core/src/agent/agent.spec.ts:
    • should prefer user-defined callTool/searchTools when tool routing is enabled
    • should allow user-defined callTool/searchTools when tool routing is disabled per request
  • Test run:
    • pnpm --filter @voltagent/core exec vitest run src/agent/agent.spec.ts -t "callTool/searchTools"

Summary by cubic

Preserves user-defined callTool and searchTools with tool routing and removes name-based conflicts. Internal tools use a symbol marker, pool filtering is tighter, and per-request toolRouting: false works with same-name user tools.

  • Bug Fixes
    • Detect internal routing tools by symbol only; no reserved names.
    • Skip inserting internal support tools when a user tool uses the same name; no error thrown.
    • Filter routing pool/state and toolRouting: false checks to exclude only true internal tools. Regression tests added.

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

Summary by CodeRabbit

  • Bug Fixes

    • Tool routing now correctly prioritizes user-defined tools (including those named searchTools or callTool) over internal routing helpers.
    • Prevented user-defined tools with those names from being silently filtered out of the routing pool.
    • Fixed failures when tool routing was disabled but user tools reused those names.
  • Tests

    • Added tests validating user-defined tool behavior with routing enabled and disabled.

@changeset-bot

changeset-bot Bot commented Feb 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f850d08

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

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb9eca2 and f850d08.

📒 Files selected for processing (2)
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/agent/agent.spec.ts

📝 Walkthrough

Walkthrough

Reworks agent tool-routing conflict detection to prefer user-defined tools over internal routing helpers by detecting internal routing tools via a symbol property instead of reserved names; adds tests and a release note documenting the behavior change so user tools named callTool or searchTools are preserved and routed correctly.

Changes

Cohort / File(s) Summary
Release Documentation
.changeset/soft-dots-bow.md
Added patch release note describing that user-defined searchTools and callTool now take precedence over internal routing tools when tool routing is enabled.
Tests
packages/core/src/agent/agent.spec.ts
Added tests to prepareTools validating that user-defined callTool and searchTools are preferred and executed both when tool routing is enabled and when tool routing is disabled per request; exposes test internals for harnessing.
Core Tool Routing Logic
packages/core/src/agent/agent.ts
Changed detection of internal routing tools to use the TOOL_ROUTING_INTERNAL_TOOL_SYMBOL property (via own-property check) instead of name matching; updated conflict collection and messaging to reference internal routing tools; changed upsert behavior to skip (debug log) rather than throw on reserved-name collisions; updated pool filtering to exclude internal routing tools by symbol.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped where names once caused a fight,
Symbols glowed and cleared the night,
User tools now lead the dance,
callTool, searchTools get their chance,
Hooray — routed right! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: preserving user-defined callTool/searchTools tools when tool routing is enabled, fixing the core issue addressed in the PR.
Description check ✅ Passed The PR description follows the template with all required sections completed: checklist items marked, current/new behavior explained, linked issue referenced, tests documented, and changeset added.
Linked Issues check ✅ Passed The code changes fully address issue #1116 by ensuring user-defined callTool and searchTools are preserved, use symbol-based detection instead of name-based filtering, and prioritize user tools over internal ones.
Out of Scope Changes check ✅ Passed All changes in the PR are directly related to fixing the tool routing conflict issue: the changeset documents the fix, tests verify the expected behavior, and agent.ts implements the necessary modifications to preserve user tools.
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 (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 fix/core-tool-routing-user-tool-precedence-1116

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.

@ghost

This comment has been minimized.

@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 3 files

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

🧹 Nitpick comments (2)
packages/core/src/agent/agent.spec.ts (1)

3031-3041: Avoid introducing new as any in these regression tests to maintain type safety.

The new tests at lines 3039-3040 and 3079-3080 use (agent as any) to access internal methods, which violates the TypeScript type safety guideline. Replace with a narrow test-only interface via unknown cast for better compile-time safety:

Suggested refactor
+type AgentPrepareToolsTestApi = {
+  createOperationContext: (input: string) => {
+    traceContext: { end: (status: string) => void };
+  };
+  prepareTools: (
+    runtimeTools: Tool[],
+    operationContext: { traceContext: { end: (status: string) => void } },
+    maxSteps: number,
+    options: { toolRouting?: false | Record<string, unknown> },
+  ) => Promise<Record<string, { execute: (args: Record<string, string>) => Promise<unknown> }>>;
+};

-      const operationContext = (agent as any).createOperationContext("input message");
-      const prepared = await (agent as any).prepareTools([], operationContext, 3, {});
+      const testAgent = agent as unknown as AgentPrepareToolsTestApi;
+      const operationContext = testAgent.createOperationContext("input message");
+      const prepared = await testAgent.prepareTools([], operationContext, 3, {});

Apply the same change at lines 3079-3080.

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

In `@packages/core/src/agent/agent.spec.ts` around lines 3031 - 3041, The tests
currently use unsafe `(agent as any)` casts to call internal methods; define a
narrow test-only interface (e.g., `interface TestAgentInternals {
createOperationContext(input: string): ReturnType; prepareTools(args: any[],
ctx: any, n: number, opts: any): Promise<any>; }`) that declares only the
internal members you need, then replace `(agent as any)` with `agent as unknown
as TestAgentInternals` when calling `createOperationContext` and `prepareTools`
(apply the same change for both call sites), removing the `as any` usages to
restore type safety while limiting exposure to internals.
packages/core/src/agent/agent.ts (1)

7189-7197: Prefer marker-based filtering before API projection (avoid name-based fallback).

Line 7197 still excludes by name. If internal and user-defined same-name tools ever coexist in the pool, this can hide the user tool from toolRouting.pool state output.

♻️ Suggested refactor
-          const internalPoolSupportNames = new Set(
-            this.toolPoolManager
-              .getAllTools()
-              .filter((tool) => this.isToolRoutingSupportTool(tool))
-              .map((tool) => tool.name),
-          );
-          const poolApiTools = this.toolPoolManager
-            .getToolsForApi()
-            .filter((tool) => !internalPoolSupportNames.has(tool.name));
+          const poolToolsWithoutInternalSupport = this.toolPoolManager
+            .getAllTools()
+            .filter((tool) => !this.isToolRoutingSupportTool(tool));
+          const poolApiTools = new ToolManager(
+            poolToolsWithoutInternalSupport,
+            this.logger,
+          ).getToolsForApi();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 7189 - 7197, The current
exclusion uses tool.name which can hide user tools if names collide; instead
derive the support-tool identity from the marker-based predicate and exclude by
a stable identifier before producing the API projection: create a Set of support
tool ids from this.toolPoolManager.getAllTools().filter(tool =>
this.isToolRoutingSupportTool(tool)).map(t => t.id) and then build poolApiTools
by filtering this.toolPoolManager.getToolsForApi() by !supportIds.has(tool.id)
(not by name), so toolRouting.pool uses marker-aware filtering rather than
name-based fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/agent/agent.spec.ts`:
- Around line 3031-3041: The tests currently use unsafe `(agent as any)` casts
to call internal methods; define a narrow test-only interface (e.g., `interface
TestAgentInternals { createOperationContext(input: string): ReturnType;
prepareTools(args: any[], ctx: any, n: number, opts: any): Promise<any>; }`)
that declares only the internal members you need, then replace `(agent as any)`
with `agent as unknown as TestAgentInternals` when calling
`createOperationContext` and `prepareTools` (apply the same change for both call
sites), removing the `as any` usages to restore type safety while limiting
exposure to internals.

In `@packages/core/src/agent/agent.ts`:
- Around line 7189-7197: The current exclusion uses tool.name which can hide
user tools if names collide; instead derive the support-tool identity from the
marker-based predicate and exclude by a stable identifier before producing the
API projection: create a Set of support tool ids from
this.toolPoolManager.getAllTools().filter(tool =>
this.isToolRoutingSupportTool(tool)).map(t => t.id) and then build poolApiTools
by filtering this.toolPoolManager.getToolsForApi() by !supportIds.has(tool.id)
(not by name), so toolRouting.pool uses marker-aware filtering rather than
name-based fallback.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0f1915 and eb9eca2.

📒 Files selected for processing (3)
  • .changeset/soft-dots-bow.md
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Feb 28, 2026

Copy link
Copy Markdown

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: f850d08
Status:⚡️  Build in progress...

View logs

@omeraplak
omeraplak merged commit 8cb05dc into main Feb 28, 2026
22 of 23 checks passed
@omeraplak
omeraplak deleted the fix/core-tool-routing-user-tool-precedence-1116 branch February 28, 2026 04:15
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.

Custom tools (searchTools and callTool) are being filtered out/not registered

1 participant