import { readFileSync } from "node:fs"; import { type } from "arktype"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; /** Hard cap on returned content to avoid flooding the model's context window. */ const MAX_CHARS = 12000; export const ReadFileParams = type({ path: type.string.describe("absolute path to the file to read"), "start_line?": type.number.describe("start line, 1-based inclusive (default: 1)"), "end_line?": type.number.describe("end line, 1-based inclusive (default: end of file)"), }); export function ReadFileTool(_ctx: ToolContext) { return tool({ name: "read_file", description: "Read lines from a file. Use this to read sections of the PR diff returned by checkout_pr. " + `Returns at most ${MAX_CHARS} characters. ` + "Prefer fewer, wider reads: read large contiguous ranges rather than many small ones. " + "If the TOC lists 10 files, read 3-4 wide ranges that cover them rather than 10 separate calls. " + "Example: `read_file({ path: diffPath, start_line: 5, end_line: 200 })`.", parameters: ReadFileParams, execute: execute(async ({ path, start_line, end_line }) => { let content: string; try { content = readFileSync(path, "utf-8"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new Error(`failed to read ${path}: ${msg}`); } const lines = content.split("\n"); const start = Math.max(0, (start_line ?? 1) - 1); const end = Math.min(lines.length, end_line ?? lines.length); const slice = lines.slice(start, end).join("\n"); if (slice.length <= MAX_CHARS) { return { content: slice }; } const truncated = slice.slice(0, MAX_CHARS); const linesShown = truncated.split("\n").length; const linesTotal = end - start; return { content: truncated, truncated: true, note: `Output capped at ${MAX_CHARS} chars (showed ${linesShown}/${linesTotal} lines). Use a narrower line range to read the rest.`, }; }), }); }