From eea3b24a22a1680ac220cdb1910ee8ea55ab7eb3 Mon Sep 17 00:00:00 2001 From: jbreite Date: Thu, 12 Mar 2026 18:18:49 -0400 Subject: [PATCH 1/2] Add per-line and total output truncation to Read tool Truncates lines exceeding 2000 characters and caps total output at 60000 characters using middle truncation, preventing context bloat from large files. Adds configurable maxLineLength to ToolConfig. Co-Authored-By: Claude Opus 4.6 --- src/tools/read.ts | 23 +++++++++++++++++++++-- src/types.ts | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/tools/read.ts b/src/tools/read.ts index 96432a8..9033550 100644 --- a/src/tools/read.ts +++ b/src/tools/read.ts @@ -8,6 +8,10 @@ import { debugStart, isDebugEnabled, } from "../utils/debug"; +import { middleTruncate } from "../utils/helpers"; + +const DEFAULT_MAX_LINE_LENGTH = 2000; +const DEFAULT_MAX_OUTPUT_LENGTH = 60000; export interface ReadTextOutput { type: "text"; @@ -52,6 +56,7 @@ Usage: - By default, it reads up to 500 lines starting from the beginning of the file - You can optionally specify a line offset and limit (especially handy for long files) - Results are returned with line numbers starting at 1 +- Any lines longer than 2000 characters will be truncated - This tool can only read text files, not binary files (images, PDFs, etc.) - This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool. - It is always better to speculatively read multiple potentially useful files in parallel @@ -157,12 +162,26 @@ export function createReadTool(sandbox: Sandbox, config?: ToolConfig) { const endLine = limit ? startLine + limit : allLines.length; const selectedLines = allLines.slice(startLine, endLine); - const lines = selectedLines.map((line, i) => ({ + // Per-line truncation (silent, like Claude Code's MAX_LINE_LENGTH) + const maxLineLength = + config?.maxLineLength ?? DEFAULT_MAX_LINE_LENGTH; + const truncatedLines = selectedLines.map((line) => + line.length > maxLineLength + ? line.slice(0, maxLineLength) + "…" + : line, + ); + + const lines = truncatedLines.map((line, i) => ({ line_number: startLine + i + 1, content: line, })); - const contentStr = selectedLines.join("\n"); + let contentStr = truncatedLines.join("\n"); + + // Total output cap (silent, prevents context bloat) + const maxOutputLength = + config?.maxOutputLength ?? DEFAULT_MAX_OUTPUT_LENGTH; + contentStr = middleTruncate(contentStr, maxOutputLength); const durationMs = Math.round(performance.now() - startTime); if (debugId) { debugEnd(debugId, "read", { diff --git a/src/types.ts b/src/types.ts index 0fdee32..b6d73d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,7 @@ export type ToolConfig = { timeout?: number; maxFileSize?: number; maxOutputLength?: number; + maxLineLength?: number; allowedPaths?: string[]; blockedCommands?: string[]; } & SDKToolOptions; From 6643d528a37f314c1416c546a883ad9e72bf430e Mon Sep 17 00:00:00 2001 From: jbreite Date: Thu, 12 Mar 2026 19:03:10 -0400 Subject: [PATCH 2/2] Add tests for Read tool truncation and update AGENTS.md Co-Authored-By: Claude Opus 4.6 --- src/tools/AGENTS.md | 4 +- tests/tools/read.test.ts | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/tools/AGENTS.md b/src/tools/AGENTS.md index 76934a9..15d5f84 100644 --- a/src/tools/AGENTS.md +++ b/src/tools/AGENTS.md @@ -7,7 +7,7 @@ The tools module implements all 15 AI agent tools in BashKit. These tools bridge | File | Purpose | |------|---------| | `bash.ts` | Execute shell commands with timeout and output limits | -| `read.ts` | Read files and list directories with pagination support | +| `read.ts` | Read files and list directories with pagination, per-line truncation (2000 chars), and total output truncation (60K chars) | | `write.ts` | Write files with size limits and path restrictions | | `edit.ts` | String-based find/replace editing with uniqueness validation | | `glob.ts` | Pattern-based file discovery using find command | @@ -227,7 +227,7 @@ const DEFAULT_CACHEABLE = [ ### Test Coverage Located at `/tests/tools/`: - `bash.test.ts` -- Command execution, timeouts, output truncation -- `read.test.ts` -- File reading, directory listing, pagination, binary detection +- `read.test.ts` -- File reading, directory listing, pagination, binary detection, line/output truncation - `write.test.ts` -- File creation, overwriting, size limits - `edit.test.ts` -- String replacement, uniqueness validation, replace_all - `glob.test.ts` -- Pattern matching, path filtering diff --git a/tests/tools/read.test.ts b/tests/tools/read.test.ts index dff501a..7719df8 100644 --- a/tests/tools/read.test.ts +++ b/tests/tools/read.test.ts @@ -279,6 +279,96 @@ describe("Read Tool", () => { }); }); + describe("per-line truncation", () => { + it("should truncate lines exceeding default max line length", async () => { + const longLine = "x".repeat(2500); + sandbox.setFile("/workspace/long-line.ts", `short\n${longLine}\nshort`); + + const tool = createReadTool(sandbox); + const result = await executeTool(tool, { + file_path: "/workspace/long-line.ts", + }); + + assertSuccess(result); + if (result.type === "text") { + expect(result.lines[1].content).toHaveLength(2001); // 2000 + "…" + expect(result.lines[1].content.endsWith("…")).toBe(true); + expect(result.lines[0].content).toBe("short"); + expect(result.lines[2].content).toBe("short"); + } + }); + + it("should not truncate lines at exactly max length", async () => { + const exactLine = "x".repeat(2000); + sandbox.setFile("/workspace/exact.ts", exactLine); + + const tool = createReadTool(sandbox); + const result = await executeTool(tool, { + file_path: "/workspace/exact.ts", + }); + + assertSuccess(result); + if (result.type === "text") { + expect(result.lines[0].content).toHaveLength(2000); + expect(result.lines[0].content).not.toContain("…"); + } + }); + + it("should respect custom maxLineLength config", async () => { + const line = "x".repeat(200); + sandbox.setFile("/workspace/custom.ts", line); + + const tool = createReadTool(sandbox, { maxLineLength: 100 }); + const result = await executeTool(tool, { + file_path: "/workspace/custom.ts", + }); + + assertSuccess(result); + if (result.type === "text") { + expect(result.lines[0].content).toHaveLength(101); // 100 + "…" + expect(result.lines[0].content.endsWith("…")).toBe(true); + } + }); + }); + + describe("total output truncation", () => { + it("should truncate total output exceeding maxOutputLength", async () => { + // Create a file with many lines that exceed 500 chars total + const fileLines = Array.from({ length: 50 }, (_, i) => `Line ${i + 1}: ${"a".repeat(20)}`); + const fileContent = fileLines.join("\n"); + sandbox.setFile("/workspace/big.ts", fileContent); + + const tool = createReadTool(sandbox, { maxOutputLength: 500 }); + const result = await executeTool(tool, { + file_path: "/workspace/big.ts", + limit: 50, + }); + + assertSuccess(result); + if (result.type === "text") { + // middleTruncate adds a header + marker, so content will be longer than maxOutputLength + // but shorter than the original untruncated content + expect(result.content.length).toBeLessThan(fileContent.length); + expect(result.content).toContain("truncated"); + } + }); + + it("should not truncate output within maxOutputLength", async () => { + sandbox.setFile("/workspace/small.ts", "line1\nline2\nline3"); + + const tool = createReadTool(sandbox, { maxOutputLength: 60000 }); + const result = await executeTool(tool, { + file_path: "/workspace/small.ts", + }); + + assertSuccess(result); + if (result.type === "text") { + expect(result.content).toBe("line1\nline2\nline3"); + expect(result.content).not.toContain("truncated"); + } + }); + }); + describe("line number formatting", () => { it("should return 1-indexed line numbers", async () => { sandbox.setFile("/workspace/test.ts", "line1\nline2\nline3");