mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +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:
@@ -387,11 +387,11 @@ async function countBuiltInToolTokens(
|
||||
}
|
||||
|
||||
// Check if tool search is enabled
|
||||
const { isToolSearchEnabled } = await import('./toolSearch.js')
|
||||
const { isSearchExtraToolsEnabled } = await import('./searchExtraTools.js')
|
||||
const { isDeferredTool } = await import(
|
||||
'@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
|
||||
'@claude-code-best/builtin-tools/tools/SearchExtraToolsTool/prompt.js'
|
||||
)
|
||||
const isDeferred = await isToolSearchEnabled(
|
||||
const isDeferred = await isSearchExtraToolsEnabled(
|
||||
model ?? '',
|
||||
tools,
|
||||
getToolPermissionContext,
|
||||
@@ -672,13 +672,13 @@ export async function countMcpToolTokens(
|
||||
)
|
||||
|
||||
// Check if tool search is enabled - if so, MCP tools are deferred
|
||||
// isToolSearchEnabled handles threshold calculation internally for TstAuto mode
|
||||
const { isToolSearchEnabled } = await import('./toolSearch.js')
|
||||
// isSearchExtraToolsEnabled handles threshold calculation internally for TstAuto mode
|
||||
const { isSearchExtraToolsEnabled } = await import('./searchExtraTools.js')
|
||||
const { isDeferredTool } = await import(
|
||||
'@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
|
||||
'@claude-code-best/builtin-tools/tools/SearchExtraToolsTool/prompt.js'
|
||||
)
|
||||
|
||||
const isDeferred = await isToolSearchEnabled(
|
||||
const isDeferred = await isSearchExtraToolsEnabled(
|
||||
model,
|
||||
tools,
|
||||
getToolPermissionContext,
|
||||
@@ -686,7 +686,7 @@ export async function countMcpToolTokens(
|
||||
'analyzeMcp',
|
||||
)
|
||||
|
||||
// Find MCP tools that have been used in messages (loaded via ToolSearchTool)
|
||||
// Find MCP tools that have been used in messages (loaded via SearchExtraToolsTool)
|
||||
const loadedMcpToolNames = new Set<string>()
|
||||
if (isDeferred && messages) {
|
||||
const mcpToolNameSet = new Set(mcpTools.map(t => t.name))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
||||
import type { ToolDiscoveryResult } from '../services/toolSearch/prefetch.js'
|
||||
import type { ToolDiscoveryResult } from '../services/searchExtraTools/prefetch.js'
|
||||
import {
|
||||
logEvent,
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
@@ -98,10 +98,10 @@ const skillSearchModules = feature('EXPERIMENTAL_SKILL_SEARCH')
|
||||
require('../services/skillSearch/prefetch.js') as typeof import('../services/skillSearch/prefetch.js'),
|
||||
}
|
||||
: null
|
||||
const toolSearchModules = feature('EXPERIMENTAL_TOOL_SEARCH')
|
||||
const searchExtraToolsModules = feature('EXPERIMENTAL_SEARCH_EXTRA_TOOLS')
|
||||
? {
|
||||
prefetch:
|
||||
require('../services/toolSearch/prefetch.js') as typeof import('../services/toolSearch/prefetch.js'),
|
||||
require('../services/searchExtraTools/prefetch.js') as typeof import('../services/searchExtraTools/prefetch.js'),
|
||||
}
|
||||
: null
|
||||
const autoModeStateModule = feature('TRANSCRIPT_CLASSIFIER')
|
||||
@@ -166,17 +166,17 @@ import type { QuerySource } from '../constants/querySource.js'
|
||||
import {
|
||||
getDeferredToolsDelta,
|
||||
isDeferredToolsDeltaEnabled,
|
||||
isToolSearchEnabledOptimistic,
|
||||
isToolSearchToolAvailable,
|
||||
isSearchExtraToolsEnabledOptimistic,
|
||||
isSearchExtraToolsToolAvailable,
|
||||
type DeferredToolsDeltaScanContext,
|
||||
} from './toolSearch.js'
|
||||
} from './searchExtraTools.js'
|
||||
import {
|
||||
getMcpInstructionsDelta,
|
||||
isMcpInstructionsDeltaEnabled,
|
||||
type ClientSideInstruction,
|
||||
} from './mcpInstructionsDelta.js'
|
||||
import { CLAUDE_IN_CHROME_MCP_SERVER_NAME } from './claudeInChrome/common.js'
|
||||
import { CHROME_TOOL_SEARCH_INSTRUCTIONS } from './claudeInChrome/prompt.js'
|
||||
import { CHROME_SEARCH_EXTRA_TOOLS_INSTRUCTIONS } from './claudeInChrome/prompt.js'
|
||||
import type { MCPServerConnection } from '../services/mcp/types.js'
|
||||
import type {
|
||||
HookEvent,
|
||||
@@ -845,9 +845,9 @@ export async function getAttachments(
|
||||
]
|
||||
: []),
|
||||
// Tool discovery on turn 0. Inter-turn discovery runs via
|
||||
// startToolSearchPrefetch in query.ts.
|
||||
...(feature('EXPERIMENTAL_TOOL_SEARCH') &&
|
||||
toolSearchModules &&
|
||||
// startSearchExtraToolsPrefetch in query.ts.
|
||||
...(feature('EXPERIMENTAL_SEARCH_EXTRA_TOOLS') &&
|
||||
searchExtraToolsModules &&
|
||||
!options?.skipSkillDiscovery
|
||||
? [
|
||||
maybe('tool_discovery', async () => {
|
||||
@@ -855,7 +855,7 @@ export async function getAttachments(
|
||||
return []
|
||||
}
|
||||
const result =
|
||||
await toolSearchModules.prefetch.getTurnZeroToolSearchPrefetch(
|
||||
await searchExtraToolsModules.prefetch.getTurnZeroSearchExtraToolsPrefetch(
|
||||
input,
|
||||
context.options.tools ?? [],
|
||||
)
|
||||
@@ -1513,15 +1513,15 @@ export function getDeferredToolsDeltaAttachment(
|
||||
scanContext?: DeferredToolsDeltaScanContext,
|
||||
): Attachment[] {
|
||||
if (!isDeferredToolsDeltaEnabled()) return []
|
||||
// These three checks mirror the sync parts of isToolSearchEnabled —
|
||||
// the attachment text says "available via ToolSearch", so ToolSearch
|
||||
// These three checks mirror the sync parts of isSearchExtraToolsEnabled —
|
||||
// the attachment text says "available via SearchExtraTools", so SearchExtraTools
|
||||
// has to actually be in the request. The async auto-threshold check
|
||||
// is not replicated (would double-fire tengu_tool_search_mode_decision);
|
||||
// in tst-auto below-threshold the attachment can fire while ToolSearch
|
||||
// is not replicated (would double-fire tengu_search_extra_tools_mode_decision);
|
||||
// in tst-auto below-threshold the attachment can fire while SearchExtraTools
|
||||
// is filtered out, but that's a narrow case and the tools announced
|
||||
// are directly callable anyway.
|
||||
if (!isToolSearchEnabledOptimistic()) return []
|
||||
if (!isToolSearchToolAvailable(tools)) return []
|
||||
if (!isSearchExtraToolsEnabledOptimistic()) return []
|
||||
if (!isSearchExtraToolsToolAvailable(tools)) return []
|
||||
const delta = getDeferredToolsDelta(tools, messages ?? [], scanContext)
|
||||
if (!delta) return []
|
||||
return [{ type: 'deferred_tools_delta', ...delta }]
|
||||
@@ -1618,14 +1618,17 @@ export function getMcpInstructionsDeltaAttachment(
|
||||
): Attachment[] {
|
||||
if (!isMcpInstructionsDeltaEnabled()) return []
|
||||
|
||||
// The chrome ToolSearch hint is client-authored and ToolSearch-conditional;
|
||||
// The chrome SearchExtraTools hint is client-authored and SearchExtraTools-conditional;
|
||||
// actual server `instructions` are unconditional. Decide the chrome part
|
||||
// here, pass it into the pure diff as a synthesized entry.
|
||||
const clientSide: ClientSideInstruction[] = []
|
||||
if (isToolSearchEnabledOptimistic() && isToolSearchToolAvailable(tools)) {
|
||||
if (
|
||||
isSearchExtraToolsEnabledOptimistic() &&
|
||||
isSearchExtraToolsToolAvailable(tools)
|
||||
) {
|
||||
clientSide.push({
|
||||
serverName: CLAUDE_IN_CHROME_MCP_SERVER_NAME,
|
||||
block: CHROME_TOOL_SEARCH_INSTRUCTIONS,
|
||||
block: CHROME_SEARCH_EXTRA_TOOLS_INSTRUCTIONS,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
REDACT_THINKING_BETA_HEADER,
|
||||
STRUCTURED_OUTPUTS_BETA_HEADER,
|
||||
TOKEN_EFFICIENT_TOOLS_BETA_HEADER,
|
||||
TOOL_SEARCH_BETA_HEADER_1P,
|
||||
TOOL_SEARCH_BETA_HEADER_3P,
|
||||
SEARCH_EXTRA_TOOLS_BETA_HEADER_1P,
|
||||
SEARCH_EXTRA_TOOLS_BETA_HEADER_3P,
|
||||
WEB_SEARCH_BETA_HEADER,
|
||||
} from '../constants/betas.js'
|
||||
import { OAUTH_BETA_HEADER } from '../constants/oauth.js'
|
||||
@@ -200,12 +200,12 @@ export function modelSupportsAutoMode(model: string): boolean {
|
||||
* - Vertex AI / Bedrock: tool-search-tool-2025-10-19
|
||||
* - All other providers: advanced-tool-use-2025-11-20
|
||||
*/
|
||||
export function getToolSearchBetaHeader(): string {
|
||||
export function getSearchExtraToolsBetaHeader(): string {
|
||||
const provider = getAPIProvider()
|
||||
if (provider === 'vertex' || provider === 'bedrock') {
|
||||
return TOOL_SEARCH_BETA_HEADER_3P
|
||||
return SEARCH_EXTRA_TOOLS_BETA_HEADER_3P
|
||||
}
|
||||
return TOOL_SEARCH_BETA_HEADER_1P
|
||||
return SEARCH_EXTRA_TOOLS_BETA_HEADER_1P
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,17 +47,17 @@ Never reuse tab IDs from a previous/other session. Follow these guidelines:
|
||||
|
||||
/**
|
||||
* Additional instructions for chrome tools when tool search is enabled.
|
||||
* These instruct the model to load chrome tools via ToolSearch before using them.
|
||||
* These instruct the model to load chrome tools via SearchExtraTools before using them.
|
||||
* Only injected when tool search is actually enabled (not just optimistically possible).
|
||||
*/
|
||||
export const CHROME_TOOL_SEARCH_INSTRUCTIONS = `**IMPORTANT: Before using any chrome browser tools, you MUST first load them using ToolSearch.**
|
||||
export const CHROME_SEARCH_EXTRA_TOOLS_INSTRUCTIONS = `**IMPORTANT: Before using any chrome browser tools, you MUST first load them using SearchExtraTools.**
|
||||
|
||||
Chrome browser tools are MCP tools that require loading before use. Before calling any mcp__claude-in-chrome__* tool:
|
||||
1. Use ToolSearch with \`select:mcp__claude-in-chrome__<tool_name>\` to load the specific tool
|
||||
1. Use SearchExtraTools with \`select:mcp__claude-in-chrome__<tool_name>\` to load the specific tool
|
||||
2. Then call the tool
|
||||
|
||||
For example, to get tab context:
|
||||
1. First: ToolSearch with query "select:mcp__claude-in-chrome__tabs_context_mcp"
|
||||
1. First: SearchExtraTools with query "select:mcp__claude-in-chrome__tabs_context_mcp"
|
||||
2. Then: Call mcp__claude-in-chrome__tabs_context_mcp`
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
detectGitOperation,
|
||||
type PrAction,
|
||||
} from '@claude-code-best/builtin-tools/tools/shared/gitOperationTracking.js'
|
||||
import { TOOL_SEARCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
|
||||
import { SEARCH_EXTRA_TOOLS_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SearchExtraToolsTool/prompt.js'
|
||||
import type {
|
||||
CollapsedReadSearchGroup,
|
||||
CollapsibleMessage,
|
||||
@@ -76,7 +76,7 @@ export type SearchOrReadResult = {
|
||||
isMemoryWrite: boolean
|
||||
/**
|
||||
* True for meta-operations that should be absorbed into a collapse group
|
||||
* without incrementing any count (Snip, ToolSearch). They remain visible
|
||||
* without incrementing any count (Snip, SearchExtraTools). They remain visible
|
||||
* in verbose mode via the groupMessages iteration.
|
||||
*/
|
||||
isAbsorbedSilently: boolean
|
||||
@@ -162,7 +162,7 @@ function commandAsHint(command: string): string {
|
||||
* Also treats Write/Edit of memory files as collapsible.
|
||||
* Returns detailed information about whether it's a search or read operation.
|
||||
*/
|
||||
export function getToolSearchOrReadInfo(
|
||||
export function getSearchExtraToolsOrReadInfo(
|
||||
toolName: string,
|
||||
toolInput: unknown,
|
||||
tools: Tools,
|
||||
@@ -196,12 +196,12 @@ export function getToolSearchOrReadInfo(
|
||||
}
|
||||
}
|
||||
|
||||
// Meta-operations absorbed silently: Snip (context cleanup) and ToolSearch
|
||||
// Meta-operations absorbed silently: Snip (context cleanup) and SearchExtraTools
|
||||
// (lazy tool schema loading). Neither should break a collapse group or
|
||||
// contribute to its count, but both stay visible in verbose mode.
|
||||
if (
|
||||
(feature('HISTORY_SNIP') && toolName === SNIP_TOOL_NAME) ||
|
||||
(isFullscreenEnvEnabled() && toolName === TOOL_SEARCH_TOOL_NAME)
|
||||
(isFullscreenEnvEnabled() && toolName === SEARCH_EXTRA_TOOLS_TOOL_NAME)
|
||||
) {
|
||||
return {
|
||||
isCollapsible: true,
|
||||
@@ -277,7 +277,11 @@ export function getSearchOrReadFromContent(
|
||||
isBash?: boolean
|
||||
} | null {
|
||||
if (content?.type === 'tool_use' && content.name) {
|
||||
const info = getToolSearchOrReadInfo(content.name, content.input, tools)
|
||||
const info = getSearchExtraToolsOrReadInfo(
|
||||
content.name,
|
||||
content.input,
|
||||
tools,
|
||||
)
|
||||
if (info.isCollapsible || info.isREPL) {
|
||||
return {
|
||||
isSearch: info.isSearch,
|
||||
@@ -297,12 +301,12 @@ export function getSearchOrReadFromContent(
|
||||
/**
|
||||
* Checks if a tool is a search/read operation (for backwards compatibility).
|
||||
*/
|
||||
function isToolSearchOrRead(
|
||||
function isSearchExtraToolsOrRead(
|
||||
toolName: string,
|
||||
toolInput: unknown,
|
||||
tools: Tools,
|
||||
): boolean {
|
||||
return getToolSearchOrReadInfo(toolName, toolInput, tools).isCollapsible
|
||||
return getSearchExtraToolsOrReadInfo(toolName, toolInput, tools).isCollapsible
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,7 +393,7 @@ function isNonCollapsibleToolUse(
|
||||
if (
|
||||
content &&
|
||||
content.type === 'tool_use' &&
|
||||
!isToolSearchOrRead(
|
||||
!isSearchExtraToolsOrRead(
|
||||
(content as { name: string }).name,
|
||||
(content as { input: unknown }).input,
|
||||
tools,
|
||||
@@ -403,7 +407,7 @@ function isNonCollapsibleToolUse(
|
||||
if (
|
||||
firstContent &&
|
||||
firstContent.type === 'tool_use' &&
|
||||
!isToolSearchOrRead(
|
||||
!isSearchExtraToolsOrRead(
|
||||
msg.toolName,
|
||||
(firstContent as { input: unknown }).input,
|
||||
tools,
|
||||
@@ -463,7 +467,7 @@ function isCollapsibleToolUse(
|
||||
return (
|
||||
content !== undefined &&
|
||||
content.type === 'tool_use' &&
|
||||
isToolSearchOrRead(
|
||||
isSearchExtraToolsOrRead(
|
||||
(content as { name: string }).name,
|
||||
(content as { input: unknown }).input,
|
||||
tools,
|
||||
@@ -475,7 +479,7 @@ function isCollapsibleToolUse(
|
||||
return (
|
||||
firstContent !== undefined &&
|
||||
firstContent.type === 'tool_use' &&
|
||||
isToolSearchOrRead(
|
||||
isSearchExtraToolsOrRead(
|
||||
msg.toolName,
|
||||
(firstContent as { input: unknown }).input,
|
||||
tools,
|
||||
@@ -865,7 +869,7 @@ export function collapseReadSearchGroups(
|
||||
currentGroup.memoryWriteCount += count
|
||||
}
|
||||
} else if (toolInfo.isAbsorbedSilently) {
|
||||
// Snip/ToolSearch absorbed silently — no count, no summary text.
|
||||
// Snip/SearchExtraTools absorbed silently — no count, no summary text.
|
||||
// Hidden from the default view but still shown in verbose mode
|
||||
// (Ctrl+O) via the groupMessages iteration in CollapsedReadSearchContent.
|
||||
} else if (toolInfo.mcpServerName) {
|
||||
|
||||
@@ -221,7 +221,7 @@ export const SAFE_ENV_VARS = new Set([
|
||||
'DISABLE_ERROR_REPORTING',
|
||||
'DISABLE_FEEDBACK_COMMAND',
|
||||
'DISABLE_TELEMETRY',
|
||||
'ENABLE_TOOL_SEARCH',
|
||||
'ENABLE_SEARCH_EXTRA_TOOLS',
|
||||
'MAX_MCP_OUTPUT_TOKENS',
|
||||
'MAX_THINKING_TOKENS',
|
||||
'MCP_TIMEOUT',
|
||||
|
||||
@@ -171,8 +171,8 @@ function getTeammateMailbox(): typeof import('./teammateMailbox.js') {
|
||||
|
||||
import {
|
||||
isToolReferenceBlock,
|
||||
isToolSearchEnabledOptimistic,
|
||||
} from './toolSearch.js'
|
||||
isSearchExtraToolsEnabledOptimistic,
|
||||
} from './searchExtraTools.js'
|
||||
|
||||
const MEMORY_CORRECTION_HINT =
|
||||
"\n\nNote: The user's next message may contain a correction or preference. Pay close attention — if they explain what went wrong or how they'd prefer you to work, consider saving that to memory for future sessions."
|
||||
@@ -2058,7 +2058,7 @@ export function stripCallerFieldFromAssistantMessage(
|
||||
|
||||
/**
|
||||
* Does the content array have a tool_result block whose inner content
|
||||
* contains tool_reference (ToolSearch loaded tools)?
|
||||
* contains tool_reference (SearchExtraTools loaded tools)?
|
||||
*/
|
||||
function contentHasToolReference(
|
||||
content: ReadonlyArray<ContentBlockParam>,
|
||||
@@ -2387,7 +2387,7 @@ export function normalizeMessagesForAPI(
|
||||
// When tool search IS enabled, strip only tool_reference blocks for
|
||||
// tools that no longer exist (e.g., MCP server was disconnected).
|
||||
let normalizedMessage = message
|
||||
if (!isToolSearchEnabledOptimistic()) {
|
||||
if (!isSearchExtraToolsEnabledOptimistic()) {
|
||||
normalizedMessage = stripToolReferenceBlocksFromUserMessage(message)
|
||||
} else {
|
||||
normalizedMessage = stripUnavailableToolReferencesFromUserMessage(
|
||||
@@ -2489,7 +2489,7 @@ export function normalizeMessagesForAPI(
|
||||
// When tool search is NOT enabled, we must strip tool_search-specific fields
|
||||
// like 'caller' from tool_use blocks, as these are only valid with the
|
||||
// tool search beta header
|
||||
const toolSearchEnabled = isToolSearchEnabledOptimistic()
|
||||
const searchExtraToolsEnabled = isSearchExtraToolsEnabledOptimistic()
|
||||
const normalizedMessage: AssistantMessage = {
|
||||
...message,
|
||||
message: {
|
||||
@@ -2513,7 +2513,7 @@ export function normalizeMessagesForAPI(
|
||||
const canonicalName = tool?.name ?? toolUseBlk.name
|
||||
|
||||
// When tool search is enabled, preserve all fields including 'caller'
|
||||
if (toolSearchEnabled) {
|
||||
if (searchExtraToolsEnabled) {
|
||||
return {
|
||||
...block,
|
||||
name: canonicalName,
|
||||
@@ -3911,7 +3911,7 @@ Read the team config to discover your teammates' names. Check the task list peri
|
||||
|
||||
// tool_discovery handled here (not in the switch) so the 'tool_discovery'
|
||||
// string literal lives inside a feature()-guarded block.
|
||||
if (feature('EXPERIMENTAL_TOOL_SEARCH')) {
|
||||
if (feature('EXPERIMENTAL_SEARCH_EXTRA_TOOLS')) {
|
||||
if (attachment.type === 'tool_discovery') {
|
||||
if (attachment.tools.length === 0) return []
|
||||
const lines = attachment.tools.map(
|
||||
@@ -3919,7 +3919,7 @@ Read the team config to discover your teammates' names. Check the task list peri
|
||||
)
|
||||
return wrapMessagesInSystemReminder([
|
||||
createUserMessage({
|
||||
content: `The following tools were discovered as relevant to your task. Use ExecuteExtraTool to invoke any of them by name:\n\n${lines.join('\n')}`,
|
||||
content: `The following tools were discovered as relevant to your task. To invoke them, you MUST use ExecuteExtraTool — this is the only way to call these tools. Do not read source code or reason about whether they are callable; just call ExecuteExtraTool({"tool_name": "<name>", "params": {...}}) directly.\n\n${lines.join('\n')}`,
|
||||
isMeta: true,
|
||||
}),
|
||||
])
|
||||
@@ -4593,12 +4593,12 @@ You have exited auto mode. The user may now want to interact more directly. You
|
||||
const parts: string[] = []
|
||||
if (attachment.addedLines.length > 0) {
|
||||
parts.push(
|
||||
`The following deferred tools are now available via ToolSearch:\n${attachment.addedLines.join('\n')}`,
|
||||
`The following deferred tools are now available via SearchExtraTools:\n${attachment.addedLines.join('\n')}`,
|
||||
)
|
||||
}
|
||||
if (attachment.removedNames.length > 0) {
|
||||
parts.push(
|
||||
`The following deferred tools are no longer available (their MCP server disconnected). Do not search for them — ToolSearch will return no match:\n${attachment.removedNames.join('\n')}`,
|
||||
`The following deferred tools are no longer available (their MCP server disconnected). Do not search for them — SearchExtraTools will return no match:\n${attachment.removedNames.join('\n')}`,
|
||||
)
|
||||
}
|
||||
return wrapMessagesInSystemReminder([
|
||||
|
||||
@@ -18,7 +18,7 @@ import { TASK_UPDATE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/Tas
|
||||
import { TEAM_CREATE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/TeamCreateTool/constants.js'
|
||||
import { TEAM_DELETE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/TeamDeleteTool/constants.js'
|
||||
import { TODO_WRITE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/TodoWriteTool/constants.js'
|
||||
import { TOOL_SEARCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ToolSearchTool/prompt.js'
|
||||
import { SEARCH_EXTRA_TOOLS_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SearchExtraToolsTool/prompt.js'
|
||||
import { YOLO_CLASSIFIER_TOOL_NAME } from './yoloClassifier.js'
|
||||
|
||||
// Ant-only tool names: conditional require so Bun can DCE these in external builds.
|
||||
@@ -60,7 +60,7 @@ const SAFE_YOLO_ALLOWLISTED_TOOLS = new Set([
|
||||
GREP_TOOL_NAME,
|
||||
GLOB_TOOL_NAME,
|
||||
LSP_TOOL_NAME,
|
||||
TOOL_SEARCH_TOOL_NAME,
|
||||
SEARCH_EXTRA_TOOLS_TOOL_NAME,
|
||||
LIST_MCP_RESOURCES_TOOL_NAME,
|
||||
'ReadMcpResourceTool', // no exported constant
|
||||
// Task management (metadata only)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tool Search utilities for dynamically discovering deferred tools.
|
||||
*
|
||||
* When enabled, deferred tools (all non-core tools) are sent with
|
||||
* defer_loading: true and discovered via ToolSearchTool rather than being
|
||||
* defer_loading: true and discovered via SearchExtraToolsTool rather than being
|
||||
* loaded upfront. Core tools are defined in CORE_TOOLS (src/constants/tools.ts).
|
||||
*/
|
||||
|
||||
@@ -22,8 +22,8 @@ import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/Agen
|
||||
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 type { Message } from '../types/message.js'
|
||||
import {
|
||||
countToolDefinitionTokens,
|
||||
@@ -40,12 +40,12 @@ import { zodToJsonSchema } from './zodToJsonSchema.js'
|
||||
/**
|
||||
* Default percentage of context window at which to auto-enable tool search.
|
||||
* When MCP tool descriptions exceed this percentage (in tokens), tool search is enabled.
|
||||
* Can be overridden via ENABLE_TOOL_SEARCH=auto:N where N is 0-100.
|
||||
* Can be overridden via ENABLE_SEARCH_EXTRA_TOOLS=auto:N where N is 0-100.
|
||||
*/
|
||||
const DEFAULT_AUTO_TOOL_SEARCH_PERCENTAGE = 10 // 10%
|
||||
const DEFAULT_AUTO_SEARCH_EXTRA_TOOLS_PERCENTAGE = 10 // 10%
|
||||
|
||||
/**
|
||||
* Parse auto:N syntax from ENABLE_TOOL_SEARCH env var.
|
||||
* Parse auto:N syntax from ENABLE_SEARCH_EXTRA_TOOLS env var.
|
||||
* Returns the percentage clamped to 0-100, or null if not auto:N format or not a number.
|
||||
*/
|
||||
function parseAutoPercentage(value: string): number | null {
|
||||
@@ -56,7 +56,7 @@ function parseAutoPercentage(value: string): number | null {
|
||||
|
||||
if (isNaN(percent)) {
|
||||
logForDebugging(
|
||||
`Invalid ENABLE_TOOL_SEARCH value "${value}": expected auto:N where N is a number.`,
|
||||
`Invalid ENABLE_SEARCH_EXTRA_TOOLS value "${value}": expected auto:N where N is a number.`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -66,9 +66,9 @@ function parseAutoPercentage(value: string): number | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ENABLE_TOOL_SEARCH is set to auto mode (auto or auto:N).
|
||||
* Check if ENABLE_SEARCH_EXTRA_TOOLS is set to auto mode (auto or auto:N).
|
||||
*/
|
||||
function isAutoToolSearchMode(value: string | undefined): boolean {
|
||||
function isAutoSearchExtraToolsMode(value: string | undefined): boolean {
|
||||
if (!value) return false
|
||||
return value === 'auto' || value.startsWith('auto:')
|
||||
}
|
||||
@@ -76,16 +76,16 @@ function isAutoToolSearchMode(value: string | undefined): boolean {
|
||||
/**
|
||||
* Get the auto-enable percentage from env var or default.
|
||||
*/
|
||||
function getAutoToolSearchPercentage(): number {
|
||||
const value = process.env.ENABLE_TOOL_SEARCH
|
||||
if (!value) return DEFAULT_AUTO_TOOL_SEARCH_PERCENTAGE
|
||||
function getAutoSearchExtraToolsPercentage(): number {
|
||||
const value = process.env.ENABLE_SEARCH_EXTRA_TOOLS
|
||||
if (!value) return DEFAULT_AUTO_SEARCH_EXTRA_TOOLS_PERCENTAGE
|
||||
|
||||
if (value === 'auto') return DEFAULT_AUTO_TOOL_SEARCH_PERCENTAGE
|
||||
if (value === 'auto') return DEFAULT_AUTO_SEARCH_EXTRA_TOOLS_PERCENTAGE
|
||||
|
||||
const parsed = parseAutoPercentage(value)
|
||||
if (parsed !== null) return parsed
|
||||
|
||||
return DEFAULT_AUTO_TOOL_SEARCH_PERCENTAGE
|
||||
return DEFAULT_AUTO_SEARCH_EXTRA_TOOLS_PERCENTAGE
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,10 +97,10 @@ const CHARS_PER_TOKEN = 2.5
|
||||
/**
|
||||
* Get the token threshold for auto-enabling tool search for a given model.
|
||||
*/
|
||||
function getAutoToolSearchTokenThreshold(model: string): number {
|
||||
function getAutoSearchExtraToolsTokenThreshold(model: string): number {
|
||||
const betas = getMergedBetas(model)
|
||||
const contextWindow = getContextWindowForModel(model, betas)
|
||||
const percentage = getAutoToolSearchPercentage() / 100
|
||||
const percentage = getAutoSearchExtraToolsPercentage() / 100
|
||||
return Math.floor(contextWindow * percentage)
|
||||
}
|
||||
|
||||
@@ -108,8 +108,10 @@ function getAutoToolSearchTokenThreshold(model: string): number {
|
||||
* Get the character threshold for auto-enabling tool search for a given model.
|
||||
* Used as fallback when the token counting API is unavailable.
|
||||
*/
|
||||
export function getAutoToolSearchCharThreshold(model: string): number {
|
||||
return Math.floor(getAutoToolSearchTokenThreshold(model) * CHARS_PER_TOKEN)
|
||||
export function getAutoSearchExtraToolsCharThreshold(model: string): number {
|
||||
return Math.floor(
|
||||
getAutoSearchExtraToolsTokenThreshold(model) * CHARS_PER_TOKEN,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,22 +152,22 @@ const getDeferredToolTokenCount = memoize(
|
||||
/**
|
||||
* Tool search mode. Determines how deferred tools (all non-core tools)
|
||||
* are surfaced:
|
||||
* - 'tst': Tool Search Tool — deferred tools discovered via ToolSearchTool (always enabled)
|
||||
* - 'tst': Tool Search Tool — deferred tools discovered via SearchExtraToolsTool (always enabled)
|
||||
* - 'tst-auto': auto — tools deferred only when they exceed threshold
|
||||
* - 'standard': tool search disabled — all tools exposed inline
|
||||
*/
|
||||
export type ToolSearchMode = 'tst' | 'tst-auto' | 'standard'
|
||||
export type SearchExtraToolsMode = 'tst' | 'tst-auto' | 'standard'
|
||||
|
||||
/**
|
||||
* Determines the tool search mode from ENABLE_TOOL_SEARCH.
|
||||
* Determines the tool search mode from ENABLE_SEARCH_EXTRA_TOOLS.
|
||||
*
|
||||
* ENABLE_TOOL_SEARCH Mode
|
||||
* ENABLE_SEARCH_EXTRA_TOOLS Mode
|
||||
* auto / auto:1-99 tst-auto
|
||||
* true / auto:0 tst
|
||||
* false / auto:100 standard
|
||||
* (unset) tst (default: always defer non-core tools)
|
||||
*/
|
||||
export function getToolSearchMode(): ToolSearchMode {
|
||||
export function getSearchExtraToolsMode(): SearchExtraToolsMode {
|
||||
// CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS still acts as a kill switch
|
||||
// for tool search, even though we no longer send beta headers.
|
||||
// Users who set this flag explicitly opt out of tool search.
|
||||
@@ -173,18 +175,19 @@ export function getToolSearchMode(): ToolSearchMode {
|
||||
return 'standard'
|
||||
}
|
||||
|
||||
const value = process.env.ENABLE_TOOL_SEARCH
|
||||
const value = process.env.ENABLE_SEARCH_EXTRA_TOOLS
|
||||
|
||||
// Handle auto:N syntax - check edge cases first
|
||||
const autoPercent = value ? parseAutoPercentage(value) : null
|
||||
if (autoPercent === 0) return 'tst' // auto:0 = always enabled
|
||||
if (autoPercent === 100) return 'standard'
|
||||
if (isAutoToolSearchMode(value)) {
|
||||
if (isAutoSearchExtraToolsMode(value)) {
|
||||
return 'tst-auto' // auto or auto:1-99
|
||||
}
|
||||
|
||||
if (isEnvTruthy(value)) return 'tst'
|
||||
if (isEnvDefinedFalsy(process.env.ENABLE_TOOL_SEARCH)) return 'standard'
|
||||
if (isEnvDefinedFalsy(process.env.ENABLE_SEARCH_EXTRA_TOOLS))
|
||||
return 'standard'
|
||||
return 'tst' // default: always defer non-core tools
|
||||
}
|
||||
|
||||
@@ -193,22 +196,22 @@ export function getToolSearchMode(): ToolSearchMode {
|
||||
*
|
||||
* Returns true if tool search could potentially be enabled, without checking
|
||||
* dynamic factors like threshold. Use this for:
|
||||
* - Including ToolSearchTool in base tools (so it's available if needed)
|
||||
* - Checking if ToolSearchTool should report itself as enabled
|
||||
* - Including SearchExtraToolsTool in base tools (so it's available if needed)
|
||||
* - Checking if SearchExtraToolsTool should report itself as enabled
|
||||
*
|
||||
* Returns false only when tool search is definitively disabled (standard mode).
|
||||
*
|
||||
* For the definitive check that includes threshold, use isToolSearchEnabled().
|
||||
* For the definitive check that includes threshold, use isSearchExtraToolsEnabled().
|
||||
*/
|
||||
let loggedOptimistic = false
|
||||
|
||||
export function isToolSearchEnabledOptimistic(): boolean {
|
||||
const mode = getToolSearchMode()
|
||||
export function isSearchExtraToolsEnabledOptimistic(): boolean {
|
||||
const mode = getSearchExtraToolsMode()
|
||||
if (mode === 'standard') {
|
||||
if (!loggedOptimistic) {
|
||||
loggedOptimistic = true
|
||||
logForDebugging(
|
||||
`[ToolSearch:optimistic] mode=${mode}, ENABLE_TOOL_SEARCH=${process.env.ENABLE_TOOL_SEARCH}, result=false`,
|
||||
`[SearchExtraTools:optimistic] mode=${mode}, ENABLE_SEARCH_EXTRA_TOOLS=${process.env.ENABLE_SEARCH_EXTRA_TOOLS}, result=false`,
|
||||
)
|
||||
}
|
||||
return false
|
||||
@@ -216,29 +219,29 @@ export function isToolSearchEnabledOptimistic(): boolean {
|
||||
|
||||
// All providers use the unified self-built tool search (TF-IDF + keyword).
|
||||
// No first-party / tool_reference / defer_loading distinction.
|
||||
// Users can still disable via ENABLE_TOOL_SEARCH=false.
|
||||
// Users can still disable via ENABLE_SEARCH_EXTRA_TOOLS=false.
|
||||
|
||||
if (!loggedOptimistic) {
|
||||
loggedOptimistic = true
|
||||
logForDebugging(
|
||||
`[ToolSearch:optimistic] mode=${mode}, ENABLE_TOOL_SEARCH=${process.env.ENABLE_TOOL_SEARCH}, result=true`,
|
||||
`[SearchExtraTools:optimistic] mode=${mode}, ENABLE_SEARCH_EXTRA_TOOLS=${process.env.ENABLE_SEARCH_EXTRA_TOOLS}, result=true`,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ToolSearchTool is available in the provided tools list.
|
||||
* If ToolSearchTool is not available (e.g., disallowed via disallowedTools),
|
||||
* Check if SearchExtraToolsTool is available in the provided tools list.
|
||||
* If SearchExtraToolsTool is not available (e.g., disallowed via disallowedTools),
|
||||
* tool search cannot function and should be disabled.
|
||||
*
|
||||
* @param tools Array of tools with a 'name' property
|
||||
* @returns true if ToolSearchTool is in the tools list, false otherwise
|
||||
* @returns true if SearchExtraToolsTool is in the tools list, false otherwise
|
||||
*/
|
||||
export function isToolSearchToolAvailable(
|
||||
export function isSearchExtraToolsToolAvailable(
|
||||
tools: readonly { name: string }[],
|
||||
): boolean {
|
||||
return tools.some(tool => toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME))
|
||||
return tools.some(tool => toolMatchesName(tool, SEARCH_EXTRA_TOOLS_TOOL_NAME))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,7 +281,7 @@ async function calculateDeferredToolDescriptionChars(
|
||||
* This is the definitive check that includes:
|
||||
* - MCP mode (Tst, TstAuto, McpCli, Standard)
|
||||
* - Model compatibility (haiku doesn't support tool_reference)
|
||||
* - ToolSearchTool availability (must be in tools list)
|
||||
* - SearchExtraToolsTool availability (must be in tools list)
|
||||
* - Threshold check for TstAuto mode
|
||||
*
|
||||
* Use this when making actual API calls where all context is available.
|
||||
@@ -290,7 +293,7 @@ async function calculateDeferredToolDescriptionChars(
|
||||
* @param source Optional identifier for the caller (for debugging)
|
||||
* @returns true if tool search should be enabled for this request
|
||||
*/
|
||||
export async function isToolSearchEnabled(
|
||||
export async function isSearchExtraToolsEnabled(
|
||||
model: string,
|
||||
tools: Tools,
|
||||
getToolPermissionContext: () => Promise<ToolPermissionContext>,
|
||||
@@ -302,11 +305,11 @@ export async function isToolSearchEnabled(
|
||||
// Helper to log the mode decision event
|
||||
function logModeDecision(
|
||||
enabled: boolean,
|
||||
mode: ToolSearchMode,
|
||||
mode: SearchExtraToolsMode,
|
||||
reason: string,
|
||||
extraProps?: Record<string, number>,
|
||||
): void {
|
||||
logEvent('tengu_tool_search_mode_decision', {
|
||||
logEvent('tengu_search_extra_tools_mode_decision', {
|
||||
enabled,
|
||||
mode: mode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
reason:
|
||||
@@ -324,18 +327,18 @@ export async function isToolSearchEnabled(
|
||||
}
|
||||
|
||||
// Tool search is enabled uniformly regardless of provider or model.
|
||||
// All providers use self-built TF-IDF + keyword search via ToolSearchTool + ExecuteExtraTool.
|
||||
// All providers use self-built TF-IDF + keyword search via SearchExtraToolsTool + ExecuteExtraTool.
|
||||
|
||||
// Check if ToolSearchTool is available (respects disallowedTools)
|
||||
if (!isToolSearchToolAvailable(tools)) {
|
||||
// Check if SearchExtraToolsTool is available (respects disallowedTools)
|
||||
if (!isSearchExtraToolsToolAvailable(tools)) {
|
||||
logForDebugging(
|
||||
`Tool search disabled: ToolSearchTool is not available (may have been disallowed via disallowedTools).`,
|
||||
`Tool search disabled: SearchExtraToolsTool is not available (may have been disallowed via disallowedTools).`,
|
||||
)
|
||||
logModeDecision(false, 'standard', 'mcp_search_unavailable')
|
||||
return false
|
||||
}
|
||||
|
||||
const mode = getToolSearchMode()
|
||||
const mode = getSearchExtraToolsMode()
|
||||
|
||||
switch (mode) {
|
||||
case 'tst':
|
||||
@@ -401,7 +404,7 @@ function isToolReferenceWithName(
|
||||
|
||||
/**
|
||||
* Type representing a tool_result block with array content.
|
||||
* Used for extracting tool_reference blocks from ToolSearchTool results.
|
||||
* Used for extracting tool_reference blocks from SearchExtraToolsTool results.
|
||||
*/
|
||||
type ToolResultBlock = {
|
||||
type: 'tool_result'
|
||||
@@ -410,7 +413,7 @@ type ToolResultBlock = {
|
||||
|
||||
/**
|
||||
* Type representing a tool_result block with string content.
|
||||
* Used for extracting tool names from ToolSearchTool text output.
|
||||
* Used for extracting tool names from SearchExtraToolsTool text output.
|
||||
*/
|
||||
type ToolResultBlockWithStringContent = {
|
||||
type: 'tool_result'
|
||||
@@ -448,14 +451,14 @@ function isToolResultBlockWithStringContent(
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex to extract tool names from ToolSearchTool text output.
|
||||
* Regex to extract tool names from SearchExtraToolsTool text output.
|
||||
* Matches: "Found N deferred tool(s): ToolA, mcp.server.ToolB."
|
||||
* Uses multiline + end-of-line anchor so dots inside tool names (e.g. mcp__s__t) don't break parsing.
|
||||
*/
|
||||
const DISCOVERED_TOOLS_PATTERN = /^Found \d+ deferred tool\(s\): (.+)\.$/m
|
||||
|
||||
/**
|
||||
* Extract tool names from ToolSearchTool text output.
|
||||
* Extract tool names from SearchExtraToolsTool text output.
|
||||
* Format: "Found N deferred tool(s): ToolA, ToolB.\n..."
|
||||
*/
|
||||
function extractToolNamesFromText(text: string): string[] {
|
||||
@@ -468,7 +471,7 @@ function extractToolNamesFromText(text: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool names from ToolSearchTool results in message history.
|
||||
* Extract tool names from SearchExtraToolsTool results in message history.
|
||||
*
|
||||
* Supports two formats:
|
||||
* 1. Legacy tool_reference blocks (backward compat with old sessions)
|
||||
@@ -530,7 +533,7 @@ export function extractDiscoveredToolNames(messages: Message[]): Set<string> {
|
||||
}
|
||||
}
|
||||
|
||||
// Unified self-built search: text output from ToolSearchTool
|
||||
// Unified self-built search: text output from SearchExtraToolsTool
|
||||
if (isToolResultBlockWithStringContent(block)) {
|
||||
const names = extractToolNamesFromText(block.content)
|
||||
for (const name of names) {
|
||||
@@ -689,12 +692,12 @@ async function checkAutoThreshold(
|
||||
)
|
||||
|
||||
if (deferredToolTokens !== null) {
|
||||
const threshold = getAutoToolSearchTokenThreshold(model)
|
||||
const threshold = getAutoSearchExtraToolsTokenThreshold(model)
|
||||
return {
|
||||
enabled: deferredToolTokens >= threshold,
|
||||
debugDescription:
|
||||
`${deferredToolTokens} tokens (threshold: ${threshold}, ` +
|
||||
`${getAutoToolSearchPercentage()}% of context)`,
|
||||
`${getAutoSearchExtraToolsPercentage()}% of context)`,
|
||||
metrics: { deferredToolTokens, threshold },
|
||||
}
|
||||
}
|
||||
@@ -706,12 +709,12 @@ async function checkAutoThreshold(
|
||||
getToolPermissionContext,
|
||||
agents,
|
||||
)
|
||||
const charThreshold = getAutoToolSearchCharThreshold(model)
|
||||
const charThreshold = getAutoSearchExtraToolsCharThreshold(model)
|
||||
return {
|
||||
enabled: deferredToolDescriptionChars >= charThreshold,
|
||||
debugDescription:
|
||||
`${deferredToolDescriptionChars} chars (threshold: ${charThreshold}, ` +
|
||||
`${getAutoToolSearchPercentage()}% of context) (char fallback)`,
|
||||
`${getAutoSearchExtraToolsPercentage()}% of context) (char fallback)`,
|
||||
metrics: { deferredToolDescriptionChars, charThreshold },
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user