From 14f334b93d59c1082e1756723291273ef36ebd0b Mon Sep 17 00:00:00 2001 From: IronRookieCoder Date: Thu, 21 May 2026 16:14:00 +0800 Subject: [PATCH] fix(search-extra-tools): return input schema in discover mode results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Bug 详情 SearchExtraTools 的 discover: 查询模式计算了包含 description 和 inputSchema 的文本,但调用 buildSearchResult() 时只传入工具名数组,schema 信息被丢弃。 LLM 在 discover 后无法看到 MCP 等 deferred 工具的参数定义。 ## 根因 Output 类型中缺少承载工具详情的字段,buildSearchResult() 和 mapToolResultToToolResultBlockParam() 均未处理 discover 模式的详情渲染。 ## 修复方案 - Output 类型新增可选 details 字段(name、description、score、inputSchema) - buildSearchResult() 接受并透传 details - discover 分支将 TF-IDF 结果结构化传入输出 - mapToolResultToToolResultBlockParam() 新增 discover 模式渲染路径 - 更新 prompt 中 discover 模式描述,提及 input schema 和执行链 ## 变更要点 - SearchExtraToolsTool.ts:4 处改动(Output schema、buildSearchResult、 discover 分支、渲染逻辑) - SearchExtraToolsTool.test.ts:更新 1 个测试 + 新增 1 个渲染测试 - prompt.ts:更新 discover 描述 ## 自测 - 7 tests / 0 fail(新增 1 个渲染测试,更新 1 个 discover 测试) - tsc --noEmit 无新增类型错误 - JSONL 日志验证 LLM 可正确解析返回的 schema Co-Authored-By: CoStrict-DeepSeek-V4-Pro --- .../SearchExtraToolsTool.ts | 53 +++++++++++++++---- .../__tests__/SearchExtraToolsTool.test.ts | 36 +++++++++++-- .../src/tools/SearchExtraToolsTool/prompt.ts | 2 +- 3 files changed, 76 insertions(+), 15 deletions(-) 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`