diff options
| author | t <t@tjp.lol> | 2026-08-19 19:02:30 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-08-19 19:03:52 -0600 |
| commit | d1306506aa7f504b0e91c9c6ed7314afbf99978e (patch) | |
| tree | e5a7295dbc566a3679e99e14dae13376b0469e84 /subagents/spawn.lua | |
| parent | 94e3fd8358bbdb5d6ed81aed475fab7fc73e2097 (diff) | |
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.
Diffstat (limited to 'subagents/spawn.lua')
| -rw-r--r-- | subagents/spawn.lua | 82 |
1 files changed, 28 insertions, 54 deletions
diff --git a/subagents/spawn.lua b/subagents/spawn.lua index caf65aa..9117fdd 100644 --- a/subagents/spawn.lua +++ b/subagents/spawn.lua @@ -14,12 +14,14 @@ -- two fields resolve independently: a call may override reasoning while -- inheriting the model. -- --- A new child's conversation starts with the primary's effective system --- context, then the fixed child-role instruction, then — when the profile has --- a body — the profile prompt as a further system message. That profile --- message carries the immutable manifest metadata (owning primary session id + --- profile name); workflow-local profiles also carry an inline marker so the --- progress replay path can expose only prompts the caller explicitly supplied. +-- A new child's conversation starts with the fixed child-role instruction, +-- then — when the profile has a body — the profile prompt as a further system +-- message. The primary's system messages and dialogue are never copied: the +-- profile defines the child's system context rather than augmenting the +-- primary agent's prompt. That profile message carries the immutable manifest +-- metadata (owning primary session id + profile name); workflow-local profiles +-- also carry an inline marker so the progress replay path can expose only +-- prompts the caller explicitly supplied. -- The per-turn user metadata records the effective model/reasoning, the -- per-outer-call card sequence, terminal presentation status, and — when the -- spawn happened inside a bound extension tool — that outer tool call id. @@ -130,7 +132,7 @@ end -- build_spec(input, profiles) -> spec | nil, err -- --- input = { agent | id, prompt, model?, reasoning?, output? } +-- input = { agent | system_prompt | id, prompt, model?, reasoning?, output? } function M.build_spec(input, profiles) if type(input) ~= "table" then return nil, "expected a table of arguments" @@ -148,11 +150,14 @@ function M.build_spec(input, profiles) if err then return nil, err end - if agent and id then - return nil, "pass exactly one of `agent` (start a new child) or `id` (continue one), not both" + local system_prompt + system_prompt, err = optional_string(input.system_prompt, "system_prompt") + if err then + return nil, err end - if not agent and not id then - return nil, "pass exactly one of `agent` (start a new child) or `id` (continue one)" + local selectors = (agent and 1 or 0) + (system_prompt and 1 or 0) + (id and 1 or 0) + if selectors ~= 1 then + return nil, "pass exactly one of `agent` (start from a profile), `system_prompt` (start without a profile), or `id` (continue one)" end local model @@ -182,6 +187,12 @@ function M.build_spec(input, profiles) if not profile then return nil, string.format("unknown agent '%s'; known: %s", agent, M.agent_names(profiles)) end + elseif system_prompt then + profile = { + name = "subagent", + body = system_prompt, + inline = true, + } end -- child_store_dir returns the session info alongside the directory on @@ -208,17 +219,18 @@ function M.build_spec(input, profiles) spec.model = model or profile.model spec.reasoning = reasoning or profile.reasoning + local inline = profile.inline == true or profile.layer == "workflow" local system_messages = { { text = M.CHILD_ROLE } } if profile.body and profile.body:match("%S") then local manifest = { owner = info_or_err.session_id, agent = profile.name } - if profile.layer == "workflow" then manifest.inline = true end + if inline then manifest.inline = true end system_messages[#system_messages + 1] = { text = profile.body, metadata = { subagents = manifest }, } end spec.system_messages = system_messages - if profile.layer == "workflow" then + if inline then spec.presentation_system_prompt = profile.body end @@ -262,44 +274,6 @@ local function read_stored(conv) return defaults, manifest end --- The primary's effective system context, which every new child starts with. A --- replace-mode system block supersedes everything before it, exactly as the --- primary's own provider sees it. -local function primary_system_texts() - local primary = host().agent - if primary == nil then - return {} - end - local ok, conv = try(primary.conversation, primary) - if not ok or conv == nil then - return {} - end - local read, messages = try(conv.messages, conv) - if not read or type(messages) ~= "table" then - return {} - end - - local texts = {} - for _, message in ipairs(messages) do - if message.role == "system" then - local parts = {} - for _, block in ipairs(message.blocks or {}) do - if block.mode == "replace" then - texts, parts = {}, {} - end - if type(block.text) == "string" and (block.type == "system" or block.type == "text") then - parts[#parts + 1] = block.text - end - end - local text = table.concat(parts, "\n") - if text ~= "" then - texts[#texts + 1] = text - end - end - end - return texts -end - -- Everything the primary can call except the delegation tools themselves. The -- decls carry opaque source tags, so a child registering them reaches the same -- handlers on the same runtime. @@ -323,9 +297,6 @@ end local function seed_conversation(agent, spec) local conv = agent:conversation() - for _, text in ipairs(primary_system_texts()) do - conv:add_system_message(text) - end for _, message in ipairs(spec.system_messages or {}) do if message.metadata ~= nil then conv:add_system_message(message.text, { metadata = message.metadata }) @@ -527,6 +498,9 @@ function M.spawn(spec) pcall(agent.set_message_metadata, agent, turn_index, turn_metadata) end card:done(result.status, result.error) + if type(spec.on_settle) == "function" then + pcall(spec.on_settle, result) + end return result end |
