feat(core): migrate read_file to 1-based start_line/end_line parameters (#19526)

This commit is contained in:
Adam Weidman
2026-02-20 17:59:18 -05:00
committed by GitHub
parent 58d637f919
commit 547f5d45f5
10 changed files with 140 additions and 106 deletions

View File

@@ -549,7 +549,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -705,7 +705,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -827,7 +827,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -1422,7 +1422,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -1574,7 +1574,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -1717,7 +1717,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -1860,7 +1860,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -1999,7 +1999,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -2138,7 +2138,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -2276,7 +2276,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -2656,7 +2656,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -2795,7 +2795,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -3046,7 +3046,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>
@@ -3185,7 +3185,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>

View File

@@ -192,7 +192,7 @@ Use the following guidelines to optimize your search and read patterns.
- **Searching:** utilize search tools like ${GREP_TOOL_NAME} and ${GLOB_TOOL_NAME} with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters).
- **Searching and editing:** utilize search tools like ${GREP_TOOL_NAME} with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches.
- **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety.
- **Large files:** utilize search tools like ${GREP_TOOL_NAME} and/or ${READ_FILE_TOOL_NAME} called in parallel with an offset and a limit to reduce the impact on context. Minmize extra turns, unless unavoidable due to the file being too large.
- **Large files:** utilize search tools like ${GREP_TOOL_NAME} and/or ${READ_FILE_TOOL_NAME} called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large.
- **Navigating:** read the minimum required to not require additional turns spent reading the file.
</examples>

View File

