summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWanlin Du <[email protected]>2025-08-11 16:12:41 -0700
committerGitHub <[email protected]>2025-08-11 23:12:41 +0000
commitd9fb08c9da3d2e8c501ec9badb2e2bd79eb15b93 (patch)
tree7d07e1586b736c1b6c44e28828aa29205af335ac
parentf52d073dfbfa4d5091a74bf33ac1c66e51265247 (diff)
feat: migrate tools to use parametersJsonSchema. (#5330)
-rw-r--r--packages/core/src/tools/edit.ts16
-rw-r--r--packages/core/src/tools/glob.ts16
-rw-r--r--packages/core/src/tools/grep.ts14
-rw-r--r--packages/core/src/tools/ls.ts20
-rw-r--r--packages/core/src/tools/mcp-tool.ts26
-rw-r--r--packages/core/src/tools/memoryTool.test.ts12
-rw-r--r--packages/core/src/tools/memoryTool.ts10
-rw-r--r--packages/core/src/tools/read-file.ts15
-rw-r--r--packages/core/src/tools/read-many-files.ts41
-rw-r--r--packages/core/src/tools/shell.ts14
-rw-r--r--packages/core/src/tools/tool-registry.test.ts222
-rw-r--r--packages/core/src/tools/tool-registry.ts84
-rw-r--r--packages/core/src/tools/tools.ts10
-rw-r--r--packages/core/src/tools/web-fetch.ts10
-rw-r--r--packages/core/src/tools/web-search.ts5
-rw-r--r--packages/core/src/tools/write-file.ts12
-rw-r--r--packages/core/src/utils/schemaValidator.ts37
17 files changed, 141 insertions, 423 deletions
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<GlobToolParams, ToolResult> {
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<GlobToolParams, ToolResult> {
* 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<GrepToolParams, ToolResult> {
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<GrepToolParams, ToolResult> {
* @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<LSToolParams, ToolResult> {
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<LSToolParams, ToolResult> {
* @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<string, unknown>;
@@ -64,7 +58,7 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
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<ToolParams, ToolResult> {
`${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<ToolParams, ToolResult> {
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<string, unknown>,
+ memoryToolSchemaData.parametersJsonSchema as Record<string, unknown>,
);
}
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<ShellToolParams, ToolResult> {
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 <command>`',
},
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<ShellToolParams, ToolResult> {
}
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<Schema>());
-}
-
-/**
- * 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<Schema>) {
- 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<WebFetchToolParams, ToolResult> {
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<string, unknown> = { ...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<string, unknown> = {};
- 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;
- }
}