Refactor: Standardize Tool Naming and Configuration System (#1004)

This commit is contained in:
tanzhenxin
2025-11-12 19:46:05 +08:00
committed by GitHub
parent 22edef0cb9
commit 06141cda8d
23 changed files with 480 additions and 87 deletions

View File

@@ -578,7 +578,7 @@ Arguments passed directly when running the CLI can override other configurations
- Example: `qwen --approval-mode auto-edit` - Example: `qwen --approval-mode auto-edit`
- **`--allowed-tools <tool1,tool2,...>`**: - **`--allowed-tools <tool1,tool2,...>`**:
- A comma-separated list of tool names that will bypass the confirmation dialog. - A comma-separated list of tool names that will bypass the confirmation dialog.
- Example: `qwen --allowed-tools "ShellTool(git status)"` - Example: `qwen --allowed-tools "Shell(git status)"`
- **`--telemetry`**: - **`--telemetry`**:
- Enables [telemetry](../telemetry.md). - Enables [telemetry](../telemetry.md).
- **`--telemetry-target`**: - **`--telemetry-target`**:

View File

@@ -21,7 +21,7 @@ The Qwen Code core (`packages/core`) features a robust system for defining, regi
- **Returning Rich Content:** Tools are not limited to returning simple text. The `llmContent` can be a `PartListUnion`, which is an array that can contain a mix of `Part` objects (for images, audio, etc.) and `string`s. This allows a single tool execution to return multiple pieces of rich content. - **Returning Rich Content:** Tools are not limited to returning simple text. The `llmContent` can be a `PartListUnion`, which is an array that can contain a mix of `Part` objects (for images, audio, etc.) and `string`s. This allows a single tool execution to return multiple pieces of rich content.
- **Tool Registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible for: - **Tool Registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible for:
- **Registering Tools:** Holding a collection of all available built-in tools (e.g., `ReadFileTool`, `ShellTool`). - **Registering Tools:** Holding a collection of all available built-in tools (e.g., `ListFiles`, `ReadFile`).
- **Discovering Tools:** It can also discover tools dynamically: - **Discovering Tools:** It can also discover tools dynamically:
- **Command-based Discovery:** If `tools.toolDiscoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances. - **Command-based Discovery:** If `tools.toolDiscoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances.
- **MCP-based Discovery:** If `mcp.mcpServerCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`). - **MCP-based Discovery:** If `mcp.mcpServerCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`).
@@ -33,20 +33,24 @@ The Qwen Code core (`packages/core`) features a robust system for defining, regi
The core comes with a suite of pre-defined tools, typically found in `packages/core/src/tools/`. These include: The core comes with a suite of pre-defined tools, typically found in `packages/core/src/tools/`. These include:
- **File System Tools:** - **File System Tools:**
- `LSTool` (`ls.ts`): Lists directory contents. - `ListFiles` (`ls.ts`): Lists directory contents.
- `ReadFileTool` (`read-file.ts`): Reads the content of a single file. It takes an `absolute_path` parameter, which must be an absolute path. - `ReadFile` (`read-file.ts`): Reads the content of a single file. It takes an `absolute_path` parameter, which must be an absolute path.
- `WriteFileTool` (`write-file.ts`): Writes content to a file. - `WriteFile` (`write-file.ts`): Writes content to a file.
- `GrepTool` (`grep.ts`): Searches for patterns in files. - `ReadManyFiles` (`read-many-files.ts`): Reads and concatenates content from multiple files or glob patterns (used by the `@` command in CLI).
- `GlobTool` (`glob.ts`): Finds files matching glob patterns. - `Grep` (`grep.ts`): Searches for patterns in files.
- `EditTool` (`edit.ts`): Performs in-place modifications to files (often requiring confirmation). - `Glob` (`glob.ts`): Finds files matching glob patterns.
- `ReadManyFilesTool` (`read-many-files.ts`): Reads and concatenates content from multiple files or glob patterns (used by the `@` command in CLI). - `Edit` (`edit.ts`): Performs in-place modifications to files (often requiring confirmation).
- **Execution Tools:** - **Execution Tools:**
- `ShellTool` (`shell.ts`): Executes arbitrary shell commands (requires careful sandboxing and user confirmation). - `Shell` (`shell.ts`): Executes arbitrary shell commands (requires careful sandboxing and user confirmation).
- **Web Tools:** - **Web Tools:**
- `WebFetchTool` (`web-fetch.ts`): Fetches content from a URL. - `WebFetch` (`web-fetch.ts`): Fetches content from a URL.
- `WebSearchTool` (`web-search.ts`): Performs a web search. - `WebSearch` (`web-search.ts`): Performs a web search.
- **Memory Tools:** - **Memory Tools:**
- `MemoryTool` (`memoryTool.ts`): Interacts with the AI's memory. - `SaveMemory` (`memoryTool.ts`): Interacts with the AI's memory.
- **Planning Tools:**
- `Task` (`task.ts`): Delegates tasks to specialized subagents.
- `TodoWrite` (`todoWrite.ts`): Creates and manages a structured task list.
- `ExitPlanMode` (`exitPlanMode.ts`): Exits plan mode and returns to normal operation.
Each of these tools extends `BaseTool` and implements the required methods for its specific functionality. Each of these tools extends `BaseTool` and implements the required methods for its specific functionality.

