fix(spawn_utils): one answer to "which provider instance is anthropic?" (67u) - #355
Conversation
…c`?"
Investigating model_performance-67u ("why does an explicitly-named delegate
bypass matrix resolution?") produced two results.
1. THE NAMED-DELEGATE PATH IS NOT A BYPASS -- characterization tests.
tool-delegate has exactly ONE resolver call site (__init__.py:1636),
guarded on a `model_role` read only from the tool input (:1625). The
agent's own declared model_role is never consulted there -- the sole
agent-level fallback is `provider_preferences` (:1822). So a delegate
call that names an agent and supplies no model_role resolves nothing and
takes the documented, opt-out session-default fall-through (:1699-1712).
The "organic" path is the same code with one extra tool argument.
The agent-frontmatter model_role is resolved elsewhere -- hooks-routing
writes preferences into agent configs at session:start (see app-cli
session_spawner.py:568-575). Out of this repo; reported, not chased.
Four characterization tests pin this so it stops being folklore. They
assert INTENDED behaviour and pass before and after this change.
2. A REAL DEFECT, FIXED HERE -- fail-before tests.
Three helpers answered "which mounted instance does the bare module type
`anthropic` mean?" three different ways:
_find_provider_instance -> highest priority
_find_provider_index -> first declared
_build_provider_lookup -> last declared (dict last-write-wins)
apply_provider_preferences_with_resolution uses TWO of them in one pass:
it resolves the candidate's model glob against the instance the first
picks, then promotes the index the third returns. On a 10-mount plan with
2 module types (the eval-harness roster) those are different mounts, so
the model resolved from instance A's model list was written onto instance
B's config and B was promoted to priority 0 -- right model, wrong
instance, and with it B's base_url / long-context / cache-retention
settings. Silently: the model name still looks correct.
This only fires with >=2 instances of one provider module, which is
exactly the shape a routing matrix asks for (a matrix addresses providers
by bare module type; distinct `id:`s exist for matrix disambiguation).
Fix: one rule, shared via _provider_priority() -- highest priority wins,
ties by declaration order, and an explicit instance `id` beats a
module-type key. Single-instance plans are unchanged (pinned by test).
Multi-instance plans with no `priority` set now resolve to the first
declared instance rather than the last, which is what
_find_provider_instance already did.
Suite: 1924 -> 1939 passed, 1 skipped, 0 regressions.
Reverting only spawn_utils.py: 7 failed, 8 passed.
Refs: model_performance-67u
Manager verification — two parts, judged separately. Merging.Head Part 1 — THE ANSWER (no code change): accepted, anchors re-checked in current source
I re-ran the grep rather than trusting it. Part 2 — THE DEFECT FIX: fail-before reproduced against current main67u's tests against The 7 failures are exactly the split-brain set ( Full suite: The behaviour change — stated plainly, because it is realThis does change which instance a bare module type resolves to, and I am merging anyway. On the Blast radius, as the lane scopes it: only where ≥2 instances of one provider module are mounted AND a Aligning |
Item:
model_performance-67u— "why does an EXPLICITLY-NAMED delegate bypass matrix resolution when an organic one does not?"Spend: $0.00 against a $0 authority. Code read + pure-function reproduction; no API calls, no DTU, no probe re-runs.
Full write-up:
docs/lanes/67u-named-delegate-matrix-bypass/DONE-NOTE.md.Answer to the item: there is no bypass
tool-delegatehas exactly one resolver call site, guarded on amodel_rolethat is read only from the tool input:The agent's own declared
model_roleis never consulted there — the sole agent-level fallback isprovider_preferences:grep 'agent_cfg\|agents\.get'returns only that pair plus two return-contract sites. So the "organic" and "named" paths are the same code; they differ only in whether the calling model putmodel_rolein the tool arguments. The probe's prompt fully specified the call ("delegate to agent X with instruction Y"), the model emitted exactly those two arguments, the guard was false, and the child took the documented, opt-out session-default fall-through (:1699-1712,strict_model_role).Corroborated independently by effort:
anthropic.yaml'sreasoningcarriesreasoning_effort: highand a promotion does carry a candidate'sconfig(spawn_utils.py:767-773), yet the child'ssession:configshows the root'sxhigh— so no promotion occurred.The agent-frontmatter
model_roleis resolved elsewhere — hooks-routing writes preferences into agent configs at session:start (amplifier-app-cli/session_spawner.py:568-575). Out of this repo; reported, not chased, per the item's scope-out.All three reproductions verdict (b) INTENDED, argued from code. The
economyrow (Anthropic globs → OpenAI models) is ordered-candidate fallback authored intoeconomy.yamlitself: both children landed on candidate #2 of their own role (reasoning→openai: gpt-?.?-terra*,coding→openai: gpt-?.?-luna*). Reproduced as a pure function. That row is also the one that proves per-role resolution ran — a session default gives both children the same model; these differ from each other and from the root.No shipped routing decision is wrong today on this axis. The organic path is what real workloads use, h7n proved it healthy, and it is untouched here.
The defect this code read did find (what this PR actually changes)
Three helpers in
spawn_utils.pygave three answers to "which mounted instance does the bare module typeanthropicmean?":_find_provider_instance:601-617_find_provider_index:636-645_build_provider_lookup:660-673apply_provider_preferences_with_resolutionuses two of them in one pass: resolves the candidate's model glob against the instance the first picks (:430), then promotes the index the third returns (:859-888).Measured on the eval-harness roster (10 mounts, 2 module types, cell forced to priority 0):
The model resolved from one instance's list is written as
default_modelonto a different instance, and that one is promoted to priority 0 — carrying its ownbase_url, long-context and cache-retention settings. Silently: the model name still looks right.Only fires with ≥2 instances of one provider module — which is exactly the shape a routing matrix asks for (a matrix addresses providers by bare module type; distinct
id:s exist "for routing-matrix disambiguation",:585). Not the default single-instance install.Fix: one rule via a shared
_provider_priority()— highest priority wins, ties by declaration order; an explicit instanceidbeats a module-type key. Applied to all three helpers.Review notes
test_single_instance_plans_are_unchanged.prioritypreviously resolved a bare type to the last declared instance and now resolves to the first — which is what_find_provider_instancealready did, so this removes a disagreement rather than inventing a rule._find_provider_indexhas no production caller today (definition + tests only). Aligned anyway: leaving a third disagreeing rule in the same file is how this gets re-filed.Tests
tests/test_named_delegate_matrix_67u.py— 15 tests: 4 characterization (intended named-delegate behaviour — do not "fix" these), 8 fail-before defect tests, 3 economy-fallback reproductions.Placed in
tests/rather thanmodules/tool-delegate/tests/because CI runspytest tests/only;pyproject.tomlsetspythonpath = ["modules/tool-delegate"], sotests/importsDelegateTooldirectly and these actually run in CI. (Themodules/tool-delegate/testsCI exclusion is a known gap, noted, not fixed here.)Full suite, repo root:
Fail-before, reverting only
spawn_utils.py:ruff check/ruff format --checkclean on both touched files. The one repo-widerufferror (F401 ParsedURIinamplifier_foundation/updates/__init__.py) is pre-existing on18efe87— verified by stashing this branch's changes — and left alone.Open thread handed on (not this repo)
Why hooks-routing produced
n_prefs: 0for 11 of 13 agents in that container — i.e. why layer B (agent-frontmattermodel_roleresolved at session:start) did not populate preferences — lives inamplifier-bundle-routing-matrix/amplifier-app-cli. Recorded as open rather than guessed; no fresh run was needed to answer this item, so none is being requested.