diff --git a/packages/builtin-tools/src/tools/SearchExtraToolsTool/SearchExtraToolsTool.ts b/packages/builtin-tools/src/tools/SearchExtraToolsTool/SearchExtraToolsTool.ts index 5fa35649f..95c5cbe77 100644 --- a/packages/builtin-tools/src/tools/SearchExtraToolsTool/SearchExtraToolsTool.ts +++ b/packages/builtin-tools/src/tools/SearchExtraToolsTool/SearchExtraToolsTool.ts @@ -58,6 +58,17 @@ export const outputSchema = lazySchema(() => pending_mcp_servers: z.array(z.string()).optional(), /** Matches that are already loaded (core tools) and can be called directly. */ already_loaded: z.array(z.string()).optional(), + /** Tool details (name, description, schema) for discover mode. */ + details: z + .array( + z.object({ + name: z.string(), + description: z.string(), + score: z.number(), + inputSchema: z.record(z.string(), z.unknown()).optional(), + }), + ) + .optional(), }), ) type OutputSchema = ReturnType @@ -131,6 +142,7 @@ function buildSearchResult( totalDeferredTools: number, pendingMcpServers?: string[], alreadyLoaded?: string[], + details?: Output['details'], ): { data: Output } { return { data: { @@ -143,6 +155,7 @@ function buildSearchResult( ...(alreadyLoaded && alreadyLoaded.length > 0 ? { already_loaded: alreadyLoaded } : {}), + ...(details && details.length > 0 ? { details } : {}), }, } } @@ -446,17 +459,12 @@ export const SearchExtraToolsTool = buildTool({ const discoverQuery = discoverMatch[1]!.trim() const index = await getToolIndex(deferredTools) const tfIdfResults = searchTools(discoverQuery, index, max_results) - const textResults = tfIdfResults.map(r => { - let line = `**${r.name}** (score: ${r.score.toFixed(2)})\n${r.description}` - if (r.inputSchema) { - line += `\nSchema: ${JSON.stringify(r.inputSchema)}` - } - return line - }) - const text = - textResults.length > 0 - ? `Found ${textResults.length} tools:\n${textResults.join('\n\n')}` - : 'No matching deferred tools found' + const details = tfIdfResults.map(r => ({ + name: r.name, + description: r.description, + score: r.score, + ...(r.inputSchema ? { inputSchema: r.inputSchema as Record } : {}), + })) logSearchOutcome( tfIdfResults.map(r => r.name), 'keyword', @@ -465,6 +473,9 @@ export const SearchExtraToolsTool = buildTool({ tfIdfResults.map(r => r.name), query, deferredTools.length, + undefined, + undefined, + details, ) } @@ -559,6 +570,26 @@ export const SearchExtraToolsTool = buildTool({ } } + // Discover mode: render tool details with name, description, and input schema. + // This lets the model inspect parameters before selecting and invoking a tool. + if (content.details && content.details.length > 0) { + const lines = content.details.map(d => { + let line = `**${d.name}** (score: ${d.score.toFixed(2)})\n${d.description}` + if (d.inputSchema) { + line += `\nSchema: ${JSON.stringify(d.inputSchema)}` + } + return line + }) + const text = + `Found ${lines.length} tool(s) matching your query:\n\n` + + lines.join('\n\n') + return { + type: 'tool_result', + tool_use_id: toolUseID, + content: text, + } + } + // Separate already-loaded (core) tools from truly deferred tools const alreadyLoadedNames = content.already_loaded ?? [] const deferredNames = content.matches.filter( diff --git a/packages/builtin-tools/src/tools/SearchExtraToolsTool/__tests__/SearchExtraToolsTool.test.ts b/packages/builtin-tools/src/tools/SearchExtraToolsTool/__tests__/SearchExtraToolsTool.test.ts index e8faed328..627d5463e 100644 --- a/packages/builtin-tools/src/tools/SearchExtraToolsTool/__tests__/SearchExtraToolsTool.test.ts +++ b/packages/builtin-tools/src/tools/SearchExtraToolsTool/__tests__/SearchExtraToolsTool.test.ts @@ -101,7 +101,7 @@ function makeContext(tools: unknown[] = []) { } describe('SearchExtraToolsTool search enhancements', () => { - test('discover: prefix triggers TF-IDF search and returns matches', async () => { + test('discover: prefix triggers TF-IDF search and returns matches with details', async () => { const mockTool = makeDeferredTool('CronCreate', 'Schedule cron jobs') mockGetToolIndex.mockResolvedValueOnce([]) mockSearchTools.mockReturnValueOnce([ @@ -112,11 +112,11 @@ describe('SearchExtraToolsTool search enhancements', () => { score: 0.85, isMcp: false, isDeferred: true, - inputSchema: undefined, + inputSchema: { interval: { type: 'string' } }, }, ]) - const result: { data: { matches: string[] } } = await ( + const result: { data: { matches: string[]; details?: Array<{ name: string; description: string; score: number; inputSchema?: Record }> } } = await ( SearchExtraToolsTool as any ).call( { query: 'discover:schedule cron job', max_results: 5 }, @@ -127,6 +127,11 @@ describe('SearchExtraToolsTool search enhancements', () => { ) expect(result.data.matches).toContain('CronCreate') + expect(result.data.details).toBeDefined() + expect(result.data.details!.length).toBe(1) + expect(result.data.details![0]!.name).toBe('CronCreate') + expect(result.data.details![0]!.description).toBe('Schedule cron jobs') + expect(result.data.details![0]!.inputSchema).toEqual({ interval: { type: 'string' } }) }) test('keyword + TF-IDF parallel search merges results', async () => { @@ -232,4 +237,29 @@ describe('SearchExtraToolsTool search enhancements', () => { expect(blockParam.content).toContain('No matching deferred tools found') }) + + test('discover mode renders details with description and schema', async () => { + const blockParam = SearchExtraToolsTool.mapToolResultToToolResultBlockParam( + { + matches: ['CronCreate'], + query: 'discover:schedule', + total_deferred_tools: 5, + details: [ + { + name: 'CronCreate', + description: 'Schedule cron jobs', + score: 0.85, + inputSchema: { interval: { type: 'string' } }, + }, + ], + }, + 'tool-use-123', + ) + + expect(typeof blockParam.content).toBe('string') + expect(blockParam.content as string).toContain('CronCreate') + expect(blockParam.content as string).toContain('Schedule cron jobs') + expect(blockParam.content as string).toContain('Schema') + expect(blockParam.content as string).toContain('"interval"') + }) }) diff --git a/packages/builtin-tools/src/tools/SearchExtraToolsTool/prompt.ts b/packages/builtin-tools/src/tools/SearchExtraToolsTool/prompt.ts index 6f66c192a..1bf7a0a11 100644 --- a/packages/builtin-tools/src/tools/SearchExtraToolsTool/prompt.ts +++ b/packages/builtin-tools/src/tools/SearchExtraToolsTool/prompt.ts @@ -29,7 +29,7 @@ IMPORTANT: ExecuteExtraTool is always available in your tool list. After this se Query forms: - "select:CronCreate,Snip" — fetch these exact tools by name -- "discover:schedule cron job" — pure discovery, returns tool info (name, description) without loading. Use when you want to understand available tools before deciding which to invoke. +- "discover:schedule cron job" — pure discovery, returns tool info (name, description, input schema) without loading. Use when you need to understand a tool's parameters before invoking it, then call ExecuteExtraTool with the discovered tool name and params. - "notebook jupyter" — keyword search, up to max_results best matches - "+slack send" — require "slack" in the name, rank by remaining terms`