From 4f0a91ef55fe96835172bdad34feec1e2a0a0977 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 16 Aug 2026 20:42:43 -0600 Subject: subagents extension on the generic host surfaces The rock now owns all subagent policy on top of libpanto-lua's generic APIs: children are ordinary panto.agent instances over rock-constructed stores, started with agent:run_async and awaited by arming uv.new_poll on each job's wake_fd from the tool handler's coroutine. subagents/jobs.lua carries the session policy the host used to own: the concurrency gate (4 running, FIFO queue, cancel-while-queued never starts), the await contract (results in input order; "first" returns settled plus remaining by identity), and settle-time shaping. subagents/spawn.lua seeds new children (primary system context, child role, profile body with manifest metadata), resolves model/reasoning through panto.ext.resolve_model, filters subagents.* out of the inherited tool set via agent:set_tools, and reads resume defaults back from stored message metadata. One-shot structured workers are a null_store agent with a declaration-only output tool, tool_choice forced, dispatch_tools=false. subagents/progress.lua renders per-tool-entry cards through the component handle's invalidate seam; turn_interrupt cancels live children, turn_end closes them. Spec suite rewritten against fakes of the new surfaces (98 cases), including gate/queue/cancel bounds, resume-default extraction, one-shot capture via unresolved tool calls, tool filtering, and manifest seeding. --- subagents/run.lua | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 subagents/run.lua (limited to 'subagents/run.lua') diff --git a/subagents/run.lua b/subagents/run.lua new file mode 100644 index 0000000..375af91 --- /dev/null +++ b/subagents/run.lua @@ -0,0 +1,80 @@ +-- The `subagents.run` tool: start one child agent, or continue one, and wait. +-- +-- One call handles both cases. `agent` starts a new child from that profile; +-- `id` continues a child this primary session started earlier. Exactly one is +-- required, and `prompt` is always required — a child cannot see the parent +-- dialogue, so the prompt is the only task context it gets. +-- +-- The call blocks until the child settles. Parallelism is ordinary tool +-- batching: several subagents.run calls emitted in one batch run concurrently +-- under the session-wide bound in subagents/jobs.lua, and one failure does not +-- disturb its siblings. +-- +-- Failures are values, not exceptions. Everything the model could plausibly +-- have caused — a missing prompt, both selectors at once, an unknown profile, +-- an unresumable id, a child that errored or was cancelled — comes back as +-- readable text. Validation that fails before a child is allocated has +-- no id to report, and a new child that dies before its first assistant +-- message has no durable file, so it reports `resumable: false` rather than +-- promising a continuation that would not resolve. +-- +-- The result block is plain `key: value` lines rather than JSON: it is read by +-- a model, and the field names match the design's result shape (id, agent, +-- status, resumable, then the output or the error message). + +local jobs = require("subagents.jobs") +local spawn = require("subagents.spawn") + +local M = {} + +-- A resumed child has no profile in hand; its identity comes back from the +-- manifest metadata stored on its first profile system message. +local function manifest_agent(result) + local manifest = result.manifest + if type(manifest) ~= "table" then + return nil + end + local mine = manifest.subagents + if type(mine) ~= "table" or type(mine.agent) ~= "string" then + return nil + end + return mine.agent +end + +-- format_result(result, agent_name) -> the model-visible block. +function M.format_result(result, agent_name) + local body = result.output + if body == nil or body == "" then + body = result.error or "" + end + return table.concat({ + "id: " .. (result.id or "(none)"), + "agent: " .. (agent_name or manifest_agent(result) or "?"), + "status: " .. (result.status or "unknown"), + "resumable: " .. tostring(result.resumable == true), + "--- output ---", + tostring(body), + }, "\n") +end + +function M.handle(input, profiles) + local spec, err = spawn.build_spec(input, profiles) + if not spec then + return "Error: " .. tostring(err) + end + + local handle, spawn_err = spawn.spawn(spec) + if not handle then + return "Error: " .. tostring(spawn_err) + end + + local results = jobs.await({ handle }, "all") + local result = results and results[1] + if type(result) ~= "table" then + return "Error: the subagent produced no result." + end + + return M.format_result(result, spec.label) +end + +return M -- cgit v1.3