View File

@@ -4,12 +4,12 @@ Qwen Code provides a comprehensive suite of tools for interacting with the local
**Note:** All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security. Paths that you provide to these tools are generally expected to be absolute or are resolved relative to this root directory. **Note:** All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security. Paths that you provide to these tools are generally expected to be absolute or are resolved relative to this root directory.
## 1. `list_directory` (ReadFolder) ## 1. `list_directory` (ListFiles)
`list_directory` lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns. `list_directory` lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns.
- **Tool name:** `list_directory` - **Tool name:** `list_directory`
- **Display name:** ReadFolder - **Display name:** ListFiles
- **File:** `ls.ts` - **File:** `ls.ts`
- **Parameters:** - **Parameters:**
- `path` (string, required): The absolute path to the directory to list. - `path` (string, required): The absolute path to the directory to list.
@@ -59,12 +59,12 @@ Qwen Code provides a comprehensive suite of tools for interacting with the local
- **Output (`llmContent`):** A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` or `Successfully created and wrote to new file: /path/to/new/file.txt`. - **Output (`llmContent`):** A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` or `Successfully created and wrote to new file: /path/to/new/file.txt`.
- **Confirmation:** Yes. Shows a diff of changes and asks for user approval before writing. - **Confirmation:** Yes. Shows a diff of changes and asks for user approval before writing.
## 4. `glob` (FindFiles) ## 4. `glob` (Glob)
`glob` finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first). `glob` finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first).
- **Tool name:** `glob` - **Tool name:** `glob`
- **Display name:** FindFiles - **Display name:** Glob
- **File:** `glob.ts` - **File:** `glob.ts`
- **Parameters:** - **Parameters:**
- `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`). - `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`).

View File

