summaryrefslogtreecommitdiff
path: root/subagents/models.lua
blob: 72722985453998f102394fcefcee62ce64d13f87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
-- 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. Three forms:
--
--   { model = "anthropic:sonnet"} exact lookup: wire name, reasoning levels
--   { provider =, query =, limit=} bounded search
--   { }                           inherited model/reasoning + provider counts
--
-- 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 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

function M.handle(input)
    input = input or {}
    if type(input) ~= "table" then
        return "Error: expected a table of arguments."
    end

    local 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 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