From c283ff070ead7c6a42783dd6e9f94ad5a404e4d6 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Wed, 4 Feb 2026 20:13:57 +0000 Subject: [PATCH 01/13] feat: implement model-dependent tool definitions architecture and refactor read_file and shell tools --- package-lock.json | 25 ++++- packages/core/src/core/client.ts | 8 +- .../core/src/tools/definitions/coreTools.ts | 91 +++++++++++++++++++ .../core/src/tools/definitions/resolver.ts | 59 ++++++++++++ packages/core/src/tools/definitions/types.ts | 25 +++++ packages/core/src/tools/read-file.ts | 9 ++ packages/core/src/tools/shell.ts | 17 ++++ packages/core/src/tools/tool-registry.ts | 13 ++- packages/core/src/tools/tools.ts | 7 +- 9 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/tools/definitions/coreTools.ts create mode 100644 packages/core/src/tools/definitions/resolver.ts create mode 100644 packages/core/src/tools/definitions/types.ts diff --git a/package-lock.json b/package-lock.json index 60e16019539..7aa5ac02371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2251,6 +2251,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2431,6 +2432,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2464,6 +2466,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2832,6 +2835,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2865,6 +2869,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -2917,6 +2922,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -4122,6 +4128,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4399,6 +4406,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5391,6 +5399,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8400,6 +8409,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8940,6 +8950,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -10541,6 +10552,7 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz", "integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -14299,6 +14311,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14309,6 +14322,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16545,6 +16559,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16768,7 +16783,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -16776,6 +16792,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16948,6 +16965,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17155,6 +17173,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -17268,6 +17287,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17280,6 +17300,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -17984,6 +18005,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18278,6 +18300,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index d6c3bb8520c..6af2383fb91 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -253,9 +253,9 @@ export class GeminiClient { this.forceFullIdeContext = true; } - async setTools(): Promise { + async setTools(modelId?: string): Promise { const toolRegistry = this.config.getToolRegistry(); - const toolDeclarations = toolRegistry.getFunctionDeclarations(); + const toolDeclarations = toolRegistry.getFunctionDeclarations(modelId); const tools: Tool[] = [{ functionDeclarations: toolDeclarations }]; this.getChat().setTools(tools); } @@ -648,6 +648,10 @@ export class GeminiClient { yield { type: GeminiEventType.ModelInfo, value: modelToUse }; } this.currentSequenceModel = modelToUse; + + // Update tools with the final modelId to ensure model-dependent descriptions are used. + await this.setTools(modelToUse); + const resultStream = turn.run( modelConfigKey, request, diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts new file mode 100644 index 00000000000..828b0c90d89 --- /dev/null +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { ToolDefinition } from './types.js'; +import { READ_FILE_TOOL_NAME, SHELL_TOOL_NAME } from '../tool-names.js'; + +export const READ_FILE_DEFINITION: ToolDefinition = { + base: { + 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.`, + parameters: { + type: 'object', + properties: { + file_path: { + description: 'The path to the file to read.', + 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: '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: 'number', + }, + }, + required: ['file_path'], + }, + }, + variants: { + flash: { + description: + 'Reads a file from the local filesystem. Fast and efficient for checking file content.', + }, + pro: { + description: + 'Reads and returns the content of a specified file. Use this for comprehensive analysis of source code, configuration, or documentation.', + }, + }, +}; + +/** + * Note: Shell tool has platform-specific and dynamic parts. + * The base here contains the core schema. + */ +export const SHELL_DEFINITION: ToolDefinition = { + base: { + name: SHELL_TOOL_NAME, + description: 'Executes a shell command.', + parameters: { + type: 'object', + properties: { + command: { + type: 'string', + description: 'The command to execute.', + }, + description: { + 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: '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: '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.', + }, + }, + required: ['command'], + }, + }, + variants: { + flash: { + description: + 'Executes a single shell command. Use for simple operations like listing files or moving data.', + }, + pro: { + description: + 'Executes a shell command. Can be used for complex workflows, multi-step installations, or deep system investigations.', + }, + }, +}; diff --git a/packages/core/src/tools/definitions/resolver.ts b/packages/core/src/tools/definitions/resolver.ts new file mode 100644 index 00000000000..2883b66c585 --- /dev/null +++ b/packages/core/src/tools/definitions/resolver.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type FunctionDeclaration } from '@google/genai'; +import type { ToolDefinition } from './types.js'; + +/** + * Resolves a model-specific declaration for a tool. + * + * @param definition The tool definition containing base and variants. + * @param modelId The concrete model ID (e.g., 'gemini-1.5-flash'). + * @returns The final FunctionDeclaration to be sent to the API. + */ +export function resolveToolDeclaration( + definition: ToolDefinition, + modelId: string, +): FunctionDeclaration { + const { base, variants } = definition; + + if (!variants) { + return base; + } + + // Simplified mapping logic: check if the modelId contains 'flash' or 'pro'. + // This can be made more robust as needed. + let variantKey: 'flash' | 'pro' | undefined; + if (modelId.toLowerCase().includes('flash')) { + variantKey = 'flash'; + } else if (modelId.toLowerCase().includes('pro')) { + variantKey = 'pro'; + } + + const variant = variantKey ? variants[variantKey] : undefined; + + if (!variant) { + return base; + } + + // Deep merge strategy for the declaration. + return { + ...base, + ...variant, + parameters: + variant.parameters && base.parameters + ? { + ...base.parameters, + ...variant.parameters, + properties: { + ...(base.parameters.properties || {}), + ...(variant.parameters.properties || {}), + }, + required: variant.parameters.required || base.parameters.required, + } + : (variant.parameters ?? base.parameters), + }; +} diff --git a/packages/core/src/tools/definitions/types.ts b/packages/core/src/tools/definitions/types.ts new file mode 100644 index 00000000000..428d224ec97 --- /dev/null +++ b/packages/core/src/tools/definitions/types.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type FunctionDeclaration } from '@google/genai'; + +/** + * Defines a tool's identity with potential model-specific flavor variants. + */ +export interface ToolDefinition { + /** The base declaration used by default. */ + base: FunctionDeclaration; + + /** + * Model-specific overrides for the tool declaration. + * Can override description, parameters, or any other field. + */ + variants?: { + flash?: Partial; + pro?: Partial; + [modelKey: string]: Partial | undefined; + }; +} diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 2fa57721879..ed9fc93f18b 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -23,6 +23,8 @@ import { logFileOperation } from '../telemetry/loggers.js'; import { FileOperationEvent } from '../telemetry/types.js'; import { READ_FILE_TOOL_NAME } from './tool-names.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; +import { READ_FILE_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; /** * Parameters for the ReadFile tool @@ -252,4 +254,11 @@ export class ReadFileTool extends BaseDeclarativeTool< _toolDisplayName, ); } + + override getSchema(modelId?: string) { + if (!modelId) { + return super.getSchema(); + } + return resolveToolDeclaration(READ_FILE_DEFINITION, modelId); + } } diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index e29419913ef..994c4a9a905 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -43,6 +43,8 @@ import { } from '../utils/shell-utils.js'; import { SHELL_TOOL_NAME } from './tool-names.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; +import { SHELL_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; export const OUTPUT_UPDATE_INTERVAL_MS = 1000; @@ -564,4 +566,19 @@ export class ShellTool extends BaseDeclarativeTool< _toolDisplayName, ); } + + override getSchema(modelId?: string) { + const declaration = modelId + ? resolveToolDeclaration(SHELL_DEFINITION, modelId) + : super.getSchema(); + + // Append platform-specific info which is currently not in the static definition + const platformInfo = getShellToolDescription( + this.config.getEnableInteractiveShell(), + ); + return { + ...declaration, + description: `${declaration.description}\n\n${platformInfo}`, + }; + } } diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 9da0932cdea..b039daef6a4 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -470,12 +470,13 @@ export class ToolRegistry { * Retrieves the list of tool schemas (FunctionDeclaration array). * Extracts the declarations from the ToolListUnion structure. * Includes discovered (vs registered) tools if configured. + * @param modelId Optional model identifier to get model-specific schemas. * @returns An array of FunctionDeclarations. */ - getFunctionDeclarations(): FunctionDeclaration[] { + getFunctionDeclarations(modelId?: string): FunctionDeclaration[] { const declarations: FunctionDeclaration[] = []; this.getActiveTools().forEach((tool) => { - declarations.push(tool.schema); + declarations.push(tool.getSchema(modelId)); }); return declarations; } @@ -483,14 +484,18 @@ export class ToolRegistry { /** * Retrieves a filtered list of tool schemas based on a list of tool names. * @param toolNames - An array of tool names to include. + * @param modelId Optional model identifier to get model-specific schemas. * @returns An array of FunctionDeclarations for the specified tools. */ - getFunctionDeclarationsFiltered(toolNames: string[]): FunctionDeclaration[] { + getFunctionDeclarationsFiltered( + toolNames: string[], + modelId?: string, + ): FunctionDeclaration[] { const declarations: FunctionDeclaration[] = []; for (const name of toolNames) { const tool = this.getTool(name); if (tool) { - declarations.push(tool.schema); + declarations.push(tool.getSchema(modelId)); } } return declarations; diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index 65aeb0884fc..ec4e308c9a8 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -312,8 +312,9 @@ export interface ToolBuilder< /** * Function declaration schema from @google/genai. + * @param modelId Optional model identifier to get a model-specific schema. */ - schema: FunctionDeclaration; + getSchema(modelId?: string): FunctionDeclaration; /** * Whether the tool's output should be rendered as markdown. @@ -355,7 +356,7 @@ export abstract class DeclarativeTool< readonly extensionId?: string, ) {} - get schema(): FunctionDeclaration { + getSchema(_modelId?: string): FunctionDeclaration { return { name: this.name, description: this.description, @@ -486,7 +487,7 @@ export abstract class BaseDeclarativeTool< override validateToolParams(params: TParams): string | null { const errors = SchemaValidator.validate( - this.schema.parametersJsonSchema, + this.getSchema().parametersJsonSchema, params, ); From 3dd3f58d79eed8d8571427d449878e3f832419d7 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Wed, 4 Feb 2026 20:48:46 +0000 Subject: [PATCH 02/13] refactor: extract tool definitions and prepare architecture for model-dependent descriptions --- .../core/src/tools/definitions/coreTools.ts | 24 --------- .../core/src/tools/definitions/resolver.ts | 49 +++---------------- packages/core/src/tools/definitions/types.ts | 14 +----- 3 files changed, 8 insertions(+), 79 deletions(-) diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index 828b0c90d89..68445efcb64 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -32,22 +32,8 @@ export const READ_FILE_DEFINITION: ToolDefinition = { required: ['file_path'], }, }, - variants: { - flash: { - description: - 'Reads a file from the local filesystem. Fast and efficient for checking file content.', - }, - pro: { - description: - 'Reads and returns the content of a specified file. Use this for comprehensive analysis of source code, configuration, or documentation.', - }, - }, }; -/** - * Note: Shell tool has platform-specific and dynamic parts. - * The base here contains the core schema. - */ export const SHELL_DEFINITION: ToolDefinition = { base: { name: SHELL_TOOL_NAME, @@ -78,14 +64,4 @@ export const SHELL_DEFINITION: ToolDefinition = { required: ['command'], }, }, - variants: { - flash: { - description: - 'Executes a single shell command. Use for simple operations like listing files or moving data.', - }, - pro: { - description: - 'Executes a shell command. Can be used for complex workflows, multi-step installations, or deep system investigations.', - }, - }, }; diff --git a/packages/core/src/tools/definitions/resolver.ts b/packages/core/src/tools/definitions/resolver.ts index 2883b66c585..8176e481044 100644 --- a/packages/core/src/tools/definitions/resolver.ts +++ b/packages/core/src/tools/definitions/resolver.ts @@ -8,52 +8,15 @@ import { type FunctionDeclaration } from '@google/genai'; import type { ToolDefinition } from './types.js'; /** - * Resolves a model-specific declaration for a tool. + * Resolves the declaration for a tool. * - * @param definition The tool definition containing base and variants. - * @param modelId The concrete model ID (e.g., 'gemini-1.5-flash'). - * @returns The final FunctionDeclaration to be sent to the API. + * @param definition The tool definition containing the base declaration. + * @param _modelId Optional model identifier (ignored in this plain refactor). + * @returns The FunctionDeclaration to be sent to the API. */ export function resolveToolDeclaration( definition: ToolDefinition, - modelId: string, + _modelId?: string, ): FunctionDeclaration { - const { base, variants } = definition; - - if (!variants) { - return base; - } - - // Simplified mapping logic: check if the modelId contains 'flash' or 'pro'. - // This can be made more robust as needed. - let variantKey: 'flash' | 'pro' | undefined; - if (modelId.toLowerCase().includes('flash')) { - variantKey = 'flash'; - } else if (modelId.toLowerCase().includes('pro')) { - variantKey = 'pro'; - } - - const variant = variantKey ? variants[variantKey] : undefined; - - if (!variant) { - return base; - } - - // Deep merge strategy for the declaration. - return { - ...base, - ...variant, - parameters: - variant.parameters && base.parameters - ? { - ...base.parameters, - ...variant.parameters, - properties: { - ...(base.parameters.properties || {}), - ...(variant.parameters.properties || {}), - }, - required: variant.parameters.required || base.parameters.required, - } - : (variant.parameters ?? base.parameters), - }; + return definition.base; } diff --git a/packages/core/src/tools/definitions/types.ts b/packages/core/src/tools/definitions/types.ts index 428d224ec97..dc928e0a668 100644 --- a/packages/core/src/tools/definitions/types.ts +++ b/packages/core/src/tools/definitions/types.ts @@ -7,19 +7,9 @@ import { type FunctionDeclaration } from '@google/genai'; /** - * Defines a tool's identity with potential model-specific flavor variants. + * Defines a tool's identity using a structured declaration. */ export interface ToolDefinition { - /** The base declaration used by default. */ + /** The base declaration for the tool. */ base: FunctionDeclaration; - - /** - * Model-specific overrides for the tool declaration. - * Can override description, parameters, or any other field. - */ - variants?: { - flash?: Partial; - pro?: Partial; - [modelKey: string]: Partial | undefined; - }; } From 950b13b755c2a6436bdf10b2c83055c9c085675e Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Wed, 4 Feb 2026 22:00:07 +0000 Subject: [PATCH 03/13] refactor: remove hardcoded schema details from tool files and use external definitions --- .../core/src/tools/definitions/coreTools.ts | 3 +- packages/core/src/tools/read-file.ts | 23 ++----------- packages/core/src/tools/shell.ts | 32 +++---------------- 3 files changed, 9 insertions(+), 49 deletions(-) diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index 68445efcb64..8470db41dc5 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -6,6 +6,7 @@ import type { ToolDefinition } from './types.js'; import { READ_FILE_TOOL_NAME, SHELL_TOOL_NAME } from '../tool-names.js'; +import { getCommandDescription } from '../shell.js'; export const READ_FILE_DEFINITION: ToolDefinition = { base: { @@ -43,7 +44,7 @@ export const SHELL_DEFINITION: ToolDefinition = { properties: { command: { type: 'string', - description: 'The command to execute.', + description: getCommandDescription(), }, description: { type: 'string', diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index ed9fc93f18b..27b13f1f9ab 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -171,28 +171,9 @@ export class ReadFileTool extends BaseDeclarativeTool< super( ReadFileTool.Name, 'ReadFile', - `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.`, + READ_FILE_DEFINITION.base.description!, Kind.Read, - { - properties: { - file_path: { - description: 'The path to the file to read.', - 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: '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: 'number', - }, - }, - required: ['file_path'], - type: 'object', - }, + READ_FILE_DEFINITION.base.parameters!, messageBus, true, false, diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 994c4a9a905..0d4fdcb3685 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -453,7 +453,9 @@ export class ShellToolInvocation extends BaseToolInvocation< } } -function getShellToolDescription(enableInteractiveShell: boolean): string { +export function getShellToolDescription( + enableInteractiveShell: boolean, +): string { const returnedInfo = ` The following information is returned: @@ -478,7 +480,7 @@ function getShellToolDescription(enableInteractiveShell: boolean): string { } } -function getCommandDescription(): string { +export function getCommandDescription(): string { if (os.platform() === 'win32') { return 'Exact command to execute as `powershell.exe -NoProfile -Command `'; } else { @@ -504,31 +506,7 @@ export class ShellTool extends BaseDeclarativeTool< 'Shell', getShellToolDescription(config.getEnableInteractiveShell()), Kind.Execute, - { - type: 'object', - properties: { - command: { - type: 'string', - description: getCommandDescription(), - }, - description: { - 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: '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: '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.', - }, - }, - required: ['command'], - }, + SHELL_DEFINITION.base.parameters!, messageBus, false, // output is not markdown true, // output can be updated From 9c27dfc276aea23ecd28c4b6b8776f2c64cc33d7 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Fri, 6 Feb 2026 19:05:30 +0000 Subject: [PATCH 04/13] test: add tests for model-dependent tool definitions --- .../__snapshots__/read-file.test.ts.snap | 5 +++ .../tools/__snapshots__/shell.test.ts.snap | 26 ++++++++++++ .../core/src/tools/definitions/coreTools.ts | 19 ++++----- .../src/tools/definitions/resolver.test.ts | 40 +++++++++++++++++++ packages/core/src/tools/read-file.test.ts | 15 +++++++ packages/core/src/tools/read-file.ts | 10 ++--- packages/core/src/tools/shell.test.ts | 15 +++++++ packages/core/src/tools/shell.ts | 18 ++------- packages/core/src/tools/tool-registry.test.ts | 11 +++++ packages/core/src/tools/tools.ts | 12 +++++- 10 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 packages/core/src/tools/__snapshots__/read-file.test.ts.snap create mode 100644 packages/core/src/tools/definitions/resolver.test.ts diff --git a/packages/core/src/tools/__snapshots__/read-file.test.ts.snap b/packages/core/src/tools/__snapshots__/read-file.test.ts.snap new file mode 100644 index 00000000000..c6adf2819d8 --- /dev/null +++ b/packages/core/src/tools/__snapshots__/read-file.test.ts.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ReadFileTool > getSchema > should return the base schema when no modelId is provided 1`] = `"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."`; + +exports[`ReadFileTool > getSchema > should return the schema from the resolver when modelId is provided 1`] = `"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."`; diff --git a/packages/core/src/tools/__snapshots__/shell.test.ts.snap b/packages/core/src/tools/__snapshots__/shell.test.ts.snap index 6592993160b..3cfef4d8ca1 100644 --- a/packages/core/src/tools/__snapshots__/shell.test.ts.snap +++ b/packages/core/src/tools/__snapshots__/shell.test.ts.snap @@ -25,3 +25,29 @@ exports[`ShellTool > getDescription > should return the windows description when Background PIDs: Only included if background processes were started. Process Group PGID: Only included if available." `; + +exports[`ShellTool > getSchema > should return the base schema when no modelId is provided 1`] = ` +"This tool executes a given shell command as \`bash -c \`. Command can start background processes using \`&\`. Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`. + + The following information is returned: + + Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes. + Exit Code: Only included if non-zero (command failed). + Error: Only included if a process-level error occurred (e.g., spawn failure). + Signal: Only included if process was terminated by a signal. + Background PIDs: Only included if background processes were started. + Process Group PGID: Only included if available." +`; + +exports[`ShellTool > getSchema > should return the schema from the resolver when modelId is provided 1`] = ` +"This tool executes a given shell command as \`bash -c \`. Command can start background processes using \`&\`. Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`. + + The following information is returned: + + Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes. + Exit Code: Only included if non-zero (command failed). + Error: Only included if a process-level error occurred (e.g., spawn failure). + Signal: Only included if process was terminated by a signal. + Background PIDs: Only included if background processes were started. + Process Group PGID: Only included if available." +`; diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index 8470db41dc5..9678ab81d30 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Type } from '@google/genai'; import type { ToolDefinition } from './types.js'; import { READ_FILE_TOOL_NAME, SHELL_TOOL_NAME } from '../tool-names.js'; import { getCommandDescription } from '../shell.js'; @@ -13,21 +14,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.`, parameters: { - type: 'object', + type: Type.OBJECT, properties: { file_path: { description: 'The path to the file to read.', - type: 'string', + type: 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: 'number', + type: 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: 'number', + type: Type.NUMBER, }, }, required: ['file_path'], @@ -40,24 +41,24 @@ export const SHELL_DEFINITION: ToolDefinition = { name: SHELL_TOOL_NAME, description: 'Executes a shell command.', parameters: { - type: 'object', + type: Type.OBJECT, properties: { command: { - type: 'string', + type: Type.STRING, description: getCommandDescription(), }, description: { - type: 'string', + type: 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: 'string', + type: 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: 'boolean', + type: 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 new file mode 100644 index 00000000000..a765608ac7d --- /dev/null +++ b/packages/core/src/tools/definitions/resolver.test.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { Type } from '@google/genai'; +import { resolveToolDeclaration } from './resolver.js'; +import type { ToolDefinition } from './types.js'; + +describe('resolveToolDeclaration', () => { + const mockDefinition: ToolDefinition = { + base: { + name: 'test_tool', + description: 'A test tool description', + parameters: { + type: Type.OBJECT, + properties: { + param1: { type: Type.STRING }, + }, + }, + }, + }; + + it('should return the base definition when no modelId is provided', () => { + const result = resolveToolDeclaration(mockDefinition); + 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 the same object reference as base (current implementation)', () => { + const result = resolveToolDeclaration(mockDefinition); + expect(result).toBe(mockDefinition.base); + }); +}); diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index 15071f26201..494b007dec0 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -563,4 +563,19 @@ describe('ReadFileTool', () => { }); }); }); + + describe('getSchema', () => { + it('should return the base schema when no modelId is provided', () => { + const schema = tool.getSchema(); + expect(schema.name).toBe(ReadFileTool.Name); + expect(schema.description).toMatchSnapshot(); + }); + + it('should return the schema from the resolver when modelId is provided', () => { + const modelId = 'gemini-2.0-flash'; + const schema = tool.getSchema(modelId); + expect(schema.name).toBe(ReadFileTool.Name); + expect(schema.description).toMatchSnapshot(); + }); + }); }); diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 27b13f1f9ab..dbed3b72201 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -24,7 +24,6 @@ import { FileOperationEvent } from '../telemetry/types.js'; import { READ_FILE_TOOL_NAME } from './tool-names.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { READ_FILE_DEFINITION } from './definitions/coreTools.js'; -import { resolveToolDeclaration } from './definitions/resolver.js'; /** * Parameters for the ReadFile tool @@ -236,10 +235,9 @@ export class ReadFileTool extends BaseDeclarativeTool< ); } - override getSchema(modelId?: string) { - if (!modelId) { - return super.getSchema(); - } - return resolveToolDeclaration(READ_FILE_DEFINITION, modelId); + override getSchema(_modelId?: string) { + // Pure refactor: maintain existing behavior. + // getSchema(modelId) is now available for future model-specific overrides. + return super.getSchema(); } } diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index b851ee99d4e..01c4f4a8f12 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -815,4 +815,19 @@ describe('ShellTool', () => { } }); }); + + describe('getSchema', () => { + it('should return the base schema when no modelId is provided', () => { + const schema = shellTool.getSchema(); + expect(schema.name).toBe(SHELL_TOOL_NAME); + expect(schema.description).toMatchSnapshot(); + }); + + it('should return the schema from the resolver when modelId is provided', () => { + const modelId = 'gemini-2.0-flash'; + const schema = shellTool.getSchema(modelId); + expect(schema.name).toBe(SHELL_TOOL_NAME); + expect(schema.description).toMatchSnapshot(); + }); + }); }); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 0d4fdcb3685..acda68b7c07 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -44,7 +44,6 @@ import { import { SHELL_TOOL_NAME } from './tool-names.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { SHELL_DEFINITION } from './definitions/coreTools.js'; -import { resolveToolDeclaration } from './definitions/resolver.js'; export const OUTPUT_UPDATE_INTERVAL_MS = 1000; @@ -545,18 +544,9 @@ export class ShellTool extends BaseDeclarativeTool< ); } - override getSchema(modelId?: string) { - const declaration = modelId - ? resolveToolDeclaration(SHELL_DEFINITION, modelId) - : super.getSchema(); - - // Append platform-specific info which is currently not in the static definition - const platformInfo = getShellToolDescription( - this.config.getEnableInteractiveShell(), - ); - return { - ...declaration, - description: `${declaration.description}\n\n${platformInfo}`, - }; + override getSchema(_modelId?: string) { + // Pure refactor: maintain existing behavior. + // getSchema(modelId) is now available for future model-specific overrides. + return super.getSchema(); } } diff --git a/packages/core/src/tools/tool-registry.test.ts b/packages/core/src/tools/tool-registry.test.ts index 73bb351f7a0..c55234a3eb2 100644 --- a/packages/core/src/tools/tool-registry.test.ts +++ b/packages/core/src/tools/tool-registry.test.ts @@ -248,6 +248,17 @@ describe('ToolRegistry', () => { toolRegistry.registerTool(tool); expect(toolRegistry.getTool('mock-tool')).toBe(tool); }); + + it('should pass modelId to getSchema when getting function declarations', () => { + const tool = new MockTool({ name: 'mock-tool' }); + const getSchemaSpy = vi.spyOn(tool, 'getSchema'); + toolRegistry.registerTool(tool); + + const modelId = 'test-model-id'; + toolRegistry.getFunctionDeclarations(modelId); + + expect(getSchemaSpy).toHaveBeenCalledWith(modelId); + }); }); describe('excluded tools', () => { diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index ec4e308c9a8..2811653b20d 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -316,6 +316,12 @@ export interface ToolBuilder< */ getSchema(modelId?: string): FunctionDeclaration; + /** + * Function declaration schema for the default model. + * @deprecated Use getSchema(modelId) for model-specific schemas. + */ + readonly schema: FunctionDeclaration; + /** * Whether the tool's output should be rendered as markdown. */ @@ -364,6 +370,10 @@ export abstract class DeclarativeTool< }; } + get schema(): FunctionDeclaration { + return this.getSchema(); + } + /** * Validates the raw tool parameters. * Subclasses should override this to add custom validation logic @@ -487,7 +497,7 @@ export abstract class BaseDeclarativeTool< override validateToolParams(params: TParams): string | null { const errors = SchemaValidator.validate( - this.getSchema().parametersJsonSchema, + this.schema.parametersJsonSchema, params, ); From e66862df74af228f6e9be7f73cffce6239a9393c Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Fri, 6 Feb 2026 19:13:34 +0000 Subject: [PATCH 05/13] refactor: integrate resolver into read_file and shell tools --- packages/core/src/tools/read-file.ts | 10 ++++++---- packages/core/src/tools/shell.ts | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index dbed3b72201..27b13f1f9ab 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -24,6 +24,7 @@ import { FileOperationEvent } from '../telemetry/types.js'; import { READ_FILE_TOOL_NAME } from './tool-names.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { READ_FILE_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; /** * Parameters for the ReadFile tool @@ -235,9 +236,10 @@ export class ReadFileTool extends BaseDeclarativeTool< ); } - override getSchema(_modelId?: string) { - // Pure refactor: maintain existing behavior. - // getSchema(modelId) is now available for future model-specific overrides. - return super.getSchema(); + override getSchema(modelId?: string) { + if (!modelId) { + return super.getSchema(); + } + return resolveToolDeclaration(READ_FILE_DEFINITION, modelId); } } diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index acda68b7c07..87c8912fde7 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -44,6 +44,7 @@ import { import { SHELL_TOOL_NAME } from './tool-names.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { SHELL_DEFINITION } from './definitions/coreTools.js'; +import { resolveToolDeclaration } from './definitions/resolver.js'; export const OUTPUT_UPDATE_INTERVAL_MS = 1000; @@ -544,9 +545,19 @@ export class ShellTool extends BaseDeclarativeTool< ); } - override getSchema(_modelId?: string) { - // Pure refactor: maintain existing behavior. - // getSchema(modelId) is now available for future model-specific overrides. - return super.getSchema(); + override getSchema(modelId?: string) { + if (!modelId) { + return super.getSchema(); + } + + const declaration = resolveToolDeclaration(SHELL_DEFINITION, modelId); + const schema = super.getSchema(); + + // We use the resolved declaration but preserve the dynamic platform-specific description + // from the tool instance to ensure behavior remains 100% identical for now. + return { + ...declaration, + description: schema.description, + }; } } From 6d59880c6ea9d966790f40fe18c2f8057094b797 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Fri, 6 Feb 2026 20:15:21 +0000 Subject: [PATCH 06/13] refactor: centralize all tool definitions in coreTools.ts --- .../core/src/tools/definitions/coreTools.ts | 96 +++++++++++++------ packages/core/src/tools/shell.ts | 55 ++--------- 2 files changed, 75 insertions(+), 76 deletions(-) diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index 9678ab81d30..4a585ebd1f8 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -7,7 +7,41 @@ import { Type } from '@google/genai'; import type { ToolDefinition } from './types.js'; import { READ_FILE_TOOL_NAME, SHELL_TOOL_NAME } from '../tool-names.js'; -import { getCommandDescription } from '../shell.js'; +import * as os from 'node:os'; + +export function getShellToolDescription( + enableInteractiveShell: boolean, +): string { + const returnedInfo = ` + + The following information is returned: + + Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes. + Exit Code: Only included if non-zero (command failed). + Error: Only included if a process-level error occurred (e.g., spawn failure). + Signal: Only included if process was terminated by a signal. + Background PIDs: Only included if background processes were started. + Process Group PGID: Only included if available.`; + + if (os.platform() === 'win32') { + const backgroundInstructions = enableInteractiveShell + ? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use PowerShell background constructs.' + : 'Command can start background processes using PowerShell constructs such as `Start-Process -NoNewWindow` or `Start-Job`.'; + return `This tool executes a given shell command as \`powershell.exe -NoProfile -Command \`. ${backgroundInstructions}${returnedInfo}`; + } else { + const backgroundInstructions = enableInteractiveShell + ? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use `&` to background commands.' + : 'Command can start background processes using `&`.'; + return `This tool executes a given shell command as \`bash -c \`. ${backgroundInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.${returnedInfo}`; + } +} + +export function getCommandDescription(): string { + if (os.platform() === 'win32') { + return 'Exact command to execute as `powershell.exe -NoProfile -Command `'; + } + return 'Exact bash command to execute as `bash -c `'; +} export const READ_FILE_DEFINITION: ToolDefinition = { base: { @@ -36,34 +70,38 @@ export const READ_FILE_DEFINITION: ToolDefinition = { }, }; -export const SHELL_DEFINITION: ToolDefinition = { - base: { - name: SHELL_TOOL_NAME, - description: 'Executes a shell command.', - parameters: { - type: Type.OBJECT, - properties: { - command: { - type: Type.STRING, - description: getCommandDescription(), - }, - description: { - type: 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, - 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, - 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.', +export function getShellDefinition( + enableInteractiveShell: boolean, +): ToolDefinition { + return { + base: { + name: SHELL_TOOL_NAME, + description: getShellToolDescription(enableInteractiveShell), + parameters: { + type: Type.OBJECT, + properties: { + command: { + type: Type.STRING, + description: getCommandDescription(), + }, + description: { + type: 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, + 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, + 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.', + }, }, + required: ['command'], }, - required: ['command'], }, - }, -}; + }; +} diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 87c8912fde7..38e58f85c94 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -43,7 +43,7 @@ import { } from '../utils/shell-utils.js'; import { SHELL_TOOL_NAME } from './tool-names.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; -import { SHELL_DEFINITION } from './definitions/coreTools.js'; +import { getShellDefinition } from './definitions/coreTools.js'; import { resolveToolDeclaration } from './definitions/resolver.js'; export const OUTPUT_UPDATE_INTERVAL_MS = 1000; @@ -453,41 +453,6 @@ export class ShellToolInvocation extends BaseToolInvocation< } } -export function getShellToolDescription( - enableInteractiveShell: boolean, -): string { - const returnedInfo = ` - - The following information is returned: - - Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes. - Exit Code: Only included if non-zero (command failed). - Error: Only included if a process-level error occurred (e.g., spawn failure). - Signal: Only included if process was terminated by a signal. - Background PIDs: Only included if background processes were started. - Process Group PGID: Only included if available.`; - - if (os.platform() === 'win32') { - const backgroundInstructions = enableInteractiveShell - ? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use PowerShell background constructs.' - : 'Command can start background processes using PowerShell constructs such as `Start-Process -NoNewWindow` or `Start-Job`.'; - return `This tool executes a given shell command as \`powershell.exe -NoProfile -Command \`. ${backgroundInstructions}${returnedInfo}`; - } else { - const backgroundInstructions = enableInteractiveShell - ? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use `&` to background commands.' - : 'Command can start background processes using `&`.'; - return `This tool executes a given shell command as \`bash -c \`. ${backgroundInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.${returnedInfo}`; - } -} - -export function getCommandDescription(): string { - if (os.platform() === 'win32') { - return 'Exact command to execute as `powershell.exe -NoProfile -Command `'; - } else { - return 'Exact bash command to execute as `bash -c `'; - } -} - export class ShellTool extends BaseDeclarativeTool< ShellToolParams, ToolResult @@ -501,12 +466,13 @@ export class ShellTool extends BaseDeclarativeTool< void initializeShellParsers().catch(() => { // Errors are surfaced when parsing commands. }); + const definition = getShellDefinition(config.getEnableInteractiveShell()); super( ShellTool.Name, 'Shell', - getShellToolDescription(config.getEnableInteractiveShell()), + definition.base.description!, Kind.Execute, - SHELL_DEFINITION.base.parameters!, + definition.base.parameters!, messageBus, false, // output is not markdown true, // output can be updated @@ -546,18 +512,13 @@ export class ShellTool extends BaseDeclarativeTool< } override getSchema(modelId?: string) { + const definition = getShellDefinition( + this.config.getEnableInteractiveShell(), + ); if (!modelId) { return super.getSchema(); } - const declaration = resolveToolDeclaration(SHELL_DEFINITION, modelId); - const schema = super.getSchema(); - - // We use the resolved declaration but preserve the dynamic platform-specific description - // from the tool instance to ensure behavior remains 100% identical for now. - return { - ...declaration, - description: schema.description, - }; + return resolveToolDeclaration(definition, modelId); } } From 0b8df5f715d46d8c428405bb9d1204b626cb6381 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Fri, 6 Feb 2026 20:20:35 +0000 Subject: [PATCH 07/13] style: reorganize coreTools.ts for better readability --- .../core/src/tools/definitions/coreTools.ts | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index 4a585ebd1f8..2156653d788 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -9,6 +9,44 @@ import type { ToolDefinition } from './types.js'; import { READ_FILE_TOOL_NAME, SHELL_TOOL_NAME } from '../tool-names.js'; import * as os from 'node:os'; +// ============================================================================ +// READ_FILE TOOL +// ============================================================================ + +export const READ_FILE_DEFINITION: ToolDefinition = { + base: { + 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.`, + parameters: { + type: Type.OBJECT, + properties: { + file_path: { + description: 'The path to the file to read.', + type: 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, + }, + 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, + }, + }, + required: ['file_path'], + }, + }, +}; + +// ============================================================================ +// SHELL TOOL +// ============================================================================ + +/** + * Generates the platform-specific description for the shell tool. + */ export function getShellToolDescription( enableInteractiveShell: boolean, ): string { @@ -36,6 +74,9 @@ export function getShellToolDescription( } } +/** + * Returns the platform-specific description for the 'command' parameter. + */ export function getCommandDescription(): string { if (os.platform() === 'win32') { return 'Exact command to execute as `powershell.exe -NoProfile -Command `'; @@ -43,33 +84,9 @@ export function getCommandDescription(): string { return 'Exact bash command to execute as `bash -c `'; } -export const READ_FILE_DEFINITION: ToolDefinition = { - base: { - 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.`, - parameters: { - type: Type.OBJECT, - properties: { - file_path: { - description: 'The path to the file to read.', - type: 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, - }, - 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, - }, - }, - required: ['file_path'], - }, - }, -}; - +/** + * Returns the tool definition for the shell tool, customized for the platform. + */ export function getShellDefinition( enableInteractiveShell: boolean, ): ToolDefinition { From 90becb3b0b8a45b9b1dfa0bbdbe52f55fcfd46f3 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Fri, 6 Feb 2026 20:28:45 +0000 Subject: [PATCH 08/13] feat: implement late-binding tool updates in GeminiChat --- packages/core/src/core/client.ts | 7 +++++++ packages/core/src/core/geminiChat.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index c793ed0bd70..fdfa1defcef 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -256,7 +256,14 @@ export class GeminiClient { this.forceFullIdeContext = true; } + private lastUsedModelId?: string; + async setTools(modelId?: string): Promise { + if (modelId && modelId === this.lastUsedModelId) { + return; + } + this.lastUsedModelId = modelId; + const toolRegistry = this.config.getToolRegistry(); const toolDeclarations = toolRegistry.getFunctionDeclarations(modelId); const tools: Tool[] = [{ functionDeclarations: toolDeclarations }]; diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index c45642c7be5..d92188d0a81 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -240,6 +240,7 @@ export class GeminiChat { private sendPromise: Promise = Promise.resolve(); private readonly chatRecordingService: ChatRecordingService; private lastPromptTokenCount: number; + private lastUsedModel?: string; constructor( private readonly config: Config, @@ -581,6 +582,10 @@ export class GeminiChat { } // Track final request parameters for AfterModel hooks + if (modelToUse !== this.lastUsedModel) { + await this.config.getGeminiClient().setTools(modelToUse); + this.lastUsedModel = modelToUse; + } lastModelToUse = modelToUse; lastConfig = config; lastContentsToUse = contentsToUse; From 0b01c049c9572ede4edd15d73218a8c6a7157367 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Sun, 8 Feb 2026 04:01:07 +0000 Subject: [PATCH 09/13] refactor: use parametersJsonSchema consistently across tool definitions --- packages/core/src/core/geminiChat.ts | 6 +----- packages/core/src/tools/definitions/coreTools.ts | 4 ++-- packages/core/src/tools/read-file.ts | 3 --- packages/core/src/tools/shell.ts | 4 ---- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index de9dd6c6b06..35a8e79855d 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -240,7 +240,6 @@ export class GeminiChat { private sendPromise: Promise = Promise.resolve(); private readonly chatRecordingService: ChatRecordingService; private lastPromptTokenCount: number; - private lastUsedModel?: string; constructor( private readonly config: Config, @@ -582,10 +581,7 @@ export class GeminiChat { } // Track final request parameters for AfterModel hooks - if (modelToUse !== this.lastUsedModel) { - await this.config.getGeminiClient().setTools(modelToUse); - this.lastUsedModel = modelToUse; - } + await this.config.getGeminiClient().setTools(modelToUse); lastModelToUse = modelToUse; lastConfig = config; lastContentsToUse = contentsToUse; diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index 2156653d788..e215c28d417 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -17,7 +17,7 @@ export const READ_FILE_DEFINITION: ToolDefinition = { base: { 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.`, - parameters: { + parametersJsonSchema: { type: Type.OBJECT, properties: { file_path: { @@ -94,7 +94,7 @@ export function getShellDefinition( base: { name: SHELL_TOOL_NAME, description: getShellToolDescription(enableInteractiveShell), - parameters: { + parametersJsonSchema: { type: Type.OBJECT, properties: { command: { diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 27b13f1f9ab..b0ffe32922e 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -237,9 +237,6 @@ export class ReadFileTool extends BaseDeclarativeTool< } override getSchema(modelId?: string) { - if (!modelId) { - return super.getSchema(); - } return resolveToolDeclaration(READ_FILE_DEFINITION, modelId); } } diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 38e58f85c94..5baa593805a 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -515,10 +515,6 @@ export class ShellTool extends BaseDeclarativeTool< const definition = getShellDefinition( this.config.getEnableInteractiveShell(), ); - if (!modelId) { - return super.getSchema(); - } - return resolveToolDeclaration(definition, modelId); } } From 1e3e0e0ee7c47e4471fd3c93259568903fc66dcd Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Sun, 8 Feb 2026 04:38:20 +0000 Subject: [PATCH 10/13] test: fix GeminiChat mocks in client.test.ts --- packages/core/src/core/client.test.ts | 26 ++++++++++++++++++++++++++ packages/core/src/core/client.ts | 11 +++++++++++ packages/core/src/core/geminiChat.ts | 5 ++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index ac8d9f1bd66..b7e85962a53 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -291,6 +291,7 @@ describe('Gemini Client (client.ts)', () => { it('should call chat.addHistory with the provided content', async () => { const mockChat = { addHistory: vi.fn(), + setTools: vi.fn(), } as unknown as GeminiChat; client['chat'] = mockChat; @@ -389,6 +390,7 @@ describe('Gemini Client (client.ts)', () => { getHistory: mockGetHistory, addHistory: vi.fn(), setHistory: vi.fn(), + setTools: vi.fn(), getLastPromptTokenCount: vi.fn(), } as unknown as GeminiChat; }); @@ -805,6 +807,7 @@ describe('Gemini Client (client.ts)', () => { const mockChat = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), } as unknown as GeminiChat; @@ -868,6 +871,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -926,6 +930,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1003,6 +1008,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1119,6 +1125,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1167,6 +1174,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1232,6 +1240,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1289,6 +1298,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1349,6 +1359,7 @@ ${JSON.stringify( const lastPromptTokenCount = 900; const mockChat: Partial = { getLastPromptTokenCount: vi.fn().mockReturnValue(lastPromptTokenCount), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), }; client['chat'] = mockChat as GeminiChat; @@ -1409,6 +1420,7 @@ ${JSON.stringify( const lastPromptTokenCount = 900; const mockChat: Partial = { getLastPromptTokenCount: vi.fn().mockReturnValue(lastPromptTokenCount), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), }; client['chat'] = mockChat as GeminiChat; @@ -1467,6 +1479,7 @@ ${JSON.stringify( .fn() .mockReturnValue([{ role: 'user', parts: [{ text: 'old' }] }]), addHistory: vi.fn(), + setTools: vi.fn(), getChatRecordingService: vi.fn().mockReturnValue({ getConversation: vi.fn(), getConversationFilePath: vi.fn(), @@ -1479,6 +1492,7 @@ ${JSON.stringify( .fn() .mockReturnValue([{ role: 'user', parts: [{ text: 'old' }] }]), addHistory: vi.fn(), + setTools: vi.fn(), getChatRecordingService: vi.fn().mockReturnValue({ getConversation: vi.fn(), getConversationFilePath: vi.fn(), @@ -1616,6 +1630,7 @@ ${JSON.stringify( const lastPromptTokenCount = 10000; const mockChat: Partial = { getLastPromptTokenCount: vi.fn().mockReturnValue(lastPromptTokenCount), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), }; client['chat'] = mockChat as GeminiChat; @@ -1689,6 +1704,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1892,6 +1908,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1947,6 +1964,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -1984,6 +2002,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -2028,6 +2047,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), setHistory: vi.fn(), + setTools: vi.fn(), // Assume history is not empty for delta checks getHistory: vi .fn() @@ -2443,6 +2463,7 @@ ${JSON.stringify( addHistory: vi.fn(), getHistory: vi.fn().mockReturnValue([]), // Default empty history setHistory: vi.fn(), + setTools: vi.fn(), getLastPromptTokenCount: vi.fn(), }; client['chat'] = mockChat as GeminiChat; @@ -2783,6 +2804,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -2820,6 +2842,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -2857,6 +2880,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -3069,6 +3093,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; @@ -3103,6 +3128,7 @@ ${JSON.stringify( const mockChat: Partial = { addHistory: vi.fn(), + setTools: vi.fn(), getHistory: vi.fn().mockReturnValue([]), getLastPromptTokenCount: vi.fn(), }; diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index fdfa1defcef..d6afeac4be0 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -259,6 +259,10 @@ export class GeminiClient { private lastUsedModelId?: string; async setTools(modelId?: string): Promise { + if (!this.chat) { + return; + } + if (modelId && modelId === this.lastUsedModelId) { return; } @@ -346,6 +350,13 @@ export class GeminiClient { tools, history, resumedSessionData, + async (modelId: string) => { + this.lastUsedModelId = modelId; + const toolRegistry = this.config.getToolRegistry(); + const toolDeclarations = + toolRegistry.getFunctionDeclarations(modelId); + return [{ functionDeclarations: toolDeclarations }]; + }, ); } catch (error) { await reportError( diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 35a8e79855d..6bea67dc0e3 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -247,6 +247,7 @@ export class GeminiChat { private tools: Tool[] = [], private history: Content[] = [], resumedSessionData?: ResumedSessionData, + private readonly onModelChanged?: (modelId: string) => Promise, ) { validateHistory(history); this.chatRecordingService = new ChatRecordingService(config); @@ -581,7 +582,9 @@ export class GeminiChat { } // Track final request parameters for AfterModel hooks - await this.config.getGeminiClient().setTools(modelToUse); + if (this.onModelChanged) { + this.tools = await this.onModelChanged(modelToUse); + } lastModelToUse = modelToUse; lastConfig = config; lastContentsToUse = contentsToUse; From 615926879fbca62c383430676a3c4535c356c0b2 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Sun, 8 Feb 2026 15:57:04 +0000 Subject: [PATCH 11/13] style: move AfterModel hooks comment in GeminiChat --- packages/core/src/core/geminiChat.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 6bea67dc0e3..8f2c4b92670 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -581,10 +581,11 @@ export class GeminiChat { } } - // Track final request parameters for AfterModel hooks if (this.onModelChanged) { this.tools = await this.onModelChanged(modelToUse); } + + // Track final request parameters for AfterModel hooks lastModelToUse = modelToUse; lastConfig = config; lastContentsToUse = contentsToUse; From 4e98b3a5f8d4e841fcb047af6ca8b41178be7455 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Sun, 8 Feb 2026 16:29:35 +0000 Subject: [PATCH 12/13] fix(core): reset lastUsedModelId in startChat to prevent stale tool cache --- packages/core/src/core/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index d6afeac4be0..4781dd7618d 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -332,6 +332,7 @@ export class GeminiClient { ): Promise { this.forceFullIdeContext = true; this.hasFailedCompressionAttempt = false; + this.lastUsedModelId = undefined; const toolRegistry = this.config.getToolRegistry(); const toolDeclarations = toolRegistry.getFunctionDeclarations(); From b1486ebc91355a63d6117ed6cf60d62a71eafd08 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Mon, 9 Feb 2026 20:34:45 +0000 Subject: [PATCH 13/13] test(core): update tool snapshots for centralized definitions --- packages/core/src/tools/__snapshots__/shell.test.ts.snap | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/src/tools/__snapshots__/shell.test.ts.snap b/packages/core/src/tools/__snapshots__/shell.test.ts.snap index b3478c1019f..471ce45f6e9 100644 --- a/packages/core/src/tools/__snapshots__/shell.test.ts.snap +++ b/packages/core/src/tools/__snapshots__/shell.test.ts.snap @@ -37,6 +37,10 @@ exports[`ShellTool > getDescription > should return the windows description when exports[`ShellTool > getSchema > should return the base schema when no modelId is provided 1`] = ` "This tool executes a given shell command as \`bash -c \`. Command can start background processes using \`&\`. Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`. + Efficiency Guidelines: + - Quiet Flags: Always prefer silent or quiet flags (e.g., \`npm install --silent\`, \`git --no-pager\`) to reduce output volume while still capturing necessary information. + - Pagination: Always disable terminal pagination to ensure commands terminate (e.g., use \`git --no-pager\`, \`systemctl --no-pager\`, or set \`PAGER=cat\`). + The following information is returned: Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes. @@ -50,6 +54,10 @@ exports[`ShellTool > getSchema > should return the base schema when no modelId i exports[`ShellTool > getSchema > should return the schema from the resolver when modelId is provided 1`] = ` "This tool executes a given shell command as \`bash -c \`. Command can start background processes using \`&\`. Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`. + Efficiency Guidelines: + - Quiet Flags: Always prefer silent or quiet flags (e.g., \`npm install --silent\`, \`git --no-pager\`) to reduce output volume while still capturing necessary information. + - Pagination: Always disable terminal pagination to ensure commands terminate (e.g., use \`git --no-pager\`, \`systemctl --no-pager\`, or set \`PAGER=cat\`). + The following information is returned: Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes.