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

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

View File

@@ -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,
})
}

View File

@@ -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
}
/**

View File

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

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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([

View File

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

View File

@@ -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 },
}
}