Promote model delivery to a ResolvedModel capability - #833
Open
mpfaffenberger wants to merge 2 commits into
Open
Promote model delivery to a ResolvedModel capability#833mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
Both agent construction paths handed their fully resolved pydantic-ai model to the Agent(model=...) constructor kwarg. ResolvedModel moves that delivery onto the dedicated get_model() capability seam, so the model travels in the same capabilities=[...] block as the rest of the agent's behavior. Sixth in the capability series (#828 SteerInjection, #829 HistoryCompaction, #830 PluginMessageTransform, #831 PerModelSettings, #832 AssembledInstructions). Resolution logic is untouched: load_model_with_fallback still owns the fallback chain and warning dedup; sub-agent pin/override resolution is unchanged. The capability only owns the last mile. Parity (pydantic-ai 2.31.0): a static get_model() contribution is resolved once per run exactly like the agent-slot model was; run(model=...) and Agent.override(model=...) both set model_is_explicit and stay authoritative; Agent.__aenter__ enters a static capability model identically. One observable divergence: the built agent's .model property reads None -- no code_puppy call site reads it (everything uses BaseAgent.cur_model), and wiggum's default-judge probe degrades to its own get_model_name() fallback. Pinned by regression test. get_serialization_name() -> None: the capability holds a live, provider-configured Model instance, so it opts out of spec-based construction (SteerInjection/HistoryCompaction precedent).
Apply the three optional review findings on PR #833: pin the Agent.__aenter__/__aexit__ lifecycle of a capability-delivered model, prove the static contribution serves every step of a multi-step (tool-call) run, and cover the second construction site end-to-end by driving subagent_invocation._invoke_agent_impl with a streaming FunctionModel delivered via the capability.
Owner
Author
|
Review trail: code-puppy clone did two passes. Pass 1: clean APPROVE with three optional test-hardening findings (pin the Agent.aenter/aexit lifecycle of the capability model, prove static delivery across a multi-step tool-call run, cover the sub-agent construction site end-to-end). All three applied in c511377. Pass 2: APPROVE, confirmed the new tests are non-vacuous (lifecycle counters, forced two-step run, subagent test fails if the capability is dropped). 10/10 contract tests green; full suite matches main's failure profile (3 documented pre-existing reds, all reproduce on clean main). |
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sixth in the capability series (#828 SteerInjection, #829 HistoryCompaction, #830 PluginMessageTransform, #831 PerModelSettings, #832 AssembledInstructions): promotes model delivery from the
Agent(model=...)constructor kwarg to a first-class pydantic-ai capability,ResolvedModel, on the dedicatedget_model()seam.Both construction sites (
agents/_builder.pyandtools/subagent_invocation.py) previously handed their fully resolvedModelinstance to the constructor kwarg while everything else about the agent's behavior already composes throughcapabilities=[...].What changed
code_puppy/agents/_resolved_model.py—@dataclass ResolvedModel(AbstractCapability[Any])with a singlemodel: Modelfield, delivering viaget_model()._builder.build_pydantic_agentdropsmodel=and hoists one sharedResolvedModelinstance across the probe + final construction passes (mirrors the singleload_model_with_fallbackcall; safe because the capability is a static snapshot, defaultfor_runreturnsself, and it carries no id).subagent_invocationdropsmodel=and constructsResolvedModel(model)inline.load_model_with_fallbackstill owns the fallback chain + per-conversation warning dedup; sub-agent pin/explicit-override resolution unchanged;BaseAgent.cur_modelstill set.build_tool_probe_for_agentkeeps itsmodel=kwarg deliberately (stripped probe, per the refactor: promote system-prompt delivery to an AssembledInstructions capability #832 precedent forinstructions="").Parity (verified against pydantic-ai 2.31.0 source)
get_model()return is used directly as the run's model (Agent._evaluate_model_contribution), exactly like the agent-slot model._check_dynamic_model_resumeis a no-op for static contributions — no selector semantics introduced.run(model=...)andAgent.override(model=...)stay authoritative. Both setmodel_is_explicit, which short-circuits capability contribution evaluation entirely — same precedence as the old kwarg. Pinned by tests.Agent.__aenter__enters a static capability model's context identically (line ~3736:static_selection = capability_model), so provider HTTP clients open/close the same..modelproperty now readsNone(the model no longer occupies the agent slot). Verified: zero code_puppy readers — everything goes throughBaseAgent.cur_model. One plugin reader exists: wiggum's_resolve_judgesprobesget_pydantic_agent().model.model_namefor its default-judge fallback and now takes its own next fallback,BaseAgent.get_model_name()— the configured model name, which is what judge configs resolve against anyway (arguably more correct than the provider model id it used to get). Degrades gracefully by design (getattr(None, "model_name", None)); documented in the module docstring and pinned bytest_agent_model_slot_reads_none.get_serialization_name() -> Nonebecause the capability holds a live, provider-configuredModelinstance (HTTP clients and all) — same precedent as SteerInjection/HistoryCompaction.get_modelis a configuration seam, not a request hook; placed first in both blocks for readability.Tests
7 contract tests in
tests/agents/test_resolved_model_capability.py:get_model() is model)FunctionModel(model-visible content compared; timestamps excluded)run(model=...)authoritative over the capabilityoverride(model=...)authoritative in scope, capability restored outside.model is Nonedivergencebuild_pydantic_agentdrive: built agent runs on the capability-delivered model,cur_modeltracks it, agent slot emptyFull suite: 7583 passed; the 3 reds are the documented pre-existing main failures (
test_render_version_check_current, plus the two full-run-orderFieldInfo has no attribute 'strip'flakes noted in #832) — bothsubagent_invocation-adjacent ones pass in isolation on this branch.Merge-order note
Like its five siblings, this touches the shared
capabilities=[...]blocks in_builder.py+subagent_invocation.py. Whichever PRs land last eat trivial rebases.