mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-12 20:37:08 -07:00
feat(tools): implement tactful extraction rework for token efficiency
- Restore precision to read_file with 1-based start_line and end_line for Gemini 3. - Update tool descriptions to establish extraction hierarchy (rg > shell/sed > read_file). - Codify 'Be Token-Frugal' mandate in system prompt snippets. - Refine research workflow to allow context-based validation via search tools. - Update unit tests and verified build integrity.
This commit is contained in:
Generated
+24
-1
@@ -2253,6 +2253,7 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2433,6 +2434,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2466,6 +2468,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2834,6 +2837,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2867,6 +2871,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1"
|
||||
@@ -2919,6 +2924,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1",
|
||||
@@ -4134,6 +4140,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4428,6 +4435,7 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5420,6 +5428,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8429,6 +8438,7 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8969,6 +8979,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -10570,6 +10581,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -14354,6 +14366,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14364,6 +14377,7 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -16600,6 +16614,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16823,7 +16838,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16831,6 +16847,7 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -17003,6 +17020,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17210,6 +17228,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17323,6 +17342,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17335,6 +17355,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -18039,6 +18060,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -18335,6 +18357,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -315,7 +315,7 @@ export function renderFinalReminder(options?: FinalReminderOptions): string {
|
||||
if (!options) return '';
|
||||
return `
|
||||
# Final Reminder
|
||||
Your core function is efficient and safe assistance. Balance extreme conciseness with the crucial need for clarity, especially regarding safety and potential system modifications. Always prioritize user control and project conventions. Never make assumptions about the contents of files; instead use '${options.readFileToolName}' to ensure you aren't making broad assumptions. Finally, you are an agent - please keep going until the user's query is completely resolved.`.trim();
|
||||
Your core function is efficient and safe assistance. Balance extreme conciseness with the crucial need for clarity, especially regarding safety and potential system modifications. Always prioritize user control and project conventions. Never make assumptions about the contents of files; instead use appropriate search and extraction tools to ensure you aren't making broad assumptions. Finally, you are an agent - please keep going until the user's query is completely resolved.`.trim();
|
||||
}
|
||||
|
||||
export function renderUserMemory(memory?: string): string {
|
||||
@@ -417,10 +417,10 @@ function mandateContinueWork(interactive: boolean): string {
|
||||
|
||||
function workflowStepUnderstand(options: PrimaryWorkflowsOptions): string {
|
||||
if (options.enableCodebaseInvestigator) {
|
||||
return `1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the 'codebase_investigator' agent using the 'codebase_investigator' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly.`;
|
||||
return `1. **Understand & Strategize:** Think about the user's request and the relevant codebase context. When the task involves **complex refactoring, codebase exploration or system-wide analysis**, your **first and primary action** must be to delegate to the 'codebase_investigator' agent using the 'codebase_investigator' tool. Use it to build a comprehensive understanding of the code, its structure, and dependencies. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), you should use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly. Use '${GREP_TOOL_NAME}' with context or '${READ_FILE_TOOL_NAME}' with precise ranges to validate any assumptions you may have.`;
|
||||
}
|
||||
return `1. **Understand:** Think about the user's request and the relevant codebase context. Use '${GREP_TOOL_NAME}' and '${GLOB_TOOL_NAME}' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions.
|
||||
Use '${READ_FILE_TOOL_NAME}' to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to '${READ_FILE_TOOL_NAME}'.`;
|
||||
Use '${GREP_TOOL_NAME}' with context or '${READ_FILE_TOOL_NAME}' with precise ranges to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to these tools.`;
|
||||
}
|
||||
|
||||
function workflowStepPlan(options: PrimaryWorkflowsOptions): string {
|
||||
|
||||
@@ -261,6 +261,9 @@ export function renderOperationalGuidelines(
|
||||
|
||||
${shellEfficiencyGuidelines(options.enableShellEfficiency)}
|
||||
|
||||
## Token Efficiency
|
||||
- **Be Token-Frugal:** Every line of code or long tool output you pull into the conversation history increases the complexity and cost of the entire session. **Context persists.** Prefer surgical extraction tools (like \`grep_search\` with context or \`sed\`) over broad file reads.
|
||||
|
||||
## Tone and Style
|
||||
|
||||
- **Role:** A senior software engineer and collaborative peer programmer.
|
||||
@@ -447,9 +450,9 @@ function workflowStepResearch(options: PrimaryWorkflowsOptions): string {
|
||||
}
|
||||
|
||||
if (options.enableCodebaseInvestigator) {
|
||||
return `1. **Research:** Systematically map the codebase and validate assumptions. Utilize specialized sub-agents (e.g., \`codebase_investigator\`) as the primary mechanism for initial discovery when the task involves **complex refactoring, codebase exploration or system-wide analysis**. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly in parallel. Use '${READ_FILE_TOOL_NAME}' to validate all assumptions. **Prioritize empirical reproduction of reported issues to confirm the failure state.**${suggestion}`;
|
||||
return `1. **Research:** Systematically map the codebase and validate assumptions. Utilize specialized sub-agents (e.g., \`codebase_investigator\`) as the primary mechanism for initial discovery when the task involves **complex refactoring, codebase exploration or system-wide analysis**. For **simple, targeted searches** (like finding a specific function name, file path, or variable declaration), use '${GREP_TOOL_NAME}' or '${GLOB_TOOL_NAME}' directly in parallel. Use '${GREP_TOOL_NAME}' with context or '${READ_FILE_TOOL_NAME}' with precise ranges to validate all assumptions. **Prioritize empirical reproduction of reported issues to confirm the failure state.**${suggestion}`;
|
||||
}
|
||||
return `1. **Research:** Systematically map the codebase and validate assumptions. Use '${GREP_TOOL_NAME}' and '${GLOB_TOOL_NAME}' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use '${READ_FILE_TOOL_NAME}' to validate all assumptions. **Prioritize empirical reproduction of reported issues to confirm the failure state.**${suggestion}`;
|
||||
return `1. **Research:** Systematically map the codebase and validate assumptions. Use '${GREP_TOOL_NAME}' and '${GLOB_TOOL_NAME}' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use '${GREP_TOOL_NAME}' with context or '${READ_FILE_TOOL_NAME}' with precise ranges to validate all assumptions. **Prioritize empirical reproduction of reported issues to confirm the failure state.**${suggestion}`;
|
||||
}
|
||||
|
||||
function workflowStepStrategy(options: PrimaryWorkflowsOptions): string {
|
||||
|
||||
@@ -268,7 +268,7 @@ export class GlobTool extends BaseDeclarativeTool<GlobToolParams, ToolResult> {
|
||||
super(
|
||||
GlobTool.Name,
|
||||
'FindFiles',
|
||||
'Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.',
|
||||
'Finds files matching glob patterns (e.g., `src/**/*.ts`). Results are sorted by modification time (newest first). Ideal for structural discovery and identifying recent changes. **Avoid using this tool just to list files before reading them;** if you know the symbols you need, use `grep_search` directly.',
|
||||
Kind.Search,
|
||||
{
|
||||
properties: {
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('ReadFileTool', () => {
|
||||
expect(schema.description).toContain('limit');
|
||||
});
|
||||
|
||||
it('should NOT include pagination parameters for Gemini 3', () => {
|
||||
it('should include line range parameters for Gemini 3', () => {
|
||||
vi.mocked(tool['config'].getActiveModel).mockReturnValue(
|
||||
'gemini-3-pro-preview',
|
||||
);
|
||||
@@ -105,9 +105,11 @@ describe('ReadFileTool', () => {
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
).properties;
|
||||
expect(properties).toHaveProperty('start_line');
|
||||
expect(properties).toHaveProperty('end_line');
|
||||
expect(properties).not.toHaveProperty('offset');
|
||||
expect(properties).not.toHaveProperty('limit');
|
||||
expect(schema.description).toContain('grep');
|
||||
expect(schema.description).toContain('grep_search');
|
||||
expect(schema.description).toContain('sed');
|
||||
});
|
||||
});
|
||||
@@ -184,6 +186,33 @@ describe('ReadFileTool', () => {
|
||||
'Limit must be a positive number',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if start_line is less than 1', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
file_path: path.join(tempRootDir, 'test.txt'),
|
||||
start_line: 0,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow('start_line must be at least 1');
|
||||
});
|
||||
|
||||
it('should throw error if end_line is less than 1', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
file_path: path.join(tempRootDir, 'test.txt'),
|
||||
end_line: 0,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow('end_line must be at least 1');
|
||||
});
|
||||
|
||||
it('should throw error if start_line is greater than end_line', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
file_path: path.join(tempRootDir, 'test.txt'),
|
||||
start_line: 10,
|
||||
end_line: 5,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
'start_line cannot be greater than end_line',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDescription', () => {
|
||||
@@ -456,6 +485,31 @@ describe('ReadFileTool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should support start_line and end_line for text files', async () => {
|
||||
const filePath = path.join(tempRootDir, 'lines.txt');
|
||||
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
|
||||
const fileContent = lines.join('\n');
|
||||
await fsp.writeFile(filePath, fileContent, 'utf-8');
|
||||
|
||||
const params: ReadFileToolParams = {
|
||||
file_path: filePath,
|
||||
start_line: 5,
|
||||
end_line: 10,
|
||||
};
|
||||
const invocation = tool.build(params);
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'IMPORTANT: The file content has been truncated',
|
||||
);
|
||||
expect(result.llmContent).toContain(
|
||||
'Status: Showing lines 5-10 of 20 total lines',
|
||||
);
|
||||
expect(result.llmContent).toContain('Line 5');
|
||||
expect(result.llmContent).toContain('Line 10');
|
||||
expect(result.returnDisplay).toBe('Read lines 5-10 of 20 from lines.txt');
|
||||
});
|
||||
|
||||
it('should use first-2000-lines truncation and shell-tool guidance for Gemini 3', async () => {
|
||||
vi.mocked(tool['config'].getActiveModel).mockReturnValue(
|
||||
'gemini-3-pro-preview',
|
||||
@@ -472,7 +526,7 @@ describe('ReadFileTool', () => {
|
||||
const invocation = tool.build(params);
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain('Showing the first 2000 lines');
|
||||
expect(result.llmContent).toContain('Showing lines 1-2000');
|
||||
expect(result.llmContent).toContain("use the 'grep_search' tool");
|
||||
expect(result.llmContent).toContain('Line 1');
|
||||
expect(result.llmContent).not.toContain('Line 2001');
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface ReadFileToolParams {
|
||||
file_path: string;
|
||||
|
||||
/**
|
||||
* The line number to start reading from (optional)
|
||||
* The line number to start reading from (optional, 0-based)
|
||||
*/
|
||||
offset?: number;
|
||||
|
||||
@@ -45,6 +45,16 @@ export interface ReadFileToolParams {
|
||||
* The number of lines to read (optional)
|
||||
*/
|
||||
limit?: number;
|
||||
|
||||
/**
|
||||
* The line number to start reading from (optional, 1-based)
|
||||
*/
|
||||
start_line?: number;
|
||||
|
||||
/**
|
||||
* The line number to end reading at (optional, 1-based, inclusive)
|
||||
*/
|
||||
end_line?: number;
|
||||
}
|
||||
|
||||
class ReadFileToolInvocation extends BaseToolInvocation<
|
||||
@@ -75,7 +85,12 @@ class ReadFileToolInvocation extends BaseToolInvocation<
|
||||
}
|
||||
|
||||
override toolLocations(): ToolLocation[] {
|
||||
return [{ path: this.resolvedPath, line: this.params.offset }];
|
||||
return [
|
||||
{
|
||||
path: this.resolvedPath,
|
||||
line: this.params.start_line ?? this.params.offset,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async execute(): Promise<ToolResult> {
|
||||
@@ -102,6 +117,8 @@ class ReadFileToolInvocation extends BaseToolInvocation<
|
||||
this.config.getFileSystemService(),
|
||||
isGemini3 ? undefined : this.params.offset,
|
||||
isGemini3 ? undefined : this.params.limit,
|
||||
this.params.start_line,
|
||||
this.params.end_line,
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
@@ -117,21 +134,21 @@ class ReadFileToolInvocation extends BaseToolInvocation<
|
||||
|
||||
let llmContent: PartUnion;
|
||||
if (result.isTruncated) {
|
||||
const [start, end] = result.linesShown!;
|
||||
const total = result.originalLineCount!;
|
||||
|
||||
if (isGemini3) {
|
||||
const total = result.originalLineCount!;
|
||||
llmContent = `
|
||||
IMPORTANT: The file content has been truncated.
|
||||
Status: Showing the first 2000 lines of ${total} total lines.
|
||||
Status: Showing lines ${start}-${end} of ${total} total lines.
|
||||
Action:
|
||||
- To find specific patterns, use the 'grep_search' tool.
|
||||
- To read a specific line range, use 'run_shell_command' with 'sed'. For example: 'sed -n "500,600p" ${this.params.file_path}'.
|
||||
- For surgical extraction of code blocks (especially ranges larger than 2,000 lines), prefer 'run_shell_command' with 'sed'. For example: 'sed -n "500,600p" ${this.params.file_path}'.
|
||||
- You can also use other Unix utilities like 'awk', 'head', or 'tail' via 'run_shell_command'.
|
||||
|
||||
--- FILE CONTENT (truncated) ---
|
||||
${result.llmContent}`;
|
||||
} else {
|
||||
const [start, end] = result.linesShown!;
|
||||
const total = result.originalLineCount!;
|
||||
const nextOffset = this.params.offset
|
||||
? this.params.offset + end - start + 1
|
||||
: end;
|
||||
@@ -236,7 +253,17 @@ export class ReadFileTool extends BaseDeclarativeTool<
|
||||
},
|
||||
};
|
||||
|
||||
if (!isGemini3) {
|
||||
if (isGemini3) {
|
||||
properties['start_line'] = {
|
||||
description: 'Optional: The 1-based line number to start reading from.',
|
||||
type: 'number',
|
||||
};
|
||||
properties['end_line'] = {
|
||||
description:
|
||||
'Optional: The 1-based line number to end reading at (inclusive).',
|
||||
type: 'number',
|
||||
};
|
||||
} else {
|
||||
properties['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.",
|
||||
@@ -252,8 +279,8 @@ export class ReadFileTool extends BaseDeclarativeTool<
|
||||
return {
|
||||
name: this.name,
|
||||
description: isGemini3
|
||||
? `Reads and returns the content of a specified file. If the file is large (exceeding 2000 lines), the content will be truncated to the first 2000 lines. The tool's response will clearly indicate if truncation has occurred. To examine specific sections of large files, use the 'run_shell_command' tool with standard Unix utilities like 'grep', 'sed', 'awk', 'head', or 'tail'. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), audio files (MP3, WAV, AIFF, AAC, OGG, FLAC), and PDF files.`
|
||||
: `Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), audio files (MP3, WAV, AIFF, AAC, OGG, FLAC), and PDF files. For text files, it can read specific line ranges.`,
|
||||
? `Reads a specific range of a file (up to 2,000 lines). **Important:** For high token efficiency, avoid reading large files in their entirety. Use 'grep_search' to find symbols or 'run_shell_command' with 'sed' for surgical block extraction instead of broad file reads. Handles text, images, audio, and PDF files.`
|
||||
: `Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images, audio, and PDF files. For text files, it can read specific line ranges.`,
|
||||
parametersJsonSchema: {
|
||||
properties,
|
||||
required: ['file_path'],
|
||||
@@ -285,6 +312,19 @@ export class ReadFileTool extends BaseDeclarativeTool<
|
||||
if (params.limit !== undefined && params.limit <= 0) {
|
||||
return 'Limit must be a positive number';
|
||||
}
|
||||
if (params.start_line !== undefined && params.start_line < 1) {
|
||||
return 'start_line must be at least 1';
|
||||
}
|
||||
if (params.end_line !== undefined && params.end_line < 1) {
|
||||
return 'end_line must be at least 1';
|
||||
}
|
||||
if (
|
||||
params.start_line !== undefined &&
|
||||
params.end_line !== undefined &&
|
||||
params.start_line > params.end_line
|
||||
) {
|
||||
return 'start_line cannot be greater than end_line';
|
||||
}
|
||||
|
||||
const fileFilteringOptions = this.config.getFileFilteringOptions();
|
||||
if (
|
||||
|
||||
@@ -499,7 +499,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
|
||||
super(
|
||||
RipGrepTool.Name,
|
||||
'SearchText',
|
||||
'Searches for a regular expression pattern within file contents. Max 100 matches.',
|
||||
'FAST regular expression search. This is the **primary discovery tool** for locating code. Use context parameters (`context`, `after`, `before`) to read code surrounding matches in a single turn, often eliminating the need for a separate `read_file` call. (max 100 matches).',
|
||||
Kind.Search,
|
||||
{
|
||||
properties: {
|
||||
|
||||
@@ -463,16 +463,19 @@ function getShellToolDescription(enableInteractiveShell: boolean): string {
|
||||
Background PIDs: Only included if background processes were started.
|
||||
Process Group PGID: Only included if available.`;
|
||||
|
||||
const efficiencyGuidance =
|
||||
"Output is limited to the last 2,000 lines. **This is the preferred tool for surgical extraction of code blocks.** Use `sed -n '50,100p' file` for ranges, or `sed -n '/class X/,/^}/p' file` for semantic blocks. Avoid 'cat' on large files to prevent context bloat.";
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
const backgroundInstructions = enableInteractiveShell
|
||||
? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use PowerShell background constructs.'
|
||||
: 'Command can start background processes using PowerShell constructs such as `Start-Process -NoNewWindow` or `Start-Job`.';
|
||||
return `This tool executes a given shell command as \`powershell.exe -NoProfile -Command <command>\`. ${backgroundInstructions}${returnedInfo}`;
|
||||
return `This tool executes a given shell command as \`powershell.exe -NoProfile -Command <command>\`. ${backgroundInstructions} ${efficiencyGuidance}${returnedInfo}`;
|
||||
} else {
|
||||
const backgroundInstructions = enableInteractiveShell
|
||||
? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use `&` to background commands.'
|
||||
: 'Command can start background processes using `&`.';
|
||||
return `This tool executes a given shell command as \`bash -c <command>\`. ${backgroundInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.${returnedInfo}`;
|
||||
return `This tool executes a given shell command as \`bash -c <command>\`. ${backgroundInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`. ${efficiencyGuidance}${returnedInfo}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -950,6 +950,28 @@ describe('fileUtils', () => {
|
||||
expect(result.linesShown).toEqual([6, 10]);
|
||||
});
|
||||
|
||||
it('should support startLine and endLine for text files', async () => {
|
||||
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
|
||||
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
|
||||
|
||||
const result = await processSingleFileContent(
|
||||
testTextFilePath,
|
||||
tempRootDir,
|
||||
new StandardFileSystemService(),
|
||||
undefined,
|
||||
undefined,
|
||||
5,
|
||||
10,
|
||||
); // Read lines 5-10 (1-based)
|
||||
const expectedContent = lines.slice(4, 10).join('\n');
|
||||
|
||||
expect(result.llmContent).toBe(expectedContent);
|
||||
expect(result.returnDisplay).toBe('Read lines 5-10 of 20 from test.txt');
|
||||
expect(result.isTruncated).toBe(true);
|
||||
expect(result.originalLineCount).toBe(20);
|
||||
expect(result.linesShown).toEqual([5, 10]);
|
||||
});
|
||||
|
||||
it('should identify truncation when reading the end of a file', async () => {
|
||||
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
|
||||
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
|
||||
|
||||
@@ -399,8 +399,11 @@ export interface ProcessedFileReadResult {
|
||||
* Reads and processes a single file, handling text, images, and PDFs.
|
||||
* @param filePath Absolute path to the file.
|
||||
* @param rootDirectory Absolute path to the project root for relative path display.
|
||||
* @param _fileSystemService Placeholder for backward compatibility.
|
||||
* @param offset Optional offset for text files (0-based line number).
|
||||
* @param limit Optional limit for text files (number of lines to read).
|
||||
* @param startLine Optional 1-based line number to start reading from.
|
||||
* @param endLine Optional 1-based line number to end reading at (inclusive).
|
||||
* @returns ProcessedFileReadResult object.
|
||||
*/
|
||||
export async function processSingleFileContent(
|
||||
@@ -410,6 +413,8 @@ export async function processSingleFileContent(
|
||||
_fileSystemService?: FileSystemService,
|
||||
offset?: number,
|
||||
limit?: number,
|
||||
startLine?: number,
|
||||
endLine?: number,
|
||||
): Promise<ProcessedFileReadResult> {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
@@ -475,14 +480,26 @@ export async function processSingleFileContent(
|
||||
const lines = content.split('\n');
|
||||
const originalLineCount = lines.length;
|
||||
|
||||
const startLine = offset || 0;
|
||||
const effectiveLimit =
|
||||
limit === undefined ? DEFAULT_MAX_LINES_TEXT_FILE : limit;
|
||||
// Ensure endLine does not exceed originalLineCount
|
||||
const endLine = Math.min(startLine + effectiveLimit, originalLineCount);
|
||||
// Ensure selectedLines doesn't try to slice beyond array bounds if startLine is too high
|
||||
const actualStartLine = Math.min(startLine, originalLineCount);
|
||||
const selectedLines = lines.slice(actualStartLine, endLine);
|
||||
let sliceStart = 0;
|
||||
let sliceEnd = originalLineCount;
|
||||
|
||||
if (startLine !== undefined || endLine !== undefined) {
|
||||
sliceStart = startLine ? Math.max(0, startLine - 1) : 0;
|
||||
sliceEnd = endLine
|
||||
? Math.min(endLine, originalLineCount)
|
||||
: originalLineCount;
|
||||
} else if (offset !== undefined || limit !== undefined) {
|
||||
sliceStart = offset || 0;
|
||||
const effectiveLimit =
|
||||
limit === undefined ? DEFAULT_MAX_LINES_TEXT_FILE : limit;
|
||||
sliceEnd = Math.min(sliceStart + effectiveLimit, originalLineCount);
|
||||
} else {
|
||||
sliceEnd = Math.min(DEFAULT_MAX_LINES_TEXT_FILE, originalLineCount);
|
||||
}
|
||||
|
||||
// Ensure selectedLines doesn't try to slice beyond array bounds
|
||||
const actualStart = Math.min(sliceStart, originalLineCount);
|
||||
const selectedLines = lines.slice(actualStart, sliceEnd);
|
||||
|
||||
let linesWereTruncatedInLength = false;
|
||||
const formattedLines = selectedLines.map((line) => {
|
||||
@@ -495,17 +512,18 @@ export async function processSingleFileContent(
|
||||
return line;
|
||||
});
|
||||
|
||||
const contentRangeTruncated =
|
||||
startLine > 0 || endLine < originalLineCount;
|
||||
const isTruncated = contentRangeTruncated || linesWereTruncatedInLength;
|
||||
const isTruncated =
|
||||
actualStart > 0 ||
|
||||
sliceEnd < originalLineCount ||
|
||||
linesWereTruncatedInLength;
|
||||
const llmContent = formattedLines.join('\n');
|
||||
|
||||
// By default, return nothing to streamline the common case of a successful read_file.
|
||||
let returnDisplay = '';
|
||||
if (contentRangeTruncated) {
|
||||
if (actualStart > 0 || sliceEnd < originalLineCount) {
|
||||
returnDisplay = `Read lines ${
|
||||
actualStartLine + 1
|
||||
}-${endLine} of ${originalLineCount} from ${relativePathForDisplay}`;
|
||||
actualStart + 1
|
||||
}-${sliceEnd} of ${originalLineCount} from ${relativePathForDisplay}`;
|
||||
if (linesWereTruncatedInLength) {
|
||||
returnDisplay += ' (some lines were shortened)';
|
||||
}
|
||||
@@ -518,7 +536,7 @@ export async function processSingleFileContent(
|
||||
returnDisplay,
|
||||
isTruncated,
|
||||
originalLineCount,
|
||||
linesShown: [actualStartLine + 1, endLine],
|
||||
linesShown: [actualStart + 1, sliceEnd],
|
||||
};
|
||||
}
|
||||
case 'image':
|
||||
|
||||
Reference in New Issue
Block a user