Promote plugin message transform to a first-class capability - #830
Open
mpfaffenberger wants to merge 2 commits into
Open
Promote plugin message transform to a first-class capability#830mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
Replace the build_model_message_transform closure (stuffed into the generic Hooks(model_request=...) adapter at two call sites) with PluginMessageTransform, a dataclass AbstractCapability subclass that overrides wrap_model_request directly. Same seam, same list position, byte-identical semantics: shallow-copy the request context, materialize a fresh message list, fan out to transform_model_messages plugin callbacks, delegate to the handler. Request-only by construction -- plugin mutations reach the wire for exactly one request and never leak into durable history. New contract tests drive wrap_model_request directly, proving copy isolation (caller's context and list object untouched) and bare passthrough with no callbacks registered.
- Docstring now says plugin *list* mutations are isolated and notes that contained message objects are shared (in-place mutation would leak, same as under the old Hooks adapter -- inherited behavior, not new). - Passthrough test asserts copy semantics hold with zero callbacks: handler receives a distinct context and a distinct message list.
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.
What
Third entry in the capability-ification series (after #828 SteerInjection and #829 HistoryCompaction): the
transform_model_messagesplugin hook was a closure stuffed into the genericHooks(model_request=...)adapter — at two call sites. It's nowPluginMessageTransform, a@dataclassAbstractCapabilitysubclass incode_puppy/agents/_model_message_transform.py.Why
Hooks(model_request=...)registers on thewrap_model_requestseam (verified inpydantic_ai/capabilities/hooks.py— themodel_requestkwarg maps straight to'wrap_model_request'). Overriding that method on a subclass is the same seam at the same list position, so ordering vs. history processors and the response clamp is byte-identical.agents/_builder.pyandtools/subagent_invocation.py) now wirePluginMessageTransform(agent_name)directly — no factory closure, no adapter indirection. Thebuild_model_message_transformfactory is gone (YAGNI: it added nothing once the capability holds only a plainagent_namefield).Feature parity checklist
ModelRequestContext+ freshlist(...)of messages — plugin mutations are request-only and never leak into durable history (result.all_messages()stays clean)on_transform_model_messages(agent_name, messages)— callback failure isolation unchanged (lives in the fan-out, not here)capabilities=[...]at both call sitesTestModelstreaming test still green)str | None— so it keeps the inheritedget_serialization_name()default (spec-constructible)Tests
wrap_model_requestdirectly: copy isolation (caller's context and list object untouched, handler receives the mutated fresh copy, sentinel response passes through) and bare passthrough with zero callbacks registeredFunctionModel-backed run test still proves post-history-processing ordering and request-only semantics on the real wiretest_render_version_check_current) is pre-existing on main and unrelatedNotes
capabilities=[...]block, so expect at most a trivial rebase whichever lands last.