@@ -45,6 +45,15 @@ import { logRipgrepFallback } from '../telemetry/loggers.js';
import { RipgrepFallbackEvent } from '../telemetry/types.js'; import { RipgrepFallbackEvent } from '../telemetry/types.js';
import { ToolRegistry } from '../tools/tool-registry.js'; import { ToolRegistry } from '../tools/tool-registry.js';
function createToolMock(toolName: string) {
const ToolMock = vi.fn();
Object.defineProperty(ToolMock, 'Name', {
value: toolName,
writable: true,
});
return ToolMock;
}
vi.mock('fs', async (importOriginal) => { vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>(); const actual = await importOriginal<typeof import('fs')>();
return { return {
@@ -73,23 +82,41 @@ vi.mock('../utils/memoryDiscovery.js', () => ({
})); }));
// Mock individual tools if their constructors are complex or have side effects // Mock individual tools if their constructors are complex or have side effects
vi.mock('../tools/ls'); vi.mock('../tools/ls', () => ({
vi.mock('../tools/read-file'); LSTool: createToolMock('list_directory'),
vi.mock('../tools/grep.js'); }));
vi.mock('../tools/read-file', () => ({
ReadFileTool: createToolMock('read_file'),
}));
vi.mock('../tools/grep.js', () => ({
GrepTool: createToolMock('grep_search'),
}));
vi.mock('../tools/ripGrep.js', () => ({ vi.mock('../tools/ripGrep.js', () => ({
RipGrepTool: class MockRipGrepTool {}, RipGrepTool: createToolMock('grep_search'),
})); }));
vi.mock('../utils/ripgrepUtils.js', () => ({ vi.mock('../utils/ripgrepUtils.js', () => ({
canUseRipgrep: vi.fn(), canUseRipgrep: vi.fn(),
})); }));
vi.mock('../tools/glob'); vi.mock('../tools/glob', () => ({
vi.mock('../tools/edit'); GlobTool: createToolMock('glob'),
vi.mock('../tools/shell'); }));
vi.mock('../tools/write-file'); vi.mock('../tools/edit', () => ({
vi.mock('../tools/web-fetch'); EditTool: createToolMock('edit'),
vi.mock('../tools/read-many-files'); }));
vi.mock('../tools/shell', () => ({
ShellTool: createToolMock('run_shell_command'),
}));
vi.mock('../tools/write-file', () => ({
WriteFileTool: createToolMock('write_file'),
}));
vi.mock('../tools/web-fetch', () => ({
WebFetchTool: createToolMock('web_fetch'),
}));
vi.mock('../tools/read-many-files', () => ({
ReadManyFilesTool: createToolMock('read_many_files'),
}));
vi.mock('../tools/memoryTool', () => ({ vi.mock('../tools/memoryTool', () => ({
MemoryTool: vi.fn(), MemoryTool: createToolMock('save_memory'),
setGeminiMdFilename: vi.fn(), setGeminiMdFilename: vi.fn(),
getCurrentGeminiMdFilename: vi.fn(() => 'QWEN.md'), // Mock the original filename getCurrentGeminiMdFilename: vi.fn(() => 'QWEN.md'), // Mock the original filename
DEFAULT_CONTEXT_FILENAME: 'QWEN.md', DEFAULT_CONTEXT_FILENAME: 'QWEN.md',
@@ -621,7 +648,7 @@ describe('Server Config (config.ts)', () => {
it('should register a tool if coreTools contains an argument-specific pattern', async () => { it('should register a tool if coreTools contains an argument-specific pattern', async () => {
const params: ConfigParameters = { const params: ConfigParameters = {
...baseParams, ...baseParams,
coreTools: ['ShellTool(git status)'], coreTools: ['Shell(git status)'], // Use display name instead of class name
}; };
const config = new Config(params); const config = new Config(params);
await config.initialize(); await config.initialize();
@@ -646,6 +673,89 @@ describe('Server Config (config.ts)', () => {
expect(wasReadFileToolRegistered).toBe(false); expect(wasReadFileToolRegistered).toBe(false);
}); });
it('should register a tool if coreTools contains the displayName', async () => {
const params: ConfigParameters = {
...baseParams,
coreTools: ['Shell'],
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasShellToolRegistered = (registerToolMock as Mock).mock.calls.some(
(call) => call[0] instanceof vi.mocked(ShellTool),
);
expect(wasShellToolRegistered).toBe(true);
});
it('should register a tool if coreTools contains the displayName with argument-specific pattern', async () => {
const params: ConfigParameters = {
...baseParams,
coreTools: ['Shell(git status)'],
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasShellToolRegistered = (registerToolMock as Mock).mock.calls.some(
(call) => call[0] instanceof vi.mocked(ShellTool),
);
expect(wasShellToolRegistered).toBe(true);
});
it('should register a tool if coreTools contains a legacy tool name alias', async () => {
const params: ConfigParameters = {
...baseParams,
useRipgrep: false,
coreTools: ['search_file_content'],
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasGrepToolRegistered = (registerToolMock as Mock).mock.calls.some(
(call) => call[0] instanceof vi.mocked(GrepTool),
);
expect(wasGrepToolRegistered).toBe(true);
});
it('should not register a tool if excludeTools contains a legacy display name alias', async () => {
const params: ConfigParameters = {
...baseParams,
useRipgrep: false,
coreTools: undefined,
excludeTools: ['SearchFiles'],
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasGrepToolRegistered = (registerToolMock as Mock).mock.calls.some(
(call) => call[0] instanceof vi.mocked(GrepTool),
);
expect(wasGrepToolRegistered).toBe(false);
});
describe('with minified tool class names', () => { describe('with minified tool class names', () => {
beforeEach(() => { beforeEach(() => {
Object.defineProperty( Object.defineProperty(
@@ -671,7 +781,27 @@ describe('Server Config (config.ts)', () => {
it('should register a tool if coreTools contains the non-minified class name', async () => { it('should register a tool if coreTools contains the non-minified class name', async () => {
const params: ConfigParameters = { const params: ConfigParameters = {
...baseParams, ...baseParams,
coreTools: ['ShellTool'], coreTools: ['Shell'], // Use display name instead of class name
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasShellToolRegistered = (
registerToolMock as Mock
).mock.calls.some((call) => call[0] instanceof vi.mocked(ShellTool));
expect(wasShellToolRegistered).toBe(true);
});
it('should register a tool if coreTools contains the displayName', async () => {
const params: ConfigParameters = {
...baseParams,
coreTools: ['Shell'],
}; };
const config = new Config(params); const config = new Config(params);
await config.initialize(); await config.initialize();
@@ -692,7 +822,28 @@ describe('Server Config (config.ts)', () => {
const params: ConfigParameters = { const params: ConfigParameters = {
...baseParams, ...baseParams,
coreTools: undefined, // all tools enabled by default coreTools: undefined, // all tools enabled by default
excludeTools: ['ShellTool'], excludeTools: ['Shell'], // Use display name instead of class name
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasShellToolRegistered = (
registerToolMock as Mock
).mock.calls.some((call) => call[0] instanceof vi.mocked(ShellTool));
expect(wasShellToolRegistered).toBe(false);
});
it('should not register a tool if excludeTools contains the displayName', async () => {
const params: ConfigParameters = {
...baseParams,
coreTools: undefined, // all tools enabled by default
excludeTools: ['Shell'],
}; };
const config = new Config(params); const config = new Config(params);
await config.initialize(); await config.initialize();
@@ -712,7 +863,27 @@ describe('Server Config (config.ts)', () => {
it('should register a tool if coreTools contains an argument-specific pattern with the non-minified class name', async () => { it('should register a tool if coreTools contains an argument-specific pattern with the non-minified class name', async () => {
const params: ConfigParameters = { const params: ConfigParameters = {
...baseParams, ...baseParams,
coreTools: ['ShellTool(git status)'], coreTools: ['Shell(git status)'], // Use display name instead of class name
};
const config = new Config(params);
await config.initialize();
const registerToolMock = (
(await vi.importMock('../tools/tool-registry')) as {
ToolRegistry: { prototype: { registerTool: Mock } };
}
).ToolRegistry.prototype.registerTool;
const wasShellToolRegistered = (
registerToolMock as Mock
).mock.calls.some((call) => call[0] instanceof vi.mocked(ShellTool));
expect(wasShellToolRegistered).toBe(true);
});
it('should register a tool if coreTools contains an argument-specific pattern with the displayName', async () => {
const params: ConfigParameters = {
...baseParams,
coreTools: ['Shell(git status)'],
}; };
const config = new Config(params); const config = new Config(params);
await config.initialize(); await config.initialize();

View File

@@ -81,6 +81,7 @@ import {
import { shouldAttemptBrowserLaunch } from '../utils/browser.js'; import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
import { FileExclusions } from '../utils/ignorePatterns.js'; import { FileExclusions } from '../utils/ignorePatterns.js';
import { WorkspaceContext } from '../utils/workspaceContext.js'; import { WorkspaceContext } from '../utils/workspaceContext.js';
import { isToolEnabled, type ToolName } from '../utils/tool-utils.js';
// Local config modules // Local config modules
import type { FileFilteringOptions } from './constants.js'; import type { FileFilteringOptions } from './constants.js';
@@ -1110,37 +1111,35 @@ export class Config {
async createToolRegistry(): Promise<ToolRegistry> { async createToolRegistry(): Promise<ToolRegistry> {
const registry = new ToolRegistry(this, this.eventEmitter); const registry = new ToolRegistry(this, this.eventEmitter);
// helper to create & register core tools that are enabled const coreToolsConfig = this.getCoreTools();
const excludeToolsConfig = this.getExcludeTools();
// Helper to create & register core tools that are enabled
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const registerCoreTool = (ToolClass: any, ...args: unknown[]) => { const registerCoreTool = (ToolClass: any, ...args: unknown[]) => {
const className = ToolClass.name; const toolName = ToolClass?.Name as ToolName | undefined;
const toolName = ToolClass.Name || className; const className = ToolClass?.name ?? 'UnknownTool';
const coreTools = this.getCoreTools();
const excludeTools = this.getExcludeTools() || [];
// On some platforms, the className can be minified to _ClassName.
const normalizedClassName = className.replace(/^_+/, '');
let isEnabled = true; // Enabled by default if coreTools is not set. if (!toolName) {
if (coreTools) { // Log warning and skip this tool instead of crashing
isEnabled = coreTools.some( console.warn(
(tool) => `[Config] Skipping tool registration: ${className} is missing static Name property. ` +
tool === toolName || `Tools must define a static Name property to be registered. ` +
tool === normalizedClassName || `Location: config.ts:registerCoreTool`,
tool.startsWith(`${toolName}(`) ||
tool.startsWith(`${normalizedClassName}(`),
); );
return;
} }
const isExcluded = excludeTools.some( if (isToolEnabled(toolName, coreToolsConfig, excludeToolsConfig)) {
(tool) => tool === toolName || tool === normalizedClassName, try {
); registry.registerTool(new ToolClass(...args));
} catch (error) {
if (isExcluded) { console.error(
isEnabled = false; `[Config] Failed to register tool ${className} (${toolName}):`,
} error,
);
if (isEnabled) { throw error; // Re-throw after logging context
registry.registerTool(new ToolClass(...args)); }
} }
}; };

View File

@@ -29,6 +29,7 @@ import { SubagentValidator } from './validation.js';
import { SubAgentScope } from './subagent.js'; import { SubAgentScope } from './subagent.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { BuiltinAgentRegistry } from './builtin-agents.js'; import { BuiltinAgentRegistry } from './builtin-agents.js';
import { ToolDisplayNamesMigration } from '../tools/tool-names.js';
const QWEN_CONFIG_DIR = '.qwen'; const QWEN_CONFIG_DIR = '.qwen';
const AGENT_CONFIG_DIR = 'agents'; const AGENT_CONFIG_DIR = 'agents';
@@ -632,7 +633,12 @@ export class SubagentManager {
// If no exact name match, try to find by display name // If no exact name match, try to find by display name
const displayNameMatch = allTools.find( const displayNameMatch = allTools.find(
(tool) => tool.displayName === toolIdentifier, (tool) =>
tool.displayName === toolIdentifier ||
tool.displayName ===
(ToolDisplayNamesMigration[
toolIdentifier as keyof typeof ToolDisplayNamesMigration
] as string | undefined),
); );
if (displayNameMatch) { if (displayNameMatch) {
result.push(displayNameMatch.name); result.push(displayNameMatch.name);

View File

@@ -22,7 +22,7 @@ import type { Config } from '../config/config.js';
import { ApprovalMode } from '../config/config.js'; import { ApprovalMode } from '../config/config.js';
import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js'; import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js';
import { ReadFileTool } from './read-file.js'; import { ReadFileTool } from './read-file.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { logFileOperation } from '../telemetry/loggers.js'; import { logFileOperation } from '../telemetry/loggers.js';
import { FileOperationEvent } from '../telemetry/types.js'; import { FileOperationEvent } from '../telemetry/types.js';
import { FileOperation } from '../telemetry/metrics.js'; import { FileOperation } from '../telemetry/metrics.js';
@@ -469,7 +469,7 @@ export class EditTool
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
EditTool.Name, EditTool.Name,
'Edit', ToolDisplayNames.EDIT,
`Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when \`expected_replacements\` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the ${ReadFileTool.Name} tool to examine the file's current content before attempting a text replacement. `Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when \`expected_replacements\` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the ${ReadFileTool.Name} tool to examine the file's current content before attempting a text replacement.
The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response. The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.

View File

@@ -14,6 +14,7 @@ import {
import type { FunctionDeclaration } from '@google/genai'; import type { FunctionDeclaration } from '@google/genai';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { ApprovalMode } from '../config/config.js'; import { ApprovalMode } from '../config/config.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
export interface ExitPlanModeParams { export interface ExitPlanModeParams {
plan: string; plan: string;
@@ -152,12 +153,12 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
ExitPlanModeParams, ExitPlanModeParams,
ToolResult ToolResult
> { > {
static readonly Name: string = exitPlanModeToolSchemaData.name!; static readonly Name: string = ToolNames.EXIT_PLAN_MODE;
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
ExitPlanModeTool.Name, ExitPlanModeTool.Name,
'ExitPlanMode', ToolDisplayNames.EXIT_PLAN_MODE,
exitPlanModeToolDescription, exitPlanModeToolDescription,
Kind.Think, Kind.Think,
exitPlanModeToolSchemaData.parametersJsonSchema as Record< exitPlanModeToolSchemaData.parametersJsonSchema as Record<

View File

@@ -9,7 +9,7 @@ import path from 'node:path';
import { glob, escape } from 'glob'; import { glob, escape } from 'glob';
import type { ToolInvocation, ToolResult } from './tools.js'; import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { resolveAndValidatePath } from '../utils/paths.js'; import { resolveAndValidatePath } from '../utils/paths.js';
import { type Config } from '../config/config.js'; import { type Config } from '../config/config.js';
import { import {
@@ -229,7 +229,7 @@ export class GlobTool extends BaseDeclarativeTool<GlobToolParams, ToolResult> {
constructor(private config: Config) { constructor(private config: Config) {
super( super(
GlobTool.Name, GlobTool.Name,
'FindFiles', ToolDisplayNames.GLOB,
'Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like "**/*.js" or "src/**/*.ts"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.', 'Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like "**/*.js" or "src/**/*.ts"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.',
Kind.Search, Kind.Search,
{ {

View File

@@ -11,7 +11,7 @@ import { spawn } from 'node:child_process';
import { globStream } from 'glob'; import { globStream } from 'glob';
import type { ToolInvocation, ToolResult } from './tools.js'; import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { resolveAndValidatePath } from '../utils/paths.js'; import { resolveAndValidatePath } from '../utils/paths.js';
import { getErrorMessage, isNodeError } from '../utils/errors.js'; import { getErrorMessage, isNodeError } from '../utils/errors.js';
import { isGitRepository } from '../utils/gitUtils.js'; import { isGitRepository } from '../utils/gitUtils.js';
@@ -522,7 +522,7 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
GrepTool.Name, GrepTool.Name,
'Grep', ToolDisplayNames.GREP,
'A powerful search tool for finding patterns in files\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n - Filter files with glob parameter (e.g., "*.js", "**/*.tsx")\n - Case-insensitive by default\n - Use Task tool for open-ended searches requiring multiple rounds\n', 'A powerful search tool for finding patterns in files\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n - Filter files with glob parameter (e.g., "*.js", "**/*.tsx")\n - Case-insensitive by default\n - Use Task tool for open-ended searches requiring multiple rounds\n',
Kind.Search, Kind.Search,
{ {

View File

@@ -12,6 +12,7 @@ import { makeRelative, shortenPath } from '../utils/paths.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
/** /**
* Parameters for the LS tool * Parameters for the LS tool
@@ -252,12 +253,12 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
* Implementation of the LS tool logic * Implementation of the LS tool logic
*/ */
export class LSTool extends BaseDeclarativeTool<LSToolParams, ToolResult> { export class LSTool extends BaseDeclarativeTool<LSToolParams, ToolResult> {
static readonly Name = 'list_directory'; static readonly Name = ToolNames.LS;
constructor(private config: Config) { constructor(private config: Config) {
super( super(
LSTool.Name, LSTool.Name,
'ReadFolder', ToolDisplayNames.LS,
'Lists the names of files and subdirectories directly within a specified directory path. Can optionally ignore entries matching provided glob patterns.', 'Lists the names of files and subdirectories directly within a specified directory path. Can optionally ignore entries matching provided glob patterns.',
Kind.Search, Kind.Search,
{ {

View File

@@ -18,6 +18,7 @@ import { Storage } from '../config/storage.js';
import * as Diff from 'diff'; import * as Diff from 'diff';
import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js'; import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
import { tildeifyPath } from '../utils/paths.js'; import { tildeifyPath } from '../utils/paths.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import type { import type {
ModifiableDeclarativeTool, ModifiableDeclarativeTool,
ModifyContext, ModifyContext,
@@ -380,11 +381,11 @@ export class MemoryTool
extends BaseDeclarativeTool<SaveMemoryParams, ToolResult> extends BaseDeclarativeTool<SaveMemoryParams, ToolResult>
implements ModifiableDeclarativeTool<SaveMemoryParams> implements ModifiableDeclarativeTool<SaveMemoryParams>
{ {
static readonly Name: string = memoryToolSchemaData.name!; static readonly Name: string = ToolNames.MEMORY;
constructor() { constructor() {
super( super(
MemoryTool.Name, MemoryTool.Name,
'SaveMemory', ToolDisplayNames.MEMORY,
memoryToolDescription, memoryToolDescription,
Kind.Think, Kind.Think,
memoryToolSchemaData.parametersJsonSchema as Record<string, unknown>, memoryToolSchemaData.parametersJsonSchema as Record<string, unknown>,

View File

@@ -8,7 +8,7 @@ import path from 'node:path';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
import type { ToolInvocation, ToolLocation, ToolResult } from './tools.js'; import type { ToolInvocation, ToolLocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import type { PartUnion } from '@google/genai'; import type { PartUnion } from '@google/genai';
import { import {
@@ -131,7 +131,7 @@ export class ReadFileTool extends BaseDeclarativeTool<
constructor(private config: Config) { constructor(private config: Config) {
super( super(
ReadFileTool.Name, ReadFileTool.Name,
'ReadFile', ToolDisplayNames.READ_FILE,
`Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges.`, `Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges.`,
Kind.Read, Kind.Read,
{ {

View File

@@ -6,7 +6,7 @@
import type { ToolInvocation, ToolResult } from './tools.js'; import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { getErrorMessage } from '../utils/errors.js'; import { getErrorMessage } from '../utils/errors.js';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
@@ -554,7 +554,7 @@ export class ReadManyFilesTool extends BaseDeclarativeTool<
super( super(
ReadManyFilesTool.Name, ReadManyFilesTool.Name,
'ReadManyFiles', ToolDisplayNames.READ_MANY_FILES,
`Reads content from multiple files specified by paths or glob patterns within a configured target directory. For text files, it concatenates their content into a single string. It is primarily designed for text-based files. However, it can also process image (e.g., .png, .jpg) and PDF (.pdf) files if their file names or extensions are explicitly included in the 'paths' argument. For these explicitly requested non-text files, their data is read and included in a format suitable for model consumption (e.g., base64 encoded). `Reads content from multiple files specified by paths or glob patterns within a configured target directory. For text files, it concatenates their content into a single string. It is primarily designed for text-based files. However, it can also process image (e.g., .png, .jpg) and PDF (.pdf) files if their file names or extensions are explicitly included in the 'paths' argument. For these explicitly requested non-text files, their data is read and included in a format suitable for model consumption (e.g., base64 encoded).
This tool is useful when you need to understand or analyze a collection of files, such as: This tool is useful when you need to understand or analyze a collection of files, such as:

View File

@@ -9,7 +9,7 @@ import path from 'node:path';
import os, { EOL } from 'node:os'; import os, { EOL } from 'node:os';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import type { import type {
ToolInvocation, ToolInvocation,
@@ -429,7 +429,7 @@ export class ShellTool extends BaseDeclarativeTool<
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
ShellTool.Name, ShellTool.Name,
'Shell', ToolDisplayNames.SHELL,
getShellToolDescription(), getShellToolDescription(),
Kind.Execute, Kind.Execute,
{ {

View File

@@ -5,7 +5,7 @@
*/ */
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import type { import type {
ToolResult, ToolResult,
ToolResultDisplay, ToolResultDisplay,
@@ -77,7 +77,7 @@ export class TaskTool extends BaseDeclarativeTool<TaskParams, ToolResult> {
super( super(
TaskTool.Name, TaskTool.Name,
'Task', ToolDisplayNames.TASK,
'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description 'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description
Kind.Other, Kind.Other,
initialSchema, initialSchema,

View File

@@ -14,6 +14,7 @@ import * as process from 'process';
import { QWEN_DIR } from '../utils/paths.js'; import { QWEN_DIR } from '../utils/paths.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
export interface TodoItem { export interface TodoItem {
id: string; id: string;
@@ -422,12 +423,12 @@ export class TodoWriteTool extends BaseDeclarativeTool<
TodoWriteParams, TodoWriteParams,
ToolResult ToolResult
> { > {
static readonly Name: string = todoWriteToolSchemaData.name!; static readonly Name: string = ToolNames.TODO_WRITE;
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
TodoWriteTool.Name, TodoWriteTool.Name,
'TodoWrite', ToolDisplayNames.TODO_WRITE,
todoWriteToolDescription, todoWriteToolDescription,
Kind.Think, Kind.Think,
todoWriteToolSchemaData.parametersJsonSchema as Record<string, unknown>, todoWriteToolSchemaData.parametersJsonSchema as Record<string, unknown>,

View File

@@ -23,4 +23,43 @@ export const ToolNames = {
EXIT_PLAN_MODE: 'exit_plan_mode', EXIT_PLAN_MODE: 'exit_plan_mode',
WEB_FETCH: 'web_fetch', WEB_FETCH: 'web_fetch',
WEB_SEARCH: 'web_search', WEB_SEARCH: 'web_search',
LS: 'list_directory',
} as const;
/**
* Tool display name constants to avoid circular dependencies.
* These constants are used across multiple files and should be kept in sync
* with the actual tool display names.
*/
export const ToolDisplayNames = {
EDIT: 'Edit',
WRITE_FILE: 'WriteFile',
READ_FILE: 'ReadFile',
READ_MANY_FILES: 'ReadManyFiles',
GREP: 'Grep',
GLOB: 'Glob',
SHELL: 'Shell',
TODO_WRITE: 'TodoWrite',
MEMORY: 'SaveMemory',
TASK: 'Task',
EXIT_PLAN_MODE: 'ExitPlanMode',
WEB_FETCH: 'WebFetch',
WEB_SEARCH: 'WebSearch',
LS: 'ListFiles',
} as const;
// Migration from old tool names to new tool names
// These legacy tool names were used in earlier versions and need to be supported
// for backward compatibility with existing user configurations
export const ToolNamesMigration = {
search_file_content: ToolNames.GREP, // Legacy name from grep tool
replace: ToolNames.EDIT, // Legacy name from edit tool
} as const;
// Migration from old tool display names to new tool display names
// These legacy display names were used before the tool naming standardization
export const ToolDisplayNamesMigration = {
SearchFiles: ToolDisplayNames.GREP, // Old display name for Grep
FindFiles: ToolDisplayNames.GLOB, // Old display name for Glob
ReadFolder: ToolDisplayNames.LS, // Old display name for ListFiles
} as const; } as const;

View File

@@ -23,7 +23,7 @@ import {
ToolConfirmationOutcome, ToolConfirmationOutcome,
} from './tools.js'; } from './tools.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js'; import { DEFAULT_QWEN_MODEL } from '../config/models.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
const URL_FETCH_TIMEOUT_MS = 10000; const URL_FETCH_TIMEOUT_MS = 10000;
const MAX_CONTENT_LENGTH = 100000; const MAX_CONTENT_LENGTH = 100000;
@@ -196,7 +196,7 @@ export class WebFetchTool extends BaseDeclarativeTool<
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
WebFetchTool.Name, WebFetchTool.Name,
'WebFetch', ToolDisplayNames.WEB_FETCH,
'Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model\'s response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".\n - The URL must be a fully-formed valid URL\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Supports both public and private/localhost URLs using direct fetch', 'Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model\'s response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".\n - The URL must be a fully-formed valid URL\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Supports both public and private/localhost URLs using direct fetch',
Kind.Fetch, Kind.Fetch,
{ {

View File

@@ -30,7 +30,7 @@ import type {
WebSearchProviderConfig, WebSearchProviderConfig,
DashScopeProviderConfig, DashScopeProviderConfig,
} from './types.js'; } from './types.js';
import { ToolNames } from '../tool-names.js'; import { ToolNames, ToolDisplayNames } from '../tool-names.js';
class WebSearchToolInvocation extends BaseToolInvocation< class WebSearchToolInvocation extends BaseToolInvocation<
WebSearchToolParams, WebSearchToolParams,
@@ -280,7 +280,7 @@ export class WebSearchTool extends BaseDeclarativeTool<
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
WebSearchTool.Name, WebSearchTool.Name,
'WebSearch', ToolDisplayNames.WEB_SEARCH,
'Allows searching the web and using results to inform responses. Provides up-to-date information for current events and recent data beyond the training data cutoff. Returns search results formatted with concise answers and source links. Use this tool when accessing information that may be outdated or beyond the knowledge cutoff.', 'Allows searching the web and using results to inform responses. Provides up-to-date information for current events and recent data beyond the training data cutoff. Returns search results formatted with concise answers and source links. Use this tool when accessing information that may be outdated or beyond the knowledge cutoff.',
Kind.Search, Kind.Search,
{ {

View File

@@ -27,7 +27,7 @@ import { ToolErrorType } from './tool-error.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
import { getErrorMessage, isNodeError } from '../utils/errors.js'; import { getErrorMessage, isNodeError } from '../utils/errors.js';
import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js'; import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js';
import { ToolNames } from './tool-names.js'; import { ToolNames, ToolDisplayNames } from './tool-names.js';
import type { import type {
ModifiableDeclarativeTool, ModifiableDeclarativeTool,
ModifyContext, ModifyContext,
@@ -361,7 +361,7 @@ export class WriteFileTool
constructor(private readonly config: Config) { constructor(private readonly config: Config) {
super( super(
WriteFileTool.Name, WriteFileTool.Name,
'WriteFile', ToolDisplayNames.WRITE_FILE,
`Writes content to a specified file in the local filesystem. `Writes content to a specified file in the local filesystem.
The user has the ability to modify \`content\`. If modified, this will be stated in the response.`, The user has the ability to modify \`content\`. If modified, this will be stated in the response.`,

View File

@@ -5,9 +5,10 @@
*/ */
import { expect, describe, it } from 'vitest'; import { expect, describe, it } from 'vitest';
import { doesToolInvocationMatch } from './tool-utils.js'; import { doesToolInvocationMatch, isToolEnabled } from './tool-utils.js';
import type { AnyToolInvocation, Config } from '../index.js'; import type { AnyToolInvocation, Config } from '../index.js';
import { ReadFileTool } from '../tools/read-file.js'; import { ReadFileTool } from '../tools/read-file.js';
import { ToolNames } from '../tools/tool-names.js';
describe('doesToolInvocationMatch', () => { describe('doesToolInvocationMatch', () => {
it('should not match a partial command prefix', () => { it('should not match a partial command prefix', () => {
@@ -92,3 +93,67 @@ describe('doesToolInvocationMatch', () => {
}); });
}); });
}); });
describe('isToolEnabled', () => {
it('enables tool when coreTools is undefined and tool is not excluded', () => {
expect(isToolEnabled(ToolNames.SHELL, undefined, undefined)).toBe(true);
});
it('disables tool when excluded by canonical tool name', () => {
expect(
isToolEnabled(ToolNames.SHELL, undefined, ['run_shell_command']),
).toBe(false);
});
it('enables tool when explicitly listed by display name', () => {
expect(isToolEnabled(ToolNames.SHELL, ['Shell'], undefined)).toBe(true);
});
it('enables tool when explicitly listed by class name', () => {
expect(isToolEnabled(ToolNames.SHELL, ['ShellTool'], undefined)).toBe(true);
});
it('supports class names with leading underscores', () => {
expect(isToolEnabled(ToolNames.SHELL, ['__ShellTool'], undefined)).toBe(
true,
);
});
it('enables tool when coreTools contains a legacy tool name alias', () => {
expect(
isToolEnabled(ToolNames.GREP, ['search_file_content'], undefined),
).toBe(true);
});
it('enables tool when coreTools contains a legacy display name alias', () => {
expect(isToolEnabled(ToolNames.GLOB, ['FindFiles'], undefined)).toBe(true);
});
it('enables tool when coreTools contains an argument-specific pattern', () => {
expect(
isToolEnabled(ToolNames.SHELL, ['Shell(git status)'], undefined),
).toBe(true);
});
it('disables tool when not present in coreTools', () => {
expect(isToolEnabled(ToolNames.SHELL, ['Edit'], undefined)).toBe(false);
});
it('uses legacy display name aliases when excluding tools', () => {
expect(isToolEnabled(ToolNames.GREP, undefined, ['SearchFiles'])).toBe(
false,
);
});
it('does not treat argument-specific exclusions as matches', () => {
expect(
isToolEnabled(ToolNames.SHELL, undefined, ['Shell(git status)']),
).toBe(true);
});
it('considers excludeTools even when tool is explicitly enabled', () => {
expect(isToolEnabled(ToolNames.SHELL, ['Shell'], ['ShellTool'])).toBe(
false,
);
});
});

View File

@@ -6,6 +6,111 @@
import type { AnyDeclarativeTool, AnyToolInvocation } from '../index.js'; import type { AnyDeclarativeTool, AnyToolInvocation } from '../index.js';
import { isTool } from '../index.js'; import { isTool } from '../index.js';
import {
ToolNames,
ToolDisplayNames,
ToolNamesMigration,
ToolDisplayNamesMigration,
} from '../tools/tool-names.js';
export type ToolName = (typeof ToolNames)[keyof typeof ToolNames];
const normalizeIdentifier = (identifier: string): string =>
identifier.trim().replace(/^_+/, '');
const toolNameKeys = Object.keys(ToolNames) as Array<keyof typeof ToolNames>;
const TOOL_ALIAS_MAP: Map<ToolName, Set<string>> = (() => {
const map = new Map<ToolName, Set<string>>();
const addAlias = (set: Set<string>, alias?: string) => {
if (!alias) {
return;
}
set.add(normalizeIdentifier(alias));
};
for (const key of toolNameKeys) {
const canonicalName = ToolNames[key];
const displayName = ToolDisplayNames[key];
const aliases = new Set<string>();
addAlias(aliases, canonicalName);
addAlias(aliases, displayName);
addAlias(aliases, `${displayName}Tool`);
for (const [legacyName, mappedName] of Object.entries(ToolNamesMigration)) {
if (mappedName === canonicalName) {
addAlias(aliases, legacyName);
}
}
for (const [legacyDisplay, mappedDisplay] of Object.entries(
ToolDisplayNamesMigration,
)) {
if (mappedDisplay === displayName) {
addAlias(aliases, legacyDisplay);
}
}
map.set(canonicalName, aliases);
}
return map;
})();
const getAliasSetForTool = (toolName: ToolName): Set<string> => {
const aliases = TOOL_ALIAS_MAP.get(toolName);
if (!aliases) {
return new Set([normalizeIdentifier(toolName)]);
}
return aliases;
};
const sanitizeExactIdentifier = (value: string): string =>
normalizeIdentifier(value);
const sanitizePatternIdentifier = (value: string): string => {
const openParenIndex = value.indexOf('(');
if (openParenIndex === -1) {
return normalizeIdentifier(value);
}
return normalizeIdentifier(value.slice(0, openParenIndex));
};
const filterList = (list?: string[]): string[] =>
(list ?? []).filter((entry): entry is string =>
Boolean(entry && entry.trim()),
);
export function isToolEnabled(
toolName: ToolName,
coreTools?: string[],
excludeTools?: string[],
): boolean {
const aliasSet = getAliasSetForTool(toolName);
const matchesIdentifier = (value: string): boolean =>
aliasSet.has(sanitizeExactIdentifier(value));
const matchesIdentifierWithArgs = (value: string): boolean =>
aliasSet.has(sanitizePatternIdentifier(value));
const filteredCore = filterList(coreTools);
const filteredExclude = filterList(excludeTools);
if (filteredCore.length === 0) {
return !filteredExclude.some((entry) => matchesIdentifier(entry));
}
const isExplicitlyEnabled = filteredCore.some(
(entry) => matchesIdentifier(entry) || matchesIdentifierWithArgs(entry),
);
if (!isExplicitlyEnabled) {
return false;
}
return !filteredExclude.some((entry) => matchesIdentifier(entry));
}
const SHELL_TOOL_NAMES = ['run_shell_command', 'ShellTool']; const SHELL_TOOL_NAMES = ['run_shell_command', 'ShellTool'];