diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index cfc33b7b6ae..71fe1793e9b 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Type } from '@google/genai'; import type { ToolDefinition } from './types.js'; import * as os from 'node:os'; @@ -25,21 +24,21 @@ export const READ_FILE_DEFINITION: ToolDefinition = { name: READ_FILE_TOOL_NAME, description: `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), audio files (MP3, WAV, AIFF, AAC, OGG, FLAC), and PDF files. For text files, it can read specific line ranges.`, parametersJsonSchema: { - type: Type.OBJECT, + type: 'object', properties: { file_path: { description: 'The path to the file to read.', - type: Type.STRING, + type: 'string', }, offset: { description: "Optional: For text files, the 0-based line number to start reading from. Requires 'limit' to be set. Use for paginating through large files.", - type: Type.NUMBER, + type: 'number', }, limit: { description: "Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit).", - type: Type.NUMBER, + type: 'number', }, }, required: ['file_path'], @@ -58,15 +57,15 @@ export const WRITE_FILE_DEFINITION: ToolDefinition = { The user has the ability to modify \`content\`. If modified, this will be stated in the response.`, parametersJsonSchema: { - type: Type.OBJECT, + type: 'object', properties: { file_path: { description: 'The path to the file to write to.', - type: Type.STRING, + type: 'string', }, content: { description: 'The content to write to the file.', - type: Type.STRING, + type: 'string', }, }, required: ['file_path', 'content'], @@ -84,20 +83,20 @@ export const GREP_DEFINITION: ToolDefinition = { description: 'Searches for a regular expression pattern within file contents. Max 100 matches.', parametersJsonSchema: { - type: Type.OBJECT, + type: 'object', properties: { pattern: { description: `The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').`, - type: Type.STRING, + type: 'string', }, dir_path: { description: 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.', - type: Type.STRING, + type: 'string', }, include: { description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`, - type: Type.STRING, + type: 'string', }, }, required: ['pattern'], @@ -115,32 +114,32 @@ export const GLOB_DEFINITION: ToolDefinition = { description: 'Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.', parametersJsonSchema: { - type: Type.OBJECT, + type: 'object', properties: { pattern: { description: "The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').", - type: Type.STRING, + type: 'string', }, dir_path: { description: 'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.', - type: Type.STRING, + type: 'string', }, case_sensitive: { description: 'Optional: Whether the search should be case-sensitive. Defaults to false.', - type: Type.BOOLEAN, + type: 'boolean', }, respect_git_ignore: { description: 'Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true.', - type: Type.BOOLEAN, + type: 'boolean', }, respect_gemini_ignore: { description: 'Optional: Whether to respect .geminiignore patterns when finding files. Defaults to true.', - type: Type.BOOLEAN, + type: 'boolean', }, }, required: ['pattern'], @@ -158,33 +157,33 @@ export const LS_DEFINITION: ToolDefinition = { description: 'Lists the names of files and subdirectories directly within a specified directory path. Can optionally ignore entries matching provided glob patterns.', parametersJsonSchema: { - type: Type.OBJECT, + type: 'object', properties: { dir_path: { description: 'The path to the directory to list', - type: Type.STRING, + type: 'string', }, ignore: { description: 'List of glob patterns to ignore', items: { - type: Type.STRING, + type: 'string', }, - type: Type.ARRAY, + type: 'array', }, file_filtering_options: { description: 'Optional: Whether to respect ignore patterns from .gitignore or .geminiignore', - type: Type.OBJECT, + type: 'object', properties: { respect_git_ignore: { description: 'Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true.', - type: Type.BOOLEAN, + type: 'boolean', }, respect_gemini_ignore: { description: 'Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true.', - type: Type.BOOLEAN, + type: 'boolean', }, }, }, @@ -262,24 +261,24 @@ export function getShellDefinition( enableEfficiency, ), parametersJsonSchema: { - type: Type.OBJECT, + type: 'object', properties: { command: { - type: Type.STRING, + type: 'string', description: getCommandDescription(), }, description: { - type: Type.STRING, + type: 'string', description: 'Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks.', }, dir_path: { - type: Type.STRING, + type: 'string', description: '(OPTIONAL) The path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.', }, is_background: { - type: Type.BOOLEAN, + type: 'boolean', description: 'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.', }, diff --git a/packages/core/src/tools/definitions/resolver.test.ts b/packages/core/src/tools/definitions/resolver.test.ts index a765608ac7d..fadc7f65d40 100644 --- a/packages/core/src/tools/definitions/resolver.test.ts +++ b/packages/core/src/tools/definitions/resolver.test.ts @@ -28,13 +28,45 @@ describe('resolveToolDeclaration', () => { expect(result).toEqual(mockDefinition.base); }); - it('should return the base definition when a modelId is provided (current implementation)', () => { - const result = resolveToolDeclaration(mockDefinition, 'gemini-1.5-pro'); - expect(result).toEqual(mockDefinition.base); + it('should return overridden description when modelId matches override criteria', () => { + const definitionWithOverride: ToolDefinition = { + ...mockDefinition, + overrides: (modelId: string) => { + if (modelId === 'special-model') { + return { description: 'Overridden description' }; + } + return undefined; + }, + }; + + const result = resolveToolDeclaration( + definitionWithOverride, + 'special-model', + ); + expect(result.description).toBe('Overridden description'); + expect(result.name).toBe(mockDefinition.base.name); }); - it('should return the same object reference as base (current implementation)', () => { - const result = resolveToolDeclaration(mockDefinition); - expect(result).toBe(mockDefinition.base); + it('should return base definition when modelId does not match override criteria', () => { + const definitionWithOverride: ToolDefinition = { + ...mockDefinition, + overrides: (modelId: string) => { + if (modelId === 'special-model') { + return { description: 'Overridden description' }; + } + return undefined; + }, + }; + + const result = resolveToolDeclaration( + definitionWithOverride, + 'regular-model', + ); + expect(result.description).toBe(mockDefinition.base.description); + }); + + it('should return the base definition when a modelId is provided but no overrides exist', () => { + const result = resolveToolDeclaration(mockDefinition, 'gemini-1.5-pro'); + expect(result).toEqual(mockDefinition.base); }); }); diff --git a/packages/core/src/tools/definitions/resolver.ts b/packages/core/src/tools/definitions/resolver.ts index 8176e481044..06ec9210f43 100644 --- a/packages/core/src/tools/definitions/resolver.ts +++ b/packages/core/src/tools/definitions/resolver.ts @@ -10,13 +10,25 @@ import type { ToolDefinition } from './types.js'; /** * Resolves the declaration for a tool. * - * @param definition The tool definition containing the base declaration. - * @param _modelId Optional model identifier (ignored in this plain refactor). + * @param definition The tool definition containing the base declaration and optional overrides. + * @param modelId Optional model identifier to apply specific overrides. * @returns The FunctionDeclaration to be sent to the API. */ export function resolveToolDeclaration( definition: ToolDefinition, - _modelId?: string, + modelId?: string, ): FunctionDeclaration { - return definition.base; + if (!modelId || !definition.overrides) { + return definition.base; + } + + const override = definition.overrides(modelId); + if (!override) { + return definition.base; + } + + return { + ...definition.base, + ...override, + }; } diff --git a/packages/core/src/tools/definitions/types.ts b/packages/core/src/tools/definitions/types.ts index dc928e0a668..d7e1a3ceda0 100644 --- a/packages/core/src/tools/definitions/types.ts +++ b/packages/core/src/tools/definitions/types.ts @@ -12,4 +12,9 @@ import { type FunctionDeclaration } from '@google/genai'; export interface ToolDefinition { /** The base declaration for the tool. */ base: FunctionDeclaration; + + /** + * Optional overrides for specific model families or versions. + */ + overrides?: (modelId: string) => Partial | undefined; } diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts index a734d76794c..ea1ec994e53 100644 --- a/packages/core/src/tools/glob.ts +++ b/packages/core/src/tools/glob.ts @@ -17,6 +17,8 @@ import { ToolErrorType } from './tool-error.js'; import { GLOB_TOOL_NAME } from './tool-names.js'; import { getErrorMessage } from '../utils/errors.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { GLOB_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; // Subset of 'Path' interface provided by 'glob' that we can implement for testing export interface GlobPath { @@ -270,39 +272,9 @@ export class GlobTool extends BaseDeclarativeTool { super( GlobTool.Name, 'FindFiles', - 'Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.', + GLOB_DEFINITION.base.description!, Kind.Search, - { - properties: { - pattern: { - description: - "The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').", - type: 'string', - }, - dir_path: { - description: - 'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.', - type: 'string', - }, - case_sensitive: { - description: - 'Optional: Whether the search should be case-sensitive. Defaults to false.', - type: 'boolean', - }, - respect_git_ignore: { - description: - 'Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true.', - type: 'boolean', - }, - respect_gemini_ignore: { - description: - 'Optional: Whether to respect .geminiignore patterns when finding files. Defaults to true.', - type: 'boolean', - }, - }, - required: ['pattern'], - type: 'object', - }, + GLOB_DEFINITION.base.parametersJsonSchema, messageBus, true, false, @@ -365,4 +337,8 @@ export class GlobTool extends BaseDeclarativeTool { _toolDisplayName, ); } + + override getSchema(modelId?: string) { + return resolveToolDeclaration(GLOB_DEFINITION, modelId); + } } diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts index c47d65c37b3..48f68f96096 100644 --- a/packages/core/src/tools/grep.ts +++ b/packages/core/src/tools/grep.ts @@ -25,6 +25,8 @@ import type { FileExclusions } from '../utils/ignorePatterns.js'; import { ToolErrorType } from './tool-error.js'; import { GREP_TOOL_NAME } from './tool-names.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { GREP_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; // --- Interfaces --- @@ -579,27 +581,9 @@ export class GrepTool extends BaseDeclarativeTool { super( GrepTool.Name, 'SearchText', - 'Searches for a regular expression pattern within file contents. Max 100 matches.', + GREP_DEFINITION.base.description!, Kind.Search, - { - properties: { - pattern: { - description: `The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').`, - type: 'string', - }, - dir_path: { - description: - 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.', - type: 'string', - }, - include: { - description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`, - type: 'string', - }, - }, - required: ['pattern'], - type: 'object', - }, + GREP_DEFINITION.base.parametersJsonSchema, messageBus, true, false, @@ -665,4 +649,8 @@ export class GrepTool extends BaseDeclarativeTool { _toolDisplayName, ); } + + override getSchema(modelId?: string) { + return resolveToolDeclaration(GREP_DEFINITION, modelId); + } } diff --git a/packages/core/src/tools/ls.ts b/packages/core/src/tools/ls.ts index a264f5cf549..9ca2918b2c2 100644 --- a/packages/core/src/tools/ls.ts +++ b/packages/core/src/tools/ls.ts @@ -15,6 +15,8 @@ import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { ToolErrorType } from './tool-error.js'; import { LS_TOOL_NAME } from './tool-names.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { LS_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; /** * Parameters for the LS tool @@ -280,42 +282,9 @@ export class LSTool extends BaseDeclarativeTool { super( LSTool.Name, 'ReadFolder', - 'Lists the names of files and subdirectories directly within a specified directory path. Can optionally ignore entries matching provided glob patterns.', + LS_DEFINITION.base.description!, Kind.Search, - { - properties: { - dir_path: { - description: 'The path to the directory to list', - type: 'string', - }, - ignore: { - description: 'List of glob patterns to ignore', - items: { - type: 'string', - }, - type: 'array', - }, - file_filtering_options: { - description: - 'Optional: Whether to respect ignore patterns from .gitignore or .geminiignore', - type: 'object', - properties: { - respect_git_ignore: { - description: - 'Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true.', - type: 'boolean', - }, - respect_gemini_ignore: { - description: - 'Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true.', - type: 'boolean', - }, - }, - }, - }, - required: ['dir_path'], - type: 'object', - }, + LS_DEFINITION.base.parametersJsonSchema, messageBus, true, false, @@ -351,4 +320,8 @@ export class LSTool extends BaseDeclarativeTool { _toolDisplayName, ); } + + override getSchema(modelId?: string) { + return resolveToolDeclaration(LS_DEFINITION, modelId); + } } diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 8aa823ecda0..62209c4d2e2 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -176,7 +176,7 @@ export class ReadFileTool extends BaseDeclarativeTool< 'ReadFile', READ_FILE_DEFINITION.base.description!, Kind.Read, - READ_FILE_DEFINITION.base.parameters!, + READ_FILE_DEFINITION.base.parametersJsonSchema, messageBus, true, false, diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index 5b8f89d4f59..70e882ebe19 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -4,21 +4,35 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + GLOB_TOOL_NAME, + GREP_TOOL_NAME, + LS_TOOL_NAME, + READ_FILE_TOOL_NAME, + SHELL_TOOL_NAME, + WRITE_FILE_TOOL_NAME, +} from './definitions/coreTools.js'; + // Centralized constants for tool names. // This prevents circular dependencies that can occur when other modules (like agents) // need to reference a tool's name without importing the tool's implementation. -export const GLOB_TOOL_NAME = 'glob'; +export { + GLOB_TOOL_NAME, + GREP_TOOL_NAME, + LS_TOOL_NAME, + READ_FILE_TOOL_NAME, + SHELL_TOOL_NAME, + WRITE_FILE_TOOL_NAME, +}; + export const WRITE_TODOS_TOOL_NAME = 'write_todos'; -export const WRITE_FILE_TOOL_NAME = 'write_file'; export const WEB_SEARCH_TOOL_NAME = 'google_web_search'; export const WEB_FETCH_TOOL_NAME = 'web_fetch'; export const EDIT_TOOL_NAME = 'replace'; -export const SHELL_TOOL_NAME = 'run_shell_command'; -export const GREP_TOOL_NAME = 'grep_search'; export const READ_MANY_FILES_TOOL_NAME = 'read_many_files'; -export const READ_FILE_TOOL_NAME = 'read_file'; -export const LS_TOOL_NAME = 'list_directory'; +export const LS_TOOL_NAME_LEGACY = 'list_directory'; // Just to be safe if anything used the old exported name directly + export const MEMORY_TOOL_NAME = 'save_memory'; export const GET_INTERNAL_DOCS_TOOL_NAME = 'get_internal_docs'; export const ACTIVATE_SKILL_TOOL_NAME = 'activate_skill'; diff --git a/packages/core/src/tools/write-file.ts b/packages/core/src/tools/write-file.ts index 8dfc4d7855a..467bee663e9 100644 --- a/packages/core/src/tools/write-file.ts +++ b/packages/core/src/tools/write-file.ts @@ -48,6 +48,8 @@ import { getSpecificMimeType } from '../utils/fileUtils.js'; import { getLanguageFromFilePath } from '../utils/language-detection.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { WRITE_FILE_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; /** * Parameters for the WriteFile tool @@ -445,24 +447,9 @@ export class WriteFileTool super( WriteFileTool.Name, 'WriteFile', - `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.`, + WRITE_FILE_DEFINITION.base.description!, Kind.Edit, - { - properties: { - file_path: { - description: 'The path to the file to write to.', - type: 'string', - }, - content: { - description: 'The content to write to the file.', - type: 'string', - }, - }, - required: ['file_path', 'content'], - type: 'object', - }, + WRITE_FILE_DEFINITION.base.parametersJsonSchema, messageBus, true, false, @@ -514,6 +501,10 @@ export class WriteFileTool ); } + override getSchema(modelId?: string) { + return resolveToolDeclaration(WRITE_FILE_DEFINITION, modelId); + } + getModifyContext( abortSignal: AbortSignal, ): ModifyContext {