1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
-- 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 panto = require("panto")
local uv = require("luv")
local tool = {
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" },
},
}
panto.ext.on("tool", function(e)
if e.tool_name ~= tool.name then return end
e:set_component(e:get_component())
end)
-- ---------------------------------------------------------------------------
-- 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
tool.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
return tool
|