diff --git a/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts b/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts
index 52103bb1b..1ba030e98 100644
--- a/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts
+++ b/packages/builtin-tools/src/tools/ExecuteTool/ExecuteTool.ts
@@ -10,8 +10,14 @@ import {
} from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import { createUserMessage } from 'src/utils/messages.js'
+import {
+ extractDiscoveredToolNames,
+ isSearchExtraToolsEnabledOptimistic,
+ isSearchExtraToolsToolAvailable,
+} from 'src/utils/searchExtraTools.js'
import { DESCRIPTION, getPrompt } from './prompt.js'
import { EXECUTE_TOOL_NAME } from './constants.js'
+import { isDeferredTool } from '../SearchExtraToolsTool/prompt.js'
export const inputSchema = lazySchema(() =>
z.object({
@@ -74,6 +80,32 @@ export const ExecuteTool = buildTool({
}
}
+ // Guard: block execution of undiscovered deferred tools.
+ // When tool search is active, deferred tools must be discovered via
+ // SearchExtraTools first so the model has seen their schemas and knows
+ // the correct parameters. Executing an undiscovered tool almost always
+ // fails with parameter validation errors.
+ if (
+ isSearchExtraToolsEnabledOptimistic() &&
+ isSearchExtraToolsToolAvailable(tools) &&
+ isDeferredTool(targetTool)
+ ) {
+ const discovered = extractDiscoveredToolNames(context.messages)
+ if (!discovered.has(input.tool_name)) {
+ return {
+ data: {
+ result: null,
+ tool_name: input.tool_name,
+ },
+ newMessages: [
+ createUserMessage({
+ content: `Tool "${input.tool_name}" has not been discovered yet. You must first use SearchExtraTools to discover this tool before executing it.\n\nUsage: SearchExtraTools("select:${input.tool_name}")`,
+ }),
+ ],
+ }
+ }
+ }
+
// Check if the target tool is currently enabled
if (!targetTool.isEnabled()) {
return {
diff --git a/packages/builtin-tools/src/tools/ExecuteTool/__tests__/ExecuteTool.runner.ts b/packages/builtin-tools/src/tools/ExecuteTool/__tests__/ExecuteTool.runner.ts
index eb10facf1..c8ab3c8f8 100644
--- a/packages/builtin-tools/src/tools/ExecuteTool/__tests__/ExecuteTool.runner.ts
+++ b/packages/builtin-tools/src/tools/ExecuteTool/__tests__/ExecuteTool.runner.ts
@@ -33,10 +33,10 @@ mock.module('src/utils/searchExtraTools.js', () => ({
isSearchExtraToolsEnabledOptimistic: () => true,
getAutoSearchExtraToolsCharThreshold: () => 100,
getSearchExtraToolsMode: () => 'tst' as const,
- isSearchExtraToolsToolAvailable: async () => true,
+ isSearchExtraToolsToolAvailable: () => true,
isSearchExtraToolsEnabled: async () => true,
isToolReferenceBlock: () => false,
- extractDiscoveredToolNames: () => new Set(),
+ extractDiscoveredToolNames: () => new Set(['TestTool', 'SecretTool']),
isDeferredToolsDeltaEnabled: () => false,
getDeferredToolsDelta: () => null,
}))
@@ -154,6 +154,26 @@ describe('ExecuteTool', () => {
expect(result.newMessages).toBeDefined()
})
+ test('returns error when deferred tool has not been discovered via SearchExtraTools', async () => {
+ const mockTarget = makeMockTool('UndiscoveredTool', 'result')
+ const ctx = makeContext([mockTarget])
+
+ const result = await ExecuteTool.call(
+ { tool_name: 'UndiscoveredTool', params: {} },
+ ctx,
+ async () => ({ behavior: 'allow' }),
+ { type: 'assistant', content: [], uuid: 'msg1' } as never,
+ undefined,
+ )
+
+ expect(result.data).toEqual({
+ result: null,
+ tool_name: 'UndiscoveredTool',
+ })
+ expect(result.newMessages).toBeDefined()
+ expect(result.newMessages![0].content).toContain('has not been discovered')
+ })
+
test('has correct name', () => {
expect(ExecuteTool.name).toBe(EXECUTE_TOOL_NAME)
})
diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts
index a5116855f..1d4b4435d 100644
--- a/src/services/api/claude.ts
+++ b/src/services/api/claude.ts
@@ -1396,7 +1396,7 @@ async function* queryModel(
messagesForAPI = [
...messagesForAPI,
createUserMessage({
- content: `\n\n${deferredToolList}\n\nTo invoke any tool listed above, use ExecuteExtraTool with {"tool_name": "", "params": {...}}. This is the ONLY way to call deferred tools — do not read source code or analyze implementation, just call ExecuteExtraTool directly.\n`,
+ content: `\n\n${deferredToolList}\n\nIMPORTANT: These tools are deferred-loading. You MUST first discover a tool via SearchExtraTools before invoking it with ExecuteExtraTool. Do NOT call ExecuteExtraTool directly — it will fail if the tool has not been discovered.\n\nSteps:\n1. SearchExtraTools("select:") — discover the tool and its schema\n2. ExecuteExtraTool({"tool_name": "", "params": {...}}) — invoke it with correct parameters\n`,
isMeta: true,
}),
]