summaryrefslogtreecommitdiff
path: root/agent/tools/read.lua
diff options
context:
space:
mode:
Diffstat (limited to 'agent/tools/read.lua')
-rw-r--r--agent/tools/read.lua108
1 files changed, 29 insertions, 79 deletions
diff --git a/agent/tools/read.lua b/agent/tools/read.lua
index f691f44..cd7f26b 100644
--- a/agent/tools/read.lua
+++ b/agent/tools/read.lua
@@ -1,5 +1,4 @@
-local panto = require("panto")
-local uv = require("luv")
+local std = require("_std")
local tool = {
name = "std.read",
@@ -15,11 +14,6 @@ local tool = {
},
}
-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
@@ -63,71 +57,10 @@ local MAX_BYTES = 50 * 1024 -- 50 KB
local MAX_LINES = 2000
local READ_CHUNK_SIZE = MAX_BYTES
--- ---------------------------------------------------------------------------
--- Coroutine-synchronous libuv wrappers.
---
--- panto runs every tool handler inside its own Lua coroutine and drives
--- a single `uv.run()` to completion. The contract for an async handler
--- is: the ONLY way it may block is to yield on a pending libuv request
--- whose callback resumes it exactly once. These helpers encapsulate
--- that pattern — each fires one async libuv op, yields, and returns the
--- callback's results once resumed. They look synchronous to the caller
--- but cooperate with sibling tool calls sharing the event loop.
---
--- INVARIANTS each wrapper upholds (and every async tool must too):
--- * exactly one libuv op armed per yield;
--- * the callback resumes the coroutine exactly once;
--- * `coroutine.resume` errors are re-raised so a crash in the resumed
--- handler isn't silently swallowed inside the uv callback.
--- ---------------------------------------------------------------------------
-
--- Run a single callback-style libuv fs operation and block the
--- coroutine until its callback fires. `arm` receives a `resolve`
--- function and must start exactly one libuv op that calls it.
-local function await(arm)
- local co = assert(coroutine.running(),
- "read: await must run inside a tool handler coroutine")
- local res, fired = nil, false
- arm(function(...)
- assert(not fired, "read: libuv callback fired twice")
- fired = true
- res = table.pack(...)
- local ok, err = coroutine.resume(co)
- if not ok then error(err, 0) end
- end)
- coroutine.yield()
- return table.unpack(res, 1, res.n)
-end
-
-local function fs_stat(path)
- return await(function(resolve) uv.fs_stat(path, resolve) end)
-end
-
-local function fs_open(path, flags, mode)
- return await(function(resolve) uv.fs_open(path, flags, mode, resolve) end)
-end
-
-local function fs_read(fd, size, offset)
- return await(function(resolve) uv.fs_read(fd, size, offset, resolve) end)
-end
-
-local function fs_close(fd)
- return await(function(resolve) uv.fs_close(fd, resolve) end)
-end
-
-- `uv.fs_opendir` takes the entries-per-readdir batch hint as its
-- SECOND positional arg, before the callback. We pass it explicitly so
-- each `fs_readdir` returns up to `n` entries per round-trip.
local READDIR_BATCH = 256
-local function fs_opendir(path)
- return await(function(resolve) uv.fs_opendir(path, resolve, READDIR_BATCH) end)
-end
-local function fs_readdir(dir)
- return await(function(resolve) uv.fs_readdir(dir, resolve) end)
-end
-local function fs_closedir(dir)
- return await(function(resolve) uv.fs_closedir(dir, resolve) end)
-end
local function scandir_sorted(path)
-- Use the streaming opendir/readdir/closedir API rather than
@@ -135,16 +68,16 @@ local function scandir_sorted(path)
-- with synchronous `readdir(3)` syscalls, which would block the
-- shared event loop on large directories. opendir/readdir route
-- every batch through libuv's thread pool, keeping us cooperative.
- local err, dir = fs_opendir(path)
+ local err, dir = std.fs_opendir(path, READDIR_BATCH)
if not dir then
return nil, err
end
local entries = {}
while true do
- local rerr, batch = fs_readdir(dir)
+ local rerr, batch = std.fs_readdir(dir)
if rerr then
- fs_closedir(dir)
+ std.fs_closedir(dir)
return nil, rerr
end
-- A nil/empty batch signals end of the directory stream.
@@ -154,7 +87,7 @@ local function scandir_sorted(path)
entries[#entries + 1] = { name = e.name, typ = e.type }
end
end
- fs_closedir(dir)
+ std.fs_closedir(dir)
table.sort(entries, function(a, b)
return a.name < b.name
@@ -248,7 +181,7 @@ tool.handler = function(input)
return string.format("Error: limit (%d) must be >= 1.", limit)
end
- local stat_err, stat = fs_stat(path)
+ local stat_err, stat = std.fs_stat(path)
if not stat then
return "Error: " .. (stat_err or ("could not stat " .. path))
end
@@ -256,7 +189,7 @@ tool.handler = function(input)
return render_directory_listing(path, offset, limit)
end
- local open_err, fd = fs_open(path, "r", 0)
+ local open_err, fd = std.fs_open(path, "r", 0)
if not fd then
return "Error: " .. (open_err or ("could not open " .. path))
end
@@ -266,13 +199,13 @@ tool.handler = function(input)
-- 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)
+ local herr, header = std.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)
+ local e, d = std.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
@@ -280,7 +213,7 @@ tool.handler = function(input)
end
return table.concat(chunks)
end)()
- fs_close(fd)
+ std.fs_close(fd)
if not whole then
return "Error: read failed: " .. tostring(rerr)
end
@@ -332,7 +265,7 @@ tool.handler = function(input)
end
while not done do
- local err, data = fs_read(fd, READ_CHUNK_SIZE, file_offset)
+ local err, data = std.fs_read(fd, READ_CHUNK_SIZE, file_offset)
if err then
read_err = err
break
@@ -372,7 +305,7 @@ tool.handler = function(input)
end
end
- fs_close(fd)
+ std.fs_close(fd)
if read_err then
return "Error: read failed: " .. tostring(read_err)
@@ -418,4 +351,21 @@ tool.handler = function(input)
return body
end
+local function quote(v)
+ if v == nil or v == "" then return nil end
+ return tostring(v)
+end
+
+std.install_renderer(tool.name, function(st)
+ local obj = std.decode(st.input)
+ local path = obj and quote(obj.path)
+ if not path then return "read" end
+ if type(obj.offset) == "number" and type(obj.limit) == "number" then
+ return string.format("read %s (lines %d-%d)", path, obj.offset, obj.offset + obj.limit - 1)
+ elseif type(obj.offset) == "number" then
+ return string.format("read %s (from line %d)", path, obj.offset)
+ end
+ return "read " .. path
+end)
+
return tool