diff options
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 |
