diff options
| author | t <t@tjp.lol> | 2026-08-18 14:18:14 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-08-18 14:20:28 -0600 |
| commit | cc69a2e431779528578211a5cb3385bb23e076bf (patch) | |
| tree | 8833171e25bb3dfb71fc4a2e0b8fe583782da764 /subagents/luatool.lua | |
| parent | 372ef8ff40991644ec2654c61328f31779f4ad21 (diff) | |
Persist and replay subagent progress across sessions
Record durable child-turn metadata, restore bounded progress cards during
startup replay, and keep settled cards attached to transcript entries. Add
workflow-local Lua profiles and expose discovered agents and workflows in the
session header.
Diffstat (limited to 'subagents/luatool.lua')
| -rw-r--r-- | subagents/luatool.lua | 74 |
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) |
