summaryrefslogtreecommitdiff
path: root/subagents/models.lua
diff options
context:
space:
mode:
Diffstat (limited to 'subagents/models.lua')
-rw-r--r--subagents/models.lua233
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