From 4f0a91ef55fe96835172bdad34feec1e2a0a0977 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 16 Aug 2026 20:42:43 -0600 Subject: 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. --- subagents/frontmatter.lua | 96 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 subagents/frontmatter.lua (limited to 'subagents/frontmatter.lua') 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 -- cgit v1.3