diff options
| author | t <t@tjp.lol> | 2026-08-16 20:42:43 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-08-17 20:31:29 -0600 |
| commit | 4f0a91ef55fe96835172bdad34feec1e2a0a0977 (patch) | |
| tree | ed4f3e86575aa6243043bc22f8037be153a609c6 /subagents/frontmatter.lua | |
| parent | c1ab34754d3f3695fafd344fe1a181ecf0740761 (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.lua | 96 |
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 |
