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, }), ]