summaryrefslogtreecommitdiff
path: root/subagents/jobs.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-19 19:02:30 -0600
committert <t@tjp.lol>2026-08-19 19:03:52 -0600
commitd1306506aa7f504b0e91c9c6ed7314afbf99978e (patch)
treee5a7295dbc566a3679e99e14dae13376b0469e84 /subagents/jobs.lua
parent94e3fd8358bbdb5d6ed81aed475fab7fc73e2097 (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/jobs.lua')
-rw-r--r--subagents/jobs.lua35
1 files changed, 27 insertions, 8 deletions
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