diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index bb9317fd..8c11afab 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -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', () => { diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 3a05da06..7ef9d2b5 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -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: { diff --git a/packages/core/src/tools/read-many-files.test.ts b/packages/core/src/tools/read-many-files.test.ts index 6ddd2a08..4035a6b7 100644 --- a/packages/core/src/tools/read-many-files.test.ts +++ b/packages/core/src/tools/read-many-files.test.ts @@ -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', () => { diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts index 1fa2e15c..a380ea91 100644 --- a/packages/core/src/tools/read-many-files.ts +++ b/packages/core/src/tools/read-many-files.ts @@ -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); diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index fb6b6820..cfedfe27 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -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)', ); diff --git a/packages/core/src/utils/fileUtils.ts b/packages/core/src/utils/fileUtils.ts index a153d205..30ab69c6 100644 --- a/packages/core/src/utils/fileUtils.ts +++ b/packages/core/src/utils/fileUtils.ts @@ -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,