Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/tools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions src/tools/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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", {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type ToolConfig = {
timeout?: number;
maxFileSize?: number;
maxOutputLength?: number;
maxLineLength?: number;
allowedPaths?: string[];
blockedCommands?: string[];
} & SDKToolOptions;
Expand Down
90 changes: 90 additions & 0 deletions tests/tools/read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReadSuccessOutput>(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<ReadSuccessOutput>(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<ReadSuccessOutput>(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<ReadSuccessOutput>(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<ReadSuccessOutput>(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");
Expand Down
Loading