feat: migrate tools to use parametersJsonSchema. (#5330)
This commit is contained in:
parent
f52d073dfb
commit
d9fb08c9da
|
@ -19,7 +19,6 @@ import {
|
||||||
ToolResultDisplay,
|
ToolResultDisplay,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { ToolErrorType } from './tool-error.js';
|
import { ToolErrorType } from './tool-error.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||||
import { isNodeError } from '../utils/errors.js';
|
import { isNodeError } from '../utils/errors.js';
|
||||||
|
@ -443,27 +442,27 @@ Expectation for required parameters:
|
||||||
file_path: {
|
file_path: {
|
||||||
description:
|
description:
|
||||||
"The absolute path to the file to modify. Must start with '/'.",
|
"The absolute path to the file to modify. Must start with '/'.",
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
old_string: {
|
old_string: {
|
||||||
description:
|
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.',
|
'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: {
|
new_string: {
|
||||||
description:
|
description:
|
||||||
'The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic.',
|
'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: {
|
expected_replacements: {
|
||||||
type: Type.NUMBER,
|
type: 'number',
|
||||||
description:
|
description:
|
||||||
'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.',
|
'Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['file_path', 'old_string', 'new_string'],
|
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
|
* @returns Error message string or null if valid
|
||||||
*/
|
*/
|
||||||
validateToolParams(params: EditToolParams): string | null {
|
validateToolParams(params: EditToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
ToolInvocation,
|
ToolInvocation,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { shortenPath, makeRelative } from '../utils/paths.js';
|
import { shortenPath, makeRelative } from '../utils/paths.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
|
|
||||||
|
@ -255,26 +254,26 @@ export class GlobTool extends BaseDeclarativeTool<GlobToolParams, ToolResult> {
|
||||||
pattern: {
|
pattern: {
|
||||||
description:
|
description:
|
||||||
"The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').",
|
"The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').",
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
description:
|
description:
|
||||||
'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.',
|
'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.',
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
case_sensitive: {
|
case_sensitive: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether the search should be case-sensitive. Defaults to false.',
|
'Optional: Whether the search should be case-sensitive. Defaults to false.',
|
||||||
type: Type.BOOLEAN,
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
respect_git_ignore: {
|
respect_git_ignore: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true.',
|
'Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true.',
|
||||||
type: Type.BOOLEAN,
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['pattern'],
|
required: ['pattern'],
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -283,7 +282,10 @@ export class GlobTool extends BaseDeclarativeTool<GlobToolParams, ToolResult> {
|
||||||
* Validates the parameters for the tool.
|
* Validates the parameters for the tool.
|
||||||
*/
|
*/
|
||||||
validateToolParams(params: GlobToolParams): string | null {
|
validateToolParams(params: GlobToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
ToolInvocation,
|
ToolInvocation,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||||
import { getErrorMessage, isNodeError } from '../utils/errors.js';
|
import { getErrorMessage, isNodeError } from '../utils/errors.js';
|
||||||
|
@ -550,21 +549,21 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
|
||||||
pattern: {
|
pattern: {
|
||||||
description:
|
description:
|
||||||
"The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
|
"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: {
|
path: {
|
||||||
description:
|
description:
|
||||||
'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
|
'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
description:
|
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).",
|
"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'],
|
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
|
* @returns An error message string if invalid, null otherwise
|
||||||
*/
|
*/
|
||||||
validateToolParams(params: GrepToolParams): string | null {
|
validateToolParams(params: GrepToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { BaseTool, Icon, ToolResult } from './tools.js';
|
import { BaseTool, Icon, ToolResult } from './tools.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||||
import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js';
|
import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js';
|
||||||
|
@ -82,35 +81,35 @@ export class LSTool extends BaseTool<LSToolParams, ToolResult> {
|
||||||
path: {
|
path: {
|
||||||
description:
|
description:
|
||||||
'The absolute path to the directory to list (must be absolute, not relative)',
|
'The absolute path to the directory to list (must be absolute, not relative)',
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
ignore: {
|
ignore: {
|
||||||
description: 'List of glob patterns to ignore',
|
description: 'List of glob patterns to ignore',
|
||||||
items: {
|
items: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
type: Type.ARRAY,
|
type: 'array',
|
||||||
},
|
},
|
||||||
file_filtering_options: {
|
file_filtering_options: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether to respect ignore patterns from .gitignore or .geminiignore',
|
'Optional: Whether to respect ignore patterns from .gitignore or .geminiignore',
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
respect_git_ignore: {
|
respect_git_ignore: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true.',
|
'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: {
|
respect_gemini_ignore: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true.',
|
'Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true.',
|
||||||
type: Type.BOOLEAN,
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['path'],
|
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
|
* @returns An error message string if invalid, null otherwise
|
||||||
*/
|
*/
|
||||||
validateToolParams(params: LSToolParams): string | null {
|
validateToolParams(params: LSToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,7 @@ import {
|
||||||
ToolMcpConfirmationDetails,
|
ToolMcpConfirmationDetails,
|
||||||
Icon,
|
Icon,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import {
|
import { CallableTool, Part, FunctionCall } from '@google/genai';
|
||||||
CallableTool,
|
|
||||||
Part,
|
|
||||||
FunctionCall,
|
|
||||||
FunctionDeclaration,
|
|
||||||
Type,
|
|
||||||
} from '@google/genai';
|
|
||||||
|
|
||||||
type ToolParams = Record<string, unknown>;
|
type ToolParams = Record<string, unknown>;
|
||||||
|
|
||||||
|
@ -64,7 +58,7 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
|
||||||
readonly serverName: string,
|
readonly serverName: string,
|
||||||
readonly serverToolName: string,
|
readonly serverToolName: string,
|
||||||
description: string,
|
description: string,
|
||||||
readonly parameterSchemaJson: unknown,
|
readonly parameterSchema: unknown,
|
||||||
readonly timeout?: number,
|
readonly timeout?: number,
|
||||||
readonly trust?: boolean,
|
readonly trust?: boolean,
|
||||||
nameOverride?: string,
|
nameOverride?: string,
|
||||||
|
@ -74,7 +68,7 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
|
||||||
`${serverToolName} (${serverName} MCP Server)`,
|
`${serverToolName} (${serverName} MCP Server)`,
|
||||||
description,
|
description,
|
||||||
Icon.Hammer,
|
Icon.Hammer,
|
||||||
{ type: Type.OBJECT }, // this is a dummy Schema for MCP, will be not be used to construct the FunctionDeclaration
|
parameterSchema,
|
||||||
true, // isOutputMarkdown
|
true, // isOutputMarkdown
|
||||||
false, // canUpdateOutput
|
false, // canUpdateOutput
|
||||||
);
|
);
|
||||||
|
@ -86,25 +80,13 @@ export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
|
||||||
this.serverName,
|
this.serverName,
|
||||||
this.serverToolName,
|
this.serverToolName,
|
||||||
this.description,
|
this.description,
|
||||||
this.parameterSchemaJson,
|
this.parameterSchema,
|
||||||
this.timeout,
|
this.timeout,
|
||||||
this.trust,
|
this.trust,
|
||||||
`${this.serverName}__${this.serverToolName}`,
|
`${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(
|
async shouldConfirmExecute(
|
||||||
_params: ToolParams,
|
_params: ToolParams,
|
||||||
_abortSignal: AbortSignal,
|
_abortSignal: AbortSignal,
|
||||||
|
|
|
@ -203,7 +203,17 @@ describe('MemoryTool', () => {
|
||||||
);
|
);
|
||||||
expect(memoryTool.schema).toBeDefined();
|
expect(memoryTool.schema).toBeDefined();
|
||||||
expect(memoryTool.schema.name).toBe('save_memory');
|
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 () => {
|
it('should call performAddMemoryEntry with correct parameters and return success', async () => {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
Icon,
|
Icon,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { FunctionDeclaration, Type } from '@google/genai';
|
import { FunctionDeclaration } from '@google/genai';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
@ -24,11 +24,11 @@ const memoryToolSchemaData: FunctionDeclaration = {
|
||||||
name: 'save_memory',
|
name: 'save_memory',
|
||||||
description:
|
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.',
|
'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: {
|
parametersJsonSchema: {
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
fact: {
|
fact: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
description:
|
description:
|
||||||
'The specific fact or piece of information to remember. Should be a clear, self-contained statement.',
|
'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',
|
'Save Memory',
|
||||||
memoryToolDescription,
|
memoryToolDescription,
|
||||||
Icon.LightBulb,
|
Icon.LightBulb,
|
||||||
memoryToolSchemaData.parameters as Record<string, unknown>,
|
memoryToolSchemaData.parametersJsonSchema as Record<string, unknown>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
ToolResult,
|
ToolResult,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { ToolErrorType } from './tool-error.js';
|
import { ToolErrorType } from './tool-error.js';
|
||||||
import { PartUnion, Type } from '@google/genai';
|
import { PartUnion } from '@google/genai';
|
||||||
import {
|
import {
|
||||||
processSingleFileContent,
|
processSingleFileContent,
|
||||||
getSpecificMimeType,
|
getSpecificMimeType,
|
||||||
|
@ -179,27 +179,30 @@ export class ReadFileTool extends BaseDeclarativeTool<
|
||||||
absolute_path: {
|
absolute_path: {
|
||||||
description:
|
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.",
|
"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: {
|
offset: {
|
||||||
description:
|
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.",
|
"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: {
|
limit: {
|
||||||
description:
|
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).",
|
"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'],
|
required: ['absolute_path'],
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected validateToolParams(params: ReadFileToolParams): string | null {
|
protected validateToolParams(params: ReadFileToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
DEFAULT_ENCODING,
|
DEFAULT_ENCODING,
|
||||||
getSpecificMimeType,
|
getSpecificMimeType,
|
||||||
} from '../utils/fileUtils.js';
|
} 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 { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js';
|
||||||
import {
|
import {
|
||||||
recordFileOperationMetric,
|
recordFileOperationMetric,
|
||||||
|
@ -150,47 +150,47 @@ export class ReadManyFilesTool extends BaseTool<
|
||||||
static readonly Name: string = 'read_many_files';
|
static readonly Name: string = 'read_many_files';
|
||||||
|
|
||||||
constructor(private config: Config) {
|
constructor(private config: Config) {
|
||||||
const parameterSchema: Schema = {
|
const parameterSchema = {
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
paths: {
|
paths: {
|
||||||
type: Type.ARRAY,
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
minLength: '1',
|
minLength: 1,
|
||||||
},
|
},
|
||||||
minItems: '1',
|
minItems: 1,
|
||||||
description:
|
description:
|
||||||
"Required. An array of glob patterns or paths relative to the tool's target directory. Examples: ['src/**/*.ts'], ['README.md', 'docs/']",
|
"Required. An array of glob patterns or paths relative to the tool's target directory. Examples: ['src/**/*.ts'], ['README.md', 'docs/']",
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
type: Type.ARRAY,
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
minLength: '1',
|
minLength: 1,
|
||||||
},
|
},
|
||||||
description:
|
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.',
|
'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: [],
|
default: [],
|
||||||
},
|
},
|
||||||
exclude: {
|
exclude: {
|
||||||
type: Type.ARRAY,
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
minLength: '1',
|
minLength: 1,
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Optional. Glob patterns for files/directories to exclude. Added to default excludes if useDefaultExcludes is true. Example: ["**/*.log", "temp/"]',
|
'Optional. Glob patterns for files/directories to exclude. Added to default excludes if useDefaultExcludes is true. Example: ["**/*.log", "temp/"]',
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
recursive: {
|
recursive: {
|
||||||
type: Type.BOOLEAN,
|
type: 'boolean',
|
||||||
description:
|
description:
|
||||||
'Optional. Whether to search recursively (primarily controlled by `**` in glob patterns). Defaults to true.',
|
'Optional. Whether to search recursively (primarily controlled by `**` in glob patterns). Defaults to true.',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
useDefaultExcludes: {
|
useDefaultExcludes: {
|
||||||
type: Type.BOOLEAN,
|
type: 'boolean',
|
||||||
description:
|
description:
|
||||||
'Optional. Whether to apply a list of default exclusion patterns (e.g., node_modules, .git, binary files). Defaults to true.',
|
'Optional. Whether to apply a list of default exclusion patterns (e.g., node_modules, .git, binary files). Defaults to true.',
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -198,17 +198,17 @@ export class ReadManyFilesTool extends BaseTool<
|
||||||
file_filtering_options: {
|
file_filtering_options: {
|
||||||
description:
|
description:
|
||||||
'Whether to respect ignore patterns from .gitignore or .geminiignore',
|
'Whether to respect ignore patterns from .gitignore or .geminiignore',
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
respect_git_ignore: {
|
respect_git_ignore: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true.',
|
'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: {
|
respect_gemini_ignore: {
|
||||||
description:
|
description:
|
||||||
'Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true.',
|
'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 {
|
validateParams(params: ReadManyFilesParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { ToolErrorType } from './tool-error.js';
|
import { ToolErrorType } from './tool-error.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import { summarizeToolOutput } from '../utils/summarizer.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)\``,
|
Process Group PGID: Process group started or \`(none)\``,
|
||||||
Icon.Terminal,
|
Icon.Terminal,
|
||||||
{
|
{
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
command: {
|
command: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
description: 'Exact bash command to execute as `bash -c <command>`',
|
description: 'Exact bash command to execute as `bash -c <command>`',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
description:
|
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.',
|
'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: {
|
directory: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
description:
|
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.',
|
'(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;
|
return commandCheck.reason;
|
||||||
}
|
}
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,19 +15,9 @@ import {
|
||||||
Mocked,
|
Mocked,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { Config, ConfigParameters, ApprovalMode } from '../config/config.js';
|
import { Config, ConfigParameters, ApprovalMode } from '../config/config.js';
|
||||||
import {
|
import { ToolRegistry, DiscoveredTool } from './tool-registry.js';
|
||||||
ToolRegistry,
|
|
||||||
DiscoveredTool,
|
|
||||||
sanitizeParameters,
|
|
||||||
} from './tool-registry.js';
|
|
||||||
import { DiscoveredMCPTool } from './mcp-tool.js';
|
import { DiscoveredMCPTool } from './mcp-tool.js';
|
||||||
import {
|
import { FunctionDeclaration, CallableTool, mcpToTool } from '@google/genai';
|
||||||
FunctionDeclaration,
|
|
||||||
CallableTool,
|
|
||||||
mcpToTool,
|
|
||||||
Type,
|
|
||||||
Schema,
|
|
||||||
} from '@google/genai';
|
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
@ -254,18 +244,18 @@ describe('ToolRegistry', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('discoverTools', () => {
|
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';
|
const discoveryCommand = 'my-discovery-command';
|
||||||
mockConfigGetToolDiscoveryCommand.mockReturnValue(discoveryCommand);
|
mockConfigGetToolDiscoveryCommand.mockReturnValue(discoveryCommand);
|
||||||
|
|
||||||
const unsanitizedToolDeclaration: FunctionDeclaration = {
|
const unsanitizedToolDeclaration: FunctionDeclaration = {
|
||||||
name: 'tool-with-bad-format',
|
name: 'tool-with-bad-format',
|
||||||
description: 'A tool with an invalid format property',
|
description: 'A tool with an invalid format property',
|
||||||
parameters: {
|
parametersJsonSchema: {
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
some_string: {
|
some_string: {
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
format: 'uuid', // This is an unsupported format
|
format: 'uuid', // This is an unsupported format
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -308,12 +298,16 @@ describe('ToolRegistry', () => {
|
||||||
expect(discoveredTool).toBeDefined();
|
expect(discoveredTool).toBeDefined();
|
||||||
|
|
||||||
const registeredParams = (discoveredTool as DiscoveredTool).schema
|
const registeredParams = (discoveredTool as DiscoveredTool).schema
|
||||||
.parameters as Schema;
|
.parametersJsonSchema;
|
||||||
expect(registeredParams.properties?.['some_string']).toBeDefined();
|
expect(registeredParams).toStrictEqual({
|
||||||
expect(registeredParams.properties?.['some_string']).toHaveProperty(
|
type: 'object',
|
||||||
'format',
|
properties: {
|
||||||
undefined,
|
some_string: {
|
||||||
);
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discover tools using MCP servers defined in getMcpServers', async () => {
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { AnyDeclarativeTool, Icon, ToolResult, BaseTool } from './tools.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
@ -331,14 +331,12 @@ export class ToolRegistry {
|
||||||
console.warn('Discovered a tool with no name. Skipping.');
|
console.warn('Discovered a tool with no name. Skipping.');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Sanitize the parameters before registering the tool.
|
|
||||||
const parameters =
|
const parameters =
|
||||||
func.parameters &&
|
func.parametersJsonSchema &&
|
||||||
typeof func.parameters === 'object' &&
|
typeof func.parametersJsonSchema === 'object' &&
|
||||||
!Array.isArray(func.parameters)
|
!Array.isArray(func.parametersJsonSchema)
|
||||||
? (func.parameters as Schema)
|
? func.parametersJsonSchema
|
||||||
: {};
|
: {};
|
||||||
sanitizeParameters(parameters);
|
|
||||||
this.registerTool(
|
this.registerTool(
|
||||||
new DiscoveredTool(
|
new DiscoveredTool(
|
||||||
this.config,
|
this.config,
|
||||||
|
@ -413,75 +411,3 @@ export class ToolRegistry {
|
||||||
return this.tools.get(name);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { ToolErrorType } from './tool-error.js';
|
||||||
import { DiffUpdateResult } from '../ide/ideContext.js';
|
import { DiffUpdateResult } from '../ide/ideContext.js';
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ export abstract class DeclarativeTool<
|
||||||
readonly displayName: string,
|
readonly displayName: string,
|
||||||
readonly description: string,
|
readonly description: string,
|
||||||
readonly icon: Icon,
|
readonly icon: Icon,
|
||||||
readonly parameterSchema: Schema,
|
readonly parameterSchema: unknown,
|
||||||
readonly isOutputMarkdown: boolean = true,
|
readonly isOutputMarkdown: boolean = true,
|
||||||
readonly canUpdateOutput: boolean = false,
|
readonly canUpdateOutput: boolean = false,
|
||||||
) {}
|
) {}
|
||||||
|
@ -195,7 +195,7 @@ export abstract class DeclarativeTool<
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
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 description Description of what the tool does
|
||||||
* @param isOutputMarkdown Whether the tool's output should be rendered as markdown
|
* @param isOutputMarkdown Whether the tool's output should be rendered as markdown
|
||||||
* @param canUpdateOutput Whether the tool supports live (streaming) output
|
* @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(
|
constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
readonly displayName: string,
|
readonly displayName: string,
|
||||||
readonly description: string,
|
readonly description: string,
|
||||||
readonly icon: Icon,
|
readonly icon: Icon,
|
||||||
readonly parameterSchema: Schema,
|
readonly parameterSchema: unknown,
|
||||||
readonly isOutputMarkdown: boolean = true,
|
readonly isOutputMarkdown: boolean = true,
|
||||||
readonly canUpdateOutput: boolean = false,
|
readonly canUpdateOutput: boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
Icon,
|
Icon,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import { Config, ApprovalMode } from '../config/config.js';
|
import { Config, ApprovalMode } from '../config/config.js';
|
||||||
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
||||||
|
@ -77,11 +76,11 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||||
prompt: {
|
prompt: {
|
||||||
description:
|
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://.',
|
'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'],
|
required: ['prompt'],
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const proxy = config.getProxy();
|
const proxy = config.getProxy();
|
||||||
|
@ -156,7 +155,10 @@ ${textContent}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateParams(params: WebFetchToolParams): string | null {
|
validateParams(params: WebFetchToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,10 @@ export class WebSearchTool extends BaseTool<
|
||||||
* @returns An error message string if validation fails, null if valid
|
* @returns An error message string if validation fails, null if valid
|
||||||
*/
|
*/
|
||||||
validateParams(params: WebSearchToolParams): string | null {
|
validateParams(params: WebSearchToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { ToolErrorType } from './tool-error.js';
|
import { ToolErrorType } from './tool-error.js';
|
||||||
import { Type } from '@google/genai';
|
|
||||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||||
import { getErrorMessage, isNodeError } from '../utils/errors.js';
|
import { getErrorMessage, isNodeError } from '../utils/errors.js';
|
||||||
|
@ -89,21 +88,24 @@ export class WriteFileTool
|
||||||
file_path: {
|
file_path: {
|
||||||
description:
|
description:
|
||||||
"The absolute path to the file to write to (e.g., '/home/user/project/file.txt'). Relative paths are not supported.",
|
"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: {
|
content: {
|
||||||
description: 'The content to write to the file.',
|
description: 'The content to write to the file.',
|
||||||
type: Type.STRING,
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['file_path', 'content'],
|
required: ['file_path', 'content'],
|
||||||
type: Type.OBJECT,
|
type: 'object',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateToolParams(params: WriteFileToolParams): string | null {
|
validateToolParams(params: WriteFileToolParams): string | null {
|
||||||
const errors = SchemaValidator.validate(this.schema.parameters, params);
|
const errors = SchemaValidator.validate(
|
||||||
|
this.schema.parametersJsonSchema,
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (errors) {
|
if (errors) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Schema } from '@google/genai';
|
|
||||||
import AjvPkg from 'ajv';
|
import AjvPkg from 'ajv';
|
||||||
// Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs
|
// Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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
|
* Returns null if the data confroms to the schema described by schema (or if schema
|
||||||
* is null). Otherwise, returns a string describing the error.
|
* 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) {
|
if (!schema) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof data !== 'object' || data === null) {
|
if (typeof data !== 'object' || data === null) {
|
||||||
return 'Value of params must be an object';
|
return 'Value of params must be an object';
|
||||||
}
|
}
|
||||||
const validate = ajValidator.compile(this.toObjectSchema(schema));
|
const validate = ajValidator.compile(schema);
|
||||||
const valid = validate(data);
|
const valid = validate(data);
|
||||||
if (!valid && validate.errors) {
|
if (!valid && validate.errors) {
|
||||||
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
||||||
}
|
}
|
||||||
return null;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue