From cc69a2e431779528578211a5cb3385bb23e076bf Mon Sep 17 00:00:00 2001 From: t Date: Tue, 18 Aug 2026 14:18:14 -0600 Subject: 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. --- init.lua | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 5 deletions(-) (limited to 'init.lua') 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 { -- cgit v1.3