-- 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 lyaml 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 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, "lyaml") then return "skip", "lyaml 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(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() 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 be able to close its children") -- 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) -- and the drain that notices it does the close. jobs.await({ started }, "all") assert(job._closed, "a child closes as soon as its pump exits") end) jobs.close_all() assert(checked, failure) 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 = "call-1", tool_name = "subagents.run", 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 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", { 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 }, }