summaryrefslogtreecommitdiff
path: root/agent/tools/read.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-15 22:11:53 -0600
committert <t@tjp.lol>2026-06-16 11:17:14 -0600
commitf8c6d45755acb5abf9ac68931268b7945da0fae0 (patch)
treea88ed7edd4aa3e5acced9ff99ac61b7f36b267a6 /agent/tools/read.lua
parent8206642d3a1736aaf928a5b9a732e747f5294228 (diff)
display fixes: markdown rendering, tool-specific components
Diffstat (limited to 'agent/tools/read.lua')
-rw-r--r--agent/tools/read.lua351
1 files changed, 180 insertions, 171 deletions
diff --git a/agent/tools/read.lua b/agent/tools/read.lua
index 0328efc..f691f44 100644
--- a/agent/tools/read.lua
+++ b/agent/tools/read.lua
@@ -1,3 +1,25 @@
+local panto = require("panto")
+local uv = require("luv")
+
+local tool = {
+ name = "std.read",
+ description = "Read a file from disk. Output is truncated to the first 50KB or 2000 lines, whichever is hit first; directories return a clearly tagged non-recursive listing. Use offset/limit to slice. Works with images and PDFs.",
+ schema = {
+ type = "object",
+ properties = {
+ path = { type = "string", description = "Path to the file or directory (relative or absolute)." },
+ offset = { type = "integer", description = "1-based line to start at.", minimum = 1 },
+ limit = { type = "integer", description = "Maximum lines to return.", minimum = 1 },
+ },
+ required = { "path" },
+ },
+}
+
+panto.ext.on("tool", function(e)
+ if e.tool_name ~= tool.name then return end
+ e:set_component(e:get_component())
+end)
+
-- Read a file from disk and return its text verbatim.
--
-- Output cap: 50 KB and 2000 lines, whichever is hit first. Large
@@ -13,8 +35,6 @@
-- If `path` names a directory, return a clearly tagged non-recursive
-- listing of its immediate children instead of an error.
-local uv = require("luv")
-
-- Minimal magic-byte sniff: binary attachment (image/PDF) vs. text.
-- We only decide *whether* the bytes are an attachment; libpanto does the
-- precise type detection, resizing, and encoding from the raw bytes. Keep
@@ -216,197 +236,186 @@ local function render_directory_listing(path, offset, limit)
return body
end
-return {
- name = "std.read",
- description = "Read a file from disk. Output is truncated to the first 50KB or 2000 lines, whichever is hit first; directories return a clearly tagged non-recursive listing. Use offset/limit to slice. Works with images and PDFs.",
- schema = {
- type = "object",
- properties = {
- path = { type = "string", description = "Path to the file or directory (relative or absolute)." },
- offset = { type = "integer", description = "1-based line to start at.", minimum = 1 },
- limit = { type = "integer", description = "Maximum 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"
+tool.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
+ 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 stat_err, stat = fs_stat(path)
- if not stat then
- return "Error: " .. (stat_err or ("could not stat " .. path))
- end
- if stat.type == "directory" then
- return render_directory_listing(path, offset, limit)
- end
+ local stat_err, stat = fs_stat(path)
+ if not stat then
+ return "Error: " .. (stat_err or ("could not stat " .. path))
+ end
+ if stat.type == "directory" then
+ return render_directory_listing(path, offset, limit)
+ end
- local open_err, fd = fs_open(path, "r", 0)
- if not fd then
- return "Error: " .. (open_err or ("could not open " .. path))
- end
+ local open_err, fd = fs_open(path, "r", 0)
+ if not fd then
+ return "Error: " .. (open_err or ("could not open " .. path))
+ end
- -- Binary attachment path. We do the *minimal* magic-byte sniff
- -- here — only enough to decide "binary attachment vs. text". If
- -- it's an attachment, we return the raw file bytes; libpanto does
- -- the real work (precise type detection, resizing, encoding).
- do
- local herr, header = fs_read(fd, 16, 0)
- if not herr and header and #header > 0 and is_attachment(header) then
- local whole, rerr = (function()
- local chunks = {}
- local off = 0
- while true do
- local e, d = fs_read(fd, READ_CHUNK_SIZE, off)
- if e then return nil, e end
- if d == nil or #d == 0 then break end
- chunks[#chunks + 1] = d
- off = off + #d
- end
- return table.concat(chunks)
- end)()
- fs_close(fd)
- if not whole then
- return "Error: read failed: " .. tostring(rerr)
+ -- Binary attachment path. We do the *minimal* magic-byte sniff
+ -- here — only enough to decide "binary attachment vs. text". If
+ -- it's an attachment, we return the raw file bytes; libpanto does
+ -- the real work (precise type detection, resizing, encoding).
+ do
+ local herr, header = fs_read(fd, 16, 0)
+ if not herr and header and #header > 0 and is_attachment(header) then
+ local whole, rerr = (function()
+ local chunks = {}
+ local off = 0
+ while true do
+ local e, d = fs_read(fd, READ_CHUNK_SIZE, off)
+ if e then return nil, e end
+ if d == nil or #d == 0 then break end
+ chunks[#chunks + 1] = d
+ off = off + #d
end
- return {
- text = string.format("[read %s as binary attachment]", path),
- attachments = { { data = whole } },
- }
+ return table.concat(chunks)
+ end)()
+ fs_close(fd)
+ if not whole then
+ return "Error: read failed: " .. tostring(rerr)
end
+ return {
+ text = string.format("[read %s as binary attachment]", path),
+ attachments = { { data = whole } },
+ }
end
+ end
- local parts = {}
- local emitted_lines = 0
- local emitted_bytes = 0
- local lineno = 0
- local truncated_reason = nil
- local stopped_at_line = nil
- local effective_line_cap = MAX_LINES
- if limit and limit < effective_line_cap then
- effective_line_cap = limit
- end
+ local parts = {}
+ local emitted_lines = 0
+ local emitted_bytes = 0
+ local lineno = 0
+ local truncated_reason = nil
+ local stopped_at_line = nil
+ local effective_line_cap = MAX_LINES
+ if limit and limit < effective_line_cap then
+ effective_line_cap = limit
+ end
- local file_offset = 0
- local done = false
- local read_err = nil
- local saw_any_bytes = false
- local saw_trailing_newline = false
+ local file_offset = 0
+ local done = false
+ local read_err = nil
+ local saw_any_bytes = false
+ local saw_trailing_newline = false
- local carry = ""
- local function process_line(line)
- lineno = lineno + 1
- if lineno < offset then
- return
- end
+ local carry = ""
+ local function process_line(line)
+ lineno = lineno + 1
+ if lineno < offset then
+ return
+ end
- local rendered = line .. "\n"
- if emitted_bytes + #rendered > MAX_BYTES then
- truncated_reason = "bytes"
- stopped_at_line = lineno
- done = true
- return
- end
- parts[#parts + 1] = rendered
- emitted_bytes = emitted_bytes + #rendered
- emitted_lines = emitted_lines + 1
- if emitted_lines >= effective_line_cap then
- stopped_at_line = lineno
- done = true
- end
+ local rendered = line .. "\n"
+ if emitted_bytes + #rendered > MAX_BYTES then
+ truncated_reason = "bytes"
+ stopped_at_line = lineno
+ done = true
+ return
end
+ parts[#parts + 1] = rendered
+ emitted_bytes = emitted_bytes + #rendered
+ emitted_lines = emitted_lines + 1
+ if emitted_lines >= effective_line_cap then
+ stopped_at_line = lineno
+ done = true
+ end
+ end
- while not done do
- local err, data = fs_read(fd, READ_CHUNK_SIZE, file_offset)
- if err then
- read_err = err
- break
- end
- -- luv's `fs_read` signals EOF by returning the empty
- -- string, NOT nil (nil only accompanies an error). Treat
- -- both nil and "" as EOF; otherwise the loop re-reads at
- -- the same offset forever, pinning a core at 100%.
- if data == nil or #data == 0 then
- if #carry > 0 then
- process_line(carry)
- carry = ""
- end
- break
+ while not done do
+ local err, data = fs_read(fd, READ_CHUNK_SIZE, file_offset)
+ if err then
+ read_err = err
+ break
+ end
+ -- luv's `fs_read` signals EOF by returning the empty
+ -- string, NOT nil (nil only accompanies an error). Treat
+ -- both nil and "" as EOF; otherwise the loop re-reads at
+ -- the same offset forever, pinning a core at 100%.
+ if data == nil or #data == 0 then
+ if #carry > 0 then
+ process_line(carry)
+ carry = ""
end
+ break
+ end
- saw_any_bytes = true
- file_offset = file_offset + #data
- saw_trailing_newline = data:sub(-1) == "\n"
- local chunk = carry .. data
- local start = 1
+ saw_any_bytes = true
+ file_offset = file_offset + #data
+ saw_trailing_newline = data:sub(-1) == "\n"
+ local chunk = carry .. data
+ local start = 1
- while not done do
- local nl = chunk:find("\n", start, true)
- if not nl then break end
- local line = chunk:sub(start, nl - 1)
- if line:sub(-1) == "\r" then
- line = line:sub(1, -2)
- end
- process_line(line)
- start = nl + 1
+ while not done do
+ local nl = chunk:find("\n", start, true)
+ if not nl then break end
+ local line = chunk:sub(start, nl - 1)
+ if line:sub(-1) == "\r" then
+ line = line:sub(1, -2)
end
+ process_line(line)
+ start = nl + 1
+ end
- carry = chunk:sub(start)
- if done and truncated_reason == nil and emitted_lines >= effective_line_cap then
- truncated_reason = (limit and limit <= MAX_LINES) and "limit" or "lines"
- end
+ carry = chunk:sub(start)
+ if done and truncated_reason == nil and emitted_lines >= effective_line_cap then
+ truncated_reason = (limit and limit <= MAX_LINES) and "limit" or "lines"
end
+ end
- fs_close(fd)
+ fs_close(fd)
- if read_err then
- return "Error: read failed: " .. tostring(read_err)
- end
+ if read_err then
+ return "Error: read failed: " .. tostring(read_err)
+ end
- if #parts == 0 then
- if not saw_any_bytes 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
- if lineno == 0 and saw_any_bytes and not saw_trailing_newline then
- return "(empty file)\n"
- end
- return "(no lines in requested range)\n"
+ if #parts == 0 then
+ if not saw_any_bytes then
+ return "(empty file)\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
+ if offset > lineno then
+ return string.format(
+ "Error: offset (%d) is past end of file (%d lines).",
+ offset, lineno
)
end
- return body
- end,
-}
+ if lineno == 0 and saw_any_bytes and not saw_trailing_newline then
+ return "(empty file)\n"
+ 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
+
+return tool