-- subagents/run.lua and subagents/spawn.lua: what a child is built out of, and -- what the model is told afterwards. -- -- Every case installs a fresh fake host, so `handle.spawns` holds exactly the -- children this case produced: one record per child agent, carrying the -- resolve_model arguments, the store directory, the system messages it was -- seeded with, the tool declarations it was given and the run_async options. -- Profile sets are built inline rather than discovered: precedence, not -- discovery, is what these cases are about, and an explicit set also keeps the -- machine's real ~/.config out of the run. local fake = require("spec.fake_ext") local progress = require("subagents.progress") local run = require("subagents.run") local spawn = require("subagents.spawn") local function has(text, needle) assert(type(text) == "string", "expected a string result, got " .. type(text)) assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text)) end local function profile_set() local reviewer = { name = "reviewer", description = "Reviews changes", model = "anthropic:sonnet", reasoning = "high", body = "You are a reviewer.\n", } local scout = { name = "scout", description = "", body = "" } return { list = { reviewer, scout }, by_name = { reviewer = reviewer, scout = scout }, warnings = {}, } end -- with_host(fn, opts): install the fake, run fn(handle, profiles), restore. local function with_host(fn, opts) local handle = fake.install(opts) local ok, err = pcall(fn, handle, profile_set()) handle.restore() if not ok then error(err, 0) end end -- The child conversation a resumed reviewer loads: the manifest on its profile -- system message, and the model/reasoning it last ran with on its last turn. local function stored_reviewer() return { { role = "system", text = spawn.CHILD_ROLE }, { role = "system", text = "You are a reviewer.\n", metadata = { subagents = { owner = "0198-primary", agent = "reviewer" } }, }, { role = "user", text = "the first turn", metadata = { subagents = { model = "openai:gpt-5.6", reasoning = "xhigh" } }, }, { role = "assistant", text = "first answer" }, } end return { { "neither agent nor id is refused before anything is spawned", function() with_host(function(handle, profiles) local text = run.handle({ prompt = "do the thing" }, profiles) has(text, "Error:") has(text, "exactly one of `agent`") assert(#handle.spawns == 0, "nothing may be spawned by a rejected call") assert(not text:find("id:", 1, true), "a pre-allocation failure has no id") end) end }, { "both agent and id is refused", function() with_host(function(handle, profiles) local text = run.handle({ agent = "reviewer", id = "0198-x", prompt = "go" }, profiles) has(text, "Error:") has(text, "not both") assert(#handle.spawns == 0) end) end }, { "an empty or missing prompt is refused", function() with_host(function(handle, profiles) has(run.handle({ agent = "reviewer", prompt = "" }, profiles), "`prompt` must be a non-empty string") has(run.handle({ agent = "reviewer", prompt = " " }, profiles), "`prompt` must be a non-empty string") has(run.handle({ agent = "reviewer" }, profiles), "`prompt` must be a non-empty string") assert(#handle.spawns == 0) end) end }, { "an unknown agent names the known profiles", function() with_host(function(handle, profiles) local text = run.handle({ agent = "ghost", prompt = "go" }, profiles) has(text, "unknown agent 'ghost'") has(text, "reviewer, scout") assert(#handle.spawns == 0) end) end }, { "a new child is seeded with the child-role and profile system messages", function() with_host(function(handle, profiles) run.handle({ agent = "reviewer", prompt = "Review the auth change." }, profiles) assert(#handle.spawns == 1, "expected exactly one child") local child = handle.spawns[1] assert(child.store_dir == handle.session.session_dir .. "/subagents/" .. handle.session.session_id, "wrong child store dir: " .. tostring(child.store_dir)) assert(child.session_id == nil, "a new child must not name a session") assert(child.prompt == "Review the auth change.", tostring(child.prompt)) local messages = child.system_messages assert(type(messages) == "table" and #messages == 2, "expected role + profile messages, saw " .. #messages) assert(messages[1].text == spawn.CHILD_ROLE, "the first message is the fixed child role") assert(messages[1].metadata == nil, "the role message carries no manifest") assert(messages[2].text == "You are a reviewer.\n", "the profile body is the second message") local manifest = messages[2].metadata.subagents assert(manifest.owner == handle.session.session_id, tostring(manifest.owner)) assert(manifest.agent == "reviewer", tostring(manifest.agent)) end) end }, { "subagents.run presents its child prompt only when expanded", function() with_host(function(handle, profiles) handle.queue_for("reviewer", { events = { { type = "block_start", block_type = "text", index = 0 }, { type = "content_delta", index = 0, delta = "checking auth\n" }, { type = "block_start", block_type = "tool_use", index = 1 }, { type = "tool_details", index = 1, id = "tool-1", name = "std__read" }, { type = "block_complete", block_type = "tool_use", index = 1, id = "tool-1", name = "std__read", text = '{"path":"auth.lua"}' }, { type = "tool_dispatch_result", tool_results = { { tool_use_id = "tool-1", output = "file contents", is_error = false }, } }, } }) progress.reset() local component progress.claim({ id = "run-call", tool_name = "subagents.run", collapsed = true, set_component = function(_, value) component = value return { invalidate = function() end, alive = function() return true end, set_pinned = function() end, } end, }) progress.bind({ tool_call_id = "run-call" }) run.handle({ agent = "reviewer", prompt = "Review the auth change." }, profiles) local compact = table.concat(component:render(100), "\n") assert(not compact:find("Review the auth change.", 1, true), compact) progress.collapse({ collapsed = false }) local expanded = table.concat(component:render(100), "\n") has(expanded, "prompt: Review the auth change.") has(expanded, "checking auth") has(expanded, "std.read [tool-1]") has(expanded, 'input: {"path":"auth.lua"}') has(expanded, "result [tool-1]: file contents") assert(not expanded:find("system prompt:", 1, true), "a discovered profile system prompt is not presentation metadata:\n" .. expanded) progress.reset() end) end }, { "the primary's system context comes first, then the role, then the profile", function() with_host(function(handle, profiles) run.handle({ agent = "reviewer", prompt = "go" }, profiles) local messages = handle.spawns[1].system_messages assert(#messages == 4, "expected two copied messages plus role and profile, saw " .. #messages) assert(messages[1].text == "Project context.", tostring(messages[1].text)) assert(messages[2].text == "House style.", tostring(messages[2].text)) assert(messages[3].text == spawn.CHILD_ROLE, "the child role follows the primary's context") assert(messages[4].text == "You are a reviewer.\n", "the profile body is last") assert(messages[1].metadata == nil, "copied context carries no manifest") end, { primary_messages = { { role = "system", text = "Project context." }, { role = "user", text = "the parent dialogue is never copied" }, { role = "assistant", text = "nor this" }, { role = "system", text = "House style." }, }, }) end }, { "the child store directory is created before the store is opened", function() with_host(function(handle, profiles) run.handle({ agent = "reviewer", prompt = "go" }, profiles) local dir = handle.session.session_dir .. "/subagents/" .. handle.session.session_id assert(handle.made_dir(dir), "the child catalog is created, not assumed: " .. table.concat(handle.mkdirs, ", ")) assert(handle.stores[1] == dir, "the store opens on that directory: " .. tostring(handle.stores[1])) end) end }, { "a child inherits the primary's tools except the subagents ones", function() with_host(function(handle, profiles) run.handle({ agent = "reviewer", prompt = "go" }, profiles) local child = handle.spawns[1] assert(table.concat(child.tools, ",") == "bash,read_file", "expected only the non-subagents tools, saw " .. table.concat(child.tools, ",")) for _, decl in ipairs(child.tool_decls) do assert(decl._source == fake.SOURCE, "the re-registration tag must be passed through untouched") end end) end }, { "the resolved model and reasoning are recorded on the turn", function() with_host(function(handle, profiles) run.handle({ agent = "reviewer", prompt = "go" }, profiles) local metadata = handle.spawns[1].run.metadata assert(type(metadata) == "table" and type(metadata.subagents) == "table", "every child turn records what it ran on") assert(metadata.subagents.model == "anthropic:sonnet", tostring(metadata.subagents.model)) assert(metadata.subagents.reasoning == "high", tostring(metadata.subagents.reasoning)) assert(handle.spawns[1].run.dispatch_tools ~= false, "an ordinary child dispatches its tools") end) end }, { "the child-role instruction states the contract the design fixes", function() local role = spawn.CHILD_ROLE has(role, "You are a subagent") has(role, "self-contained report") has(role, "verbatim") has(role, "cannot ask the user questions") end }, { "model and reasoning resolve tool over profile over inherited", function() with_host(function(handle, profiles) run.handle({ agent = "reviewer", prompt = "a" }, profiles) assert(handle.spawns[1].model == "anthropic:sonnet", "the profile model applies") assert(handle.spawns[1].reasoning == "high", "the profile reasoning applies") run.handle({ agent = "reviewer", prompt = "b", model = "openai:gpt-5.6", reasoning = "xhigh" }, profiles) assert(handle.spawns[2].model == "openai:gpt-5.6", "the call overrides the profile") assert(handle.spawns[2].reasoning == "xhigh", "the call overrides the profile") run.handle({ agent = "reviewer", prompt = "d", reasoning = "low" }, profiles) assert(handle.spawns[3].model == "anthropic:sonnet", "model and reasoning resolve independently") assert(handle.spawns[3].reasoning == "low") end) end }, { "a profile that names neither inherits the primary's pair", function() with_host(function(handle, profiles) run.handle({ agent = "scout", prompt = "look around" }, profiles) assert(handle.spawns[1].model == "openai:gpt-5.6", tostring(handle.spawns[1].model)) assert(handle.spawns[1].reasoning == "low", tostring(handle.spawns[1].reasoning)) end, { session = { model = "openai:gpt-5.6", reasoning = "low" } }) end }, { "a profile with an empty body contributes no system message", function() with_host(function(handle, profiles) run.handle({ agent = "scout", prompt = "look around" }, profiles) local messages = handle.spawns[1].system_messages assert(#messages == 1, "expected only the child-role message, saw " .. #messages) assert(messages[1].text == spawn.CHILD_ROLE) end) end }, { "a resume loads the stored conversation and seeds nothing", function() with_host(function(handle, profiles) handle.add_session("0198-child", stored_reviewer()) handle.queue({ output = "second turn" }) local text = run.handle({ id = "0198-child", prompt = "now the tests" }, profiles) local child = handle.spawns[1] assert(child.session_id == "0198-child", tostring(child.session_id)) assert(child.resumed, "the agent is built on the resolved session") assert(#child.system_messages == 0, "the stored conversation is canonical on resume") assert(child.store_dir == handle.session.session_dir .. "/subagents/" .. handle.session.session_id) has(text, "id: 0198-child") has(text, "second turn") end) end }, { "a resumed child keeps the model and reasoning of its last turn", function() with_host(function(handle, profiles) handle.add_session("0198-child", stored_reviewer()) handle.queue({}) run.handle({ id = "0198-child", prompt = "carry on" }, profiles) assert(handle.spawns[1].model == "openai:gpt-5.6", tostring(handle.spawns[1].model)) assert(handle.spawns[1].reasoning == "xhigh", tostring(handle.spawns[1].reasoning)) end) end }, { "a per-turn override beats the stored default and only where it is given", function() with_host(function(handle, profiles) handle.add_session("0198-child", stored_reviewer()) handle.queue({}) run.handle({ id = "0198-child", prompt = "carry on", reasoning = "high" }, profiles) assert(handle.spawns[1].reasoning == "high", "the call overrides the stored default") assert(handle.spawns[1].model == "openai:gpt-5.6", "the model keeps its last effective value") end) end }, -- The binding reports a stored record it cannot decode by raising. A -- malformed message is skipped, and nothing here raises out to the tool. { "stored metadata that cannot be decoded is skipped, not raised", function() with_host(function(handle, profiles) local stored = stored_reviewer() for _, message in ipairs(stored) do if message.metadata then message.metadata = nil message.metadata_error = "message_metadata: invalid JSON" end end handle.add_session("0198-child", stored) handle.queue({ output = "second turn" }) local text = run.handle({ id = "0198-child", prompt = "carry on" }, profiles) assert(not text:find("Error:", 1, true), "a malformed record is not a tool error:\n" .. text) has(text, "second turn") has(text, "agent: ?") -- an unreadable manifest names no agent assert(handle.spawns[1].model == "anthropic:sonnet", "an unreadable stored default falls back to the primary's model, got " .. tostring(handle.spawns[1].model)) end) end }, { "a resumed child takes its agent name from the manifest", function() with_host(function(handle, profiles) handle.add_session("0198-child", stored_reviewer()) handle.queue({ output = "done" }) local text = run.handle({ id = "0198-child", prompt = "carry on" }, profiles) has(text, "agent: reviewer") end) end }, { "an id this session never started is refused before anything is built", function() with_host(function(handle, profiles) local text = run.handle({ id = "0198-nope", prompt = "carry on" }, profiles) has(text, "Error:") has(text, "unknown subagent id '0198-nope'") assert(#handle.spawns == 0, "no child is built for an id that does not resolve") assert(#handle.runs == 0, "and no turn is started") end) end }, { "a completed result renders every field", function() with_host(function(handle, profiles) handle.queue({ id = "0198-abc", status = "completed", output = "Found two issues." }) local text = run.handle({ agent = "reviewer", prompt = "review" }, profiles) assert(text == table.concat({ "id: 0198-abc", "agent: reviewer", "status: completed", "resumable: true", "--- output ---", "Found two issues.", }, "\n"), "unexpected block:\n" .. text) end) end }, { "a failed first turn reports the error and is not resumable", function() with_host(function(handle, profiles) handle.queue({ id = "0198-def", status = "failed", error = "provider refused", resumable = false }) local text = run.handle({ agent = "reviewer", prompt = "review" }, profiles) has(text, "status: failed") has(text, "resumable: false") has(text, "provider refused") end) end }, { "resumability is read back from the store after the turn settles", function() with_host(function(handle, profiles) handle.queue({ id = "0198-ghi", output = "wrote something" }) has(run.handle({ agent = "reviewer", prompt = "review" }, profiles), "resumable: true") assert(handle.sessions["0198-ghi"], "a durable child leaves a session behind") handle.queue({ id = "0198-jkl", output = "died young", resumable = false }) has(run.handle({ agent = "reviewer", prompt = "review" }, profiles), "resumable: false") assert(handle.sessions["0198-jkl"] == nil, "no file, no continuation") end) end }, { "a cancelled child settles as cancelled", function() with_host(function(handle, profiles) handle.queue({ id = "0198-ghi", status = "cancelled", error = "cancelled by the user" }) local text = run.handle({ agent = "reviewer", prompt = "review" }, profiles) has(text, "status: cancelled") has(text, "cancelled by the user") end) end }, { "a model the host cannot resolve is reported and starts nothing", function() with_host(function(handle, profiles) local text = run.handle({ agent = "reviewer", prompt = "review", model = "openai:ghost" }, profiles) assert(text == "Error: resolve_model: unknown model 'openai:ghost'", text) assert(#handle.resolves == 1, "the host decides what a model reference means, not the rock") assert(handle.resolves[1].model == "openai:ghost", tostring(handle.resolves[1].model)) assert(#handle.spawns == 0, "the child is never built") assert(#handle.runs == 0, "a rejected child is never started") end, { unknown_models = { ["openai:ghost"] = true } }) end }, }