@@ -1,5 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ReadFileTool > getSchema > should return the base schema when no modelId is provided 1`] = `"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."`;
exports[`ReadFileTool > getSchema > should return the base schema when no modelId is provided 1`] = `"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 'start_line' and 'end_line' 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."`;
exports[`ReadFileTool > getSchema > should return the schema from the resolver when modelId is provided 1`] = `"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."`;
exports[`ReadFileTool > getSchema > should return the schema from the resolver when modelId is provided 1`] = `"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 'start_line' and 'end_line' 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."`;

View File

@@ -411,20 +411,20 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snapshot for tool: read_file 1`] = `
{
"description": "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.",
"description": "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 'start_line' and 'end_line' 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.",
"name": "read_file",
"parametersJsonSchema": {
"properties": {
"end_line": {
"description": "Optional: The 1-based line number to end reading at (inclusive).",
"type": "number",
},
"file_path": {
"description": "The path to the file to read.",
"type": "string",
},
"limit": {
"description": "Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit).",
"type": "number",
},
"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.",
"start_line": {
"description": "Optional: The 1-based line number to start reading from.",
"type": "number",
},
},
@@ -1200,20 +1200,20 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > snapshot for tool: read_file 1`] = `
{
"description": "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.",
"description": "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 'start_line' and 'end_line' 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.",
"name": "read_file",
"parametersJsonSchema": {
"properties": {
"end_line": {
"description": "Optional: The 1-based line number to end reading at (inclusive).",
"type": "number",
},
"file_path": {
"description": "The path to the file to read.",
"type": "string",
},
"limit": {
"description": "Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit).",
"type": "number",
},
"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.",
"start_line": {
"description": "Optional: The 1-based line number to start reading from.",
"type": "number",
},
},

View File

@@ -35,7 +35,7 @@ import {
export const DEFAULT_LEGACY_SET: CoreToolSet = {
read_file: {
name: READ_FILE_TOOL_NAME,
description: `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.`,
description: `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 'start_line' and 'end_line' 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.`,
parametersJsonSchema: {
type: 'object',
properties: {
@@ -43,14 +43,14 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = {
description: 'The path to the file to read.',
type: 'string',
},
offset: {
start_line: {
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: The 1-based line number to start reading from.',
type: 'number',
},
limit: {
end_line: {
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: The 1-based line number to end reading at (inclusive).',
type: 'number',
},
},

View File

@@ -38,7 +38,7 @@ import {
export const GEMINI_3_SET: CoreToolSet = {
read_file: {
name: READ_FILE_TOOL_NAME,
description: `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.`,
description: `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 'start_line' and 'end_line' 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.`,
parametersJsonSchema: {
type: 'object',
properties: {
@@ -46,14 +46,14 @@ export const GEMINI_3_SET: CoreToolSet = {
description: 'The path to the file to read.',
type: 'string',
},
offset: {
start_line: {
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: The 1-based line number to start reading from.',
type: 'number',
},
limit: {
end_line: {
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: The 1-based line number to end reading at (inclusive).',
type: 'number',
},
},

View File

@@ -130,29 +130,36 @@ describe('ReadFileTool', () => {
);
});
it('should throw error if offset is negative', () => {
it('should throw error if start_line is less than 1', () => {
const params: ReadFileToolParams = {
file_path: path.join(tempRootDir, 'test.txt'),
offset: -1,
start_line: 0,
};
expect(() => tool.build(params)).toThrow(
'Offset must be a non-negative number',
);
expect(() => tool.build(params)).toThrow('start_line must be at least 1');
});
it('should throw error if limit is zero or negative', () => {
it('should throw error if end_line is less than 1', () => {
const params: ReadFileToolParams = {
file_path: path.join(tempRootDir, 'test.txt'),
limit: 0,
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(
'Limit must be a positive number',
'start_line cannot be greater than end_line',
);
});
});
describe('getDescription', () => {
it('should return relative path without limit/offset', () => {
it('should return relative path without ranges', () => {
const subDir = path.join(tempRootDir, 'sub', 'dir');
const params: ReadFileToolParams = {
file_path: path.join(subDir, 'file.txt'),
@@ -393,7 +400,7 @@ describe('ReadFileTool', () => {
expect(result.returnDisplay).toBe('');
});
it('should support offset and limit for text files', async () => {
it('should support start_line and end_line for text files', async () => {
const filePath = path.join(tempRootDir, 'paginated.txt');
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
const fileContent = lines.join('\n');
@@ -401,8 +408,8 @@ describe('ReadFileTool', () => {
const params: ReadFileToolParams = {
file_path: filePath,
offset: 5, // Start from line 6
limit: 3,
start_line: 6,
end_line: 8,
};
const invocation = tool.build(params);
@@ -569,6 +576,10 @@ describe('ReadFileTool', () => {
const schema = tool.getSchema();
expect(schema.name).toBe(ReadFileTool.Name);
expect(schema.description).toMatchSnapshot();
expect(
(schema.parametersJsonSchema as { properties: Record<string, unknown> })
.properties,
).not.toHaveProperty('offset');
});
it('should return the schema from the resolver when modelId is provided', () => {

View File

@@ -36,14 +36,14 @@ export interface ReadFileToolParams {
file_path: string;
/**
* The line number to start reading from (optional)
* The line number to start reading from (optional, 1-based)
*/
offset?: number;
start_line?: number;
/**
* The number of lines to read (optional)
* The line number to end reading at (optional, 1-based, inclusive)
*/
limit?: number;
end_line?: number;
}
class ReadFileToolInvocation extends BaseToolInvocation<
@@ -74,7 +74,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,
},
];
}
async execute(): Promise<ToolResult> {
@@ -97,8 +102,8 @@ class ReadFileToolInvocation extends BaseToolInvocation<
this.resolvedPath,
this.config.getTargetDir(),
this.config.getFileSystemService(),
this.params.offset,
this.params.limit,
this.params.start_line,
this.params.end_line,
);
if (result.error) {
@@ -116,13 +121,11 @@ class ReadFileToolInvocation extends BaseToolInvocation<
if (result.isTruncated) {
const [start, end] = result.linesShown!;
const total = result.originalLineCount!;
const nextOffset = this.params.offset
? this.params.offset + end - start + 1
: end;
llmContent = `
IMPORTANT: The file content has been truncated.
Status: Showing lines ${start}-${end} of ${total} total lines.
Action: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: ${nextOffset}.
Action: To read more of the file, you can use the 'start_line' and 'end_line' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use start_line: ${end + 1}.
--- FILE CONTENT (truncated) ---
${result.llmContent}`;
@@ -207,11 +210,18 @@ export class ReadFileTool extends BaseDeclarativeTool<
return validationError;
}
if (params.offset !== undefined && params.offset < 0) {
return 'Offset must be a non-negative number';
if (params.start_line !== undefined && params.start_line < 1) {
return 'start_line must be at least 1';
}
if (params.limit !== undefined && params.limit <= 0) {
return 'Limit must be a positive number';
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();

View File

@@ -930,7 +930,7 @@ describe('fileUtils', () => {
expect(result.returnDisplay).toContain('Path is a directory');
});
it('should paginate text files correctly (offset and limit)', async () => {
it('should paginate text files correctly (startLine and endLine)', async () => {
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
@@ -938,9 +938,9 @@ describe('fileUtils', () => {
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
5,
5,
); // Read lines 6-10
6,
10,
); // Read lines 6-10 (1-based)
const expectedContent = lines.slice(5, 10).join('\n');
expect(result.llmContent).toBe(expectedContent);
@@ -954,13 +954,13 @@ describe('fileUtils', () => {
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
// Read from line 11 to 20. The start is not 0, so it's truncated.
// Read from line 11 to 20. The start is not 1, so it's truncated.
const result = await processSingleFileContent(
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
10,
10,
11,
20,
);
const expectedContent = lines.slice(10, 20).join('\n');
@@ -971,7 +971,7 @@ describe('fileUtils', () => {
expect(result.linesShown).toEqual([11, 20]);
});
it('should handle limit exceeding file length', async () => {
it('should handle endLine exceeding file length', async () => {
const lines = ['Line 1', 'Line 2'];
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
@@ -979,7 +979,7 @@ describe('fileUtils', () => {
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
0,
1,
10,
);
const expectedContent = lines.join('\n');
@@ -1015,21 +1015,22 @@ describe('fileUtils', () => {
expect(result.isTruncated).toBe(true);
});
it('should truncate when line count exceeds the limit', async () => {
const lines = Array.from({ length: 11 }, (_, i) => `Line ${i + 1}`);
it('should truncate when line count exceeds the default limit', async () => {
const lines = Array.from({ length: 2500 }, (_, i) => `Line ${i + 1}`);
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
// Read 5 lines, but there are 11 total
// No ranges provided, should use default limit (2000)
const result = await processSingleFileContent(
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
0,
5,
);
expect(result.isTruncated).toBe(true);
expect(result.returnDisplay).toBe('Read lines 1-5 of 11 from test.txt');
expect(result.returnDisplay).toBe(
'Read lines 1-2000 of 2500 from test.txt',
);
expect(result.linesShown).toEqual([1, 2000]);
});
it('should truncate when a line length exceeds the character limit', async () => {
@@ -1043,7 +1044,7 @@ describe('fileUtils', () => {
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
0,
1,
11,
);
@@ -1069,7 +1070,7 @@ describe('fileUtils', () => {
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
0,
1,
10,
);
expect(result.isTruncated).toBe(true);

View File

@@ -53,7 +53,7 @@ export async function loadWasmBinary(
}
// Constants for text file processing
const DEFAULT_MAX_LINES_TEXT_FILE = 2000;
export const DEFAULT_MAX_LINES_TEXT_FILE = 2000;
const MAX_LINE_LENGTH_TEXT_FILE = 2000;
// Default values for encoding and separator format
@@ -399,16 +399,17 @@ 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 offset Optional offset for text files (0-based line number).
* @param limit Optional limit for text files (number of lines to read).
* @param _fileSystemService Currently unused in this function; kept for signature stability.
* @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(
filePath: string,
rootDirectory: string,
fileSystemService: FileSystemService,
offset?: number,
limit?: number,
_fileSystemService: FileSystemService,
startLine?: number,
endLine?: number,
): Promise<ProcessedFileReadResult> {
try {
if (!fs.existsSync(filePath)) {
@@ -474,14 +475,24 @@ 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 ? startLine - 1 : 0;
sliceEnd = endLine
? Math.min(endLine, originalLineCount)
: Math.min(
sliceStart + DEFAULT_MAX_LINES_TEXT_FILE,
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) => {
@@ -494,17 +505,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)';
}
@@ -517,7 +529,7 @@ export async function processSingleFileContent(
returnDisplay,
isTruncated,
originalLineCount,
linesShown: [actualStartLine + 1, endLine],
linesShown: [actualStart + 1, sliceEnd],
};
}
case 'image':