diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index 33323203..86641300 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -19,7 +19,6 @@ import { ToolResultDisplay, } from './tools.js'; import { ToolErrorType } from './tool-error.js'; -import { Type } from '@google/genai'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; import { isNodeError } from '../utils/errors.js'; @@ -443,27 +442,27 @@ Expectation for required parameters: file_path: { description: "The absolute path to the file to modify. Must start with '/'.", - type: Type.STRING, + type: 'string', }, old_string: { description: 'The exact literal text to replace, preferably unescaped. For single replacements (default), include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. For multiple replacements, specify expected_replacements parameter. If this string is not the exact literal text (i.e. you escaped it) or does not match exactly, the tool will fail.', - type: Type.STRING, + type: 'string', }, new_string: { description: 'The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic.', - type: Type.STRING, + type: 'string', }, expected_replacements: { - type: Type.NUMBER, + type: 'number', description: 'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.', minimum: 1, }, }, required: ['file_path', 'old_string', 'new_string'], - type: Type.OBJECT, + type: 'object', }, ); } @@ -474,7 +473,10 @@ Expectation for required parameters: * @returns Error message string or null if valid */ validateToolParams(params: EditToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts index df0cc348..eaedc20f 100644 --- a/packages/core/src/tools/glob.ts +++ b/packages/core/src/tools/glob.ts @@ -15,7 +15,6 @@ import { ToolInvocation, ToolResult, } from './tools.js'; -import { Type } from '@google/genai'; import { shortenPath, makeRelative } from '../utils/paths.js'; import { Config } from '../config/config.js'; @@ -255,26 +254,26 @@ export class GlobTool extends BaseDeclarativeTool { pattern: { description: "The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').", - type: Type.STRING, + type: 'string', }, 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', }, }, required: ['pattern'], - type: Type.OBJECT, + type: 'object', }, ); } @@ -283,7 +282,10 @@ export class GlobTool extends BaseDeclarativeTool { * Validates the parameters for the tool. */ validateToolParams(params: GlobToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts index 8e2b84f1..f8ecdc9c 100644 --- a/packages/core/src/tools/grep.ts +++ b/packages/core/src/tools/grep.ts @@ -17,7 +17,6 @@ import { ToolInvocation, ToolResult, } from './tools.js'; -import { Type } from '@google/genai'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; import { getErrorMessage, isNodeError } from '../utils/errors.js'; @@ -550,21 +549,21 @@ export class GrepTool extends BaseDeclarativeTool { 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', }, 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'], - type: Type.OBJECT, + type: 'object', }, ); } @@ -616,7 +615,10 @@ export class GrepTool extends BaseDeclarativeTool { * @returns An error message string if invalid, null otherwise */ validateToolParams(params: GrepToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/ls.ts b/packages/core/src/tools/ls.ts index 8490f18a..79820246 100644 --- a/packages/core/src/tools/ls.ts +++ b/packages/core/src/tools/ls.ts @@ -7,7 +7,6 @@ import fs from 'fs'; import path from 'path'; import { BaseTool, Icon, ToolResult } from './tools.js'; -import { Type } from '@google/genai'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js'; @@ -82,35 +81,35 @@ export class LSTool extends BaseTool { path: { description: 'The absolute path to the directory to list (must be absolute, not relative)', - 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', }, }, }, }, required: ['path'], - type: Type.OBJECT, + type: 'object', }, ); } @@ -121,7 +120,10 @@ export class LSTool extends BaseTool { * @returns An error message string if invalid, null otherwise */ validateToolParams(params: LSToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/mcp-tool.ts b/packages/core/src/tools/mcp-tool.ts index 3dd62e2b..4b9a9818 100644 --- a/packages/core/src/tools/mcp-tool.ts +++ b/packages/core/src/tools/mcp-tool.ts @@ -12,13 +12,7 @@ import { ToolMcpConfirmationDetails, Icon, } from './tools.js'; -import { - CallableTool, - Part, - FunctionCall, - FunctionDeclaration, - Type, -} from '@google/genai'; +import { CallableTool, Part, FunctionCall } from '@google/genai'; type ToolParams = Record; @@ -64,7 +58,7 @@ export class DiscoveredMCPTool extends BaseTool { readonly serverName: string, readonly serverToolName: string, description: string, - readonly parameterSchemaJson: unknown, + readonly parameterSchema: unknown, readonly timeout?: number, readonly trust?: boolean, nameOverride?: string, @@ -74,7 +68,7 @@ export class DiscoveredMCPTool extends BaseTool { `${serverToolName} (${serverName} MCP Server)`, description, Icon.Hammer, - { type: Type.OBJECT }, // this is a dummy Schema for MCP, will be not be used to construct the FunctionDeclaration + parameterSchema, true, // isOutputMarkdown false, // canUpdateOutput ); @@ -86,25 +80,13 @@ export class DiscoveredMCPTool extends BaseTool { this.serverName, this.serverToolName, this.description, - this.parameterSchemaJson, + this.parameterSchema, this.timeout, this.trust, `${this.serverName}__${this.serverToolName}`, ); } - /** - * Overrides the base schema to use parametersJsonSchema when building - * FunctionDeclaration - */ - override get schema(): FunctionDeclaration { - return { - name: this.name, - description: this.description, - parametersJsonSchema: this.parameterSchemaJson, - }; - } - async shouldConfirmExecute( _params: ToolParams, _abortSignal: AbortSignal, diff --git a/packages/core/src/tools/memoryTool.test.ts b/packages/core/src/tools/memoryTool.test.ts index 5a9b5f26..2a5c4c39 100644 --- a/packages/core/src/tools/memoryTool.test.ts +++ b/packages/core/src/tools/memoryTool.test.ts @@ -203,7 +203,17 @@ describe('MemoryTool', () => { ); expect(memoryTool.schema).toBeDefined(); expect(memoryTool.schema.name).toBe('save_memory'); - expect(memoryTool.schema.parameters?.properties?.fact).toBeDefined(); + expect(memoryTool.schema.parametersJsonSchema).toStrictEqual({ + type: 'object', + properties: { + fact: { + type: 'string', + description: + 'The specific fact or piece of information to remember. Should be a clear, self-contained statement.', + }, + }, + required: ['fact'], + }); }); it('should call performAddMemoryEntry with correct parameters and return success', async () => { diff --git a/packages/core/src/tools/memoryTool.ts b/packages/core/src/tools/memoryTool.ts index f3bf315b..f0c95b6a 100644 --- a/packages/core/src/tools/memoryTool.ts +++ b/packages/core/src/tools/memoryTool.ts @@ -11,7 +11,7 @@ import { ToolConfirmationOutcome, Icon, } from './tools.js'; -import { FunctionDeclaration, Type } from '@google/genai'; +import { FunctionDeclaration } from '@google/genai'; import * as fs from 'fs/promises'; import * as path from 'path'; import { homedir } from 'os'; @@ -24,11 +24,11 @@ const memoryToolSchemaData: FunctionDeclaration = { name: 'save_memory', description: 'Saves a specific piece of information or fact to your long-term memory. Use this when the user explicitly asks you to remember something, or when they state a clear, concise fact that seems important to retain for future interactions.', - parameters: { - type: Type.OBJECT, + parametersJsonSchema: { + type: 'object', properties: { fact: { - type: Type.STRING, + type: 'string', description: 'The specific fact or piece of information to remember. Should be a clear, self-contained statement.', }, @@ -123,7 +123,7 @@ export class MemoryTool 'Save Memory', memoryToolDescription, Icon.LightBulb, - memoryToolSchemaData.parameters as Record, + memoryToolSchemaData.parametersJsonSchema as Record, ); } diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 2ab50282..0c040b66 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -16,7 +16,7 @@ import { ToolResult, } from './tools.js'; import { ToolErrorType } from './tool-error.js'; -import { PartUnion, Type } from '@google/genai'; +import { PartUnion } from '@google/genai'; import { processSingleFileContent, getSpecificMimeType, @@ -179,27 +179,30 @@ export class ReadFileTool extends BaseDeclarativeTool< absolute_path: { description: "The absolute path to the file to read (e.g., '/home/user/project/file.txt'). Relative paths are not supported. You must provide an absolute path.", - 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: ['absolute_path'], - type: Type.OBJECT, + type: 'object', }, ); } protected validateToolParams(params: ReadFileToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts index a380ea91..1c92b4f3 100644 --- a/packages/core/src/tools/read-many-files.ts +++ b/packages/core/src/tools/read-many-files.ts @@ -16,7 +16,7 @@ import { DEFAULT_ENCODING, getSpecificMimeType, } from '../utils/fileUtils.js'; -import { PartListUnion, Schema, Type } from '@google/genai'; +import { PartListUnion } from '@google/genai'; import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js'; import { recordFileOperationMetric, @@ -150,47 +150,47 @@ export class ReadManyFilesTool extends BaseTool< static readonly Name: string = 'read_many_files'; constructor(private config: Config) { - const parameterSchema: Schema = { - type: Type.OBJECT, + const parameterSchema = { + type: 'object', properties: { paths: { - type: Type.ARRAY, + type: 'array', items: { - type: Type.STRING, - minLength: '1', + type: 'string', + minLength: 1, }, - minItems: '1', + minItems: 1, description: "Required. An array of glob patterns or paths relative to the tool's target directory. Examples: ['src/**/*.ts'], ['README.md', 'docs/']", }, include: { - type: Type.ARRAY, + type: 'array', items: { - type: Type.STRING, - minLength: '1', + type: 'string', + minLength: 1, }, description: 'Optional. Additional glob patterns to include. These are merged with `paths`. Example: ["*.test.ts"] to specifically add test files if they were broadly excluded.', default: [], }, exclude: { - type: Type.ARRAY, + type: 'array', items: { - type: Type.STRING, - minLength: '1', + type: 'string', + minLength: 1, }, description: 'Optional. Glob patterns for files/directories to exclude. Added to default excludes if useDefaultExcludes is true. Example: ["**/*.log", "temp/"]', default: [], }, recursive: { - type: Type.BOOLEAN, + type: 'boolean', description: 'Optional. Whether to search recursively (primarily controlled by `**` in glob patterns). Defaults to true.', default: true, }, useDefaultExcludes: { - type: Type.BOOLEAN, + type: 'boolean', description: 'Optional. Whether to apply a list of default exclusion patterns (e.g., node_modules, .git, binary files). Defaults to true.', default: true, @@ -198,17 +198,17 @@ export class ReadManyFilesTool extends BaseTool< file_filtering_options: { description: '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', }, }, }, @@ -235,7 +235,10 @@ Use this tool when the user's query implies needing the content of several files } validateParams(params: ReadManyFilesParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 6106c0cd..de9b7c2f 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -18,7 +18,6 @@ import { Icon, } from './tools.js'; import { ToolErrorType } from './tool-error.js'; -import { Type } from '@google/genai'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { getErrorMessage } from '../utils/errors.js'; import { summarizeToolOutput } from '../utils/summarizer.js'; @@ -64,19 +63,19 @@ export class ShellTool extends BaseTool { Process Group PGID: Process group started or \`(none)\``, Icon.Terminal, { - type: Type.OBJECT, + type: 'object', properties: { command: { - type: Type.STRING, + type: 'string', description: 'Exact bash command to execute as `bash -c `', }, 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.', }, directory: { - type: Type.STRING, + type: 'string', description: '(OPTIONAL) Directory to run the command in, if not the project root directory. Must be relative to the project root directory and must already exist.', }, @@ -113,7 +112,10 @@ export class ShellTool extends BaseTool { } return commandCheck.reason; } - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/tool-registry.test.ts b/packages/core/src/tools/tool-registry.test.ts index d8e536b7..13dff08c 100644 --- a/packages/core/src/tools/tool-registry.test.ts +++ b/packages/core/src/tools/tool-registry.test.ts @@ -15,19 +15,9 @@ import { Mocked, } from 'vitest'; import { Config, ConfigParameters, ApprovalMode } from '../config/config.js'; -import { - ToolRegistry, - DiscoveredTool, - sanitizeParameters, -} from './tool-registry.js'; +import { ToolRegistry, DiscoveredTool } from './tool-registry.js'; import { DiscoveredMCPTool } from './mcp-tool.js'; -import { - FunctionDeclaration, - CallableTool, - mcpToTool, - Type, - Schema, -} from '@google/genai'; +import { FunctionDeclaration, CallableTool, mcpToTool } from '@google/genai'; import { spawn } from 'node:child_process'; import fs from 'node:fs'; @@ -254,18 +244,18 @@ describe('ToolRegistry', () => { }); describe('discoverTools', () => { - it('should sanitize tool parameters during discovery from command', async () => { + it('should will preserve tool parametersJsonSchema during discovery from command', async () => { const discoveryCommand = 'my-discovery-command'; mockConfigGetToolDiscoveryCommand.mockReturnValue(discoveryCommand); const unsanitizedToolDeclaration: FunctionDeclaration = { name: 'tool-with-bad-format', description: 'A tool with an invalid format property', - parameters: { - type: Type.OBJECT, + parametersJsonSchema: { + type: 'object', properties: { some_string: { - type: Type.STRING, + type: 'string', format: 'uuid', // This is an unsupported format }, }, @@ -308,12 +298,16 @@ describe('ToolRegistry', () => { expect(discoveredTool).toBeDefined(); const registeredParams = (discoveredTool as DiscoveredTool).schema - .parameters as Schema; - expect(registeredParams.properties?.['some_string']).toBeDefined(); - expect(registeredParams.properties?.['some_string']).toHaveProperty( - 'format', - undefined, - ); + .parametersJsonSchema; + expect(registeredParams).toStrictEqual({ + type: 'object', + properties: { + some_string: { + type: 'string', + format: 'uuid', + }, + }, + }); }); it('should discover tools using MCP servers defined in getMcpServers', async () => { @@ -365,187 +359,3 @@ describe('ToolRegistry', () => { }); }); }); - -describe('sanitizeParameters', () => { - it('should remove default when anyOf is present', () => { - const schema: Schema = { - anyOf: [{ type: Type.STRING }, { type: Type.NUMBER }], - default: 'hello', - }; - sanitizeParameters(schema); - expect(schema.default).toBeUndefined(); - }); - - it('should recursively sanitize items in anyOf', () => { - const schema: Schema = { - anyOf: [ - { - anyOf: [{ type: Type.STRING }], - default: 'world', - }, - { type: Type.NUMBER }, - ], - }; - sanitizeParameters(schema); - expect(schema.anyOf![0].default).toBeUndefined(); - }); - - it('should recursively sanitize items in items', () => { - const schema: Schema = { - items: { - anyOf: [{ type: Type.STRING }], - default: 'world', - }, - }; - sanitizeParameters(schema); - expect(schema.items!.default).toBeUndefined(); - }); - - it('should recursively sanitize items in properties', () => { - const schema: Schema = { - properties: { - prop1: { - anyOf: [{ type: Type.STRING }], - default: 'world', - }, - }, - }; - sanitizeParameters(schema); - expect(schema.properties!.prop1.default).toBeUndefined(); - }); - - it('should handle complex nested schemas', () => { - const schema: Schema = { - properties: { - prop1: { - items: { - anyOf: [{ type: Type.STRING }], - default: 'world', - }, - }, - prop2: { - anyOf: [ - { - properties: { - nestedProp: { - anyOf: [{ type: Type.NUMBER }], - default: 123, - }, - }, - }, - ], - }, - }, - }; - sanitizeParameters(schema); - expect(schema.properties!.prop1.items!.default).toBeUndefined(); - const nestedProp = - schema.properties!.prop2.anyOf![0].properties!.nestedProp; - expect(nestedProp?.default).toBeUndefined(); - }); - - it('should remove unsupported format from a simple string property', () => { - const schema: Schema = { - type: Type.OBJECT, - properties: { - name: { type: Type.STRING }, - id: { type: Type.STRING, format: 'uuid' }, - }, - }; - sanitizeParameters(schema); - expect(schema.properties?.['id']).toHaveProperty('format', undefined); - expect(schema.properties?.['name']).not.toHaveProperty('format'); - }); - - it('should NOT remove supported format values', () => { - const schema: Schema = { - type: Type.OBJECT, - properties: { - date: { type: Type.STRING, format: 'date-time' }, - role: { - type: Type.STRING, - format: 'enum', - enum: ['admin', 'user'], - }, - }, - }; - const originalSchema = JSON.parse(JSON.stringify(schema)); - sanitizeParameters(schema); - expect(schema).toEqual(originalSchema); - }); - - it('should handle arrays of objects', () => { - const schema: Schema = { - type: Type.OBJECT, - properties: { - items: { - type: Type.ARRAY, - items: { - type: Type.OBJECT, - properties: { - itemId: { type: Type.STRING, format: 'uuid' }, - }, - }, - }, - }, - }; - sanitizeParameters(schema); - expect( - (schema.properties?.['items']?.items as Schema)?.properties?.['itemId'], - ).toHaveProperty('format', undefined); - }); - - it('should handle schemas with no properties to sanitize', () => { - const schema: Schema = { - type: Type.OBJECT, - properties: { - count: { type: Type.NUMBER }, - isActive: { type: Type.BOOLEAN }, - }, - }; - const originalSchema = JSON.parse(JSON.stringify(schema)); - sanitizeParameters(schema); - expect(schema).toEqual(originalSchema); - }); - - it('should not crash on an empty or undefined schema', () => { - expect(() => sanitizeParameters({})).not.toThrow(); - expect(() => sanitizeParameters(undefined)).not.toThrow(); - }); - - it('should handle complex nested schemas with cycles', () => { - const userNode: any = { - type: Type.OBJECT, - properties: { - id: { type: Type.STRING, format: 'uuid' }, - name: { type: Type.STRING }, - manager: { - type: Type.OBJECT, - properties: { - id: { type: Type.STRING, format: 'uuid' }, - }, - }, - }, - }; - userNode.properties.reports = { - type: Type.ARRAY, - items: userNode, - }; - - const schema: Schema = { - type: Type.OBJECT, - properties: { - ceo: userNode, - }, - }; - - expect(() => sanitizeParameters(schema)).not.toThrow(); - expect(schema.properties?.['ceo']?.properties?.['id']).toHaveProperty( - 'format', - undefined, - ); - expect( - schema.properties?.['ceo']?.properties?.['manager']?.properties?.['id'], - ).toHaveProperty('format', undefined); - }); -}); diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 70226052..17d324b3 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { FunctionDeclaration, Schema, Type } from '@google/genai'; +import { FunctionDeclaration } from '@google/genai'; import { AnyDeclarativeTool, Icon, ToolResult, BaseTool } from './tools.js'; import { Config } from '../config/config.js'; import { spawn } from 'node:child_process'; @@ -331,14 +331,12 @@ export class ToolRegistry { console.warn('Discovered a tool with no name. Skipping.'); continue; } - // Sanitize the parameters before registering the tool. const parameters = - func.parameters && - typeof func.parameters === 'object' && - !Array.isArray(func.parameters) - ? (func.parameters as Schema) + func.parametersJsonSchema && + typeof func.parametersJsonSchema === 'object' && + !Array.isArray(func.parametersJsonSchema) + ? func.parametersJsonSchema : {}; - sanitizeParameters(parameters); this.registerTool( new DiscoveredTool( this.config, @@ -413,75 +411,3 @@ export class ToolRegistry { return this.tools.get(name); } } - -/** - * Sanitizes a schema object in-place to ensure compatibility with the Gemini API. - * - * NOTE: This function mutates the passed schema object. - * - * It performs the following actions: - * - Removes the `default` property when `anyOf` is present. - * - Removes unsupported `format` values from string properties, keeping only 'enum' and 'date-time'. - * - Recursively sanitizes nested schemas within `anyOf`, `items`, and `properties`. - * - Handles circular references within the schema to prevent infinite loops. - * - * @param schema The schema object to sanitize. It will be modified directly. - */ -export function sanitizeParameters(schema?: Schema) { - _sanitizeParameters(schema, new Set()); -} - -/** - * Internal recursive implementation for sanitizeParameters. - * @param schema The schema object to sanitize. - * @param visited A set used to track visited schema objects during recursion. - */ -function _sanitizeParameters(schema: Schema | undefined, visited: Set) { - if (!schema || visited.has(schema)) { - return; - } - visited.add(schema); - - if (schema.anyOf) { - // Vertex AI gets confused if both anyOf and default are set. - schema.default = undefined; - for (const item of schema.anyOf) { - if (typeof item !== 'boolean') { - _sanitizeParameters(item, visited); - } - } - } - if (schema.items && typeof schema.items !== 'boolean') { - _sanitizeParameters(schema.items, visited); - } - if (schema.properties) { - for (const item of Object.values(schema.properties)) { - if (typeof item !== 'boolean') { - _sanitizeParameters(item, visited); - } - } - } - - // Handle enum values - Gemini API only allows enum for STRING type - if (schema.enum && Array.isArray(schema.enum)) { - if (schema.type !== Type.STRING) { - // If enum is present but type is not STRING, convert type to STRING - schema.type = Type.STRING; - } - // Filter out null and undefined values, then convert remaining values to strings for Gemini API compatibility - schema.enum = schema.enum - .filter((value: unknown) => value !== null && value !== undefined) - .map((value: unknown) => String(value)); - } - - // Vertex AI only supports 'enum' and 'date-time' for STRING format. - if (schema.type === Type.STRING) { - if ( - schema.format && - schema.format !== 'enum' && - schema.format !== 'date-time' - ) { - schema.format = undefined; - } - } -} diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index 8e064973..4b13174c 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { FunctionDeclaration, PartListUnion, Schema } from '@google/genai'; +import { FunctionDeclaration, PartListUnion } from '@google/genai'; import { ToolErrorType } from './tool-error.js'; import { DiffUpdateResult } from '../ide/ideContext.js'; @@ -186,7 +186,7 @@ export abstract class DeclarativeTool< readonly displayName: string, readonly description: string, readonly icon: Icon, - readonly parameterSchema: Schema, + readonly parameterSchema: unknown, readonly isOutputMarkdown: boolean = true, readonly canUpdateOutput: boolean = false, ) {} @@ -195,7 +195,7 @@ export abstract class DeclarativeTool< return { name: this.name, description: this.description, - parameters: this.parameterSchema, + parametersJsonSchema: this.parameterSchema, }; } @@ -281,14 +281,14 @@ export abstract class BaseTool< * @param description Description of what the tool does * @param isOutputMarkdown Whether the tool's output should be rendered as markdown * @param canUpdateOutput Whether the tool supports live (streaming) output - * @param parameterSchema Open API 3.0 Schema defining the parameters + * @param parameterSchema JSON Schema defining the parameters */ constructor( readonly name: string, readonly displayName: string, readonly description: string, readonly icon: Icon, - readonly parameterSchema: Schema, + readonly parameterSchema: unknown, readonly isOutputMarkdown: boolean = true, readonly canUpdateOutput: boolean = false, ) { diff --git a/packages/core/src/tools/web-fetch.ts b/packages/core/src/tools/web-fetch.ts index c96cae6c..6733c38d 100644 --- a/packages/core/src/tools/web-fetch.ts +++ b/packages/core/src/tools/web-fetch.ts @@ -12,7 +12,6 @@ import { ToolConfirmationOutcome, Icon, } from './tools.js'; -import { Type } from '@google/genai'; import { getErrorMessage } from '../utils/errors.js'; import { Config, ApprovalMode } from '../config/config.js'; import { getResponseText } from '../utils/generateContentResponseUtilities.js'; @@ -77,11 +76,11 @@ export class WebFetchTool extends BaseTool { prompt: { description: 'A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content (e.g., "Summarize https://example.com/article and extract key points from https://another.com/data"). Must contain as least one URL starting with http:// or https://.', - type: Type.STRING, + type: 'string', }, }, required: ['prompt'], - type: Type.OBJECT, + type: 'object', }, ); const proxy = config.getProxy(); @@ -156,7 +155,10 @@ ${textContent} } validateParams(params: WebFetchToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts index 480cc7e7..8fe29967 100644 --- a/packages/core/src/tools/web-search.ts +++ b/packages/core/src/tools/web-search.ts @@ -89,7 +89,10 @@ export class WebSearchTool extends BaseTool< * @returns An error message string if validation fails, null if valid */ validateParams(params: WebSearchToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/tools/write-file.ts b/packages/core/src/tools/write-file.ts index b018d653..5cdba419 100644 --- a/packages/core/src/tools/write-file.ts +++ b/packages/core/src/tools/write-file.ts @@ -18,7 +18,6 @@ import { Icon, } from './tools.js'; import { ToolErrorType } from './tool-error.js'; -import { Type } from '@google/genai'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; import { getErrorMessage, isNodeError } from '../utils/errors.js'; @@ -89,21 +88,24 @@ export class WriteFileTool file_path: { description: "The absolute path to the file to write to (e.g., '/home/user/project/file.txt'). Relative paths are not supported.", - type: Type.STRING, + type: 'string', }, content: { description: 'The content to write to the file.', - type: Type.STRING, + type: 'string', }, }, required: ['file_path', 'content'], - type: Type.OBJECT, + type: 'object', }, ); } validateToolParams(params: WriteFileToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); + const errors = SchemaValidator.validate( + this.schema.parametersJsonSchema, + params, + ); if (errors) { return errors; } diff --git a/packages/core/src/utils/schemaValidator.ts b/packages/core/src/utils/schemaValidator.ts index cb025774..f397c0b1 100644 --- a/packages/core/src/utils/schemaValidator.ts +++ b/packages/core/src/utils/schemaValidator.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Schema } from '@google/genai'; import AjvPkg from 'ajv'; // Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -19,50 +18,18 @@ export class SchemaValidator { * Returns null if the data confroms to the schema described by schema (or if schema * is null). Otherwise, returns a string describing the error. */ - static validate(schema: Schema | undefined, data: unknown): string | null { + static validate(schema: unknown | undefined, data: unknown): string | null { if (!schema) { return null; } if (typeof data !== 'object' || data === null) { return 'Value of params must be an object'; } - const validate = ajValidator.compile(this.toObjectSchema(schema)); + const validate = ajValidator.compile(schema); const valid = validate(data); if (!valid && validate.errors) { return ajValidator.errorsText(validate.errors, { dataVar: 'params' }); } return null; } - - /** - * Converts @google/genai's Schema to an object compatible with avj. - * This is necessary because it represents Types as an Enum (with - * UPPERCASE values) and minItems and minLength as strings, when they should be numbers. - */ - private static toObjectSchema(schema: Schema): object { - const newSchema: Record = { ...schema }; - if (newSchema.anyOf && Array.isArray(newSchema.anyOf)) { - newSchema.anyOf = newSchema.anyOf.map((v) => this.toObjectSchema(v)); - } - if (newSchema.items) { - newSchema.items = this.toObjectSchema(newSchema.items); - } - if (newSchema.properties && typeof newSchema.properties === 'object') { - const newProperties: Record = {}; - for (const [key, value] of Object.entries(newSchema.properties)) { - newProperties[key] = this.toObjectSchema(value as Schema); - } - newSchema.properties = newProperties; - } - if (newSchema.type) { - newSchema.type = String(newSchema.type).toLowerCase(); - } - if (newSchema.minItems) { - newSchema.minItems = Number(newSchema.minItems); - } - if (newSchema.minLength) { - newSchema.minLength = Number(newSchema.minLength); - } - return newSchema; - } }