From b350e4dee2412e450b3844061f5c1bc2ec995923 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 6 Jul 2026 15:00:37 -0600 Subject: catch up with latest panto cli developments --- lua.html | 56 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 24 deletions(-) (limited to 'lua.html') diff --git a/lua.html b/lua.html index 7baeba0..fd77655 100644 --- a/lua.html +++ b/lua.html @@ -20,44 +20,52 @@
Source
- +
-

Reference

Lua extensions

require("panto")

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:

register_toolmodel-facing
Add a tool the model can call.
register_commandyou-facing
Add a /slash command to the REPL.
on / emitUI events
Hook the UI event bus to wrap or replace how things render.

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:

LayerLocation
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.

namestringrequired
The tool name the model invokes.
descriptionstringrequired
Shown to the model; explain when to use it.
schematablerequired
A JSON-Schema table (type = "object", properties, required) describing the arguments.
handlerfunction(input)
Receives the decoded arguments table; returns a string result. May yield (async).
.panto/extensions/echo.lualua
local panto = require("panto")
-
-panto.ext.register_tool {
+

Reference

Lua extensions

require("panto")

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:

{ name, activate }
A named entry whose activate() function performs registration when the entry survives policy and shadowing.
{ name, description, schema, handler }
Sugar form for a single tool entry. Activation registers it as a tool automatically.
{ ... , ... }
A list of entries or tool declarations returned from one file.

Inside activate(), use local panto = require("panto") and the panto.ext table:

register_tool
Register a model-facing tool.
register_command
Register a slash command.
on / emit
Subscribe to transcript/UI lifecycle events or fire custom ones.

Loading extensions

panto scans both extensions/ and tools/ directories at each layer:

LayerLocation
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 in activate(), not at top level.

register_tool

The simplest way to define a tool is to return the sugar form directly:

.panto/tools/echo.lualua
return {
   name = "echo",
   description = "Echo back the given message.",
   schema = {
     type = "object",
     properties = {
-      message = { type = "string", description = "The text to echo back." },
+      message = { type = "string" },
     },
     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.

namestringrequired
Command name, without the leading /.
descriptionstringrequired
One-line summary (for listings).
handlerfunction(args)
args is the trimmed text after the command name — the empty string when none was given.
.panto/extensions/greet.lualua
local panto = require("panto")
-
-panto.ext.register_command {
+}

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:

.panto/extensions/greet.lualua
return {
   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")
+  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 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):

e.namestring
The event name.
e.<field>
Read-only payload fields (nil when absent, so you can branch on them).
e:getComponent()
The current native component, as an opaque pass-through handle.
e:setComponent(c)
Replace the component for this boundary — pass the handle back to wrap it, or a Lua component table to replace it.

The events and the payload fields they carry:

EventPayload fields
toolindex, tool_name, id, delta, input, output
thinkingindex, delta, text
assistant_textindex, delta, text
user_messagetext
session_startversion, cwd, model
compactionsummary
custom— produced by emit
.panto/extensions/skill-badge.lualua
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:

renderfunction(self, width)required
Return an array of strings — one per line. Runs synchronously (it may not yield), each line is truncated to width columns, and any error becomes a safe fallback line so the frame never crashes. An empty array is valid (zero lines).
handleInputfunction(self, data)
Optional. Handle input routed to the component.
firstLineChanged / invalidatefunction
Optional render-cache hooks.

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:

shellbash
panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'

One-time setup

On a fresh machine, run panto bootstrap once to build the rocks tree before your extensions need it.
+}

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 familyNames
tooltool, tool_details, tool_delta, tool_call_complete, tool_result, tool_collapse
thinkingthinking, thinking_delta, thinking_complete
assistant textassistant_text, assistant_text_delta, assistant_text_complete
single-shotuser_message, session_start, compaction
customwhatever 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.

.panto/extensions/skill-badge.lualua
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.

shellbash
panto update
+panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'
-- cgit v1.3