-- Write text to a file, creating parent directories as needed. Keep the -- description terse because every byte of every tool description rides -- in the system prompt on every turn. local uv = require("luv") -- --------------------------------------------------------------------------- -- Coroutine-synchronous libuv wrappers. panto runs each tool handler in -- its own coroutine and drives a single `uv.run()` to completion; a -- handler may only block by yielding on a pending libuv op whose -- callback resumes it exactly once. `await` encapsulates that. -- --------------------------------------------------------------------------- local function await(arm) local co = assert(coroutine.running(), "write: await must run inside a tool handler coroutine") local res, fired = nil, false arm(function(...) assert(not fired, "write: 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 -- Async `fs_mkdir`. luv's async callback is `(err, success)`, and on -- EEXIST it reports the error via a string code in `err`; we detect -- that textually since the async form doesn't surface the errno name -- separately the way the sync form's third return does. local function fs_mkdir(path, mode) return await(function(resolve) uv.fs_mkdir(path, mode, 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_write(fd, data, offset) return await(function(resolve) uv.fs_write(fd, data, offset, resolve) end) end local function fs_close(fd) return await(function(resolve) uv.fs_close(fd, resolve) end) end -- mkdir -p equivalent: walk the path components and create each. -- Ignores "already exists" so the call is idempotent. local function mkdir_p(path) if path == "" or path == "." or path == "/" then return true end local parts = {} for piece in string.gmatch(path, "[^/]+") do parts[#parts + 1] = piece end local cur = (path:sub(1, 1) == "/") and "" or "." for _, piece in ipairs(parts) do cur = cur .. "/" .. piece local err = fs_mkdir(cur, tonumber("755", 8)) if err and not tostring(err):find("EEXIST") then return nil, err end end return true end -- Return the dirname of `path`, or nil if there isn't one (basename only). local function dirname(path) local slash = path:find("/[^/]*$") if not slash or slash == 1 then return nil end return path:sub(1, slash - 1) end return { name = "std.write", description = "Save content to a path. Overwrites existing files; missing parent directories are created automatically.", schema = { type = "object", properties = { path = { type = "string", description = "Path to the file (relative or absolute)." }, content = { type = "string", description = "Exact bytes to write. No newline is added." }, }, required = { "path", "content" }, }, handler = function(input) local path = input.path local content = input.content if type(path) ~= "string" or path == "" then return "Error: `path` must be a non-empty string." end if type(content) ~= "string" then return "Error: `content` must be a string." end local parent = dirname(path) if parent then local ok, err = mkdir_p(parent) if not ok then return "Error: could not create parent directory " .. parent .. ": " .. tostring(err) end end local open_err, fd = fs_open(path, "w", tonumber("644", 8)) if not fd then return "Error: " .. (open_err or ("could not open " .. path .. " for writing")) end -- A single `fs_write` may short-write; loop until all bytes land. local offset = 0 while offset < #content do local werr, n = fs_write(fd, content:sub(offset + 1), offset) if not n then fs_close(fd) return "Error: write failed: " .. tostring(werr) end offset = offset + n end local close_err = fs_close(fd) if close_err then return "Error: close failed: " .. tostring(close_err) end return string.format("Wrote %d bytes to %s.", #content, path) end, }