summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-09-01 12:00:53 -0600
committert <t@tjp.lol>2026-09-02 08:30:29 -0600
commit9d6b9a68f3df389a68b6b2cd69d17e01b81024dd (patch)
tree24e921ac74663ed0c14534be276dc2a85c403bc9
parent04791c3273ab7076cc53f146d7751670d86c5c9b (diff)
Keep Herdr working until all activity finishesHEADmain
Track foreground turns and balanced background-work events independently so Herdr only reports idle after the primary and every background task settle.
-rw-r--r--README.md20
-rw-r--r--_selfcheck.lua22
-rw-r--r--panto-herdr/init.lua51
3 files changed, 66 insertions, 27 deletions
diff --git a/README.md b/README.md
index e5017be..84e5778 100644
--- a/README.md
+++ b/README.md
@@ -26,20 +26,16 @@ its JSON output, so reporting does not affect the panto terminal UI.
| panto event | Herdr state |
| --- | --- |
-| `session_start`, `session_ready` | `idle` |
-| `user_message`, `thinking`, `compaction`, `tool_details`, `assistant_text` | `working` |
-| `assistant_text_complete` | `idle` |
+| `session_start`, `session_ready` | `idle` when no work is active |
+| `turn_start`, `agent_submission`, `user_message`, `thinking`, `compaction`, `tool_details`, `assistant_text` | `working` |
+| `turn_end` | `idle` when no background work is active |
+| `background_work_start` / `background_work_end` | increments / decrements active background work |
| `session_end` | agent released |
-`session_ready` restores `idle` after panto replays historical lifecycle events
-while loading a resumed session. `session_end` removes panto's agent report when
-the process shuts down, so Herdr also clears the pane/space activity indicator
-once no reported agents remain.
-
-`assistant_text_complete` is the block-level 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 out of scope until panto
-exposes a stable session reference to extensions.
+Extensions with work that survives a turn emit balanced `background_work_start`
+and `background_work_end` events. Herdr remains `working` until both the primary
+turn and all such work finish. `session_end` removes panto's agent report when
+the process shuts down.
## Check
diff --git a/_selfcheck.lua b/_selfcheck.lua
index 08df12c..cb479d6 100644
--- a/_selfcheck.lua
+++ b/_selfcheck.lua
@@ -50,7 +50,12 @@ events.user_message()
events.user_message()
events.tool_details({ tool_name = "read" })
events.assistant_text()
-events.assistant_text_complete()
+events.background_work_start()
+events.turn_end()
+events.agent_submission()
+events.background_work_end()
+events.assistant_text()
+events.turn_end()
events.session_end()
-- A new process in the same pane/source must outrank the old process's final
@@ -61,15 +66,18 @@ entry.activate()
events.session_start()
events.assistant_text()
events.session_ready()
+events.turn_end()
-assert(#reports == 9)
+assert(#reports == 12)
assert(reports[1].command == "herdr")
assert(table.concat(reports[1].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 100000001")
assert(table.concat(reports[3].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 100000003 --message tool:read")
-assert(table.concat(reports[5].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 100000005")
-assert(table.concat(reports[6].args, " ") == "pane release-agent w1:p1 --source panto:extension --agent panto --seq 100000006")
-assert(table.concat(reports[7].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 101000001")
-assert(table.concat(reports[8].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 101000002 --message responding")
-assert(table.concat(reports[9].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 101000003")
+assert(table.concat(reports[5].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 100000005 --message background")
+assert(table.concat(reports[6].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 100000006 --message thinking")
+assert(table.concat(reports[8].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 100000008")
+assert(table.concat(reports[9].args, " ") == "pane release-agent w1:p1 --source panto:extension --agent panto --seq 100000009")
+assert(table.concat(reports[10].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 101000001")
+assert(table.concat(reports[11].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 101000002 --message responding")
+assert(table.concat(reports[12].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 101000003")
print("panto-herdr self-check: ok")
diff --git a/panto-herdr/init.lua b/panto-herdr/init.lua
index cc0b527..b3d30be 100644
--- a/panto-herdr/init.lua
+++ b/panto-herdr/init.lua
@@ -17,6 +17,9 @@ return {
local seq = os.time() * 1000000
local last_state
local last_status
+ local foreground_working = false
+ local foreground_status
+ local background_work = 0
local function spawn(args)
local handle
@@ -57,16 +60,48 @@ return {
})
end
- panto.ext.on("session_start", function() report("idle") end)
- panto.ext.on("session_ready", 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)
+ local function update_idle_state()
+ if foreground_working or background_work > 0 then
+ report("working", foreground_working and foreground_status or "background")
+ else
+ report("idle")
+ end
+ end
+
+ local function foreground(status)
+ foreground_working, foreground_status = true, status
+ report("working", status)
+ end
+
+ panto.ext.on("session_start", function()
+ foreground_working, foreground_status, background_work = false, nil, 0
+ report("idle")
+ end)
+ panto.ext.on("session_ready", function()
+ foreground_working, foreground_status = false, nil
+ update_idle_state()
+ end)
+ panto.ext.on("turn_start", function() foreground("thinking") end)
+ panto.ext.on("agent_submission", function() foreground("thinking") end)
+ panto.ext.on("user_message", function() foreground("thinking") end)
+ panto.ext.on("thinking", function() foreground("thinking") end)
+ panto.ext.on("compaction", function() foreground("compacting") end)
panto.ext.on("tool_details", function(event)
- report("working", tool_status(event.tool_name))
+ foreground(tool_status(event.tool_name))
+ end)
+ panto.ext.on("assistant_text", function() foreground("responding") end)
+ panto.ext.on("turn_end", function()
+ foreground_working, foreground_status = false, nil
+ update_idle_state()
+ end)
+ panto.ext.on("background_work_start", function()
+ background_work = background_work + 1
+ update_idle_state()
+ end)
+ panto.ext.on("background_work_end", function()
+ background_work = math.max(0, background_work - 1)
+ update_idle_state()
end)
- panto.ext.on("assistant_text", function() report("working", "responding") end)
- panto.ext.on("assistant_text_complete", function() report("idle") end)
panto.ext.on("session_end", release)
end,
}