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

@@ -5,9 +5,10 @@
*/
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 { ReadFileTool } from '../tools/read-file.js';
import { ToolNames } from '../tools/tool-names.js';
describe('doesToolInvocationMatch', () => {
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 { 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'];