From 4183fdc7d3667fb4c2af72ff0098c15168c9e950 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 20 Aug 2026 15:30:59 -0600 Subject: Expose a json codec to the subagents.lua sandbox Model-authored workflow Lua could reach string, table, math, and utf8 but had no way to parse an agent's report, so any fan-out over discovered work had to invent an ad-hoc line format and hand-roll string.gmatch. Hand the guest panto.ext.json instead. decode returns nil plus a message rather than raising: the sandbox deliberately has no pcall, so a raising decoder would let one malformed agent line kill an entire workflow. Teach the pattern in the injected guidance too: deterministic Lua owns the loops, counts, ordering, and gates, agents supply discovery and judgment, and a worker that feeds Lua ends its report with one parseable line. The existing static-graph example stays; a discover-then-fan-out example joins it. --- init.lua | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'init.lua') diff --git a/init.lua b/init.lua index 2ec5f8c..5f0b411 100644 --- a/init.lua +++ b/init.lua @@ -174,6 +174,35 @@ return subagents.workflow(function(ctx) end) ``` +**Deterministic code drives; agents advise.** A workflow is code you control wrapped around judgment you cannot. Put every decision that can be nailed down into Lua — loops, counts, ordering, retries, pass/fail gates — and spend agents only on discovery and judgment. When Lua needs a fact an agent found (a list to fan out over, a verdict, a chosen option), tell that agent to end its report with one machine-parseable line, parse it, and let the parsed value shape the rest of the graph. The sandbox has `json.encode(value)` and `json.decode(text)`, which returns the value or `nil, message`. + +```lua +return subagents.workflow(function(ctx) + local found = ctx:agent{ + name="inventory", + system_prompt="Inventory work items. Read-only.", + prompt="List every open PR in owner/repo. End with exactly one line: RESULT={\"prs\":[]}", + }:await() + + local line = found.output and found.output:match("RESULT=([^\n]+)") + local items = line and json.decode(line) + if not items or not items.prs then return "inventory produced no parseable RESULT line" end + + local reviewers = {} + for _, pr in ipairs(items.prs) do + reviewers[#reviewers + 1] = ctx:agent{ + name = "review-" .. pr, + system_prompt = "Review one pull request.", + prompt = "Review PR #" .. pr .. " in owner/repo.", + } + end + + local reviews, out = ctx:await(reviewers, "all"), {} + for _, review in ipairs(reviews) do out[#out + 1] = review.output end + return table.concat(out, "\n\n") +end) +``` + Every `ctx:agent` needs a non-empty `name` unique within that workflow. A workflow callback must return a string. Inspect a running or finished workflow with a later `subagents.lua` call, for example: ```lua -- cgit v1.3