mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
refactor: 统一自建 Tool Search — 移除 tool_reference/defer_loading 依赖,全 provider 通用
- 重命名 ExecuteTool → ExecuteExtraTool,作为一等工具始终可用 - ToolSearchTool 输出改为纯文本(区分 core/deferred),移除 tool_reference blocks - 移除 modelSupportsToolReference() 及相关 GrowthBook 配置 - 移除 API 侧 defer_loading 字段和 tool search beta header 注入 - 简化 system prompt(工具使用指南从 ~120 行压缩到 ~10 行) - extractDiscoveredToolNames 支持文本格式解析(向后兼容旧 session 的 tool_reference) - 更新 promptEngineeringAudit 测试以匹配简化后的 prompt 结构 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -128,14 +128,14 @@ export const ExecuteTool = buildTool({
|
||||
async checkPermissions() {
|
||||
return {
|
||||
behavior: 'passthrough',
|
||||
message: 'ExecuteTool delegates permission to the target tool.',
|
||||
message: 'ExecuteExtraTool delegates permission to the target tool.',
|
||||
}
|
||||
},
|
||||
renderToolUseMessage(input) {
|
||||
return `Executing ${input.tool_name}...`
|
||||
},
|
||||
userFacingName() {
|
||||
return 'ExecuteTool'
|
||||
return 'ExecuteExtraTool'
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(content, toolUseID) {
|
||||
return {
|
||||
|
||||
@@ -33,7 +33,6 @@ mock.module('src/utils/toolSearch.js', () => ({
|
||||
isToolSearchEnabledOptimistic: () => true,
|
||||
getAutoToolSearchCharThreshold: () => 100,
|
||||
getToolSearchMode: () => 'tst' as const,
|
||||
modelSupportsToolReference: () => true,
|
||||
isToolSearchToolAvailable: async () => true,
|
||||
isToolSearchEnabled: async () => true,
|
||||
isToolReferenceBlock: () => false,
|
||||
@@ -43,7 +42,7 @@ mock.module('src/utils/toolSearch.js', () => ({
|
||||
}))
|
||||
|
||||
mock.module('src/constants/tools.js', () => ({
|
||||
CORE_TOOLS: new Set(['ExecuteTool', 'ToolSearch']),
|
||||
CORE_TOOLS: new Set(['ExecuteExtraTool', 'ToolSearch']),
|
||||
}))
|
||||
|
||||
// Mock messages module
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const EXECUTE_TOOL_NAME = 'ExecuteTool'
|
||||
export const EXECUTE_TOOL_NAME = 'ExecuteExtraTool'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { EXECUTE_TOOL_NAME } from './constants.js'
|
||||
|
||||
export const DESCRIPTION =
|
||||
'Execute a deferred tool by name with parameters. Use this after discovering a tool via ToolSearch.'
|
||||
'ExecuteExtraTool — execute a deferred tool by name with parameters. This tool is always available. Use it after discovering a tool via ToolSearch.'
|
||||
|
||||
export function getPrompt(): string {
|
||||
return `Execute a deferred tool by name. This tool accepts a tool_name and params object, looks up the target tool in the global tool registry, and delegates execution to it.
|
||||
return `ExecuteExtraTool — execute a deferred tool by name. This tool is always available in your tool list. You do NOT need to search for it.
|
||||
|
||||
This tool accepts a tool_name and params object, looks up the target tool in the global tool registry, and delegates execution to it.
|
||||
|
||||
Use this tool after discovering a deferred tool via ToolSearch. The tool_name must match the exact name returned by ToolSearch (e.g., "CronCreate", "mcp__server__action").
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ import {
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import { escapeRegExp } from 'src/utils/stringUtils.js'
|
||||
import {
|
||||
isToolSearchEnabledOptimistic,
|
||||
modelSupportsToolReference,
|
||||
} from 'src/utils/toolSearch.js'
|
||||
import { isToolSearchEnabledOptimistic } from 'src/utils/toolSearch.js'
|
||||
import { getPrompt, isDeferredTool, TOOL_SEARCH_TOOL_NAME } from './prompt.js'
|
||||
import { getToolIndex, searchTools } from 'src/services/toolSearch/toolIndex.js'
|
||||
import type { ToolSearchResult } from 'src/services/toolSearch/toolIndex.js'
|
||||
@@ -48,6 +45,8 @@ export const outputSchema = lazySchema(() =>
|
||||
query: z.string(),
|
||||
total_deferred_tools: z.number(),
|
||||
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(),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
@@ -120,6 +119,7 @@ function buildSearchResult(
|
||||
query: string,
|
||||
totalDeferredTools: number,
|
||||
pendingMcpServers?: string[],
|
||||
alreadyLoaded?: string[],
|
||||
): { data: Output } {
|
||||
return {
|
||||
data: {
|
||||
@@ -129,6 +129,9 @@ function buildSearchResult(
|
||||
...(pendingMcpServers && pendingMcpServers.length > 0
|
||||
? { pending_mcp_servers: pendingMcpServers }
|
||||
: {}),
|
||||
...(alreadyLoaded && alreadyLoaded.length > 0
|
||||
? { already_loaded: alreadyLoaded }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -376,13 +379,18 @@ export const ToolSearchTool = buildTool({
|
||||
.filter(Boolean)
|
||||
|
||||
const found: string[] = []
|
||||
const alreadyLoaded: string[] = []
|
||||
const missing: string[] = []
|
||||
for (const toolName of requested) {
|
||||
const tool =
|
||||
findToolByName(deferredTools, toolName) ??
|
||||
findToolByName(tools, toolName)
|
||||
if (tool) {
|
||||
if (!found.includes(tool.name)) found.push(tool.name)
|
||||
const deferredMatch = findToolByName(deferredTools, toolName)
|
||||
const fullMatch = deferredMatch ?? findToolByName(tools, toolName)
|
||||
if (fullMatch) {
|
||||
if (!found.includes(fullMatch.name)) {
|
||||
found.push(fullMatch.name)
|
||||
if (!deferredMatch) {
|
||||
alreadyLoaded.push(fullMatch.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
missing.push(toolName)
|
||||
}
|
||||
@@ -410,7 +418,13 @@ export const ToolSearchTool = buildTool({
|
||||
logForDebugging(`ToolSearchTool: selected ${found.join(', ')}`)
|
||||
}
|
||||
logSearchOutcome(found, 'select')
|
||||
return buildSearchResult(found, query, deferredTools.length)
|
||||
return buildSearchResult(
|
||||
found,
|
||||
query,
|
||||
deferredTools.length,
|
||||
undefined,
|
||||
alreadyLoaded.length > 0 ? alreadyLoaded : undefined,
|
||||
)
|
||||
}
|
||||
|
||||
// Check for discover: prefix — pure discovery search.
|
||||
@@ -444,6 +458,7 @@ export const ToolSearchTool = buildTool({
|
||||
}
|
||||
|
||||
// Keyword search + TF-IDF search in parallel
|
||||
const deferredToolNames = new Set(deferredTools.map(t => t.name))
|
||||
const [keywordMatches, index] = await Promise.all([
|
||||
searchToolsWithKeywords(query, deferredTools, tools, max_results),
|
||||
getToolIndex(deferredTools),
|
||||
@@ -474,6 +489,9 @@ export const ToolSearchTool = buildTool({
|
||||
.slice(0, max_results)
|
||||
.map(([name]) => name)
|
||||
|
||||
// Identify already-loaded (core) tools among matches
|
||||
const alreadyLoaded = matches.filter(name => !deferredToolNames.has(name))
|
||||
|
||||
logForDebugging(
|
||||
`ToolSearchTool: keyword search for "${query}", found ${matches.length} matches`,
|
||||
)
|
||||
@@ -491,21 +509,29 @@ export const ToolSearchTool = buildTool({
|
||||
)
|
||||
}
|
||||
|
||||
return buildSearchResult(matches, query, deferredTools.length)
|
||||
return buildSearchResult(
|
||||
matches,
|
||||
query,
|
||||
deferredTools.length,
|
||||
undefined,
|
||||
alreadyLoaded.length > 0 ? alreadyLoaded : undefined,
|
||||
)
|
||||
},
|
||||
renderToolUseMessage() {
|
||||
return null
|
||||
renderToolUseMessage(input: Partial<{ query: string; max_results: number }>) {
|
||||
if (!input.query) return null
|
||||
return `"${input.query}"`
|
||||
},
|
||||
userFacingName() {
|
||||
return 'ToolSearch'
|
||||
},
|
||||
userFacingName: () => '',
|
||||
/**
|
||||
* Returns a tool_result with tool_reference blocks.
|
||||
* This format works on 1P/Foundry. Bedrock/Vertex may not support
|
||||
* client-side tool_reference expansion yet.
|
||||
* Returns a tool_result with text output guiding the model to use ExecuteExtraTool.
|
||||
* No longer uses tool_reference blocks — unified self-built tool search for all providers.
|
||||
*/
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: Output,
|
||||
toolUseID: string,
|
||||
context?: { mainLoopModel?: string },
|
||||
_context?: { mainLoopModel?: string },
|
||||
): ToolResultBlockParam {
|
||||
if (content.matches.length === 0) {
|
||||
let text = 'No matching deferred tools found'
|
||||
@@ -522,25 +548,35 @@ export const ToolSearchTool = buildTool({
|
||||
}
|
||||
}
|
||||
|
||||
const supportsToolRef = context?.mainLoopModel
|
||||
? modelSupportsToolReference(context.mainLoopModel)
|
||||
: true // default: assume supported (backwards compatible)
|
||||
if (!supportsToolRef) {
|
||||
// Text mode: return tool name list for non-Anthropic providers
|
||||
return {
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUseID,
|
||||
content: `Found ${content.matches.length} tool(s): ${content.matches.join(', ')}. Use ExecuteTool with tool_name and params to invoke.`,
|
||||
}
|
||||
// Separate already-loaded (core) tools from truly deferred tools
|
||||
const alreadyLoadedNames = content.already_loaded ?? []
|
||||
const deferredNames = content.matches.filter(
|
||||
n => !alreadyLoadedNames.includes(n),
|
||||
)
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
// Core tools: clear "call directly" message, NO ExecuteExtraTool hint
|
||||
if (alreadyLoadedNames.length > 0) {
|
||||
parts.push(
|
||||
`Already loaded as core tool(s): ${alreadyLoadedNames.join(', ')}. Call these directly using your normal tool interface — do NOT use ExecuteExtraTool for them.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Deferred tools: guide to ExecuteExtraTool
|
||||
if (deferredNames.length > 0) {
|
||||
parts.push(
|
||||
`Found ${deferredNames.length} deferred tool(s): ${deferredNames.join(', ')}.` +
|
||||
`\nUse ExecuteExtraTool with {"tool_name": "<name>", "params": {...}} to invoke any of these deferred tools.`,
|
||||
)
|
||||
}
|
||||
|
||||
const text = parts.join('\n')
|
||||
|
||||
return {
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUseID,
|
||||
content: content.matches.map(name => ({
|
||||
type: 'tool_reference' as const,
|
||||
tool_name: name,
|
||||
})),
|
||||
} as unknown as ToolResultBlockParam
|
||||
content: text,
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
|
||||
@@ -32,7 +32,6 @@ mock.module('src/utils/toolSearch.js', () => ({
|
||||
isToolSearchEnabledOptimistic: () => true,
|
||||
getAutoToolSearchCharThreshold: () => 100,
|
||||
getToolSearchMode: () => 'tst' as const,
|
||||
modelSupportsToolReference: (model: string) => !model.includes('haiku'),
|
||||
isToolSearchToolAvailable: async () => true,
|
||||
isToolSearchEnabled: async () => true,
|
||||
isToolReferenceBlock: () => false,
|
||||
@@ -42,7 +41,7 @@ mock.module('src/utils/toolSearch.js', () => ({
|
||||
}))
|
||||
|
||||
mock.module('src/constants/tools.js', () => ({
|
||||
CORE_TOOLS: new Set(['Read', 'Edit', 'ToolSearch', 'ExecuteTool']),
|
||||
CORE_TOOLS: new Set(['Read', 'Edit', 'ToolSearch', 'ExecuteExtraTool']),
|
||||
}))
|
||||
|
||||
// Mock toolIndex module
|
||||
@@ -172,7 +171,7 @@ describe('ToolSearchTool search enhancements', () => {
|
||||
expect(result.data.matches).toContain('ToolB')
|
||||
})
|
||||
|
||||
test('text mode output for non-Anthropic models', async () => {
|
||||
test('text mode output for all models (unified self-built search)', async () => {
|
||||
const tool = makeDeferredTool('TestTool', 'A test tool')
|
||||
mockGetToolIndex.mockResolvedValueOnce([])
|
||||
mockSearchTools.mockReturnValueOnce([])
|
||||
@@ -190,26 +189,28 @@ describe('ToolSearchTool search enhancements', () => {
|
||||
},
|
||||
])
|
||||
|
||||
// Use mapToolResultToToolResultBlockParam directly
|
||||
// mapToolResultToToolResultBlockParam always returns text, not tool_reference
|
||||
const blockParam = ToolSearchTool.mapToolResultToToolResultBlockParam(
|
||||
{ matches: ['TestTool'], query: 'test', total_deferred_tools: 1 },
|
||||
'tool-use-123',
|
||||
{ mainLoopModel: 'claude-3-haiku-20240307' },
|
||||
)
|
||||
|
||||
expect(blockParam.content).toContain('ExecuteTool')
|
||||
expect(typeof blockParam.content).toBe('string')
|
||||
expect(blockParam.content as string).toContain('TestTool')
|
||||
expect(blockParam.content as string).toContain('ExecuteExtraTool')
|
||||
})
|
||||
|
||||
test('tool_reference mode for Anthropic models', async () => {
|
||||
test('text output works for any model without distinction', async () => {
|
||||
const blockParam = ToolSearchTool.mapToolResultToToolResultBlockParam(
|
||||
{ matches: ['TestTool'], query: 'test', total_deferred_tools: 1 },
|
||||
'tool-use-123',
|
||||
{ mainLoopModel: 'claude-sonnet-4-20250514' },
|
||||
)
|
||||
|
||||
// Should contain tool_reference type
|
||||
const content = blockParam.content as Array<{ type: string }>
|
||||
expect(content[0]!.type).toBe('tool_reference')
|
||||
expect(typeof blockParam.content).toBe('string')
|
||||
expect(blockParam.content as string).toContain('TestTool')
|
||||
expect(blockParam.content as string).toContain('ExecuteExtraTool')
|
||||
})
|
||||
|
||||
test('backwards compatible without context parameter', async () => {
|
||||
@@ -218,9 +219,9 @@ describe('ToolSearchTool search enhancements', () => {
|
||||
'tool-use-123',
|
||||
)
|
||||
|
||||
// Should default to tool_reference mode
|
||||
const content = blockParam.content as Array<{ type: string }>
|
||||
expect(content[0]!.type).toBe('tool_reference')
|
||||
expect(typeof blockParam.content).toBe('string')
|
||||
expect(blockParam.content as string).toContain('TestTool')
|
||||
expect(blockParam.content as string).toContain('ExecuteExtraTool')
|
||||
})
|
||||
|
||||
test('empty results return helpful message', async () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ export { TOOL_SEARCH_TOOL_NAME } from './constants.js'
|
||||
|
||||
import { TOOL_SEARCH_TOOL_NAME } from './constants.js'
|
||||
|
||||
const PROMPT_HEAD = `Fetches full schema definitions for deferred tools so they can be called.
|
||||
const PROMPT_HEAD = `Search for deferred tools by name or keyword.
|
||||
|
||||
`
|
||||
|
||||
@@ -23,13 +23,13 @@ function getToolLocationHint(): string {
|
||||
: 'Deferred tools appear by name in <available-deferred-tools> messages.'
|
||||
}
|
||||
|
||||
const PROMPT_TAIL = ` Until fetched, only the name is known — there is no parameter schema, so the tool cannot be invoked. This tool takes a query, matches it against the deferred tool list, and returns the matched tools' complete JSONSchema definitions inside a <functions> block. Once a tool's schema appears in that result, it is callable exactly like any tool defined at the top of the prompt.
|
||||
const PROMPT_TAIL = ` Returns matching tool names.
|
||||
|
||||
Result format: each matched tool appears as one <function>{"description": "...", "name": "...", "parameters": {...}}</function> line inside the <functions> block — the same encoding as the tool list at the top of this prompt.
|
||||
ExecuteExtraTool is a first-class tool that is always available — you do NOT need to search for it. After this search returns tool names, call ExecuteExtraTool directly with {"tool_name": "<returned_name>", "params": {...}} to invoke any deferred tool.
|
||||
|
||||
Query forms:
|
||||
- "select:Read,Edit,Grep" — fetch these exact tools by name
|
||||
- "discover:schedule cron job" — pure discovery, returns tool info (name, description, schema) without loading. Use when you want to understand available tools before deciding which to invoke.
|
||||
- "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.
|
||||
- "notebook jupyter" — keyword search, up to max_results best matches
|
||||
- "+slack send" — require "slack" in the name, rank by remaining terms`
|
||||
|
||||
@@ -38,7 +38,7 @@ Query forms:
|
||||
* A tool is deferred if it is NOT in CORE_TOOLS and does NOT have alwaysLoad: true.
|
||||
* Core tools are always loaded — never deferred.
|
||||
* All other tools (non-core built-in + all MCP tools) are deferred
|
||||
* and must be discovered via ToolSearchTool / ExecuteTool.
|
||||
* and must be discovered via ToolSearchTool / ExecuteExtraTool.
|
||||
*/
|
||||
export function isDeferredTool(tool: Tool): boolean {
|
||||
// Explicit opt-out via _meta['anthropic/alwaysLoad']
|
||||
|
||||
Reference in New Issue
Block a user