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. --- subagents/jobs.lua | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) (limited to 'subagents/jobs.lua') diff --git a/subagents/jobs.lua b/subagents/jobs.lua index a774b27..9fe5c93 100644 --- a/subagents/jobs.lua +++ b/subagents/jobs.lua @@ -11,10 +11,9 @@ -- an edge trigger, never a count, so every wake drains the pipe, then drains -- `job:next_event()` to exhaustion, then checks `job:result()`. -- --- Waiting parks the CALLING coroutine — the tool handler's — and a poll --- callback resumes exactly that coroutine. subagents/workflow.lua's rule that --- a workflow callback must never run on a nested coroutine follows from this: --- the parked thread is the one the resume goes to. +-- Waiting parks the CALLING coroutine and a poll callback resumes that exact +-- coroutine. Foreground tools use their handler coroutine; background +-- workflows provide a dedicated coroutine anchored in their registry record. -- -- The wake pipe is therefore mandatory wherever luv is: a started job whose -- pipe or poll could not be armed is a start failure, not a degraded job. @@ -43,9 +42,9 @@ local READ_CHUNK = 4096 local M = {} --- The session-wide bound. A field, not a constant, so a caller (or a spec) can --- lower it without reaching into the queue. -M.MAX_CONCURRENT = 4 +-- The activation-time default is five; init.lua may replace it from the +-- layered `[subagents] max_concurrent` setting before any child can start. +M.MAX_CONCURRENT = 5 local handle_mt = {} handle_mt.__index = handle_mt @@ -58,6 +57,7 @@ local queued = {} local waiters = {} local running = 0 local pumping = false +local cancelling = false -- --------------------------------------------------------------------------- -- Wake pipes @@ -191,7 +191,7 @@ end -- Start queued jobs while the gate has room. Reentrant: a job that settles the -- instant it starts calls back in here, and the outer loop keeps going. function pump_queue() - if pumping then + if pumping or cancelling then return end pumping = true @@ -442,9 +442,28 @@ end -- The turn was interrupted: ask every child to stop. Cancellation is a request, -- not a settle — each job still reports its own cancelled result. function M.cancel_all() + cancelling = true for index = #live, 1, -1 do live[index]:cancel() end + cancelling = false + pump_queue() + wake() +end + +-- Close settled jobs without disturbing queued or running background work. +-- Called at ordinary turn boundaries; session teardown still uses close_all. +function M.reap() + local kept = {} + for _, handle in ipairs(live) do + if handle.settled ~= nil then + close_pipe(handle) + close_job(handle) + else + kept[#kept + 1] = handle + end + end + live = kept end -- The turn is over: cancel every child and drop the state. Never blocks — a -- cgit v1.3