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:
claude-code-best
2026-05-09 16:45:56 +08:00
parent bd2253846f
commit 2cf18c4c49
61 changed files with 753 additions and 423 deletions

View File

@@ -196,7 +196,7 @@ async function runQueryModel(
// We mock at module level. Bun's mock.module replaces the module for the
// entire file, so we configure the stream per-test via a shared variable.
let _nextEvents: BetaRawMessageStreamEvent[] = []
let _toolSearchEnabled = false
let _searchExtraToolsEnabled = false
/** Captured arguments from the last chat.completions.create() call */
let _lastCreateArgs: Record<string, any> | null = null
@@ -316,15 +316,15 @@ mock.module('../../../../utils/api.js', () => ({
toolToAPISchema: async (t: any) => t,
}))
mock.module('../../../../utils/toolSearch.js', () => ({
isToolSearchEnabled: async () => _toolSearchEnabled,
mock.module('../../../../utils/searchExtraTools.js', () => ({
isSearchExtraToolsEnabled: async () => _searchExtraToolsEnabled,
extractDiscoveredToolNames: () => new Set(),
isDeferredToolsDeltaEnabled: () => false,
}))
mock.module('../../../../tools/ToolSearchTool/prompt.js', () => ({
mock.module('../../../../tools/SearchExtraToolsTool/prompt.js', () => ({
isDeferredTool: () => false,
TOOL_SEARCH_TOOL_NAME: '__tool_search__',
SEARCH_EXTRA_TOOLS_TOOL_NAME: '__tool_search__',
}))
mock.module('../../../../cost-tracker.js', () => ({
@@ -606,14 +606,14 @@ describe('queryModelOpenAI — max_tokens forwarded to request', () => {
describe('queryModelOpenAI — deferred MCP tool visibility', () => {
test('prepends available deferred MCP tools to OpenAI messages', async () => {
_toolSearchEnabled = true
_searchExtraToolsEnabled = true
_nextEvents = [makeMessageStart(), makeMessageStop()]
try {
const { queryModelOpenAI } = await import('../index.js')
const tools: any[] = [
{
name: 'ToolSearch',
name: 'SearchExtraTools',
isMcp: false,
input_schema: { type: 'object', properties: {} },
prompt: async () => 'Search deferred tools',
@@ -655,7 +655,7 @@ describe('queryModelOpenAI — deferred MCP tool visibility', () => {
'<available-deferred-tools>\\nmcp__wechat__send_message\\n</available-deferred-tools>',
)
} finally {
_toolSearchEnabled = false
_searchExtraToolsEnabled = false
}
})
})

View File

@@ -52,14 +52,14 @@ import {
} from '../../../utils/messages.js'
import type { SDKAssistantMessageError } from '../../../entrypoints/agentSdkTypes.js'
import {
isToolSearchEnabled,
isSearchExtraToolsEnabled,
isDeferredToolsDeltaEnabled,
} from '../../../utils/toolSearch.js'
} from '../../../utils/searchExtraTools.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'
/**
* Mirrors the Anthropic request path's deferred-tool announcement for OpenAI.
@@ -67,15 +67,15 @@ import {
* OpenAI-compatible endpoints cannot consume Anthropic's `defer_loading` or
* `tool_reference` beta payloads directly, so the model needs the same textual
* list of deferred MCP tool names that Anthropic receives before it can ask
* ToolSearchTool to load their full schemas.
* SearchExtraToolsTool to load their full schemas.
*/
function prependDeferredToolListIfNeeded(
messages: (AssistantMessage | UserMessage)[],
tools: Tools,
deferredToolNames: Set<string>,
useToolSearch: boolean,
useSearchExtraTools: boolean,
): (AssistantMessage | UserMessage)[] {
if (!useToolSearch || isDeferredToolsDeltaEnabled()) return messages
if (!useSearchExtraTools || isDeferredToolsDeltaEnabled()) return messages
const deferredToolList = tools
.filter(tool => deferredToolNames.has(tool.name))
@@ -194,7 +194,7 @@ export async function* queryModelOpenAI(
const messagesForAPI = normalizeMessagesForAPI(messages, tools)
// 3. Check if tool search is enabled (similar to Anthropic path)
const useToolSearch = await isToolSearchEnabled(
const useSearchExtraTools = await isSearchExtraToolsEnabled(
options.model,
tools,
options.getToolPermissionContext ||
@@ -205,7 +205,7 @@ export async function* queryModelOpenAI(
// 4. Build deferred tools set (similar to Anthropic path)
const deferredToolNames = new Set<string>()
if (useToolSearch) {
if (useSearchExtraTools) {
for (const t of tools) {
if (isDeferredTool(t)) deferredToolNames.add(t.name)
}
@@ -216,12 +216,12 @@ export async function* queryModelOpenAI(
// via ExecuteExtraTool which looks them up from the global tool registry
// at runtime. Keeping the tools array stable preserves the prompt cache.
let filteredTools = tools
if (useToolSearch && deferredToolNames.size > 0) {
if (useSearchExtraTools && deferredToolNames.size > 0) {
filteredTools = tools.filter(tool => {
// Always include non-deferred 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
})
@@ -236,7 +236,7 @@ export async function* queryModelOpenAI(
agents: options.agents,
allowedAgentTypes: options.allowedAgentTypes,
model: options.model,
deferLoading: useToolSearch && deferredToolNames.has(tool.name),
deferLoading: useSearchExtraTools && deferredToolNames.has(tool.name),
}),
),
)
@@ -260,7 +260,7 @@ export async function* queryModelOpenAI(
openAIConvertibleMessages,
tools,
deferredToolNames,
useToolSearch,
useSearchExtraTools,
)
const openaiMessages = anthropicMessagesToOpenAI(
messagesWithDeferredToolList,
@@ -271,7 +271,7 @@ export async function* queryModelOpenAI(
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
// 9. Log tool filtering details
if (useToolSearch) {
if (useSearchExtraTools) {
const includedDeferredTools = filteredTools.filter(t =>
deferredToolNames.has(t.name),
).length