diff options
| -rw-r--r-- | README.md | 45 | ||||
| -rw-r--r-- | _selfcheck.lua | 44 | ||||
| -rw-r--r-- | panto-herdr-scm-1.rockspec | 28 | ||||
| -rw-r--r-- | panto-herdr/init.lua | 53 |
4 files changed, 170 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..18e221e --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# panto-herdr + +`panto-herdr` reports panto lifecycle state to [Herdr](https://herdr.dev/) when +panto is running in a Herdr pane. It reports `panto` as the agent label and +uses `thinking`, `responding`, `compacting`, and `tool:<name>` as short status +labels. + +Install it through panto's rock loader: + +```toml +[extensions] +rocks = ["panto-herdr"] +``` + +Then run `panto update`. Start panto in Herdr, for example: + +```sh +herdr agent start panto --cwd "$PWD" -- panto +``` + +The extension is inert outside Herdr: it only activates when `HERDR_PANE_ID` +is set. It invokes the local `herdr` executable asynchronously and discards +its JSON output, so reporting does not affect the panto terminal UI. + +## State mapping + +| panto event | Herdr state | +| --- | --- | +| `session_start` | `idle` | +| `user_message`, `thinking`, `compaction`, `tool_details`, `assistant_text` | `working` | +| `assistant_text_complete` | `idle` | + +Current panto extensions do not receive a turn-complete or interrupt event. +`assistant_text_complete` is therefore the best available idle boundary; an +assistant text block followed by a tool call can show `idle` briefly before +the next lifecycle event restores `working`. Native session restore is also +out of scope until panto exposes a stable session reference to extensions. + +## Check + +Run the module-level check with panto's Lua runtime: + +```sh +panto lua _selfcheck.lua +``` diff --git a/_selfcheck.lua b/_selfcheck.lua new file mode 100644 index 0000000..b4d0396 --- /dev/null +++ b/_selfcheck.lua @@ -0,0 +1,44 @@ +local events = {} +local reports = {} + +assert(loadfile("panto-herdr-scm-1.rockspec")) + +package.preload.panto = function() + return { + ext = { + on = function(name, handler) events[name] = handler end, + }, + } +end + +package.preload.luv = function() + return { + spawn = function(command, options, _) + reports[#reports + 1] = { command = command, args = options.args } + return { close = function() end } + end, + } +end + +local getenv = os.getenv +os.getenv = function(name) + if name == "HERDR_PANE_ID" then return "w1:p1" end + return getenv(name) +end + +local entry = require("panto-herdr") +entry.activate() +events.session_start() +events.user_message() +events.user_message() +events.tool_details({ tool_name = "read" }) +events.assistant_text() +events.assistant_text_complete() + +assert(#reports == 5) +assert(reports[1].command == "herdr") +assert(table.concat(reports[1].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 1") +assert(table.concat(reports[3].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 3 --custom-status tool:read") +assert(table.concat(reports[5].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 5") + +print("panto-herdr self-check: ok") diff --git a/panto-herdr-scm-1.rockspec b/panto-herdr-scm-1.rockspec new file mode 100644 index 0000000..d974b69 --- /dev/null +++ b/panto-herdr-scm-1.rockspec @@ -0,0 +1,28 @@ +rockspec_format = "3.0" +package = "panto-herdr" +version = "scm-1" + +source = { + -- Replace when the repository has a public remote. + url = "git+https://example.invalid/panto-herdr.git", +} + +description = { + summary = "Herdr lifecycle reporting for panto", + detailed = [[ +A panto extension that reports panto lifecycle state to Herdr when it runs +inside a Herdr pane. +]], + license = "MIT", +} + +dependencies = { + "lua >= 5.1", +} + +build = { + type = "builtin", + modules = { + ["panto-herdr"] = "panto-herdr/init.lua", + }, +} diff --git a/panto-herdr/init.lua b/panto-herdr/init.lua new file mode 100644 index 0000000..dffef51 --- /dev/null +++ b/panto-herdr/init.lua @@ -0,0 +1,53 @@ +local function tool_status(name) + name = name or "?" + return "tool:" .. name:sub(1, 27) +end + +return { + name = "herdr", + activate = function() + local pane_id = os.getenv("HERDR_PANE_ID") + if not pane_id or pane_id == "" then return end + + local panto = require("panto") + local uv = require("luv") + local seq = 0 + local last_state + local last_status + + local function report(state, status) + if state == last_state and status == last_status then return end + last_state, last_status = state, status + seq = seq + 1 + + local args = { + "pane", "report-agent", pane_id, + "--source", "panto:extension", + "--agent", "panto", + "--state", state, + "--seq", tostring(seq), + } + if status then + args[#args + 1] = "--custom-status" + args[#args + 1] = status + end + + local handle + pcall(function() + handle = uv.spawn("herdr", { args = args, stdio = { nil, nil, nil } }, function() + if handle then handle:close() end + end) + end) + end + + panto.ext.on("session_start", function() report("idle") end) + panto.ext.on("user_message", function() report("working", "thinking") end) + panto.ext.on("thinking", function() report("working", "thinking") end) + panto.ext.on("compaction", function() report("working", "compacting") end) + panto.ext.on("tool_details", function(event) + report("working", tool_status(event.tool_name)) + end) + panto.ext.on("assistant_text", function() report("working", "responding") end) + panto.ext.on("assistant_text_complete", function() report("idle") end) + end, +} |
