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/models.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/models.lua')
| -rw-r--r-- | subagents/models.lua | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/subagents/models.lua b/subagents/models.lua new file mode 100644 index 0000000..511aa51 --- /dev/null +++ b/subagents/models.lua @@ -0,0 +1,233 @@ +-- The `subagents.models` tool: a bounded window onto the model catalog. +-- +-- The catalog is far too large to inline into `subagents.run`'s description or +-- schema, so it is queried on demand instead. Four forms, chosen in this order: +-- +-- { agent = "reviewer" } what that profile will actually run on +-- { model = "anthropic:sonnet"} exact lookup: wire name, reasoning levels +-- { provider =, query =, limit=} bounded search +-- { } inherited model/reasoning + provider counts +-- +-- The agent form is the join the primary actually wants before overriding a +-- profile: it reports the profile's own model, or says the profile inherits +-- the primary model and shows what that is. +-- +-- Results are short readable lines, not JSON. A truncated search says so +-- explicitly so the primary refines the query instead of assuming it saw +-- everything; `limit` is clamped to 1..50 (default 10) so no query can dump +-- the whole registry into the conversation. +-- +-- The host owns the catalog and the provider-specific reasoning rules, +-- including effort levels a Lua protocol reports dynamically. Anything this +-- tool cannot confirm is still validated at spawn time — the catalog is +-- advice, the runtime is authoritative. + +local spawn = require("subagents.spawn") + +local DEFAULT_LIMIT = 10 +local MAX_LIMIT = 50 + +local M = {} + +local function host() + return require("panto").ext +end + +local function ask(q) + local ok, result = pcall(host().models, q) + if not ok then + return nil, tostring(result) + end + if type(result) ~= "table" then + return nil, "the host returned no model information" + end + return result +end + +local function format_overview(result) + local out = { + "inherited model: " .. (result.model or "(unknown)"), + "inherited reasoning: " .. (result.reasoning or "(provider default)"), + } + local providers = result.providers or {} + if #providers == 0 then + out[#out + 1] = "providers: (none configured)" + return table.concat(out, "\n") + end + out[#out + 1] = "providers:" + for _, provider in ipairs(providers) do + out[#out + 1] = string.format(" %s (%s, %d models)", + tostring(provider.name), tostring(provider.style or "?"), tonumber(provider.models) or 0) + end + return table.concat(out, "\n") +end + +local function format_match(match) + local detail = {} + if match.wire_model then + detail[#detail + 1] = "wire " .. tostring(match.wire_model) + end + if match.reasoning_default then + detail[#detail + 1] = "default reasoning " .. tostring(match.reasoning_default) + end + if match.context_window then + detail[#detail + 1] = "context " .. tostring(match.context_window) + end + if match.max_tokens then + detail[#detail + 1] = "max tokens " .. tostring(match.max_tokens) + end + local line = " " .. tostring(match.ref) + if #detail > 0 then + line = line .. " — " .. table.concat(detail, ", ") + end + return line +end + +local function format_exact(result, ref) + if not result.found then + return string.format( + "No configured model matches '%s'. Search with subagents.models { query = \"...\" }.", ref) + end + local out = { "model: " .. tostring(result.ref or ref) } + if result.wire_model then + out[#out + 1] = "wire model: " .. tostring(result.wire_model) + end + out[#out + 1] = "default reasoning: " .. (result.reasoning_default or "(provider default)") + local levels = result.reasoning_levels + if type(levels) == "table" and #levels > 0 then + out[#out + 1] = "reasoning levels: " .. table.concat(levels, ", ") + end + if result.context_window then + out[#out + 1] = "context window: " .. tostring(result.context_window) + end + if result.max_tokens then + out[#out + 1] = "max tokens: " .. tostring(result.max_tokens) + end + return table.concat(out, "\n") +end + +local function format_search(result) + local matches = result.matches or {} + if #matches == 0 then + return "No models match that query." + end + local out = { string.format("%d match(es):", #matches) } + for _, match in ipairs(matches) do + out[#out + 1] = format_match(match) + end + if result.truncated then + out[#out + 1] = string.format("%d shown, more exist — refine the query", #matches) + end + return table.concat(out, "\n") +end + +local function optional_string(value, field) + if value == nil then + return nil, nil + end + if type(value) ~= "string" or value == "" then + return nil, string.format("Error: `%s` must be a non-empty string when given.", field) + end + return value, nil +end + +local function clamp_limit(value) + local limit = tonumber(value) or DEFAULT_LIMIT + limit = math.floor(limit) + if limit < 1 then + return 1 + end + if limit > MAX_LIMIT then + return MAX_LIMIT + end + return limit +end + +-- The agent form: report the profile's effective model, or say it inherits. +local function describe_agent(name, profiles) + profiles = spawn.profiles(profiles) + local profile = (profiles.by_name or {})[name] + if not profile then + return string.format("Error: unknown agent '%s'; known: %s", name, spawn.agent_names(profiles)) + end + + local out = { "agent: " .. profile.name } + if profile.description ~= "" then + out[#out + 1] = "description: " .. profile.description + end + + if profile.model then + local result, err = ask({ model = profile.model }) + if not result then + return "Error: " .. err + end + out[#out + 1] = format_exact(result, profile.model) + else + out[#out + 1] = "model: inherits the primary model" + local result, err = ask({}) + if not result then + return "Error: " .. err + end + out[#out + 1] = format_overview(result) + end + + if profile.reasoning then + out[#out + 1] = "profile reasoning: " .. profile.reasoning + end + return table.concat(out, "\n") +end + +function M.handle(input, profiles) + input = input or {} + if type(input) ~= "table" then + return "Error: expected a table of arguments." + end + + local agent, err = optional_string(input.agent, "agent") + if err then + return err + end + local model + model, err = optional_string(input.model, "model") + if err then + return err + end + local provider + provider, err = optional_string(input.provider, "provider") + if err then + return err + end + local text + text, err = optional_string(input.query, "query") + if err then + return err + end + + if agent then + return describe_agent(agent, profiles) + end + + if model then + local result, ask_err = ask({ model = model }) + if not result then + return "Error: " .. ask_err + end + return format_exact(result, model) + end + + if provider or text then + local result, ask_err = ask({ provider = provider, query = text, limit = clamp_limit(input.limit) }) + if not result then + return "Error: " .. ask_err + end + return format_search(result) + end + + local result, ask_err = ask({}) + if not result then + return "Error: " .. ask_err + end + return format_overview(result) +end + +return M |
