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. --- spec/test_init.lua | 57 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) (limited to 'spec/test_init.lua') diff --git a/spec/test_init.lua b/spec/test_init.lua index 338b7a7..2d5e7d1 100644 --- a/spec/test_init.lua +++ b/spec/test_init.lua @@ -58,10 +58,16 @@ return { end local tmp = assert(uv.fs_mkdtemp("/tmp/panto-subagents-init-XXXXXX")) - assert(os.execute("mkdir -p " .. tmp .. "/agents")) + assert(os.execute("mkdir -p " .. tmp .. "/agents " .. tmp .. "/workflows")) local file = assert(io.open(tmp .. "/agents/reviewer.md", "w")) file:write("---\ndescription: Reviews changes\n---\nYou are a reviewer.\n") file:close() + file = assert(io.open(tmp .. "/workflows/review-chain.toml", "w")) + file:write('name = "review-chain"\n[[steps]]\nid = "review"\nagent = "reviewer"\nprompt = "Review it."\n') + file:close() + file = assert(io.open(tmp .. "/workflows/unusable.toml", "w")) + file:write('name = "unusable"\nsteps = []\n') + file:close() local original_roots = paths.config_roots paths.config_roots = function(kind) @@ -70,6 +76,17 @@ return { local handle = fake.install() local ok, err = pcall(entry.activate) + local header + if ok then + handle.emit("session_start", { + get_component = function() + return { render = function() return { "Panto", "" } end } + end, + set_component = function(_, component) + header = component + end, + }) + end paths.config_roots = original_roots handle.restore() @@ -91,8 +108,28 @@ return { assert(type(run_tool.handler) == "function") assert(handle.tools_by_name["subagents.lua"].schema.properties.source, "the lua tool takes source") + local inline_agents = handle.tools_by_name["subagents.lua"].schema.properties.agents + assert(inline_agents and inline_agents.items.required, + "the lua tool describes workflow-local agent profiles") + assert(inline_agents.items.properties.system_prompt, + "an inline profile carries its system prompt") assert(handle.tools_by_name["subagents.workflow"].schema.properties.steps.items.required, "the workflow tool describes its step shape") + + assert(type(header) == "table" and type(header.render) == "function", + "activation wraps the session header") + local rendered = header:render(18) + local plain = table.concat(rendered, "\n"):gsub("\27%[[%d;]*m", "") + has(plain, "Panto") + has(plain, "subagents:") + has(plain, "reviewer") + has(plain, "workflows:") + has(plain, "review-chain") + assert(not plain:find("unusable", 1, true), "invalid workflows stay out of the usable inventory") + assert(handle.commands_by_name["workflow:unusable"] == nil, + "the header inventory matches command registration") + assert(rendered[#rendered] == "", "annotations stay before the trailing blank") + assert(#rendered > 4, "inventories wrap at the component width") end }, { "an interrupted turn cancels every live child, and its end closes them", function() @@ -100,6 +137,8 @@ return { assert(ok, tostring(err)) assert(type(handle.on_by_name["turn_interrupt"]) == "function", "an interrupted turn must be able to cancel its children") + assert(type(handle.on_by_name["turn_start"]) == "function", + "startup replay must end before the first live turn") assert(type(handle.on_by_name["turn_end"]) == "function", "a finished turn must be able to close its children") @@ -132,19 +171,27 @@ return { assert(type(handle.on_by_name["tool_call_complete"]) == "function", "children report progress through the tool call that started them") - local claimed + assert(type(handle.on_by_name["tool_result"]) == "function", + "the result restores the component's transcript position") + + local claimed, pins = nil, {} handle.emit("tool_call_complete", { id = "call-1", tool_name = "subagents.run", set_component = function(_, component) claimed = component - return { invalidate = function() end, alive = function() - return true - end } + return { + invalidate = function() end, + alive = function() return true end, + set_pinned = function(_, value) pins[#pins + 1] = value end, + } end, }) assert(type(claimed) == "table" and type(claimed.render) == "function", "the entry is given a component that renders the cards") + assert(pins[1] == true, "the progress component pins after claim") + handle.emit("tool_result", { id = "call-1", tool_name = "subagents.run" }) + assert(pins[2] == false, "the matching result unpins it") local foreign handle.emit("tool_call_complete", { -- cgit v1.3