Reference
Lua extensions
panto's standard tools are Lua extensions, and your own extensions use the same loader. A source file returns one entry, one tool declaration, or a list of them; activation then registers tools, slash commands, and UI-event handlers into the live session.
Overview
Extension source files are evaluated first and activated later. A file must return one of these shapes:
activate() function performs registration when the entry survives policy and shadowing.Inside activate(), use local panto = require("panto") and the panto.ext table:
Loading extensions
panto scans both extensions/ and tools/ directories at each layer:
| Layer | Location |
|---|---|
| base | $XDG_DATA_HOME/panto/agent/{extensions,tools} |
| user | $XDG_CONFIG_HOME/panto/{extensions,tools} · else ~/.config/panto/{extensions,tools} |
| project | ./.panto/{extensions,tools} |
Additional sources can come from [extensions] paths and [extensions] rocks. Cross-layer precedence is project > user > base. Within one layer, canonical directories beat extra paths, which beat rocks. Identity is the returned entry's name, not the filename.
Eval is side-effect free
Every candidate file is evaluated before policy and shadowing are resolved, including entries that will later be denied. Put side effects inactivate(), not at top level.register_tool
The simplest way to define a tool is to return the sugar form directly:
return {
name = "echo",
description = "Echo back the given message.",
schema = {
type = "object",
properties = {
message = { type = "string" },
},
required = { "message" },
},
handler = function(input)
return "echo: " .. input.message
end,
}Use register_tool inside activate() when you want one entry to register multiple tools or commands together.
register_command
Slash commands are typically registered from an explicit entry:
return {
name = "greet",
activate = function()
local panto = require("panto")
panto.ext.register_command {
name = "greet",
description = "Print a greeting.",
handler = function(args)
local who = args ~= "" and args or "world"
io.write("\n[greet] hello, " .. who .. "!\n")
end,
}
end,
}Events: on & emit
panto.ext.on(name, handler) subscribes to transcript/UI lifecycle events. The handler receives an event userdata with snake-case methods get_component() and set_component(), plus payload fields such as tool_name, delta, text, id, input, output, cwd, and model.
| Event family | Names |
|---|---|
| tool | tool, tool_details, tool_delta, tool_call_complete, tool_result, tool_collapse |
| thinking | thinking, thinking_delta, thinking_complete |
| assistant text | assistant_text, assistant_text_delta, assistant_text_complete |
| single-shot | user_message, session_start, compaction |
| custom | whatever you emit via panto.ext.emit |
A few payload fields are writable during dispatch: user_message.text, tool_call_complete.input (assign a Lua table; panto serializes it to JSON), and tool_result.output.
return {
name = "skill-badge",
activate = function()
local panto = require("panto")
panto.ext.on("tool_details", function(e)
if e.tool_name ~= "skill" then return end
local inner = e:get_component()
e:set_component({
render = function(self, width)
local lines = inner:render(width)
lines[#lines + 1] = "▸ running skill"
return lines
end,
})
end)
end,
}Lua components
A Lua component is a table passed to set_component(). The required method is render(self, width), which returns an array of strings. handleInput(self, data) is optional. Differential repaint is cache-derived automatically; you do not need to implement firstLineChanged yourself.
Runtime & luarocks
panto embeds a pinned Lua and luarocks runtime and ships luv as its core async dependency. Extra rocks may be loaded as extension sources via [extensions] rocks and installed with panto update, or added manually with panto lua.
panto update
panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'