From 0c370469a704fe4606dd3975ddf3db1133f8f281 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Mon, 13 Jul 2026 17:42:03 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20avoid=20OpenAI=20tool=20s?=
=?UTF-8?q?earch=20name=20collision?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Rename Mux's deferred tool catalog search and preserve compatibility with completed legacy history.
> Mux worked on behalf of Mike.
---
_Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `xhigh` • Cost: `$27.56`_
---
docs/hooks/tools.mdx | 2 +-
.../features/Tools/Shared/ToolPrimitives.tsx | 1 +
.../Tools/Shared/getToolComponent.test.ts | 16 +-
.../features/Tools/Shared/getToolComponent.ts | 10 +-
.../features/Tools/ToolSearchToolCall.tsx | 4 +-
src/common/constants/experiments.ts | 2 +-
src/common/types/tools.ts | 2 +-
src/common/utils/tools/toolCatalog.test.ts | 136 ++++++++++----
src/common/utils/tools/toolCatalog.ts | 166 ++++++++++++++----
.../utils/tools/toolDefinitions.test.ts | 10 +-
src/common/utils/tools/toolDefinitions.ts | 8 +-
src/common/utils/tools/tools.ts | 4 +-
.../builtInSkillContent.generated.ts | 2 +-
src/node/services/aiService.ts | 13 +-
src/node/services/messagePipeline.ts | 5 +-
src/node/services/streamManager.test.ts | 10 +-
src/node/services/streamManager.ts | 4 +-
src/node/services/tools/toolSearch.test.ts | 85 +++++++++
src/node/services/tools/toolSearch.ts | 6 +-
19 files changed, 384 insertions(+), 102 deletions(-)
create mode 100644 src/node/services/tools/toolSearch.test.ts
diff --git a/docs/hooks/tools.mdx b/docs/hooks/tools.mdx
index 4b3399fc3c..8e9314acc6 100644
--- a/docs/hooks/tools.mdx
+++ b/docs/hooks/tools.mdx
@@ -750,7 +750,7 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
-tool_search (2)
+tool_catalog_search (2)
| Env var | JSON path | Type | Description |
| ---------------------- | --------- | ------ | --------------------------------------------------------------------------------------------------------- |
diff --git a/src/browser/features/Tools/Shared/ToolPrimitives.tsx b/src/browser/features/Tools/Shared/ToolPrimitives.tsx
index 846c438604..4f55222f54 100644
--- a/src/browser/features/Tools/Shared/ToolPrimitives.tsx
+++ b/src/browser/features/Tools/Shared/ToolPrimitives.tsx
@@ -271,6 +271,7 @@ export const TOOL_NAME_TO_ICON: Partial> = {
web_search: Globe,
"server:GOOGLE_SEARCH_WEB": Globe,
notify: Bell,
+ tool_catalog_search: Search,
tool_search: Search,
review_pane_update: Sparkles,
review_pane_get: ScanEye,
diff --git a/src/browser/features/Tools/Shared/getToolComponent.test.ts b/src/browser/features/Tools/Shared/getToolComponent.test.ts
index a14a3cc567..3824658154 100644
--- a/src/browser/features/Tools/Shared/getToolComponent.test.ts
+++ b/src/browser/features/Tools/Shared/getToolComponent.test.ts
@@ -141,17 +141,23 @@ describe("getToolComponent", () => {
expect(component).toBe(GenericToolCall);
});
- test("returns ToolSearchToolCall for tool_search with valid args", () => {
- expect(getToolComponent("tool_search", { query: "send slack message" })).toBe(
+ test("returns ToolSearchToolCall for tool_catalog_search with valid args", () => {
+ expect(getToolComponent("tool_catalog_search", { query: "send slack message" })).toBe(
+ ToolSearchToolCall
+ );
+ expect(getToolComponent("tool_catalog_search", { query: "send slack message", limit: 5 })).toBe(
ToolSearchToolCall
);
- expect(getToolComponent("tool_search", { query: "send slack message", limit: 5 })).toBe(
+ });
+
+ test("renders legacy tool_search transcript calls", () => {
+ expect(getToolComponent("tool_search", { query: "send slack message" })).toBe(
ToolSearchToolCall
);
});
- test("tool_search falls back to GenericToolCall when args don't conform", () => {
- expect(getToolComponent("tool_search", { query: 42 })).toBe(GenericToolCall);
+ test("tool_catalog_search falls back to GenericToolCall when args don't conform", () => {
+ expect(getToolComponent("tool_catalog_search", { query: 42 })).toBe(GenericToolCall);
});
test("Object.prototype member names fall back to GenericToolCall instead of throwing", () => {
diff --git a/src/browser/features/Tools/Shared/getToolComponent.ts b/src/browser/features/Tools/Shared/getToolComponent.ts
index ef390de4d7..c5c17b925e 100644
--- a/src/browser/features/Tools/Shared/getToolComponent.ts
+++ b/src/browser/features/Tools/Shared/getToolComponent.ts
@@ -160,7 +160,15 @@ const TOOL_REGISTRY: Record = {
// Legacy-only transcript renderer for historical status_set calls.
status_set: { component: StatusSetToolCall, schema: legacyStatusSetSchema },
notify: { component: NotifyToolCall, schema: TOOL_DEFINITIONS.notify.schema },
- tool_search: { component: ToolSearchToolCall, schema: TOOL_DEFINITIONS.tool_search.schema },
+ tool_catalog_search: {
+ component: ToolSearchToolCall,
+ schema: TOOL_DEFINITIONS.tool_catalog_search.schema,
+ },
+ // Legacy-only transcript renderer from before AI SDK 7 reserved tool_search.
+ tool_search: {
+ component: ToolSearchToolCall,
+ schema: TOOL_DEFINITIONS.tool_catalog_search.schema,
+ },
analytics_query: {
component: AnalyticsQueryToolCall,
schema: TOOL_DEFINITIONS.analytics_query.schema,
diff --git a/src/browser/features/Tools/ToolSearchToolCall.tsx b/src/browser/features/Tools/ToolSearchToolCall.tsx
index c21f5da686..a02b39d804 100644
--- a/src/browser/features/Tools/ToolSearchToolCall.tsx
+++ b/src/browser/features/Tools/ToolSearchToolCall.tsx
@@ -18,7 +18,7 @@ import {
} from "./Shared/toolUtils";
/**
- * Transcript card for the `tool_search` tool (tool-search experiment) — the
+ * Transcript card for the `tool_catalog_search` tool (tool-search experiment), the
* call the model makes to discover deferred MCP tools. Collapsed it reads as a
* glanceable "Tool search · · N matches"; expanded it lists the matched
* tool names with their descriptions.
@@ -76,7 +76,7 @@ export const ToolSearchToolCall: React.FC = (props) =>
▶
-
+
{props.args.query}
{view.kind === "matches" && (
// Hide the count in very narrow containers so the truncating query keeps priority.
diff --git a/src/common/constants/experiments.ts b/src/common/constants/experiments.ts
index dd3c9b160a..1de039f83d 100644
--- a/src/common/constants/experiments.ts
+++ b/src/common/constants/experiments.ts
@@ -181,7 +181,7 @@ export const EXPERIMENTS: Record = {
id: EXPERIMENT_IDS.TOOL_SEARCH,
name: "Tool Search",
description:
- "Defer MCP tool definitions out of the model-visible tool list until the model discovers them via the tool_search tool",
+ "Defer MCP tool definitions out of the model-visible tool list until the model discovers them via the tool_catalog_search tool",
enabledByDefault: false,
userOverridable: true,
showInSettings: true,
diff --git a/src/common/types/tools.ts b/src/common/types/tools.ts
index 15e132772a..6f2a2c7cff 100644
--- a/src/common/types/tools.ts
+++ b/src/common/types/tools.ts
@@ -432,7 +432,7 @@ export type WebFetchToolArgs = z.infer
export type WebFetchToolResult = z.infer;
// Tool Search Tool Types (tool-search experiment), derived from schema (avoid drift)
-export type ToolSearchToolArgs = z.infer;
+export type ToolSearchToolArgs = z.infer;
export interface ToolSearchToolResult {
query: string;
diff --git a/src/common/utils/tools/toolCatalog.test.ts b/src/common/utils/tools/toolCatalog.test.ts
index 64ad422aad..9f16cdc782 100644
--- a/src/common/utils/tools/toolCatalog.test.ts
+++ b/src/common/utils/tools/toolCatalog.test.ts
@@ -6,10 +6,13 @@ import {
buildToolCatalog,
computeActiveToolNames,
extractPreActivatedToolNames,
+ LEGACY_TOOL_SEARCH_TOOL_NAME,
+ normalizeLegacyToolSearchMessages,
prepareToolSearch,
rebuildToolSearchState,
searchToolCatalog,
seedToolSearchActivationsFromMessages,
+ TOOL_SEARCH_TOOL_NAME,
type ToolCatalogEntry,
type ToolSearchStreamState,
} from "@/common/utils/tools/toolCatalog";
@@ -38,7 +41,7 @@ function baseTools(): Record {
return {
bash: fakeTool("Run a shell command"),
file_read: fakeTool("Read a file"),
- tool_search: fakeTool("Search deferred tools"),
+ tool_catalog_search: fakeTool("Search deferred tools"),
slack_send_message: mcpTool("Send a message to a Slack channel", {
channel: { description: "Slack channel ID" },
}),
@@ -77,17 +80,17 @@ describe("buildToolCatalog", () => {
expect(result.catalog.some((entry) => entry.name === "github_create_issue")).toBe(false);
});
- test("tool_search itself is never deferred, even if listed as an MCP name", () => {
+ test("tool_catalog_search itself is never deferred, even if listed as an MCP name", () => {
const result = buildToolCatalog({
tools: baseTools(),
- mcpToolNames: [...MCP_NAMES, "tool_search"],
+ mcpToolNames: [...MCP_NAMES, "tool_catalog_search"],
});
- expect(result.deferredToolNames.has("tool_search")).toBe(false);
+ expect(result.deferredToolNames.has("tool_catalog_search")).toBe(false);
});
test("extracts description and param text; degrades gracefully on odd schemas", () => {
const tools: Record = {
- tool_search: fakeTool("Search deferred tools"),
+ tool_catalog_search: fakeTool("Search deferred tools"),
weird: fakeTool("Weird tool", "not-an-object"),
slack_send_message: mcpTool("Send a message", { channel: { description: "channel ID" } }),
};
@@ -113,27 +116,27 @@ describe("prepareToolSearch (post-policy gate)", () => {
expect(result.state!.deferredToolNames.size).toBe(3);
});
- test("tool_search policy-disabled (absent): no state, MCP tools untouched", () => {
+ test("tool_catalog_search policy-disabled (absent): no state, MCP tools untouched", () => {
const tools = baseTools();
- delete tools.tool_search;
+ delete tools.tool_catalog_search;
const result = prepareToolSearch({ tools, mcpToolNames: MCP_NAMES });
expect(result.state).toBeUndefined();
expect(result.tools).toBe(tools);
expect(Object.keys(result.tools)).toContain("slack_send_message");
});
- test("all MCP tools policy-disabled: tool_search removed, no state", () => {
+ test("all MCP tools policy-disabled: tool_catalog_search removed, no state", () => {
const tools = baseTools();
for (const name of MCP_NAMES) {
delete tools[name];
}
const result = prepareToolSearch({ tools, mcpToolNames: MCP_NAMES });
expect(result.state).toBeUndefined();
- expect(Object.keys(result.tools)).not.toContain("tool_search");
+ expect(Object.keys(result.tools)).not.toContain("tool_catalog_search");
expect(Object.keys(result.tools).sort()).toEqual(["bash", "file_read"]);
});
- test("all MCP tools required: nothing left to defer, tool_search removed", () => {
+ test("all MCP tools required: nothing left to defer, tool_catalog_search removed", () => {
const policy: ToolPolicy = MCP_NAMES.map((name) => ({
regex_match: name,
action: "require" as const,
@@ -144,17 +147,17 @@ describe("prepareToolSearch (post-policy gate)", () => {
toolPolicy: policy,
});
expect(result.state).toBeUndefined();
- expect(Object.keys(result.tools)).not.toContain("tool_search");
+ expect(Object.keys(result.tools)).not.toContain("tool_catalog_search");
});
- test("PTC enabled: deactivates, tool_search removed, MCP tools untouched", () => {
+ test("PTC enabled: deactivates, tool_catalog_search removed, MCP tools untouched", () => {
// Non-exclusive PTC embeds/exposes MCP tools through code_execution's
// bridge, bypassing activeTools scoping — deferral must deactivate.
const tools = baseTools();
tools.code_execution = fakeTool("Run JS against bridged tools");
const result = prepareToolSearch({ tools, mcpToolNames: MCP_NAMES, ptcEnabled: true });
expect(result.state).toBeUndefined();
- expect(Object.keys(result.tools)).not.toContain("tool_search");
+ expect(Object.keys(result.tools)).not.toContain("tool_catalog_search");
expect(Object.keys(result.tools)).toContain("code_execution");
expect(Object.keys(result.tools)).toContain("slack_send_message");
});
@@ -172,19 +175,19 @@ describe("prepareToolSearch (post-policy gate)", () => {
expect(result.state!.deferredToolNames.has("code_execution")).toBe(true);
});
- test("MCP name collision with tool_search: deactivates, record untouched", () => {
- // Server "tool" + tool "search" normalize to "tool_search" and the MCP
+ test("MCP name collision with tool_catalog_search: deactivates, record untouched", () => {
+ // Server "tool" + tool "search" normalize to "tool_catalog_search" and the MCP
// spread overwrites the built-in search tool — deferral must deactivate
// (no working search tool) and the colliding MCP tool must survive.
const tools = baseTools();
- tools.tool_search = mcpTool("MCP tool that happens to be named tool_search");
+ tools.tool_catalog_search = mcpTool("MCP tool that happens to be named tool_catalog_search");
const result = prepareToolSearch({
tools,
- mcpToolNames: [...MCP_NAMES, "tool_search"],
+ mcpToolNames: [...MCP_NAMES, "tool_catalog_search"],
});
expect(result.state).toBeUndefined();
expect(result.tools).toBe(tools);
- expect(Object.keys(result.tools)).toContain("tool_search");
+ expect(Object.keys(result.tools)).toContain("tool_catalog_search");
expect(Object.keys(result.tools)).toContain("slack_send_message");
});
});
@@ -215,15 +218,15 @@ describe("rebuildToolSearchState (model-fallback path)", () => {
delete nextTools[name];
}
const result = rebuildToolSearchState(state, { tools: nextTools, mcpToolNames: MCP_NAMES });
- expect(Object.keys(result.tools)).not.toContain("tool_search");
+ expect(Object.keys(result.tools)).not.toContain("tool_catalog_search");
// Same state object deactivates: prepareStep stops returning activeTools.
expect(computeActiveToolNames(state)).toBeUndefined();
});
- test("deactivates in place when tool_search is gone from the fallback toolset", () => {
+ test("deactivates in place when tool_catalog_search is gone from the fallback toolset", () => {
const state = activeState();
const nextTools = baseTools();
- delete nextTools.tool_search;
+ delete nextTools.tool_catalog_search;
const result = rebuildToolSearchState(state, { tools: nextTools, mcpToolNames: MCP_NAMES });
expect(result.tools).toBe(nextTools);
expect(computeActiveToolNames(state)).toBeUndefined();
@@ -298,22 +301,32 @@ describe("extractPreActivatedToolNames", () => {
test("reads json-encoded outputs", () => {
const names = extractPreActivatedToolNames([
- toolResultMessage("tool_search", { type: "json", value: matchesResult }),
+ toolResultMessage("tool_catalog_search", { type: "json", value: matchesResult }),
]);
expect([...names]).toEqual(["slack_send_message"]);
});
test("reads text-encoded (stringified) outputs", () => {
const names = extractPreActivatedToolNames([
- toolResultMessage("tool_search", { type: "text", value: JSON.stringify(matchesResult) }),
+ toolResultMessage("tool_catalog_search", {
+ type: "text",
+ value: JSON.stringify(matchesResult),
+ }),
]);
expect([...names]).toEqual(["slack_send_message"]);
});
test("reads raw object outputs and ignores non-JSON text", () => {
const names = extractPreActivatedToolNames([
- toolResultMessage("tool_search", matchesResult),
- toolResultMessage("tool_search", { type: "text", value: "not json" }),
+ toolResultMessage("tool_catalog_search", matchesResult),
+ toolResultMessage("tool_catalog_search", { type: "text", value: "not json" }),
+ ]);
+ expect([...names]).toEqual(["slack_send_message"]);
+ });
+
+ test("reads legacy tool_search outputs from persisted history", () => {
+ const names = extractPreActivatedToolNames([
+ toolResultMessage(LEGACY_TOOL_SEARCH_TOOL_NAME, { type: "json", value: matchesResult }),
]);
expect([...names]).toEqual(["slack_send_message"]);
});
@@ -340,12 +353,73 @@ describe("extractPreActivatedToolNames", () => {
return raw as MuxMessage;
};
const names = extractPreActivatedToolNames([
- muxMessage("tool_search", "output-available", matchesResult),
+ muxMessage("tool_catalog_search", "output-available", matchesResult),
+ muxMessage(LEGACY_TOOL_SEARCH_TOOL_NAME, "output-available", {
+ ...matchesResult,
+ matches: [{ name: "github_create_issue", description: "Create" }],
+ }),
// Pending / other-tool parts must not contribute.
- muxMessage("tool_search", "input-available"),
+ muxMessage("tool_catalog_search", "input-available"),
muxMessage("bash", "output-available", matchesResult),
]);
- expect([...names]).toEqual(["slack_send_message"]);
+ expect([...names]).toEqual(["slack_send_message", "github_create_issue"]);
+ });
+});
+
+describe("normalizeLegacyToolSearchMessages", () => {
+ test("rewrites completed Mux search history without relabeling unrelated tools", () => {
+ const muxResult = {
+ query: "slack",
+ matches: [{ name: "slack_send_message", description: "Send" }],
+ totalDeferred: 3,
+ };
+ const messages: ModelMessage[] = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "tool-call",
+ toolCallId: "mux-search",
+ toolName: LEGACY_TOOL_SEARCH_TOOL_NAME,
+ input: { query: "slack" },
+ },
+ {
+ type: "tool-call",
+ toolCallId: "mcp-search",
+ toolName: LEGACY_TOOL_SEARCH_TOOL_NAME,
+ input: { text: "slack" },
+ },
+ ],
+ },
+ {
+ role: "tool",
+ content: [
+ {
+ type: "tool-result",
+ toolCallId: "mux-search",
+ toolName: LEGACY_TOOL_SEARCH_TOOL_NAME,
+ output: { type: "json", value: muxResult },
+ },
+ {
+ type: "tool-result",
+ toolCallId: "mcp-search",
+ toolName: LEGACY_TOOL_SEARCH_TOOL_NAME,
+ output: { type: "json", value: { hits: [] } },
+ },
+ ],
+ },
+ ];
+
+ const normalized = normalizeLegacyToolSearchMessages(messages);
+ const normalizedJson = JSON.stringify(normalized);
+
+ expect(normalizedJson).toContain(
+ `"toolCallId":"mux-search","toolName":"${TOOL_SEARCH_TOOL_NAME}"`
+ );
+ expect(normalizedJson).toContain(
+ `"toolCallId":"mcp-search","toolName":"${LEGACY_TOOL_SEARCH_TOOL_NAME}"`
+ );
+ expect(JSON.stringify(messages)).not.toContain(TOOL_SEARCH_TOOL_NAME);
});
});
@@ -358,7 +432,7 @@ describe("seedToolSearchActivationsFromMessages", () => {
{
type: "tool-result",
toolCallId: "call-1",
- toolName: "tool_search",
+ toolName: "tool_catalog_search",
output: {
type: "json",
value: {
@@ -399,12 +473,12 @@ describe("computeActiveToolNames", () => {
const firstStep = computeActiveToolNames(state)!;
expect(firstStep).toContain("bash");
expect(firstStep).toContain("file_read");
- expect(firstStep).toContain("tool_search");
+ expect(firstStep).toContain("tool_catalog_search");
for (const name of MCP_NAMES) {
expect(firstStep).not.toContain(name);
}
- // Simulate tool_search.execute activating a match: the next step's
+ // Simulate tool_catalog_search.execute activating a match: the next step's
// activeTools must advertise it (acceptance criteria 2–3).
state.activatedToolNames.add("slack_send_message");
const nextStep = computeActiveToolNames(state)!;
diff --git a/src/common/utils/tools/toolCatalog.ts b/src/common/utils/tools/toolCatalog.ts
index 8293f8fd69..fdf6dc927c 100644
--- a/src/common/utils/tools/toolCatalog.ts
+++ b/src/common/utils/tools/toolCatalog.ts
@@ -2,7 +2,7 @@
* Tool search catalog (tool-search experiment, Phase 1).
*
* Client-side deferred MCP tool loading: MCP tool schemas stay out of the
- * model-visible tool list until the model discovers them via the `tool_search`
+ * model-visible tool list until the model discovers them via the `tool_catalog_search`
* tool. All tools remain in the `tools:` record passed to streamText; the AI
* SDK's `prepareStep` → `activeTools` mechanism only scopes what is advertised
* to the model on each step, so this works with every provider.
@@ -12,13 +12,20 @@
* aiService or streamText.
*/
-import type { Tool } from "ai";
+import type {
+ AssistantModelMessage,
+ Tool,
+ ToolCallPart,
+ ToolModelMessage,
+ ToolResultPart,
+} from "ai";
import type { ModelMessage, MuxMessage } from "@/common/types/message";
import { buildRequiredToolPatterns, type ToolPolicy } from "@/common/utils/tools/toolPolicy";
-export const TOOL_SEARCH_TOOL_NAME = "tool_search";
+export const TOOL_SEARCH_TOOL_NAME = "tool_catalog_search";
+export const LEGACY_TOOL_SEARCH_TOOL_NAME = "tool_search";
-/** Default / max number of matches returned by a tool_search call. */
+/** Default / max number of matches returned by a tool_catalog_search call. */
export const TOOL_SEARCH_DEFAULT_LIMIT = 10;
export const TOOL_SEARCH_MAX_LIMIT = 25;
@@ -31,7 +38,7 @@ export interface ToolCatalogEntry {
/**
* Per-stream mutable tool-search state. Created by aiService after policy
- * filtering, mutated by `tool_search.execute` (activations) and by the
+ * filtering, mutated by `tool_catalog_search.execute` (activations) and by the
* model-fallback rebuild, and read by StreamManager's prepareStep. The object
* identity must be stable for the lifetime of the stream — mutate in place,
* never replace.
@@ -42,7 +49,7 @@ export interface ToolSearchStreamState {
deferredToolNames: Set;
/** Final post-policy tool record keys (core + deferred). */
allToolNames: string[];
- /** Deferred tools discovered via tool_search (or prior-turn history). */
+ /** Deferred tools discovered via tool_catalog_search (or prior-turn history). */
activatedToolNames: Set;
}
@@ -59,6 +66,10 @@ function isPlainRecord(value: unknown): value is Record {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+function isMuxToolSearchName(toolName: string): boolean {
+ return toolName === TOOL_SEARCH_TOOL_NAME || toolName === LEGACY_TOOL_SEARCH_TOOL_NAME;
+}
+
/**
* Extract a flat scoring string from a tool's input schema. MCP tools carry
* `jsonSchema()`-wrapped JSON schemas (a `Schema` object with a `jsonSchema`
@@ -115,7 +126,7 @@ interface ToolCatalogClassification {
* Phase 1 policy: only MCP tools are deferred. Deferred = record keys ∩ MCP
* names, minus names matched by a policy `require` rule (required tools must
* stay advertised so the model can satisfy the requirement), minus
- * `tool_search` itself. Intersection semantics absorb policy-disable and
+ * `tool_catalog_search` itself. Intersection semantics absorb policy-disable and
* PTC-exclusive removals: absent tools never enter the catalog.
*/
export function buildToolCatalog(inputs: ToolCatalogInputs): ToolCatalogClassification {
@@ -147,20 +158,20 @@ export function buildToolCatalog(inputs: ToolCatalogInputs): ToolCatalogClassifi
* Post-policy gate: decides whether tool-search deferral is active for this
* stream and returns the (possibly adjusted) tool record plus the seed state.
*
- * - An MCP tool name collides with `tool_search` (e.g. server "tool" + tool
- * "search" normalize to "tool_search"): the MCP spread overwrites the
+ * - An MCP tool name collides with `tool_catalog_search` (e.g. server "tool" + tool
+ * "search" normalize to "tool_catalog_search"): the MCP spread overwrites the
* built-in search tool in the merged record, so deferring would leave MCP
* tools unreachable with no working search tool ⇒ safe fallback: no state,
* tools unchanged — the colliding entry behaves as a normal MCP tool.
* - PTC enabled (`ptcEnabled`, non-exclusive programmatic tool calling): its
* `code_execution` bridge already embeds/exposes MCP tools, bypassing
- * activeTools ⇒ drop `tool_search`, no state — MCP tools stay advertised as
+ * activeTools ⇒ drop `tool_catalog_search`, no state. MCP tools stay advertised as
* without deferral. (Exclusive mode removes MCP tools from the record, so
* the empty-catalog branch deactivates it anyway.)
- * - `tool_search` absent (policy-disabled) ⇒ safe fallback: no state, tools
+ * - `tool_catalog_search` absent (policy-disabled) ⇒ safe fallback: no state, tools
* unchanged — MCP tools stay advertised exactly as without the experiment.
* - Nothing deferred (all MCP tools policy-disabled / PTC-removed) ⇒ drop
- * `tool_search` from the record (a search tool with an empty catalog is
+ * `tool_catalog_search` from the record (a search tool with an empty catalog is
* noise) and return no state.
* - Otherwise ⇒ tools unchanged plus a fresh state with an empty activation
* set (callers seed prior-turn activations via
@@ -171,7 +182,7 @@ export function prepareToolSearch(inputs: ToolCatalogInputs): {
state?: ToolSearchStreamState;
} {
// Collision check must run before the empty-catalog branch below: when the
- // record's `tool_search` entry is actually an MCP tool, dropping it would
+ // record's `tool_catalog_search` entry is actually an MCP tool, dropping it would
// silently remove a legitimate MCP tool.
if (inputs.mcpToolNames.includes(TOOL_SEARCH_TOOL_NAME)) {
return { tools: inputs.tools };
@@ -183,7 +194,7 @@ export function prepareToolSearch(inputs: ToolCatalogInputs): {
// bridged MCP tool in its description and exposes them as callable `mux.*`
// functions, so activeTools scoping could neither reduce context nor gate
// access. Deferral would be ineffective and silently bypassable ⇒ drop
- // tool_search and run without deferral when both experiments are enabled.
+ // tool_catalog_search and run without deferral when both experiments are enabled.
// Gated on the actual PTC flag, not record presence: a `code_execution`
// record entry may be a same-named MCP tool (classified as normal deferred).
if (inputs.ptcEnabled === true) {
@@ -208,7 +219,7 @@ export function prepareToolSearch(inputs: ToolCatalogInputs): {
* intersected with the new deferred set. When the fallback record no longer
* supports deferral, the state deactivates (empty deferred set ⇒
* computeActiveToolNames returns undefined ⇒ no activeTools scoping) and
- * `tool_search` is removed from the returned record.
+ * `tool_catalog_search` is removed from the returned record.
*/
export function rebuildToolSearchState(
state: ToolSearchStreamState,
@@ -296,7 +307,7 @@ export function searchToolCatalog(
}));
}
-/** Read `matches[].name` strings from a defensively-parsed tool_search result value. */
+/** Read `matches[].name` strings from a defensively-parsed tool_catalog_search result value. */
function collectMatchNames(value: unknown, into: Set): void {
if (!isPlainRecord(value) || !Array.isArray(value.matches)) {
return;
@@ -308,25 +319,55 @@ function collectMatchNames(value: unknown, into: Set): void {
}
}
-/** Decode a tool_search output in any persisted encoding and collect match names. */
-function collectMatchNamesFromOutput(output: unknown, into: Set): void {
+/** Decode a tool_catalog_search output in any persisted encoding. */
+function decodeToolSearchOutput(output: unknown): unknown {
if (isPlainRecord(output) && output.type === "json") {
- collectMatchNames(output.value, into);
- } else if (isPlainRecord(output) && output.type === "text") {
- if (typeof output.value === "string") {
- try {
- collectMatchNames(JSON.parse(output.value), into);
- } catch {
- // Not JSON — nothing to recover from a plain-text result.
- }
+ return output.value;
+ }
+ if (isPlainRecord(output) && output.type === "text" && typeof output.value === "string") {
+ try {
+ return JSON.parse(output.value);
+ } catch {
+ return undefined;
}
- } else {
- collectMatchNames(output, into);
}
+ return output;
+}
+
+function collectMatchNamesFromOutput(output: unknown, into: Set): void {
+ collectMatchNames(decodeToolSearchOutput(output), into);
+}
+
+function isMuxToolSearchOutput(output: unknown): boolean {
+ const value = decodeToolSearchOutput(output);
+ return (
+ isPlainRecord(value) &&
+ typeof value.query === "string" &&
+ Array.isArray(value.matches) &&
+ value.matches.every(
+ (match) =>
+ isPlainRecord(match) &&
+ typeof match.name === "string" &&
+ typeof match.description === "string"
+ ) &&
+ typeof value.totalDeferred === "number"
+ );
+}
+
+function renameLegacyToolSearchCallPart(part: ToolCallPart): ToolCallPart {
+ return part.toolName === LEGACY_TOOL_SEARCH_TOOL_NAME
+ ? { ...part, toolName: TOOL_SEARCH_TOOL_NAME }
+ : part;
+}
+
+function renameLegacyToolSearchResultPart(part: ToolResultPart): ToolResultPart {
+ return part.toolName === LEGACY_TOOL_SEARCH_TOOL_NAME
+ ? { ...part, toolName: TOOL_SEARCH_TOOL_NAME }
+ : part;
}
/**
- * Scan conversation history for prior `tool_search` tool results and return
+ * Scan conversation history for prior `tool_catalog_search` tool results and return
* the tool names they matched, so tools discovered in earlier turns (or
* before a mid-turn stream retry) re-activate without a new search. Accepts
* both provider-shaped ModelMessages (tool-result parts, `{type:"json"}` /
@@ -344,7 +385,7 @@ export function extractPreActivatedToolNames(
for (const part of message.parts) {
if (
part.type === "dynamic-tool" &&
- part.toolName === TOOL_SEARCH_TOOL_NAME &&
+ isMuxToolSearchName(part.toolName) &&
part.state === "output-available"
) {
collectMatchNamesFromOutput(part.output, names);
@@ -356,7 +397,7 @@ export function extractPreActivatedToolNames(
continue;
}
for (const part of message.content) {
- if (part.type !== "tool-result" || part.toolName !== TOOL_SEARCH_TOOL_NAME) {
+ if (part.type !== "tool-result" || !isMuxToolSearchName(part.toolName)) {
continue;
}
collectMatchNamesFromOutput(part.output, names);
@@ -366,7 +407,68 @@ export function extractPreActivatedToolNames(
}
/**
- * Seed the activation set from prior tool_search results found in the
+ * AI SDK 7 reserves the legacy `tool_search` name for OpenAI's native tool.
+ * Rewrite only completed historical Mux search calls, request-only, so old
+ * workspaces remain usable without relabeling unrelated MCP tools.
+ */
+export function normalizeLegacyToolSearchMessages(messages: ModelMessage[]): ModelMessage[] {
+ const legacyCallIds = new Set();
+ for (const message of messages) {
+ if (!Array.isArray(message.content)) {
+ continue;
+ }
+ for (const part of message.content) {
+ if (
+ part.type === "tool-result" &&
+ part.toolName === LEGACY_TOOL_SEARCH_TOOL_NAME &&
+ isMuxToolSearchOutput(part.output)
+ ) {
+ legacyCallIds.add(part.toolCallId);
+ }
+ }
+ }
+
+ if (legacyCallIds.size === 0) {
+ return messages;
+ }
+
+ return messages.map((message) => {
+ if (message.role === "assistant" && Array.isArray(message.content)) {
+ const content: Exclude = message.content.map(
+ (part) => {
+ if (!legacyCallIds.has("toolCallId" in part ? part.toolCallId : "")) {
+ return part;
+ }
+ if (part.type === "tool-call") {
+ return renameLegacyToolSearchCallPart(part);
+ }
+ if (part.type === "tool-result") {
+ return renameLegacyToolSearchResultPart(part);
+ }
+ return part;
+ }
+ );
+ const normalizedMessage: AssistantModelMessage = { ...message, content };
+ return normalizedMessage;
+ }
+
+ if (message.role === "tool") {
+ const content: ToolModelMessage["content"] = message.content.map((part) => {
+ if (part.type === "tool-result" && legacyCallIds.has(part.toolCallId)) {
+ return renameLegacyToolSearchResultPart(part);
+ }
+ return part;
+ });
+ const normalizedMessage: ToolModelMessage = { ...message, content };
+ return normalizedMessage;
+ }
+
+ return message;
+ });
+}
+
+/**
+ * Seed the activation set from prior tool_catalog_search results found in the
* stream's input messages, intersected with the current deferred set.
*/
export function seedToolSearchActivationsFromMessages(
diff --git a/src/common/utils/tools/toolDefinitions.test.ts b/src/common/utils/tools/toolDefinitions.test.ts
index 767433693b..85ca359621 100644
--- a/src/common/utils/tools/toolDefinitions.test.ts
+++ b/src/common/utils/tools/toolDefinitions.test.ts
@@ -642,14 +642,16 @@ describe("TOOL_DEFINITIONS", () => {
expect(subAgentTools).not.toContain("review_pane_get");
});
- it("only includes tool_search when enableToolSearch is set", () => {
+ it("only includes tool_catalog_search when enableToolSearch is set", () => {
// Off by default: the tool-search experiment must not leak into normal assembly.
- expect(getAvailableTools("openai:gpt-4o")).not.toContain("tool_search");
+ expect(getAvailableTools("openai:gpt-4o")).not.toContain("tool_catalog_search");
expect(getAvailableTools("openai:gpt-4o", { enableToolSearch: false })).not.toContain(
- "tool_search"
+ "tool_catalog_search"
);
- expect(getAvailableTools("openai:gpt-4o", { enableToolSearch: true })).toContain("tool_search");
+ expect(getAvailableTools("openai:gpt-4o", { enableToolSearch: true })).toContain(
+ "tool_catalog_search"
+ );
});
it("requires workflow_run calls to use exactly one launch source", () => {
diff --git a/src/common/utils/tools/toolDefinitions.ts b/src/common/utils/tools/toolDefinitions.ts
index 6205476708..c84a954e9b 100644
--- a/src/common/utils/tools/toolDefinitions.ts
+++ b/src/common/utils/tools/toolDefinitions.ts
@@ -2479,11 +2479,11 @@ CREATE TABLE IF NOT EXISTS delegation_rollups (
.strict(),
},
// #endregion NOTIFY_DOCS
- tool_search: {
+ tool_catalog_search: {
description:
"Search the catalog of deferred tools. Some tools (provided by MCP servers) are deferred: " +
"they exist but are not currently visible in your tool list. " +
- "Call tool_search with task/capability keywords to discover them; matched tools become available on the next step. " +
+ "Call tool_catalog_search with task/capability keywords to discover them; matched tools become available on the next step. " +
"Returns matched tool names and descriptions plus the total number of deferred tools (there may be more undiscovered — refine the query to find them).",
schema: z
.object({
@@ -2990,7 +2990,7 @@ export function getAvailableTools(
enableDynamicWorkflows?: boolean;
/** Whether the agent memory tool is available (memory experiment enabled). */
enableMemory?: boolean;
- /** Whether tool_search is available (tool-search experiment + deferred MCP tools present). */
+ /** Whether tool_catalog_search is available (tool-search experiment + deferred MCP tools present). */
enableToolSearch?: boolean;
/**
* Whether the Review pane tools (review_pane_update/review_pane_get) are
@@ -3041,7 +3041,7 @@ export function getAvailableTools(
"file_edit_insert",
...(enableMemory ? ["memory"] : []),
...(enableAdvisor ? ["advisor"] : []),
- ...(enableToolSearch ? ["tool_search"] : []),
+ ...(enableToolSearch ? ["tool_catalog_search"] : []),
"ask_user_question",
"propose_plan",
"bash",
diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts
index c7bb5157c1..11054c30fc 100644
--- a/src/common/utils/tools/tools.ts
+++ b/src/common/utils/tools/tools.ts
@@ -307,7 +307,7 @@ export interface ToolConfiguration {
abortSignal: AbortSignal;
};
/**
- * Runtime holder for the tool_search tool (tool-search experiment; present
+ * Runtime holder for the tool_catalog_search tool (tool-search experiment; present
* only when the experiment is enabled and MCP tools exist for this stream).
* `state` is assigned by aiService after policy filtering builds the catalog.
*/
@@ -595,7 +595,7 @@ export async function getToolsForModel(
skills_catalog_search: createSkillsCatalogSearchTool(config),
skills_catalog_read: createSkillsCatalogReadTool(config),
...(config.advisorRuntime ? { advisor: createAdvisorTool(config) } : {}),
- ...(config.toolSearchRuntime ? { tool_search: createToolSearchTool(config) } : {}),
+ ...(config.toolSearchRuntime ? { tool_catalog_search: createToolSearchTool(config) } : {}),
ask_user_question: createAskUserQuestionTool(config),
propose_plan: createProposePlanTool(config),
// propose_name and propose_status are intentionally NOT registered here —
diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts
index 85452b2108..5ff084921c 100644
--- a/src/node/services/agentSkills/builtInSkillContent.generated.ts
+++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts
@@ -5491,7 +5491,7 @@ export const BUILTIN_SKILL_FILES: Record> = {
" ",
"",
"",
- "tool_search (2)
",
+ "tool_catalog_search (2)
",
"",
"| Env var | JSON path | Type | Description |",
"| ---------------------- | --------- | ------ | --------------------------------------------------------------------------------------------------------- |",
diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts
index 377e7d6297..2dcb37e699 100644
--- a/src/node/services/aiService.ts
+++ b/src/node/services/aiService.ts
@@ -137,6 +137,7 @@ import {
prepareToolSearch,
rebuildToolSearchState,
seedToolSearchActivationsFromMessages,
+ TOOL_SEARCH_TOOL_NAME,
type ToolSearchRuntime,
} from "@/common/utils/tools/toolCatalog";
import type { PTCEventWithParent } from "@/node/services/tools/code_execution";
@@ -1678,7 +1679,7 @@ export class AIService extends EventEmitter {
}
// Tool search (tool-search experiment): assembly-time gate. The runtime
- // holder makes getToolsForModel create the tool_search tool; its `state`
+ // holder makes getToolsForModel create the tool_catalog_search tool; its `state`
// is assigned only after policy filtering builds the deferred catalog
// (see prepareToolSearch below). Without MCP tools there is nothing to
// defer, so the feature stays fully inactive.
@@ -2226,7 +2227,7 @@ export class AIService extends EventEmitter {
// must consume the policy-filtered record so policy-disabled tools never
// enter the deferred catalog. This runs before every downstream consumer
// of `tools` (system-prompt rebuild, sentinel tool names, telemetry,
- // streaming) so a dropped tool_search cannot leak anywhere.
+ // streaming) so a dropped tool_catalog_search cannot leak anywhere.
// PTC gate uses the same condition toolAssembly uses to add code_execution:
// presence-sniffing the record would misfire on an MCP tool named
// code_execution (see prepareToolSearch).
@@ -2299,7 +2300,7 @@ export class AIService extends EventEmitter {
systemMessageTokens = await tokenizer.countTokens(systemMessage);
}
- // Re-activate deferred tools discovered by tool_search in earlier turns
+ // Re-activate deferred tools discovered by tool_catalog_search in earlier turns
// without requiring a new search. Must run before the sentinel list is
// computed so pre-activated tools are advertised in agent transitions.
if (toolSearchRuntime?.state) {
@@ -2724,14 +2725,14 @@ export class AIService extends EventEmitter {
toolPolicy: effectiveToolPolicy,
ptcEnabled,
}).tools;
- } else if (!(mcpTools && "tool_search" in mcpTools)) {
+ } else if (!(mcpTools && TOOL_SEARCH_TOOL_NAME in mcpTools)) {
// The primary-path gate deactivated deferral (e.g. every
// MCP tool was policy-disabled). StreamManager was never
- // handed scoping state, so tool_search must not appear in
+ // handed scoping state, so tool_catalog_search must not appear in
// the fallback toolset either. Skipped when an MCP tool
// collides with the name: that record entry is a
// legitimate MCP tool, not our search tool.
- const { tool_search: _removed, ...rest } = nextTools;
+ const { [TOOL_SEARCH_TOOL_NAME]: _removed, ...rest } = nextTools;
nextTools = rest;
}
}
diff --git a/src/node/services/messagePipeline.ts b/src/node/services/messagePipeline.ts
index 4aa887446b..5654279f89 100644
--- a/src/node/services/messagePipeline.ts
+++ b/src/node/services/messagePipeline.ts
@@ -28,6 +28,7 @@ import {
injectFileChangeNotifications,
injectPostCompactionAttachments,
} from "@/browser/utils/messages/modelMessageTransform";
+import { normalizeLegacyToolSearchMessages } from "@/common/utils/tools/toolCatalog";
import { applyCacheControl, type AnthropicCacheTtl } from "@/common/utils/ai/cacheStrategy";
import { log } from "./log";
@@ -183,7 +184,9 @@ export async function prepareMessagesForProvider(
// --- ModelMessage-level transforms ---
- const modelMessages = sanitizeAssistantModelMessages(rawModelMessages, workspaceId);
+ const modelMessages = normalizeLegacyToolSearchMessages(
+ sanitizeAssistantModelMessages(rawModelMessages, workspaceId)
+ );
log.debug_obj(`${workspaceId}/2_model_messages.json`, modelMessages);
diff --git a/src/node/services/streamManager.test.ts b/src/node/services/streamManager.test.ts
index cb0af7106c..ca7a41decb 100644
--- a/src/node/services/streamManager.test.ts
+++ b/src/node/services/streamManager.test.ts
@@ -5161,7 +5161,7 @@ describe("StreamManager - tool search activeTools scoping", () => {
{ name: "slack_list_channels", description: "List channels", paramText: "" },
],
deferredToolNames: new Set(["slack_send_message", "slack_list_channels"]),
- allToolNames: ["bash", "tool_search", "slack_send_message", "slack_list_channels"],
+ allToolNames: ["bash", "tool_catalog_search", "slack_send_message", "slack_list_channels"],
activatedToolNames: new Set(),
};
@@ -5173,15 +5173,15 @@ describe("StreamManager - tool search activeTools scoping", () => {
const prepareStep = capturePrepareStep(streamTextSpy);
const firstStep = await prepareStep({ messages });
- expect(firstStep?.activeTools).toEqual(["bash", "tool_search"]);
+ expect(firstStep?.activeTools).toEqual(["bash", "tool_catalog_search"]);
// Messages were unchanged, so no messages key should be introduced.
expect(firstStep && "messages" in firstStep).toBe(false);
- // Simulate tool_search.execute activating a tool mid-stream: the next
+ // Simulate tool_catalog_search.execute activating a tool mid-stream: the next
// prepareStep call must advertise it without rebuilding the request.
toolSearchState.activatedToolNames.add("slack_send_message");
const nextStep = await prepareStep({ messages });
- expect(nextStep?.activeTools).toEqual(["bash", "tool_search", "slack_send_message"]);
+ expect(nextStep?.activeTools).toEqual(["bash", "tool_catalog_search", "slack_send_message"]);
});
test("buildStreamRequestConfig forwards the tool search state reference", () => {
@@ -5214,7 +5214,7 @@ describe("StreamManager - tool search activeTools scoping", () => {
toolSearchState
);
- // Same reference, not a copy — tool_search.execute mutations must be
+ // Same reference, not a copy. tool_catalog_search.execute mutations must be
// visible to prepareStep.
expect(request.toolSearchState).toBe(toolSearchState);
});
diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts
index e3b41efb24..395fba973c 100644
--- a/src/node/services/streamManager.ts
+++ b/src/node/services/streamManager.ts
@@ -191,7 +191,7 @@ interface StreamRequestConfig {
toolPolicy?: ToolPolicy;
/**
* Tool-search deferral state (tool-search experiment). Owned and mutated by
- * aiService/tool_search.execute; prepareStep reads it each step to compute
+ * aiService/tool_catalog_search.execute; prepareStep reads it each step to compute
* `activeTools`. Absent when the feature is inactive.
*/
toolSearchState?: ToolSearchStreamState;
@@ -1826,7 +1826,7 @@ export class StreamManager extends EventEmitter {
request.onStepMessages?.(effectiveMessages);
// Tool search (tool-search experiment): scope the advertised tool list
// to core tools + activated deferred tools. Read per step so tools
- // activated by tool_search.execute appear on the following step.
+ // activated by tool_catalog_search.execute appear on the following step.
// undefined when the feature is inactive, keeping the return value
// byte-identical to the pre-feature behavior.
const activeTools = computeActiveToolNames(request.toolSearchState);
diff --git a/src/node/services/tools/toolSearch.test.ts b/src/node/services/tools/toolSearch.test.ts
new file mode 100644
index 0000000000..170ddbfcdf
--- /dev/null
+++ b/src/node/services/tools/toolSearch.test.ts
@@ -0,0 +1,85 @@
+import { createOpenAI } from "@ai-sdk/openai";
+import { describe, expect, test } from "bun:test";
+import { generateText, tool, type ModelMessage } from "ai";
+
+import { TOOL_DEFINITIONS } from "@/common/utils/tools/toolDefinitions";
+import {
+ LEGACY_TOOL_SEARCH_TOOL_NAME,
+ normalizeLegacyToolSearchMessages,
+ TOOL_SEARCH_TOOL_NAME,
+} from "@/common/utils/tools/toolCatalog";
+
+describe("tool catalog search provider compatibility", () => {
+ test("serializes as a custom function in OpenAI Responses history", async () => {
+ let capturedBody: Record | undefined;
+ const captureFetch = Object.assign(
+ (_input: Parameters[0], init?: Parameters[1]) => {
+ if (typeof init?.body !== "string") {
+ throw new Error("Expected the OpenAI provider to send a JSON string body");
+ }
+ capturedBody = JSON.parse(init.body) as Record;
+ return Promise.resolve(
+ new Response(
+ JSON.stringify({
+ id: "resp_test",
+ model: "gpt-5.6-sol",
+ output: [],
+ usage: { input_tokens: 1, output_tokens: 0 },
+ }),
+ { status: 200, headers: { "content-type": "application/json" } }
+ )
+ );
+ },
+ { preconnect: fetch.preconnect.bind(fetch) }
+ );
+ const openai = createOpenAI({ apiKey: "test", fetch: captureFetch });
+ const result = {
+ query: "workspace goal",
+ matches: [{ name: "set_goal", description: "Create or replace a workspace goal" }],
+ totalDeferred: 3,
+ };
+ const legacyMessages: ModelMessage[] = [
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "tool-call",
+ toolCallId: "call_1",
+ toolName: LEGACY_TOOL_SEARCH_TOOL_NAME,
+ input: { query: result.query },
+ },
+ ],
+ },
+ {
+ role: "tool",
+ content: [
+ {
+ type: "tool-result",
+ toolCallId: "call_1",
+ toolName: LEGACY_TOOL_SEARCH_TOOL_NAME,
+ output: { type: "json", value: result },
+ },
+ ],
+ },
+ { role: "user", content: "Continue" },
+ ];
+ const messages = normalizeLegacyToolSearchMessages(legacyMessages);
+
+ await generateText({
+ model: openai.responses("gpt-5.6-sol"),
+ messages,
+ tools: {
+ [TOOL_SEARCH_TOOL_NAME]: tool({
+ description: TOOL_DEFINITIONS.tool_catalog_search.description,
+ inputSchema: TOOL_DEFINITIONS.tool_catalog_search.schema,
+ }),
+ },
+ maxRetries: 0,
+ });
+
+ const input = capturedBody?.input as Array> | undefined;
+ expect(input?.some((item) => item.type === "function_call")).toBe(true);
+ expect(input?.some((item) => item.type === "function_call_output")).toBe(true);
+ expect(input?.some((item) => item.type === "tool_search_output")).toBe(false);
+ });
+});
diff --git a/src/node/services/tools/toolSearch.ts b/src/node/services/tools/toolSearch.ts
index 88e640b6e4..9dc52ac9fe 100644
--- a/src/node/services/tools/toolSearch.ts
+++ b/src/node/services/tools/toolSearch.ts
@@ -1,5 +1,5 @@
/**
- * tool_search tool (tool-search experiment, Phase 1).
+ * tool_catalog_search tool (tool-search experiment, Phase 1).
*
* Lets the model discover deferred MCP tools by keyword. Matches are added to
* the per-stream activation set, so StreamManager's prepareStep advertises
@@ -17,8 +17,8 @@ export const createToolSearchTool: ToolFactory = (config) => {
// gate builds the catalog — read it lazily at execute time.
const runtime = config.toolSearchRuntime;
return tool({
- description: TOOL_DEFINITIONS.tool_search.description,
- inputSchema: TOOL_DEFINITIONS.tool_search.schema,
+ description: TOOL_DEFINITIONS.tool_catalog_search.description,
+ inputSchema: TOOL_DEFINITIONS.tool_catalog_search.schema,
execute: ({ query, limit }): ToolSearchToolResult => {
const state = runtime?.state;
// Defensive: when the post-policy gate deactivated deferral this tool is