-- init.lua: the extension entry point pantograph evaluates and activates. -- -- Activation is the whole contract with the host: the wrong shape, a missing -- tool, a description that does not name the discovered profiles, or a missing -- lifecycle subscription is invisible until a user notices the tools are gone or -- a cancelled turn leaves children running. Discovery is pointed at a temporary -- config layer, so the assertions do not depend on this machine's ~/.config; the -- first case skips when luv or tinyyaml is missing because the profile it looks for -- could not be read without them. local fake = require("spec.fake_ext") local jobs = require("subagents.jobs") local paths = require("subagents.paths") local workflow = require("subagents.workflow") local uv = require("luv") local entry = require("init") local function has(text, needle) assert(type(text) == "string", "expected a string, got " .. type(text)) assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text)) end -- Activate against the fake host with discovery pointed at nothing, for the -- cases that care about registration rather than profiles. Profile discovery is -- cached in subagents.spawn, so a case that ran earlier may have filled it. local function activate_bare(fn, opts) local original = paths.config_roots paths.config_roots = function() return {} end local handle = fake.install(opts) local ok, err = pcall(function() if opts and opts.before then opts.before(handle) end entry.activate() end) paths.config_roots = original local result = table.pack(pcall(fn, handle, ok, err)) handle.restore() if not result[1] then error(result[2], 0) end end return { { "the entry is the extension shape pantograph expects", function() assert(entry.name == "subagents", tostring(entry.name)) assert(type(entry.activate) == "function", "activate must be a function") end }, { "activation registers the four tools and names the discovered profiles", function() local ok_uv, uv = pcall(require, "luv") if not ok_uv then return "skip", "luv is not installed" end if not pcall(require, "tinyyaml") then return "skip", "tinyyaml is not installed" end local tmp = assert(uv.fs_mkdtemp("/tmp/panto-subagents-init-XXXXXX")) 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) return { tmp .. "/" .. kind } end 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() os.execute("rm -rf " .. tmp) assert(ok, tostring(err)) for _, name in ipairs({ "subagents.run", "subagents.models", "subagents.lua", "subagents.workflow" }) do assert(handle.tools_by_name[name], "missing tool " .. name) end assert(#handle.tools == 4, "expected exactly four tools, saw " .. #handle.tools) local run_tool = handle.tools_by_name["subagents.run"] has(run_tool.description, "reviewer — Reviews changes") has(run_tool.description, "exactly one of `agent`") has(run_tool.description, "subagents.models") has(run_tool.description, "one tool batch") assert(run_tool.schema.required[1] == "prompt", "prompt is the only required field") assert(run_tool.schema.properties.agent and run_tool.schema.properties.id) assert(run_tool.schema.properties.system_prompt, "run accepts an inline system prompt") has(run_tool.schema.properties.model.description, "subagents.models") assert(type(run_tool.handler) == "function") assert(handle.tools_by_name["subagents.lua"].schema.properties.source, "the lua tool takes source") assert(handle.tools_by_name["subagents.lua"].schema.properties.prompt == nil, "workflow prompts live inside source") assert(#handle.tools_by_name["subagents.lua"].schema.required == 1 and handle.tools_by_name["subagents.lua"].schema.required[1] == "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 == nil, "inline workflow definitions belong to subagents.lua") assert(handle.tools_by_name["subagents.workflow"].schema.properties.name, "the workflow tool runs a discovered workflow by name") 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") local primary_messages = handle.ext.agent:conversation():messages() local guidance = primary_messages[#primary_messages].blocks[1].text has(guidance, "## Subagents") has(guidance, "subagents.workflows") has(guidance, "name=\"implement\"") has(guidance, "Review correctness") end }, { "layered config sets max_concurrent with later layers winning", function() local tmp = os.tmpname() os.remove(tmp) assert(os.execute("mkdir -p " .. tmp .. "/base " .. tmp .. "/project")) local file = assert(io.open(tmp .. "/base/config.toml", "w")) file:write("[other]\nmax_concurrent = 99\n\n[subagents]\nmax_concurrent = 6 # comment\n") file:close() file = assert(io.open(tmp .. "/project/config.toml", "w")) file:write("[subagents]\nmax_concurrent = 8\n") file:close() activate_bare(function(_, ok, err) os.execute("rm -rf " .. tmp) assert(ok, tostring(err)) assert(jobs.MAX_CONCURRENT == 8, tostring(jobs.MAX_CONCURRENT)) jobs.MAX_CONCURRENT = 5 end, { before = function(handle) handle.ext.dirs = { layers = { { name = "base", dir = tmp .. "/base" }, { name = "project", dir = tmp .. "/project" }, } } end, }) end }, { "max_concurrent must be a positive integer", function() local tmp = os.tmpname() os.remove(tmp) assert(os.execute("mkdir -p " .. tmp)) local file = assert(io.open(tmp .. "/config.toml", "w")) file:write("[subagents]\nmax_concurrent = 0\n") file:close() activate_bare(function(_, ok, err) os.execute("rm -rf " .. tmp) assert(not ok, "invalid concurrency must fail activation") has(tostring(err), "must be a positive integer") jobs.MAX_CONCURRENT = 5 end, { before = function(handle) handle.ext.dirs = { layers = { { name = "project", dir = tmp } } } end, }) end }, { "an interrupted turn cancels every live child, and its end closes them", function() activate_bare(function(handle, ok, err) 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 reap settled children") assert(type(handle.on_by_name["session_end"]) == "function", "session teardown must cancel background workflows") -- A child that would not settle on its own, so the lifecycle is the -- only thing that can end it. close_all runs whatever happens, or a -- leaked handle would count against the next case's gate. local job = fake.job({ settle = 99 }) local started = assert(jobs.start({ label = "alpha", build = function() return job end })) local checked, failure = pcall(function() assert(started:result() == nil, "the child is still running") handle.emit("turn_interrupt", { phase = "interrupt" }) assert(job._cancel_requested, "an interrupted turn asks its children to stop") handle.emit("turn_end", { phase = "end", reason = "interrupted" }) assert(not job._closed, "the end of the turn never joins a pump from the owner thread") -- The pump exits (the fake settles cancelled on its next poll). -- Ordinary turn_end only reaps settled jobs so background work -- can survive; the next boundary closes this settled handle. jobs.await({ started }, "all") assert(not job._closed, "settlement alone does not mutate the live catalog") handle.emit("turn_end", { phase = "end", reason = "completed" }) assert(job._closed, "the next turn boundary reaps the settled child") end) jobs.close_all() assert(checked, failure) end) end }, { "background workflows survive turn_end and interruption cancels them", function() activate_bare(function(handle, ok, err) assert(ok, tostring(err)) local tool = handle.tools_by_name["subagents.lua"] handle.queue({ output = "finished", settle = 3 }) local id = tool.handler({ source = [[ return subagents.workflow(function(ctx) local result = ctx:agent{name="work", system_prompt="Work.", prompt="go"}:await() return result.output end) ]] }, { tool_call_id = "background-call" }) handle.emit("turn_end", { reason = "completed" }) assert(workflow.workflows[id].status == "running") for _ = 1, 100 do uv.run("nowait") if workflow.workflows[id].status ~= "running" then break end end assert(workflow.workflows[id].status == "completed", tostring(workflow.workflows[id].error)) assert(handle.submissions[#handle.submissions]:find(id, 1, true)) handle.queue({ output = "too late", settle = 99 }) local cancelled = tool.handler({ source = [[ return subagents.workflow(function(ctx) local result = ctx:agent{name="slow", system_prompt="Work.", prompt="go"}:await() return result.output end) ]] }, { tool_call_id = "cancel-call" }) uv.run("nowait") local submissions = #handle.submissions handle.emit("turn_interrupt", { reason = "interrupted" }) for _ = 1, 100 do uv.run("nowait") if workflow.workflows[cancelled].status ~= "running" then break end end assert(workflow.workflows[cancelled].status == "cancelled") assert(#handle.submissions == submissions, "cancelled workflows do not wake the primary") end) end }, { "only the subagents tool calls claim a progress component", function() activate_bare(function(handle, ok, err) assert(ok, tostring(err)) assert(type(handle.on_by_name["tool_call_complete"]) == "function", "children report progress through the tool call that started them") 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 = "replayed-call", tool_name = "subagents.workflow", set_component = function(_, component) claimed = component return { invalidate = function() end, alive = function() return true end, set_pinned = function(_, value) pins[#pins + 1] = value end, } end, }) assert(#pins == 0, "a workflow restored during startup stays in transcript order") handle.emit("turn_start", {}) handle.emit("tool_call_complete", { id = "call-1", tool_name = "subagents.workflow", set_component = function(_, component) claimed = component 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 live progress component pins after claim") local output = handle.tools_by_name["subagents.workflow"].handler( { prompt = "", name = "" }, { tool_call_id = "call-1" }) assert(type(output) == "string", "the workflow handler returned its result") assert(pins[2] == false, "handler completion unpins before the next model action") handle.emit("tool_result", { id = "call-1", tool_name = "subagents.workflow" }) assert(#pins == 2, "the later host result is an inert fallback") local foreign handle.emit("tool_call_complete", { id = "call-2", tool_name = "read_file", set_component = function(_, component) foreign = component end, }) assert(foreign == nil, "another tool's entry is left alone") end) end }, { "a host without resolve_model fails activation loudly", function() activate_bare(function(handle, ok, err) assert(not ok, "activation must not silently register unusable tools") has(tostring(err), "too old") assert(#handle.tools == 0, "nothing is registered by a failed activation") end, { before = function(handle) handle.ext.resolve_model = nil end, }) end }, { "a host without panto.agent fails activation loudly", function() activate_bare(function(handle, ok, err) assert(not ok, "a host that cannot build a child agent is too old") has(tostring(err), "too old") assert(#handle.tools == 0, "nothing is registered by a failed activation") end, { before = function() package.loaded.panto.agent = nil end, }) end }, }