Merge pull request #141 from IronRookieCoder/fix/search-extra-tools-discover-schema

fix(search-extra-tools): return input schema in discover mode results
This commit is contained in:
Askhz 2026-05-21 16:24:49 +08:00 committed by GitHub
commit 02827e97cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 76 additions and 15 deletions

View File

@ -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<typeof outputSchema>
@ -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<string, unknown> } : {}),
}))
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(

View File

@ -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<string, unknown> }> } } = 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"')
})
})

View File

@ -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`