mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-27 13:34:15 -07:00
Update prompt and grep tool definition to limit context size (#18780)
This commit is contained in:
committed by
GitHub
parent
2dac98dc8d
commit
2a08456ed0
+16
@@ -45,6 +45,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
|
||||
"description": "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.",
|
||||
"type": "string",
|
||||
},
|
||||
"exclude_pattern": {
|
||||
"description": "Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.",
|
||||
"type": "string",
|
||||
},
|
||||
"include": {
|
||||
"description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).",
|
||||
"type": "string",
|
||||
@@ -54,6 +58,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"names_only": {
|
||||
"description": "Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.",
|
||||
"type": "boolean",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
|
||||
"type": "string",
|
||||
@@ -254,6 +262,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
|
||||
"description": "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.",
|
||||
"type": "string",
|
||||
},
|
||||
"exclude_pattern": {
|
||||
"description": "Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.",
|
||||
"type": "string",
|
||||
},
|
||||
"include": {
|
||||
"description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).",
|
||||
"type": "string",
|
||||
@@ -263,6 +275,10 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"names_only": {
|
||||
"description": "Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.",
|
||||
"type": "boolean",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
|
||||
"type": "string",
|
||||
|
||||
@@ -98,6 +98,16 @@ export const GREP_DEFINITION: ToolDefinition = {
|
||||
description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`,
|
||||
type: 'string',
|
||||
},
|
||||
exclude_pattern: {
|
||||
description:
|
||||
'Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.',
|
||||
type: 'string',
|
||||
},
|
||||
names_only: {
|
||||
description:
|
||||
'Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.',
|
||||
type: 'boolean',
|
||||
},
|
||||
max_matches_per_file: {
|
||||
description:
|
||||
'Optional: Maximum number of matches to return per file. Use this to prevent being overwhelmed by repetitive matches in large files.',
|
||||
|
||||
@@ -498,6 +498,41 @@ describe('GrepTool', () => {
|
||||
expect(result.llmContent).toContain('File: sub/fileC.txt');
|
||||
expect(result.llmContent).toContain('L1: another world in sub dir');
|
||||
});
|
||||
|
||||
it('should return only file paths when names_only is true', async () => {
|
||||
const params: GrepToolParams = {
|
||||
pattern: 'world',
|
||||
names_only: true,
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 2 files with matches');
|
||||
expect(result.llmContent).toContain('fileA.txt');
|
||||
expect(result.llmContent).toContain('sub/fileC.txt');
|
||||
expect(result.llmContent).not.toContain('L1:');
|
||||
expect(result.llmContent).not.toContain('hello world');
|
||||
});
|
||||
|
||||
it('should filter out matches based on exclude_pattern', async () => {
|
||||
await fs.writeFile(
|
||||
path.join(tempRootDir, 'copyright.txt'),
|
||||
'Copyright 2025 Google LLC\nCopyright 2026 Google LLC',
|
||||
);
|
||||
|
||||
const params: GrepToolParams = {
|
||||
pattern: 'Copyright .* Google LLC',
|
||||
exclude_pattern: '2026',
|
||||
dir_path: '.',
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
expect(result.llmContent).toContain('copyright.txt');
|
||||
expect(result.llmContent).toContain('Copyright 2025 Google LLC');
|
||||
expect(result.llmContent).not.toContain('Copyright 2026 Google LLC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDescription', () => {
|
||||
|
||||
@@ -49,6 +49,16 @@ export interface GrepToolParams {
|
||||
*/
|
||||
include?: string;
|
||||
|
||||
/**
|
||||
* Optional: A regular expression pattern to exclude from the search results.
|
||||
*/
|
||||
exclude_pattern?: string;
|
||||
|
||||
/**
|
||||
* Optional: If true, only the file paths of the matches will be returned.
|
||||
*/
|
||||
names_only?: boolean;
|
||||
|
||||
/**
|
||||
* Optional: Maximum number of matches to return per file. Use this to prevent being overwhelmed by repetitive matches in large files.
|
||||
*/
|
||||
@@ -225,6 +235,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
pattern: this.params.pattern,
|
||||
path: searchDir,
|
||||
include: this.params.include,
|
||||
exclude_pattern: this.params.exclude_pattern,
|
||||
maxMatches: remainingLimit,
|
||||
max_matches_per_file: this.params.max_matches_per_file,
|
||||
signal: timeoutController.signal,
|
||||
@@ -280,6 +291,16 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
const matchCount = allMatches.length;
|
||||
const matchTerm = matchCount === 1 ? 'match' : 'matches';
|
||||
|
||||
if (this.params.names_only) {
|
||||
const filePaths = Object.keys(matchesByFile).sort();
|
||||
let llmContent = `Found ${filePaths.length} files with matches for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n`;
|
||||
llmContent += filePaths.join('\n');
|
||||
return {
|
||||
llmContent: llmContent.trim(),
|
||||
returnDisplay: `Found ${filePaths.length} files${wasTruncated ? ' (limited)' : ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}`;
|
||||
|
||||
if (wasTruncated) {
|
||||
@@ -354,6 +375,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
pattern: string;
|
||||
path: string; // Expects absolute path
|
||||
include?: string;
|
||||
exclude_pattern?: string;
|
||||
maxMatches: number;
|
||||
max_matches_per_file?: number;
|
||||
signal: AbortSignal;
|
||||
@@ -362,12 +384,18 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
pattern,
|
||||
path: absolutePath,
|
||||
include,
|
||||
exclude_pattern,
|
||||
maxMatches,
|
||||
max_matches_per_file,
|
||||
} = options;
|
||||
let strategyUsed = 'none';
|
||||
|
||||
try {
|
||||
let excludeRegex: RegExp | null = null;
|
||||
if (exclude_pattern) {
|
||||
excludeRegex = new RegExp(exclude_pattern, 'i');
|
||||
}
|
||||
|
||||
// --- Strategy 1: git grep ---
|
||||
const isGit = isGitRepository(absolutePath);
|
||||
const gitAvailable = isGit && (await this.isCommandAvailable('git'));
|
||||
@@ -400,6 +428,9 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
for await (const line of generator) {
|
||||
const match = this.parseGrepLine(line, absolutePath);
|
||||
if (match) {
|
||||
if (excludeRegex && excludeRegex.test(match.line)) {
|
||||
continue;
|
||||
}
|
||||
results.push(match);
|
||||
if (results.length >= maxMatches) {
|
||||
break;
|
||||
@@ -467,6 +498,9 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
for await (const line of generator) {
|
||||
const match = this.parseGrepLine(line, absolutePath);
|
||||
if (match) {
|
||||
if (excludeRegex && excludeRegex.test(match.line)) {
|
||||
continue;
|
||||
}
|
||||
results.push(match);
|
||||
if (results.length >= maxMatches) {
|
||||
break;
|
||||
@@ -528,6 +562,9 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index];
|
||||
if (regex.test(line)) {
|
||||
if (excludeRegex && excludeRegex.test(line)) {
|
||||
continue;
|
||||
}
|
||||
allMatches.push({
|
||||
filePath:
|
||||
path.relative(absolutePath, fileAbsolutePath) ||
|
||||
@@ -637,6 +674,14 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
|
||||
return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`;
|
||||
}
|
||||
|
||||
if (params.exclude_pattern) {
|
||||
try {
|
||||
new RegExp(params.exclude_pattern);
|
||||
} catch (error) {
|
||||
return `Invalid exclude regular expression pattern provided: ${params.exclude_pattern}. Error: ${getErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
params.max_matches_per_file !== undefined &&
|
||||
params.max_matches_per_file < 1
|
||||
|
||||
@@ -1930,6 +1930,85 @@ describe('RipGrepTool', () => {
|
||||
expect(result.llmContent).not.toContain('L3: match 3');
|
||||
expect(result.returnDisplay).toBe('Found 2 matches (limited)');
|
||||
});
|
||||
|
||||
it('should return only file paths when names_only is true', async () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileB.txt' },
|
||||
line_number: 5,
|
||||
lines: { text: 'hello again\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const params: RipGrepToolParams = {
|
||||
pattern: 'hello',
|
||||
names_only: true,
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 2 files with matches');
|
||||
expect(result.llmContent).toContain('fileA.txt');
|
||||
expect(result.llmContent).toContain('fileB.txt');
|
||||
expect(result.llmContent).not.toContain('L1:');
|
||||
expect(result.llmContent).not.toContain('hello world');
|
||||
});
|
||||
|
||||
it('should filter out matches based on exclude_pattern', async () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'Copyright 2025 Google LLC\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileB.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'Copyright 2026 Google LLC\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const params: RipGrepToolParams = {
|
||||
pattern: 'Copyright .* Google LLC',
|
||||
exclude_pattern: '2026',
|
||||
};
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
expect(result.llmContent).toContain('fileA.txt');
|
||||
expect(result.llmContent).not.toContain('fileB.txt');
|
||||
expect(result.llmContent).toContain('Copyright 2025 Google LLC');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -102,6 +102,16 @@ export interface RipGrepToolParams {
|
||||
*/
|
||||
include?: string;
|
||||
|
||||
/**
|
||||
* Optional: A regular expression pattern to exclude from the search results.
|
||||
*/
|
||||
exclude_pattern?: string;
|
||||
|
||||
/**
|
||||
* Optional: If true, only the file paths of the matches will be returned.
|
||||
*/
|
||||
names_only?: boolean;
|
||||
|
||||
/**
|
||||
* If true, searches case-sensitively. Defaults to false.
|
||||
*/
|
||||
@@ -244,6 +254,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
pattern: this.params.pattern,
|
||||
path: searchDirAbs,
|
||||
include: this.params.include,
|
||||
exclude_pattern: this.params.exclude_pattern,
|
||||
case_sensitive: this.params.case_sensitive,
|
||||
fixed_strings: this.params.fixed_strings,
|
||||
context: this.params.context,
|
||||
@@ -299,6 +310,16 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
|
||||
const wasTruncated = matchCount >= totalMaxMatches;
|
||||
|
||||
if (this.params.names_only) {
|
||||
const filePaths = Object.keys(matchesByFile).sort();
|
||||
let llmContent = `Found ${filePaths.length} files with matches for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n`;
|
||||
llmContent += filePaths.join('\n');
|
||||
return {
|
||||
llmContent: llmContent.trim(),
|
||||
returnDisplay: `Found ${filePaths.length} files${wasTruncated ? ' (limited)' : ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}${wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` : ''}:\n---\n`;
|
||||
|
||||
for (const filePath in matchesByFile) {
|
||||
@@ -330,6 +351,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
pattern: string;
|
||||
path: string;
|
||||
include?: string;
|
||||
exclude_pattern?: string;
|
||||
case_sensitive?: boolean;
|
||||
fixed_strings?: boolean;
|
||||
context?: number;
|
||||
@@ -344,6 +366,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
pattern,
|
||||
path: absolutePath,
|
||||
include,
|
||||
exclude_pattern,
|
||||
case_sensitive,
|
||||
fixed_strings,
|
||||
context,
|
||||
@@ -423,9 +446,18 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
});
|
||||
|
||||
let matchesFound = 0;
|
||||
let excludeRegex: RegExp | null = null;
|
||||
if (exclude_pattern) {
|
||||
excludeRegex = new RegExp(exclude_pattern, case_sensitive ? '' : 'i');
|
||||
}
|
||||
|
||||
for await (const line of generator) {
|
||||
const match = this.parseRipgrepJsonLine(line, absolutePath);
|
||||
if (match) {
|
||||
if (excludeRegex && excludeRegex.test(match.line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(match);
|
||||
if (!match.isContext) {
|
||||
matchesFound++;
|
||||
@@ -527,7 +559,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
super(
|
||||
RipGrepTool.Name,
|
||||
'SearchText',
|
||||
'Searches for a regular expression pattern within file contents. Max 100 matches.',
|
||||
'Searches for a regular expression pattern within file contents.',
|
||||
Kind.Search,
|
||||
{
|
||||
properties: {
|
||||
@@ -546,6 +578,16 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
"Glob pattern to filter files (e.g., '*.ts', 'src/**'). Recommended for large repositories to reduce noise. Defaults to all files if omitted.",
|
||||
type: 'string',
|
||||
},
|
||||
exclude_pattern: {
|
||||
description:
|
||||
'Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.',
|
||||
type: 'string',
|
||||
},
|
||||
names_only: {
|
||||
description:
|
||||
'Optional: If true, only the file paths of the matches will be returned, without the line content or line numbers. This is useful for gathering a list of files.',
|
||||
type: 'boolean',
|
||||
},
|
||||
case_sensitive: {
|
||||
description:
|
||||
'If true, search is case-sensitive. Defaults to false (ignore case) if omitted.',
|
||||
@@ -565,11 +607,13 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
description:
|
||||
'Show this many lines after each match (equivalent to grep -A). Defaults to 0 if omitted.',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
before: {
|
||||
description:
|
||||
'Show this many lines before each match (equivalent to grep -B). Defaults to 0 if omitted.',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
no_ignore: {
|
||||
description:
|
||||
@@ -618,6 +662,14 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
}
|
||||
}
|
||||
|
||||
if (params.exclude_pattern) {
|
||||
try {
|
||||
new RegExp(params.exclude_pattern);
|
||||
} catch (error) {
|
||||
return `Invalid exclude regular expression pattern provided: ${params.exclude_pattern}. Error: ${getErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
params.max_matches_per_file !== undefined &&
|
||||
params.max_matches_per_file < 1
|
||||
|
||||
Reference in New Issue
Block a user