summaryrefslogtreecommitdiff
path: root/subagents/run.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-16 20:42:43 -0600
committert <t@tjp.lol>2026-08-17 20:31:29 -0600
commit4f0a91ef55fe96835172bdad34feec1e2a0a0977 (patch)
treeed4f3e86575aa6243043bc22f8037be153a609c6 /subagents/run.lua
parentc1ab34754d3f3695fafd344fe1a181ecf0740761 (diff)
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.
Diffstat (limited to 'subagents/run.lua')
-rw-r--r--subagents/run.lua80
1 files changed, 80 insertions, 0 deletions
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