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 /spec/test_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 'spec/test_models.lua')
| -rw-r--r-- | spec/test_models.lua | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/spec/test_models.lua b/spec/test_models.lua new file mode 100644 index 0000000..ae01396 --- /dev/null +++ b/spec/test_models.lua @@ -0,0 +1,158 @@ +-- subagents/models.lua: the four catalog query forms and the profile join. +-- +-- The fake host answers by query shape, so each case checks both what the tool +-- asked the host for (`handle.models_queries`) and how it rendered the answer. + +local fake = require("spec.fake_ext") +local models = require("subagents.models") + +local function has(text, needle) + assert(type(text) == "string", "expected a string result, got " .. type(text)) + assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text)) +end + +local function catalog(query) + if query.model then + if query.model == "anthropic:sonnet" then + return { + found = true, + ref = "anthropic:sonnet", + wire_model = "claude-sonnet-4-6", + reasoning_default = "medium", + reasoning_levels = { "low", "medium", "high" }, + context_window = 200000, + max_tokens = 64000, + } + end + return { found = false } + end + if query.provider or query.query then + return { + matches = { + { ref = "anthropic:sonnet", wire_model = "claude-sonnet-4-6", reasoning_default = "medium" }, + { ref = "anthropic:opus", wire_model = "claude-opus-4-6" }, + }, + truncated = true, + } + end + return { + model = "anthropic:sonnet", + reasoning = "medium", + providers = { { name = "anthropic", style = "messages", models = 7 } }, + } +end + +local function profile_set() + local reviewer = { + name = "reviewer", + description = "Reviews changes", + model = "anthropic:sonnet", + reasoning = "high", + body = "b", + } + local scout = { name = "scout", description = "", body = "b" } + return { + list = { reviewer, scout }, + by_name = { reviewer = reviewer, scout = scout }, + warnings = {}, + } +end + +local function with_host(fn) + local handle = fake.install({ models_response = catalog }) + local ok, err = pcall(fn, handle, profile_set()) + handle.restore() + if not ok then + error(err, 0) + end +end + +return { + { "no arguments reports the inherited model and provider counts", function() + with_host(function(handle, profiles) + local text = models.handle({}, profiles) + has(text, "inherited model: anthropic:sonnet") + has(text, "inherited reasoning: medium") + has(text, "anthropic (messages, 7 models)") + assert(next(handle.models_queries[1]) == nil, "the overview query carries no fields") + end) + end }, + + { "an exact model lookup reports its reasoning levels", function() + with_host(function(handle, profiles) + local text = models.handle({ model = "anthropic:sonnet" }, profiles) + has(text, "model: anthropic:sonnet") + has(text, "wire model: claude-sonnet-4-6") + has(text, "default reasoning: medium") + has(text, "reasoning levels: low, medium, high") + has(text, "context window: 200000") + assert(handle.models_queries[1].model == "anthropic:sonnet") + end) + end }, + + { "an unknown model says so and suggests a search", function() + with_host(function(handle, profiles) + local text = models.handle({ model = "acme:turbo" }, profiles) + has(text, "No configured model matches 'acme:turbo'") + has(text, "subagents.models") + end) + end }, + + { "a search reports matches and says when more exist", function() + with_host(function(handle, profiles) + local text = models.handle({ query = "son" }, profiles) + has(text, "2 match(es):") + has(text, "anthropic:sonnet — wire claude-sonnet-4-6, default reasoning medium") + has(text, "more exist — refine the query") + assert(handle.models_queries[1].limit == 10, "the default limit is 10") + end) + end }, + + { "limit is clamped to 1..50", function() + with_host(function(handle, profiles) + models.handle({ query = "son", limit = 500 }, profiles) + assert(handle.models_queries[1].limit == 50, tostring(handle.models_queries[1].limit)) + models.handle({ provider = "anthropic", limit = 0 }, profiles) + assert(handle.models_queries[2].limit == 1, tostring(handle.models_queries[2].limit)) + models.handle({ query = "son", limit = 7.6 }, profiles) + assert(handle.models_queries[3].limit == 7, "a fractional limit is floored") + end) + end }, + + { "an agent with a model reports that model", function() + with_host(function(handle, profiles) + local text = models.handle({ agent = "reviewer" }, profiles) + has(text, "agent: reviewer") + has(text, "description: Reviews changes") + has(text, "wire model: claude-sonnet-4-6") + has(text, "profile reasoning: high") + assert(handle.models_queries[1].model == "anthropic:sonnet", "the profile model is looked up") + end) + end }, + + { "an agent without a model says it inherits", function() + with_host(function(handle, profiles) + local text = models.handle({ agent = "scout" }, profiles) + has(text, "agent: scout") + has(text, "model: inherits the primary model") + has(text, "inherited model: anthropic:sonnet") + assert(next(handle.models_queries[1]) == nil, "the inherited case asks for the overview") + end) + end }, + + { "an unknown agent names the known profiles", function() + with_host(function(handle, profiles) + local text = models.handle({ agent = "ghost" }, profiles) + has(text, "Error: unknown agent 'ghost'") + has(text, "reviewer, scout") + end) + end }, + + { "a non-string field is refused", function() + with_host(function(handle, profiles) + has(models.handle({ query = 12 }, profiles), "Error: `query` must be a non-empty string") + has(models.handle({ model = "" }, profiles), "Error: `model` must be a non-empty string") + assert(#handle.models_queries == 0, "a refused call never reaches the host") + end) + end }, +} |
