mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
docs: 添加 ToolSearch 设计指南 + 禁用 turn-zero 工具推荐弹窗
- 新增 docs/design/tool-search-design-guide.md,涵盖架构、搜索算法、执行管道、演进历史 - 禁用 getTurnZeroSearchExtraToolsPrefetch,消除用户输入时的频繁弹窗 - inter-turn 发现机制保持不变 Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
@@ -162,7 +162,7 @@ import {
|
||||
shouldUseGlobalCacheScope,
|
||||
} from 'src/utils/betas.js'
|
||||
import { CLAUDE_IN_CHROME_MCP_SERVER_NAME } from 'src/utils/claudeInChrome/common.js'
|
||||
import { CHROME_TOOL_SEARCH_INSTRUCTIONS } from 'src/utils/claudeInChrome/prompt.js'
|
||||
import { CHROME_SEARCH_EXTRA_TOOLS_INSTRUCTIONS } from 'src/utils/claudeInChrome/prompt.js'
|
||||
import { getMaxThinkingTokensForModel } from 'src/utils/context.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { logForDiagnosticsNoPII } from 'src/utils/diagLogs.js'
|
||||
@@ -185,15 +185,15 @@ import {
|
||||
} from 'src/utils/thinking.js'
|
||||
import {
|
||||
isDeferredToolsDeltaEnabled,
|
||||
isToolSearchEnabled,
|
||||
} from 'src/utils/toolSearch.js'
|
||||
isSearchExtraToolsEnabled,
|
||||
} from 'src/utils/searchExtraTools.js'
|
||||
import { API_MAX_MEDIA_PER_REQUEST } from '../../constants/apiLimits.js'
|
||||
import { ADVISOR_BETA_HEADER } from '../../constants/betas.js'
|
||||
import {
|
||||
formatDeferredToolLine,
|
||||
isDeferredTool,
|
||||
TOOL_SEARCH_TOOL_NAME,
|
||||
} from '@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
|
||||
SEARCH_EXTRA_TOOLS_TOOL_NAME,
|
||||
} from '@claude-code-best/builtin-tools/tools/SearchExtraToolsTool/prompt.js'
|
||||
import { count } from '../../utils/array.js'
|
||||
import { insertBlockAfterToolResults } from '../../utils/contentArray.js'
|
||||
import { validateBoundedIntEnvVar } from '../../utils/envValidation.js'
|
||||
@@ -1155,7 +1155,7 @@ async function* queryModel(
|
||||
|
||||
// Check if tool search is enabled (checks mode, model support, and threshold for auto mode)
|
||||
// This is async because it may need to calculate MCP tool description sizes for TstAuto mode
|
||||
let useToolSearch = await isToolSearchEnabled(
|
||||
let useSearchExtraTools = await isSearchExtraToolsEnabled(
|
||||
options.model,
|
||||
tools,
|
||||
options.getToolPermissionContext,
|
||||
@@ -1165,7 +1165,7 @@ async function* queryModel(
|
||||
|
||||
// Precompute once — isDeferredTool does 2 GrowthBook lookups per call
|
||||
const deferredToolNames = new Set<string>()
|
||||
if (useToolSearch) {
|
||||
if (useSearchExtraTools) {
|
||||
for (const t of tools) {
|
||||
if (isDeferredTool(t)) deferredToolNames.add(t.name)
|
||||
}
|
||||
@@ -1173,25 +1173,25 @@ async function* queryModel(
|
||||
|
||||
// Even if tool search mode is enabled, skip if there are no deferred tools
|
||||
// AND no MCP servers are still connecting. When servers are pending, keep
|
||||
// ToolSearch available so the model can discover tools after they connect.
|
||||
// SearchExtraTools available so the model can discover tools after they connect.
|
||||
if (
|
||||
useToolSearch &&
|
||||
useSearchExtraTools &&
|
||||
deferredToolNames.size === 0 &&
|
||||
!options.hasPendingMcpServers
|
||||
) {
|
||||
logForDebugging(
|
||||
'Tool search disabled: no deferred tools available to search',
|
||||
)
|
||||
useToolSearch = false
|
||||
useSearchExtraTools = false
|
||||
}
|
||||
|
||||
// Dynamic tool loading: filter deferred tools that haven't been discovered yet
|
||||
let filteredTools: Tools
|
||||
|
||||
// Deferred tools that haven't been discovered are filtered out from the API
|
||||
// request — their schemas are only included after ToolSearch discovers them.
|
||||
// request — their schemas are only included after SearchExtraTools discovers them.
|
||||
|
||||
if (useToolSearch) {
|
||||
if (useSearchExtraTools) {
|
||||
// Never include deferred tools in the API tools array — they are invoked
|
||||
// via ExecuteExtraTool which looks them up from the global tool registry
|
||||
// at runtime. Keeping the tools array stable preserves the prompt cache
|
||||
@@ -1199,19 +1199,19 @@ async function* queryModel(
|
||||
filteredTools = tools.filter(tool => {
|
||||
// Always include non-deferred tools (core tools)
|
||||
if (!deferredToolNames.has(tool.name)) return true
|
||||
// Always include ToolSearchTool (so it can discover more tools)
|
||||
if (toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME)) return true
|
||||
// Always include SearchExtraToolsTool (so it can discover more tools)
|
||||
if (toolMatchesName(tool, SEARCH_EXTRA_TOOLS_TOOL_NAME)) return true
|
||||
// All other deferred tools are excluded — use ExecuteExtraTool instead
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
filteredTools = tools.filter(
|
||||
t => !toolMatchesName(t, TOOL_SEARCH_TOOL_NAME),
|
||||
t => !toolMatchesName(t, SEARCH_EXTRA_TOOLS_TOOL_NAME),
|
||||
)
|
||||
}
|
||||
|
||||
// Tool search beta header and defer_loading removed — unified self-built
|
||||
// tool search via ToolSearchTool + ExecuteExtraTool for all providers.
|
||||
// tool search via SearchExtraToolsTool + ExecuteExtraTool for all providers.
|
||||
// No longer relies on API-side tool_reference or defer_loading features.
|
||||
|
||||
// Determine if cached microcompact is enabled for this model.
|
||||
@@ -1264,7 +1264,7 @@ async function* queryModel(
|
||||
|
||||
// Build tool schemas — no defer_loading since we use self-built tool search
|
||||
// Note: We pass the full `tools` list (not filteredTools) to toolToAPISchema so that
|
||||
// ToolSearchTool's prompt can list ALL available MCP tools. The filtering only affects
|
||||
// SearchExtraToolsTool's prompt can list ALL available MCP tools. The filtering only affects
|
||||
// which tools are actually sent to the API, not what the model sees in tool descriptions.
|
||||
const toolSchemas = await Promise.all(
|
||||
filteredTools.map(tool =>
|
||||
@@ -1278,7 +1278,7 @@ async function* queryModel(
|
||||
),
|
||||
)
|
||||
|
||||
if (useToolSearch) {
|
||||
if (useSearchExtraTools) {
|
||||
logForDebugging(
|
||||
`Dynamic tool loading: 0/${deferredToolNames.size} deferred tools in API tools array (all via ExecuteExtraTool)`,
|
||||
)
|
||||
@@ -1300,17 +1300,17 @@ async function* queryModel(
|
||||
// selected model doesn't support tool search.
|
||||
//
|
||||
// Why is this needed in addition to normalizeMessagesForAPI?
|
||||
// - normalizeMessagesForAPI uses isToolSearchEnabledNoModelCheck() because it's
|
||||
// - normalizeMessagesForAPI uses isSearchExtraToolsEnabledNoModelCheck() because it's
|
||||
// called from ~20 places (analytics, feedback, sharing, etc.), many of which
|
||||
// don't have model context. Adding model to its signature would be a large refactor.
|
||||
// - This post-processing uses the model-aware isToolSearchEnabled() check
|
||||
// - This post-processing uses the model-aware isSearchExtraToolsEnabled() check
|
||||
// - This handles mid-conversation model switching (e.g., Sonnet → Haiku) where
|
||||
// stale tool-search fields from the previous model would cause 400 errors
|
||||
//
|
||||
// Note: For assistant messages, normalizeMessagesForAPI already normalized the
|
||||
// tool inputs, so stripCallerFieldFromAssistantMessage only needs to remove the
|
||||
// 'caller' field (not re-normalize inputs).
|
||||
if (!useToolSearch) {
|
||||
if (!useSearchExtraTools) {
|
||||
messagesForAPI = messagesForAPI.map(msg => {
|
||||
switch (msg.type) {
|
||||
case 'user':
|
||||
@@ -1350,7 +1350,7 @@ async function* queryModel(
|
||||
if (getAPIProvider() === 'openai') {
|
||||
const { queryModelOpenAI } = await import('./openai/index.js')
|
||||
// OpenAI emulates Anthropic's dynamic tool loading client-side. It needs
|
||||
// the full tool pool so ToolSearchTool can search deferred MCP tools that
|
||||
// the full tool pool so SearchExtraToolsTool can search deferred MCP tools that
|
||||
// were intentionally filtered out of the initial API tool list above.
|
||||
yield* queryModelOpenAI(
|
||||
messagesForAPI,
|
||||
@@ -1400,7 +1400,7 @@ async function* queryModel(
|
||||
// When the delta attachment is enabled, deferred tools are announced
|
||||
// via persisted deferred_tools_delta attachments instead of this
|
||||
// ephemeral prepend (which busts cache whenever the pool changes).
|
||||
if (useToolSearch && !isDeferredToolsDeltaEnabled()) {
|
||||
if (useSearchExtraTools && !isDeferredToolsDeltaEnabled()) {
|
||||
const deferredToolList = tools
|
||||
.filter(t => deferredToolNames.has(t.name))
|
||||
.map(formatDeferredToolLine)
|
||||
@@ -1409,7 +1409,7 @@ async function* queryModel(
|
||||
if (deferredToolList) {
|
||||
messagesForAPI = [
|
||||
createUserMessage({
|
||||
content: `<available-deferred-tools>\n${deferredToolList}\n</available-deferred-tools>`,
|
||||
content: `<available-deferred-tools>\n${deferredToolList}\n</available-deferred-tools>\nTo invoke any tool listed above, use ExecuteExtraTool with {"tool_name": "<name>", "params": {...}}. This is the ONLY way to call deferred tools — do not read source code or analyze implementation, just call ExecuteExtraTool directly.`,
|
||||
isMeta: true,
|
||||
}),
|
||||
...messagesForAPI,
|
||||
@@ -1425,7 +1425,7 @@ async function* queryModel(
|
||||
isToolFromMcpServer(t.name, CLAUDE_IN_CHROME_MCP_SERVER_NAME),
|
||||
)
|
||||
const injectChromeHere =
|
||||
useToolSearch && hasChromeTools && !isMcpInstructionsDeltaEnabled()
|
||||
useSearchExtraTools && hasChromeTools && !isMcpInstructionsDeltaEnabled()
|
||||
|
||||
// filter(Boolean) works by converting each element to a boolean - empty strings become false and are filtered out.
|
||||
systemPrompt = asSystemPrompt(
|
||||
@@ -1437,7 +1437,7 @@ async function* queryModel(
|
||||
}),
|
||||
...systemPrompt,
|
||||
...(advisorModel ? [ADVISOR_TOOL_INSTRUCTIONS] : []),
|
||||
...(injectChromeHere ? [CHROME_TOOL_SEARCH_INSTRUCTIONS] : []),
|
||||
...(injectChromeHere ? [CHROME_SEARCH_EXTRA_TOOLS_INSTRUCTIONS] : []),
|
||||
].filter(Boolean),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user