bug(core): Prompt engineering for truncated read_file. (#5161)

This commit is contained in:
joshualitt 2025-08-06 13:52:04 -07:00 committed by GitHub
parent ad5d2af4e3
commit 43510ed212
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 77 additions and 36 deletions

View File

@ -222,7 +222,7 @@ describe('ReadFileTool', () => {
});
});
it('should pass offset and limit to read a slice of a text file', async () => {
it('should return a structured message when a slice of a text file is read', async () => {
const filePath = path.join(tempRootDir, 'paginated.txt');
const fileContent = Array.from(
{ length: 20 },
@ -240,15 +240,22 @@ describe('ReadFileTool', () => {
ToolResult
>;
expect(await invocation.execute(abortSignal)).toEqual({
llmContent: [
'[File content truncated: showing lines 6-8 of 20 total lines. Use offset/limit parameters to view more.]',
'Line 6',
'Line 7',
'Line 8',
].join('\n'),
returnDisplay: 'Read lines 6-8 of 20 from paginated.txt',
});
const result = await invocation.execute(abortSignal);
const expectedLlmContent = `
IMPORTANT: The file content has been truncated.
Status: Showing lines 6-8 of 20 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: 8.
--- FILE CONTENT (truncated) ---
Line 6
Line 7
Line 8`;
expect(result.llmContent).toEqual(expectedLlmContent);
expect(result.returnDisplay).toBe(
'Read lines 6-8 of 20 from paginated.txt',
);
});
describe('with .geminiignore', () => {

View File

@ -14,7 +14,7 @@ import {
ToolLocation,
ToolResult,
} from './tools.js';
import { Type } from '@google/genai';
import { PartUnion, Type } from '@google/genai';
import {
processSingleFileContent,
getSpecificMimeType,
@ -84,6 +84,24 @@ class ReadFileToolInvocation
};
}
let llmContent: PartUnion;
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}.
--- FILE CONTENT (truncated) ---
${result.llmContent}`;
} else {
llmContent = result.llmContent || '';
}
const lines =
typeof result.llmContent === 'string'
? result.llmContent.split('\n').length
@ -98,7 +116,7 @@ class ReadFileToolInvocation
);
return {
llmContent: result.llmContent || '',
llmContent,
returnDisplay: result.returnDisplay || '',
};
}
@ -117,7 +135,7 @@ export class ReadFileTool extends BaseDeclarativeTool<
super(
ReadFileTool.Name,
'ReadFile',
'Reads and returns the content of a specified file from the local filesystem. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges.',
`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), and PDF files. For text files, it can read specific line ranges.`,
Icon.FileSearch,
{
properties: {

View File

@ -476,6 +476,34 @@ describe('ReadManyFilesTool', () => {
fs.rmSync(tempDir1, { recursive: true, force: true });
fs.rmSync(tempDir2, { recursive: true, force: true });
});
it('should add a warning for truncated files', async () => {
createFile('file1.txt', 'Content1');
// Create a file that will be "truncated" by making it long
const longContent = Array.from({ length: 2500 }, (_, i) => `L${i}`).join(
'\n',
);
createFile('large-file.txt', longContent);
const params = { paths: ['*.txt'] };
const result = await tool.execute(params, new AbortController().signal);
const content = result.llmContent as string[];
const normalFileContent = content.find((c) => c.includes('file1.txt'));
const truncatedFileContent = content.find((c) =>
c.includes('large-file.txt'),
);
expect(normalFileContent).not.toContain(
'[WARNING: This file was truncated.',
);
expect(truncatedFileContent).toContain(
"[WARNING: This file was truncated. To view the full content, use the 'read_file' tool on this specific file.]",
);
// Check that the actual content is still there but truncated
expect(truncatedFileContent).toContain('L200');
expect(truncatedFileContent).not.toContain('L2400');
});
});
describe('Batch Processing', () => {

View File

@ -524,11 +524,15 @@ Use this tool when the user's query implies needing the content of several files
'{filePath}',
filePath,
);
contentParts.push(
`${separator}\n\n${fileReadResult.llmContent}\n\n`,
);
let fileContentForLlm = '';
if (fileReadResult.isTruncated) {
fileContentForLlm += `[WARNING: This file was truncated. To view the full content, use the 'read_file' tool on this specific file.]\n\n`;
}
fileContentForLlm += fileReadResult.llmContent;
contentParts.push(`${separator}\n\n${fileContentForLlm}\n\n`);
} else {
contentParts.push(fileReadResult.llmContent); // This is a Part for image/pdf
// This is a Part for image/pdf, which we don't add the separator to.
contentParts.push(fileReadResult.llmContent);
}
processedFilesRelativePaths.push(relativePathForDisplay);

View File

@ -420,10 +420,7 @@ describe('fileUtils', () => {
); // Read lines 6-10
const expectedContent = lines.slice(5, 10).join('\n');
expect(result.llmContent).toContain(expectedContent);
expect(result.llmContent).toContain(
'[File content truncated: showing lines 6-10 of 20 total lines. Use offset/limit parameters to view more.]',
);
expect(result.llmContent).toBe(expectedContent);
expect(result.returnDisplay).toBe('Read lines 6-10 of 20 from test.txt');
expect(result.isTruncated).toBe(true);
expect(result.originalLineCount).toBe(20);
@ -444,9 +441,6 @@ describe('fileUtils', () => {
const expectedContent = lines.slice(10, 20).join('\n');
expect(result.llmContent).toContain(expectedContent);
expect(result.llmContent).toContain(
'[File content truncated: showing lines 11-20 of 20 total lines. Use offset/limit parameters to view more.]',
);
expect(result.returnDisplay).toBe('Read lines 11-20 of 20 from test.txt');
expect(result.isTruncated).toBe(true); // This is the key check for the bug
expect(result.originalLineCount).toBe(20);
@ -489,9 +483,6 @@ describe('fileUtils', () => {
longLine.substring(0, 2000) + '... [truncated]',
);
expect(result.llmContent).toContain('Another short line');
expect(result.llmContent).toContain(
'[File content partially truncated: some lines exceeded maximum length of 2000 characters.]',
);
expect(result.returnDisplay).toBe(
'Read all 3 lines from test.txt (some lines were shortened)',
);

View File

@ -303,14 +303,7 @@ export async function processSingleFileContent(
const contentRangeTruncated =
startLine > 0 || endLine < originalLineCount;
const isTruncated = contentRangeTruncated || linesWereTruncatedInLength;
let llmTextContent = '';
if (contentRangeTruncated) {
llmTextContent += `[File content truncated: showing lines ${actualStartLine + 1}-${endLine} of ${originalLineCount} total lines. Use offset/limit parameters to view more.]\n`;
} else if (linesWereTruncatedInLength) {
llmTextContent += `[File content partially truncated: some lines exceeded maximum length of ${MAX_LINE_LENGTH_TEXT_FILE} characters.]\n`;
}
llmTextContent += formattedLines.join('\n');
const llmContent = formattedLines.join('\n');
// By default, return nothing to streamline the common case of a successful read_file.
let returnDisplay = '';
@ -326,7 +319,7 @@ export async function processSingleFileContent(
}
return {
llmContent: llmTextContent,
llmContent,
returnDisplay,
isTruncated,
originalLineCount,