From d1306506aa7f504b0e91c9c6ed7314afbf99978e Mon Sep 17 00:00:00 2001 From: t Date: Wed, 19 Aug 2026 19:02:30 -0600 Subject: Add background Lua workflows, inline child prompts, and layered config subagents.lua now starts a workflow on its own coroutine and returns a session-scoped id immediately, so fan-out continues while the primary keeps working; completion wakes the primary, and later calls read immutable records from subagents.workflows. Every ctx:agent takes a workflow-unique name so those records are addressable. A session_start guidance message tells the primary when to reach for run vs lua. Children no longer inherit the primary's system context: a child starts from the fixed child-role instruction plus its profile, and subagents.run/ctx:agent accept an inline system_prompt instead of a profile. The now-redundant `agent` form of subagents.models is gone. Concurrency defaults to five and is configurable through [subagents] max_concurrent in any layered config.toml; turn boundaries reap only settled jobs so background workflows survive, while interrupt and session end cancel. Config roots come from panto.ext.dirs.layers rather than a hand-rolled XDG lookup, which picks up the base and git-ignored local layers for both agents/ and workflows/. TOML workflows tighten up: the subagents.workflow tool takes a discovered name only (inline `steps` duplicated subagents.lua at less power), an optional top-level `output` array chooses the reported steps and their order instead of the terminal set, a step with no workflow input gets no empty input heading, and a workflow naming an undiscovered agent is rejected at discovery rather than part-way through a run. --- spec/test_run.lua | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) (limited to 'spec/test_run.lua') diff --git a/spec/test_run.lua b/spec/test_run.lua index 57c6e47..b2c5783 100644 --- a/spec/test_run.lua +++ b/spec/test_run.lua @@ -75,11 +75,15 @@ return { end) end }, - { "both agent and id is refused", function() + { "multiple selectors are 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") + has(text, "exactly one of `agent`") + assert(#handle.spawns == 0) + + text = run.handle({ agent = "reviewer", system_prompt = "Be concise.", prompt = "go" }, profiles) + has(text, "exactly one of `agent`") assert(#handle.spawns == 0) end) end }, @@ -124,6 +128,21 @@ return { end) end }, + { "an inline system prompt starts a child without a profile", function() + with_host(function(handle, profiles) + local text = run.handle({ system_prompt = "Be a focused investigator.", prompt = "Find the cause." }, profiles) + assert(#handle.spawns == 1) + local messages = handle.spawns[1].system_messages + assert(#messages == 2, "expected role and inline prompt") + assert(messages[1].text == spawn.CHILD_ROLE) + assert(messages[2].text == "Be a focused investigator.") + local manifest = messages[2].metadata.subagents + assert(manifest.agent == "subagent", tostring(manifest.agent)) + assert(manifest.inline == true, "inline prompts must remain identifiable on replay") + has(text, "agent: subagent") + end) + end }, + { "subagents.run presents its child prompt only when expanded", function() with_host(function(handle, profiles) handle.queue_for("reviewer", { events = { @@ -171,22 +190,23 @@ return { end) end }, - { "the primary's system context comes first, then the role, then the profile", function() + { "the primary's system context and dialogue are not copied", 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") + assert(#messages == 2, "expected only role and profile messages, saw " .. #messages) + assert(messages[1].text == spawn.CHILD_ROLE, "the child role is first") + assert(messages[2].text == "You are a reviewer.\n", "the profile defines the child context") + for _, message in ipairs(messages) do + assert(message.text ~= "Primary core prompt.", "the primary system prompt must not reach the child") + assert(message.text ~= "Project context.", "primary system additions must not be copied") + end end, { primary_messages = { - { role = "system", text = "Project context." }, + { role = "system", text = "Primary core prompt." }, { role = "user", text = "the parent dialogue is never copied" }, { role = "assistant", text = "nor this" }, - { role = "system", text = "House style." }, + { role = "system", text = "Project context." }, }, }) end }, -- cgit v1.3