Skip to content

Commit 829a201

Browse files
ved015rfcclub
authored andcommitted
Feat/tool registry discovery (google-gemini#28113)
(cherry picked from commit d845bc5)
1 parent 70ea2a9 commit 829a201

2 files changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, expect, it } from 'vitest';
8+
import {
9+
buildToolRegistry,
10+
resolveToolName,
11+
getToolsByCategory,
12+
type ToolCategory,
13+
} from '../utils/tool-registry.js';
14+
15+
describe('tool-registry', () => {
16+
const registry = buildToolRegistry();
17+
18+
describe('buildToolRegistry', () => {
19+
it('includes all canonical built-in tools', () => {
20+
expect(registry.totalTools).toBeGreaterThanOrEqual(26);
21+
});
22+
23+
it('every tool has a valid category', () => {
24+
for (const [name, entry] of registry.tools) {
25+
expect(entry.category).toBeTruthy();
26+
expect(entry.name).toBe(name);
27+
}
28+
});
29+
30+
it('byCategory entries match tools map', () => {
31+
let categoryTotal = 0;
32+
for (const [, entries] of registry.byCategory) {
33+
for (const entry of entries) {
34+
expect(registry.tools.get(entry.name)).toBe(entry);
35+
}
36+
categoryTotal += entries.length;
37+
}
38+
expect(categoryTotal).toBe(registry.totalTools);
39+
});
40+
41+
it('aliasLookup covers every canonical name', () => {
42+
for (const name of registry.tools.keys()) {
43+
expect(registry.aliasLookup.get(name)).toBe(name);
44+
}
45+
});
46+
47+
it('aliasLookup covers every legacy alias', () => {
48+
for (const [, entry] of registry.tools) {
49+
for (const alias of entry.aliases) {
50+
expect(registry.aliasLookup.get(alias)).toBe(entry.name);
51+
}
52+
}
53+
});
54+
55+
it('is deterministic across calls', () => {
56+
const second = buildToolRegistry();
57+
expect([...second.tools.keys()]).toEqual([...registry.tools.keys()]);
58+
expect(second.totalTools).toBe(registry.totalTools);
59+
});
60+
});
61+
62+
describe('resolveToolName', () => {
63+
it('resolves canonical names to themselves', () => {
64+
expect(resolveToolName(registry, 'grep_search')).toBe('grep_search');
65+
expect(resolveToolName(registry, 'run_shell_command')).toBe(
66+
'run_shell_command',
67+
);
68+
});
69+
70+
it('resolves legacy alias to canonical name', () => {
71+
expect(resolveToolName(registry, 'search_file_content')).toBe(
72+
'grep_search',
73+
);
74+
});
75+
76+
it('returns undefined for unknown tool names', () => {
77+
expect(resolveToolName(registry, 'nonexistent_tool')).toBeUndefined();
78+
});
79+
80+
it('returns undefined for empty string', () => {
81+
expect(resolveToolName(registry, '')).toBeUndefined();
82+
});
83+
});
84+
85+
describe('getToolsByCategory', () => {
86+
it('returns file-system tools', () => {
87+
const tools = getToolsByCategory(registry, 'file-system');
88+
const names = tools.map((t) => t.name);
89+
expect(names).toContain('glob');
90+
expect(names).toContain('grep_search');
91+
expect(names).toContain('read_file');
92+
expect(names).toContain('write_file');
93+
expect(names).toContain('replace');
94+
});
95+
96+
it('returns task-tracker tools', () => {
97+
const tools = getToolsByCategory(registry, 'task-tracker');
98+
const names = tools.map((t) => t.name);
99+
expect(names).toContain('tracker_create_task');
100+
expect(names).toContain('tracker_update_task');
101+
expect(names).toContain('tracker_get_task');
102+
expect(names).toContain('tracker_list_tasks');
103+
expect(names).toContain('tracker_add_dependency');
104+
expect(names).toContain('tracker_visualize');
105+
expect(names).toHaveLength(6);
106+
});
107+
108+
it('returns agent tools', () => {
109+
const tools = getToolsByCategory(registry, 'agent');
110+
const names = tools.map((t) => t.name);
111+
expect(names).toContain('invoke_agent');
112+
expect(names).toContain('complete_task');
113+
expect(names).toContain('update_topic');
114+
});
115+
116+
it('returns empty array for unknown category', () => {
117+
expect(
118+
getToolsByCategory(registry, 'nonexistent' as ToolCategory),
119+
).toEqual([]);
120+
});
121+
122+
it('every defined category has at least one tool', () => {
123+
const expectedCategories: ToolCategory[] = [
124+
'file-system',
125+
'shell',
126+
'web',
127+
'planning',
128+
'user-interaction',
129+
'skills',
130+
'task-tracker',
131+
'agent',
132+
'mcp',
133+
];
134+
for (const cat of expectedCategories) {
135+
expect(getToolsByCategory(registry, cat).length).toBeGreaterThan(0);
136+
}
137+
});
138+
});
139+
});

