Reference
Lua extensions
Everything beyond the core is a Lua extension — including the standard tools. An extension is a Lua file that calls into the panto.ext table to add tools, slash commands, and UI behaviour. This is how panto stays small without staying useless.
Overview
Inside an extension, local panto = require("panto") gives you the API. The panto.ext table has three entry points:
/slash command to the REPL.All extensions share one long-lived Lua interpreter, so module-global state persists across calls. The runtime includes luv (libuv), so tool handlers can perform async I/O.
Loading extensions
Drop a .lua file into an extensions directory and it loads at startup. panto discovers extensions across three layers:
| Layer | Location |
|---|---|
| base | $PANTO_HOME/agent |
| user | $XDG_CONFIG_HOME/panto/extensions/ · else ~/.config/panto/extensions/ |
| project | ./.panto/extensions/ |
Project shadows user shadows base. Which extensions and tools actually load is gated by the [extensions] and [tools] allow/deny policies.
Name collisions abort startup
If two surviving extensions register a tool (or command) with the same name, panto refuses to start rather than pick one silently. A Lua command may not shadow a builtin like/compact.register_tool
A tool is something the model can call. Describe it with a JSON-Schema for its arguments and a handler that returns a string result.
type = "object", properties, required) describing the arguments.local panto = require("panto")
panto.ext.register_tool {
name = "echo",
description = "Echo back the given message.",
schema = {
type = "object",
properties = {
message = { type = "string", description = "The text to echo back." },
},
required = { "message" },
},
handler = function(input)
return "echo: " .. input.message
end,
}register_command
A slash command runs locally when you type /name in the REPL. The handler runs synchronously and acts by side effect (typically writing output); its return value is ignored.
/.args is the trimmed text after the command name — the empty string when none was given.local panto = require("panto")
panto.ext.register_command {
name = "greet",
description = "Print a greeting. Optional args name who to greet.",
handler = function(args)
local who = args ~= "" and args or "world"
io.write("\n[greet] hello, " .. who .. "!\n")
end,
}Events: on & emit
panto.ext.on(name, handler) subscribes a handler to the UI event bus — the same bus the built-in TUI uses. Handlers fire in registration order, and can wrap or replace the component that renders a given moment of the transcript.
The handler receives an event object e that is valid only during the call (copy any field you need into a local — don’t read e later at render time):
The events and the payload fields they carry:
| Event | Payload fields |
|---|---|
| tool | index, tool_name, id, delta, input, output |
| thinking | index, delta, text |
| assistant_text | index, delta, text |
| user_message | text |
| session_start | version, cwd, model |
| compaction | summary |
| custom | — produced by emit |
local panto = require("panto")
-- Change how the "skill" tool renders in the transcript.
panto.ext.on("tool", function(e)
if e.tool_name ~= "skill" then return end -- ignore every other tool
local name = e.tool_name -- snapshot now: e is short-lived
e:setComponent({
render = function(self, width)
return { "▸ running skill: " .. name }
end,
})
end)panto.ext.emit(name, data) fires a custom event on the same bus, running both native and Lua handlers. Today data is surfaced as an opaque custom payload.
Lua components
A Lua component is a table you hand to e:setComponent. The one required method is render:
width columns, and any error becomes a safe fallback line so the frame never crashes. An empty array is valid (zero lines).Runtime & luarocks
panto embeds a pinned Lua and luarocks, and ships exactly one battery: luv (libuv’s event loop), the single runtime dependency. It drives panto’s coroutine scheduler and gives extensions one rich async-I/O surface. The bootstrap reconciles the installed rocks tree against panto’s manifest on every startup — installing what’s missing, removing what’s stale.
Anything else is yours to add. Install extra rocks into panto’s tree with the embedded luarocks, via panto lua:
panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'One-time setup
On a fresh machine, runpanto bootstrap once to build the rocks tree before your extensions need it.