summaryrefslogtreecommitdiff
path: root/init.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-18 14:18:14 -0600
committert <t@tjp.lol>2026-08-18 14:20:28 -0600
commitcc69a2e431779528578211a5cb3385bb23e076bf (patch)
tree8833171e25bb3dfb71fc4a2e0b8fe583782da764 /init.lua
parent372ef8ff40991644ec2654c61328f31779f4ad21 (diff)
Persist and replay subagent progress across sessions
Record durable child-turn metadata, restore bounded progress cards during startup replay, and keep settled cards attached to transcript entries. Add workflow-local Lua profiles and expose discovered agents and workflows in the session header.
Diffstat (limited to 'init.lua')
-rw-r--r--init.lua89
1 files changed, 84 insertions, 5 deletions
diff --git a/init.lua b/init.lua
index 5f28308..2e4cab1 100644
--- a/init.lua
+++ b/init.lua
@@ -18,9 +18,9 @@
-- profile list.
--
-- Activation also subscribes to the turn lifecycle: an interrupted turn asks
--- every live child to stop, and the end of a turn joins them and drops the
--- progress cards, which are presentation state that must not survive the turn
--- that produced it.
+-- every live child to stop, and the end of a turn joins them and clears the
+-- coroutine bindings. Settled progress cards remain attached to their
+-- transcript entries until the host destroys those components.
--
-- If the host predates the model-resolution seam, activation fails loudly
-- instead of registering tools that cannot work.
@@ -34,6 +34,7 @@ local spawn = require("subagents.spawn")
local toml_workflows = require("subagents.toml_workflows")
local MAX_SHOWN_WARNINGS = 5
+local DIM, RESET = "\27[2m", "\27[0m"
local function host()
return require("panto").ext
@@ -47,6 +48,58 @@ local function subscribe(ext, name, handler)
end
end
+local function wrap_plain(text, width)
+ width = math.max(1, width or 1)
+ local lines, from = {}, 1
+ while #text - from + 1 > width do
+ local window = text:sub(from, from + width - 1)
+ local cut = window:match("^.*() ") or width
+ lines[#lines + 1] = text:sub(from, from + cut - 1):gsub("%s+$", "")
+ from = from + cut
+ while from <= #text and text:sub(from, from) == " " do
+ from = from + 1
+ end
+ end
+ lines[#lines + 1] = text:sub(from)
+ return lines
+end
+
+local function install_header(ext, profiles, workflows)
+ local profile_names, workflow_names = {}, {}
+ for _, profile in ipairs(profiles.list) do
+ profile_names[#profile_names + 1] = profile.name
+ end
+ for _, workflow in ipairs(workflows.list) do
+ if workflow.definition then workflow_names[#workflow_names + 1] = workflow.name end
+ end
+ table.sort(profile_names)
+ table.sort(workflow_names)
+
+ subscribe(ext, "session_start", function(event)
+ local inner = event:get_component()
+ event:set_component({
+ render = function(_, width)
+ local lines = inner:render(width)
+ local extras = {}
+ for _, inventory in ipairs({
+ { "subagents", profile_names },
+ { "workflows", workflow_names },
+ }) do
+ local value = #inventory[2] == 0 and "(none)" or table.concat(inventory[2], ", ")
+ for _, line in ipairs(wrap_plain(" " .. inventory[1] .. ": " .. value, width)) do
+ extras[#extras + 1] = DIM .. line .. RESET
+ end
+ end
+ local at = (#lines > 0 and lines[#lines] == "") and #lines or (#lines + 1)
+ for index = #extras, 1, -1 do
+ table.insert(lines, at, extras[index])
+ end
+ return lines
+ end,
+ })
+ end)
+end
+
local function run_description(profiles)
local lines = {
"Delegate a task to a subagent and wait for its report. Start a new child from an agent profile, or continue a child you started earlier in this session.",
@@ -100,6 +153,12 @@ local function activate()
-- Escape reaches the children through the turn, not through a tool: the
-- primary is parked inside a tool call when they are running.
+ -- Startup session replay fires tool lifecycle events before the first
+ -- live turn. After that boundary, progress.claim must stay presentation-
+ -- only and never rescan the child catalog for ordinary calls.
+ subscribe(ext, "turn_start", function()
+ progress.begin_live_turn()
+ end)
subscribe(ext, "turn_interrupt", function()
jobs.cancel_all()
end)
@@ -111,6 +170,12 @@ local function activate()
subscribe(ext, "tool_call_complete", function(event)
progress.claim(event)
end)
+ subscribe(ext, "tool_result", function(event)
+ progress.settle(event)
+ end)
+ subscribe(ext, "tool_collapse", function(event)
+ progress.collapse(event)
+ end)
ext.register_tool {
name = "subagents.run",
@@ -152,12 +217,25 @@ local function activate()
ext.register_tool {
name = "subagents.lua",
- description = "Run a one-off Lua workflow that fans several subagents out and combines their results. `source` must return subagents.workflow(function(ctx, input) ... end) and runs in a restricted environment with no filesystem, process, or module access.",
+ description = "Run a one-off Lua workflow that fans several subagents out and combines their results. `source` must return subagents.workflow(function(ctx, input) ... end) and runs in a restricted environment with no filesystem, process, or module access. Optional `agents` define profiles available only to this workflow.",
schema = {
type = "object",
properties = {
prompt = { type = "string", description = "The workflow input, passed to the callback as its second argument." },
source = { type = "string", description = "Lua source returning subagents.workflow(function(ctx, input) ... end)." },
+ agents = {
+ type = "array",
+ description = "Agent profiles available only to this workflow. Inline profiles shadow discovered profiles with the same name.",
+ items = {
+ type = "object",
+ properties = {
+ name = { type = "string", description = "Profile name used by ctx:agent." },
+ description = { type = "string", description = "Optional human-readable purpose." },
+ system_prompt = { type = "string", description = "System prompt for this profile." },
+ },
+ required = { "name", "system_prompt" },
+ },
+ },
},
required = { "prompt", "source" },
},
@@ -204,7 +282,8 @@ local function activate()
end,
}
- toml_workflows.discover_and_register(profiles)
+ local workflows = toml_workflows.discover_and_register(profiles)
+ install_header(ext, profiles, workflows)
end
return {