scripts/utils/tool-registry.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
ALL_BUILTIN_TOOL_NAMES,
9+
TOOL_LEGACY_ALIASES,
10+
} from '@google/gemini-cli-core';
11+
12+
export type ToolCategory =
13+
| 'file-system'
14+
| 'shell'
15+
| 'web'
16+
| 'planning'
17+
| 'user-interaction'
18+
| 'skills'
19+
| 'task-tracker'
20+
| 'agent'
21+
| 'mcp';
22+
23+
export interface ToolRegistryEntry {
24+
name: string;
25+
category: ToolCategory;
26+
aliases: readonly string[];
27+
}
28+
29+
export interface ToolRegistry {
30+
tools: ReadonlyMap<string, ToolRegistryEntry>;
31+
totalTools: number;
32+
byCategory: ReadonlyMap<ToolCategory, readonly ToolRegistryEntry[]>;
33+
aliasLookup: ReadonlyMap<string, string>;
34+
}
35+
36+
const TOOL_CATEGORIES: Record<
37+
(typeof ALL_BUILTIN_TOOL_NAMES)[number],
38+
ToolCategory
39+
> = {
40+
glob: 'file-system',
41+
grep_search: 'file-system',
42+
list_directory: 'file-system',
43+
read_file: 'file-system',
44+
read_many_files: 'file-system',
45+
write_file: 'file-system',
46+
replace: 'file-system',
47+
run_shell_command: 'shell',
48+
google_web_search: 'web',
49+
web_fetch: 'web',
50+
enter_plan_mode: 'planning',
51+
exit_plan_mode: 'planning',
52+
write_todos: 'planning',
53+
ask_user: 'user-interaction',
54+
activate_skill: 'skills',
55+
get_internal_docs: 'skills',
56+
tracker_create_task: 'task-tracker',
57+
tracker_update_task: 'task-tracker',
58+
tracker_get_task: 'task-tracker',
59+
tracker_list_tasks: 'task-tracker',
60+
tracker_add_dependency: 'task-tracker',
61+
tracker_visualize: 'task-tracker',
62+
invoke_agent: 'agent',
63+
complete_task: 'agent',
64+
update_topic: 'agent',
65+
read_mcp_resource: 'mcp',
66+
list_mcp_resources: 'mcp',
67+
};
68+
69+
let registryCache: ToolRegistry | undefined;
70+
71+
export function buildToolRegistry(): ToolRegistry {
72+
if (registryCache) {
73+
return registryCache;
74+
}
75+
76+
const tools = new Map<string, ToolRegistryEntry>();
77+
const aliasLookup = new Map<string, string>();
78+
const categoryGroups = new Map<ToolCategory, ToolRegistryEntry[]>();
79+
80+
for (const name of ALL_BUILTIN_TOOL_NAMES) {
81+
const category = TOOL_CATEGORIES[name];
82+
const aliases: string[] = [];
83+
84+
for (const [legacyName, canonicalName] of Object.entries(
85+
TOOL_LEGACY_ALIASES,
86+
)) {
87+
if (canonicalName === name) {
88+
aliases.push(legacyName);
89+
aliasLookup.set(legacyName, name);
90+
}
91+
}
92+
93+
aliasLookup.set(name, name);
94+
95+
const entry: ToolRegistryEntry = {
96+
name,
97+
category,
98+
aliases: Object.freeze(aliases),
99+
};
100+
101+
tools.set(name, entry);
102+
103+
const group = categoryGroups.get(category);
104+
if (group) {
105+
group.push(entry);
106+
} else {
107+
categoryGroups.set(category, [entry]);
108+
}
109+
}
110+
111+
const frozenCategories = new Map<
112+
ToolCategory,
113+
readonly ToolRegistryEntry[]
114+
>();
115+
for (const [cat, entries] of categoryGroups) {
116+
frozenCategories.set(cat, Object.freeze(entries));
117+
}
118+
119+
registryCache = {
120+
tools,
121+
totalTools: tools.size,
122+
byCategory: frozenCategories,
123+
aliasLookup,
124+
};
125+
return registryCache;
126+
}
127+
128+
export function resolveToolName(
129+
registry: ToolRegistry,
130+
name: string,
131+
): string | undefined {
132+
if (!name) {
133+
return undefined;
134+
}
135+
return registry.aliasLookup.get(name);
136+
}
137+
138+
export function getToolsByCategory(
139+
registry: ToolRegistry,
140+
category: ToolCategory,
141+
): readonly ToolRegistryEntry[] {
142+
return registry.byCategory.get(category) ?? [];
143+
}

0 commit comments

Comments
 (0)