-- Read a file from disk and return its text verbatim. -- -- Output cap: 50 KB and 2000 lines, whichever is hit first. Matches -- pi's defaults; large enough to be useful, small enough to keep the -- model's context manageable. A single tool result that blows out the -- context window is worse than one that silently asks for a follow-up -- scoped read. -- -- The slicing knobs are `offset` (1-based line to start at) and -- `limit` (max lines to return). SQL-shaped — models recognize the -- idiom — and matches pi's tool surface. When the cap is hit, a -- `[truncated: ...]` marker names the next `offset` for a follow-up -- call. The file body itself is byte-for-byte what's on disk, so -- `edit` anchors round-trip exactly. local MAX_BYTES = 50 * 1024 -- 50 KB local MAX_LINES = 2000 return { name = "read", description = "Read a file from disk. Output is truncated to the first 50KB or 2000 lines, whichever is hit first; on truncation a marker names the next offset for a follow-up call. Use offset/limit to slice.", schema = { type = "object", properties = { path = { type = "string", description = "Path to the file (relative or absolute)." }, offset = { type = "integer", description = "1-based line to start at.", minimum = 1 }, limit = { type = "integer", description = "Maximum number of lines to return.", minimum = 1 }, }, required = { "path" }, }, handler = function(input) local path = input.path local offset = input.offset or 1 local limit = input.limit -- nil means "to EOF" if type(path) ~= "string" or path == "" then return "Error: `path` must be a non-empty string." end if limit ~= nil and limit < 1 then return string.format("Error: limit (%d) must be >= 1.", limit) end local f, open_err = io.open(path, "rb") if not f then return "Error: " .. (open_err or ("could not open " .. path)) end -- Walk lines one at a time so we can both apply start/end_line -- and enforce MAX_BYTES / MAX_LINES without ever holding the -- whole file in memory. local parts = {} local emitted_lines = 0 local emitted_bytes = 0 local lineno = 0 local truncated_reason = nil -- nil, "bytes", or "lines" local stopped_at_line = nil -- Effective line cap: min(limit, MAX_LINES). The MAX_LINES -- cap is enforced regardless of what the caller passed, to -- keep tool output context-budget-bounded. local effective_line_cap = MAX_LINES if limit and limit < effective_line_cap then effective_line_cap = limit end for line in f:lines("l") do lineno = lineno + 1 if lineno < offset then -- Skip; haven't reached the window yet. else -- Re-add the newline `f:lines("l")` strips. For a -- file with no trailing newline this still appends -- one to the final line; that's a deliberate -- normalization in our output, the on-disk file is -- untouched. local rendered = line .. "\n" if emitted_bytes + #rendered > MAX_BYTES then truncated_reason = "bytes" stopped_at_line = lineno break end parts[#parts + 1] = rendered emitted_bytes = emitted_bytes + #rendered emitted_lines = emitted_lines + 1 if emitted_lines >= effective_line_cap then -- Peek for at least one more line on disk so the -- truncation marker only appears when there's -- actually more to read. local peek = f:read("l") if peek then -- "limit" if the *caller's* limit was the -- binding cap, "lines" if our MAX_LINES -- safety cap kicked in first. truncated_reason = (limit and limit <= MAX_LINES) and "limit" or "lines" stopped_at_line = lineno end break end end end f:close() if #parts == 0 then if lineno == 0 then return "(empty file)\n" end if offset > lineno then return string.format( "Error: offset (%d) is past end of file (%d lines).", offset, lineno ) end return "(no lines in requested range)\n" end local body = table.concat(parts) if truncated_reason == "bytes" then body = body .. string.format( "\n[truncated: hit %d-byte cap at line %d. " .. "Call `read` again with `offset = %d` (and a smaller " .. "`limit` if needed) to continue.]\n", MAX_BYTES, stopped_at_line, stopped_at_line ) elseif truncated_reason == "lines" then body = body .. string.format( "\n[truncated: hit %d-line cap at line %d. " .. "Call `read` again with `offset = %d` to continue.]\n", MAX_LINES, stopped_at_line, stopped_at_line + 1 ) elseif truncated_reason == "limit" then body = body .. string.format( "\n[truncated: hit caller's `limit = %d` at line %d. " .. "Call `read` again with `offset = %d` to continue.]\n", limit, stopped_at_line, stopped_at_line + 1 ) end return body end, }