summaryrefslogtreecommitdiff
path: root/subagents/frontmatter.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-16 20:42:43 -0600
committert <t@tjp.lol>2026-08-17 20:31:29 -0600
commit4f0a91ef55fe96835172bdad34feec1e2a0a0977 (patch)
treeed4f3e86575aa6243043bc22f8037be153a609c6 /subagents/frontmatter.lua
parentc1ab34754d3f3695fafd344fe1a181ecf0740761 (diff)
subagents extension on the generic host surfaces
The rock now owns all subagent policy on top of libpanto-lua's generic APIs: children are ordinary panto.agent instances over rock-constructed stores, started with agent:run_async and awaited by arming uv.new_poll on each job's wake_fd from the tool handler's coroutine. subagents/jobs.lua carries the session policy the host used to own: the concurrency gate (4 running, FIFO queue, cancel-while-queued never starts), the await contract (results in input order; "first" returns settled plus remaining by identity), and settle-time shaping. subagents/spawn.lua seeds new children (primary system context, child role, profile body with manifest metadata), resolves model/reasoning through panto.ext.resolve_model, filters subagents.* out of the inherited tool set via agent:set_tools, and reads resume defaults back from stored message metadata. One-shot structured workers are a null_store agent with a declaration-only output tool, tool_choice forced, dispatch_tools=false. subagents/progress.lua renders per-tool-entry cards through the component handle's invalidate seam; turn_interrupt cancels live children, turn_end closes them. Spec suite rewritten against fakes of the new surfaces (98 cases), including gate/queue/cancel bounds, resume-default extraction, one-shot capture via unresolved tool calls, tool filtering, and manifest seeding.
Diffstat (limited to 'subagents/frontmatter.lua')
-rw-r--r--subagents/frontmatter.lua96
1 files changed, 96 insertions, 0 deletions
diff --git a/subagents/frontmatter.lua b/subagents/frontmatter.lua
new file mode 100644
index 0000000..94fe2ac
--- /dev/null
+++ b/subagents/frontmatter.lua
@@ -0,0 +1,96 @@
+-- Split a Markdown profile into its YAML frontmatter and its body.
+--
+-- The format is the common one other agent harnesses use: if the very first
+-- line of the file is exactly `---`, everything up to the next line that is
+-- exactly `---` is a YAML mapping, and everything after that closing fence is
+-- the body. Trailing carriage returns are tolerated so CRLF files parse.
+--
+-- Anything unusual degrades to "no metadata, body only" with a warning rather
+-- than an error, because the body is the part the user cannot afford to lose:
+--
+-- * no opening fence -> the whole file is the body, no warning
+-- * unterminated opening fence-> the whole file is the body, no warning
+-- (a lone `---` at the top of a prose file is a horizontal rule, not a
+-- broken header, so this case is deliberately silent)
+-- * empty fenced block -> empty mapping, no warning
+-- * lyaml missing or erroring -> body after the fence, warning returned
+-- * YAML document not a map -> body after the fence, warning returned
+--
+-- In the warning cases the fenced block is dropped rather than folded back
+-- into the body: an unparseable header is noise the child agent should not be
+-- asked to read. The body itself is never rewritten — no trimming, no
+-- normalisation — so a prompt round-trips verbatim.
+
+local M = {}
+
+local function trim(s)
+ return (s:gsub("^%s+", ""):gsub("%s+$", ""))
+end
+
+-- Iterate lines, yielding the line plus its start offset and the offset just
+-- past its newline, so the caller can slice the original text exactly.
+local function lines(text)
+ local pos = 1
+ return function()
+ if pos > #text then
+ return nil
+ end
+ local start = pos
+ local nl = text:find("\n", pos, true)
+ local line
+ if nl then
+ line = text:sub(start, nl - 1)
+ pos = nl + 1
+ else
+ line = text:sub(start)
+ pos = #text + 1
+ end
+ return line, start, pos
+ end
+end
+
+-- parse(text) -> data|nil, body, warning|nil
+function M.parse(text)
+ if type(text) ~= "string" or text == "" then
+ return nil, "", nil
+ end
+
+ local next_line = lines(text)
+ local first, _, after_first = next_line()
+ if first == nil or trim(first) ~= "---" then
+ return nil, text, nil
+ end
+
+ local block_stop, body_start
+ for line, start, after in next_line do
+ if trim(line) == "---" then
+ block_stop = start - 1
+ body_start = after
+ break
+ end
+ end
+ if body_start == nil then
+ return nil, text, nil
+ end
+
+ local block = text:sub(after_first, block_stop)
+ local body = text:sub(body_start)
+ if trim(block) == "" then
+ return {}, body, nil
+ end
+
+ local ok_lyaml, lyaml = pcall(require, "lyaml")
+ if not ok_lyaml then
+ return nil, body, "lyaml is not installed; ignoring the YAML frontmatter"
+ end
+ local ok, data = pcall(lyaml.load, block)
+ if not ok then
+ return nil, body, "YAML frontmatter did not parse: " .. tostring(data)
+ end
+ if type(data) ~= "table" then
+ return nil, body, "YAML frontmatter is not a mapping; ignoring it"
+ end
+ return data, body, nil
+end
+
+return M