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/luatool.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/luatool.lua')
| -rw-r--r-- | subagents/luatool.lua | 80 |
1 files changed, 36 insertions, 44 deletions
diff --git a/subagents/luatool.lua b/subagents/luatool.lua index 53966aa..dee7398 100644 --- a/subagents/luatool.lua +++ b/subagents/luatool.lua @@ -1,11 +1,12 @@ -- subagents/luatool.lua -- -- The `subagents.lua` model-facing tool: run a transient, model-authored Lua --- workflow without writing a definition to disk. The source must evaluate to --- `subagents.workflow(function(ctx, input) ... end)`; the tool then executes it --- with the tool's `prompt` as the workflow input and formats the terminal --- results for the calling model. Optional inline agent profiles are overlaid --- for this execution only; they are never persisted or added to discovery. +-- workflow without writing a definition to disk. Source can start one with +-- `subagents.workflow(function(ctx) ... end)`, which immediately returns a +-- workflow id, or inspect a prior run through `subagents.workflows[id]` and +-- return any model-visible value. Optional inline agent profiles are overlaid +-- for workflows started by this call only; they are never persisted or added +-- to discovery. -- -- The source is loaded in text mode only (`load(source, chunkname, "t", env)`) -- against a restricted `_ENV`. That environment holds a safe slice of the @@ -37,13 +38,8 @@ -- -- Runaway generated Lua is bounded two ways: `max_jobs = 32` caps how many -- children one transient workflow may start, and a debug count hook is armed --- for the duration of the guest callback and disarmed as soon as it returns. --- The hook lives here rather than in workflow.lua because the guest has no --- `debug` library but the host does; workflow.execute only exposes the --- on_resume/on_yield seam the hook needs. The guest runs on the tool handler's --- own coroutine (an await parks and resumes exactly that coroutine when a child --- settles), so the budget covers the whole run rather than one slice; awaiting --- a child executes no instructions, so only real spinning trips it. +-- on each background workflow coroutine for its full execution. Top-level +-- source evaluation gets the same budget before it can schedule anything. local workflow = require("subagents.workflow") local run = require("subagents.run") @@ -69,7 +65,8 @@ end -- Build a fresh restricted environment per call: the guest may mutate anything -- it can reach, so nothing here is shared between invocations. -local function build_env() +local function build_env(schedule) + schedule = schedule or workflow.workflow local env = { assert = assert, error = error, @@ -85,7 +82,10 @@ local function build_env() math = shallow_copy(math), utf8 = shallow_copy(utf8), print = function() end, - subagents = { workflow = workflow.workflow }, + subagents = { + workflow = schedule, + workflows = workflow.workflows, + }, } env._G = env return env @@ -205,13 +205,10 @@ end -- Tool handler for `subagents.lua`. `profiles` is the discovered profile set -- from activation; when omitted the workflow API discovers it lazily. -function M.handle(input, profiles) +function M.handle(input, profiles, context) if type(input) ~= "table" then return "Error: expected an input object" end - if type(input.prompt) ~= "string" or input.prompt == "" then - return "Error: prompt is required and must be a non-empty string" - end if type(input.source) ~= "string" or input.source == "" then return "Error: source is required and must be a non-empty string" end @@ -221,40 +218,35 @@ function M.handle(input, profiles) return "Error: " .. profiles_err end - local chunk, load_err = load(input.source, CHUNK_NAME, "t", build_env()) + local function on_resume(co) + debug.sethook(co, budget_hook, "", INSTRUCTION_BUDGET) + end + local function on_yield(co) + debug.sethook(co) + end + local function schedule(fn) + return workflow.start(workflow.workflow(fn), { + max_jobs = MAX_JOBS, + profiles = profiles_for_run, + tool_call_id = type(context) == "table" and context.tool_call_id or nil, + on_resume = on_resume, + on_yield = on_yield, + }) + end + + local chunk, load_err = load(input.source, CHUNK_NAME, "t", build_env(schedule)) if not chunk then return "Error: source did not compile: " .. tostring(load_err) end + local co = coroutine.running() + if co then debug.sethook(co, budget_hook, "", INSTRUCTION_BUDGET) end local built_ok, built = pcall(chunk) + if co then debug.sethook(co) end if not built_ok then return "Error: source failed to run: " .. tostring(built) end - if not workflow.is_workflow(built) then - return "Error: source must return subagents.workflow(function(ctx, input) ... end)" - end - - local armed = nil - local ran_ok, result = pcall(workflow.execute, built, input.prompt, { - max_jobs = MAX_JOBS, - profiles = profiles_for_run, - on_resume = function(co) - armed = co - debug.sethook(co, budget_hook, "", INSTRUCTION_BUDGET) - end, - on_yield = function(co) - debug.sethook(co) - armed = nil - end, - }) - if armed then - debug.sethook(armed) - end - - if not ran_ok then - return "Error: " .. tostring(result) - end - return format_return(result) + return format_return(built) end return M |
