Skip to content

Promote plugin message transform to a first-class capability - #830

Open
mpfaffenberger wants to merge 2 commits into
mainfrom
feature/plugin-message-transform-capability
Open

Promote plugin message transform to a first-class capability#830
mpfaffenberger wants to merge 2 commits into
mainfrom
feature/plugin-message-transform-capability

Conversation

@mpfaffenberger

Copy link
Copy Markdown
Owner

What

Third entry in the capability-ification series (after #828 SteerInjection and #829 HistoryCompaction): the transform_model_messages plugin hook was a closure stuffed into the generic Hooks(model_request=...) adapter — at two call sites. It's now PluginMessageTransform, a @dataclass AbstractCapability subclass in code_puppy/agents/_model_message_transform.py.

Why

  • Hooks(model_request=...) registers on the wrap_model_request seam (verified in pydantic_ai/capabilities/hooks.py — the model_request kwarg 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.
  • Both call sites (agents/_builder.py and tools/subagent_invocation.py) now wire PluginMessageTransform(agent_name) directly — no factory closure, no adapter indirection. The build_model_message_transform factory is gone (YAGNI: it added nothing once the capability holds only a plain agent_name field).

Feature parity checklist

  • Shallow-copy of ModelRequestContext + fresh list(...) of messages — plugin mutations are request-only and never leak into durable history (result.all_messages() stays clean)
  • Async fan-out via on_transform_model_messages(agent_name, messages) — callback failure isolation unchanged (lives in the fan-out, not here)
  • Runs after compaction/steering/clamp — same position in capabilities=[...] at both call sites
  • Streaming path covered (existing TestModel streaming test still green)
  • Serialization: unlike refactor: promote steer injection to a first-class pydantic-ai capability #828/refactor: promote history compaction to a first-class pydantic-ai capability #829, this capability holds no live agent reference — just a str | None — so it keeps the inherited get_serialization_name() default (spec-constructible)

Tests

  • 2 new contract tests drive wrap_model_request directly: 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 registered
  • Existing suite updated to construct the capability directly; the FunctionModel-backed run test still proves post-history-processing ordering and request-only semantics on the real wire
  • Full suite: 7490 passed; the one red (test_render_version_check_current) is pre-existing on main and unrelated

Notes

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant