Update prompt and grep tool definition to limit context size (#18780)

This commit is contained in:
Christian Gunderman
2026-02-11 19:20:51 +00:00
committed by GitHub
parent 2dac98dc8d
commit 2a08456ed0
9 changed files with 414 additions and 1 deletions
@@ -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.',
+35
View File
@@ -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', () => {
+45
View File
@@ -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
+79
View File
@@ -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');
});
});
});
+53 -1
View File
@@ -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