summaryrefslogtreecommitdiff
path: root/subagents/luatool.lua
diff options
context:
space:
mode:
Diffstat (limited to 'subagents/luatool.lua')
-rw-r--r--subagents/luatool.lua74
1 files changed, 72 insertions, 2 deletions
diff --git a/subagents/luatool.lua b/subagents/luatool.lua
index 8aaa5c9..53966aa 100644
--- a/subagents/luatool.lua
+++ b/subagents/luatool.lua
@@ -4,7 +4,8 @@
-- 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.
+-- results for the calling model. Optional inline agent profiles are overlaid
+-- for this execution 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
@@ -46,6 +47,7 @@
local workflow = require("subagents.workflow")
local run = require("subagents.run")
+local spawn = require("subagents.spawn")
local MAX_JOBS = 32
local INSTRUCTION_BUDGET = 10000000
@@ -91,6 +93,69 @@ end
M.build_env = build_env
+-- Overlay workflow-local profiles without mutating the activation-time set.
+-- Inline names intentionally win, matching normal layered profile precedence
+-- while keeping their lifetime bounded to this one execute() call.
+local function workflow_profiles(inline, discovered)
+ if inline == nil then
+ return discovered, nil
+ end
+ if type(inline) ~= "table" then
+ return nil, "agents must be an array"
+ end
+
+ local count = 0
+ for key in pairs(inline) do
+ if type(key) ~= "number" then
+ return nil, "agents must be an array"
+ end
+ count = count + 1
+ end
+ if count ~= #inline then
+ return nil, "agents must be a dense array"
+ end
+
+ local base = spawn.profiles(discovered)
+ local by_name = {}
+ for name, profile in pairs(base.by_name or {}) do
+ by_name[name] = profile
+ end
+ local seen = {}
+ for index, raw in ipairs(inline) do
+ if type(raw) ~= "table" then
+ return nil, string.format("agents[%d] must be an object", index)
+ end
+ local name = raw.name
+ if type(name) ~= "string" or name:match("^%s*$") then
+ return nil, string.format("agents[%d].name must be a non-empty string", index)
+ end
+ if seen[name] then
+ return nil, string.format("duplicate inline agent '%s'", name)
+ end
+ local system_prompt = raw.system_prompt
+ if type(system_prompt) ~= "string" or system_prompt:match("^%s*$") then
+ return nil, string.format("agents[%d].system_prompt must be a non-empty string", index)
+ end
+ if raw.description ~= nil and type(raw.description) ~= "string" then
+ return nil, string.format("agents[%d].description must be a string when given", index)
+ end
+ seen[name] = true
+ by_name[name] = {
+ name = name,
+ description = raw.description or "",
+ body = system_prompt,
+ layer = "workflow",
+ }
+ end
+
+ local list = {}
+ for _, profile in pairs(by_name) do
+ list[#list + 1] = profile
+ end
+ table.sort(list, function(a, b) return a.name < b.name end)
+ return { list = list, by_name = by_name, warnings = base.warnings or {} }, nil
+end
+
local function budget_hook()
error("instruction budget exceeded", 0)
end
@@ -151,6 +216,11 @@ function M.handle(input, profiles)
return "Error: source is required and must be a non-empty string"
end
+ local profiles_for_run, profiles_err = workflow_profiles(input.agents, profiles)
+ if not profiles_for_run then
+ return "Error: " .. profiles_err
+ end
+
local chunk, load_err = load(input.source, CHUNK_NAME, "t", build_env())
if not chunk then
return "Error: source did not compile: " .. tostring(load_err)
@@ -167,7 +237,7 @@ function M.handle(input, profiles)
local armed = nil
local ran_ok, result = pcall(workflow.execute, built, input.prompt, {
max_jobs = MAX_JOBS,
- profiles = profiles,
+ profiles = profiles_for_run,
on_resume = function(co)
armed = co
debug.sethook(co, budget_hook, "", INSTRUCTION_BUDGET)