Skip to content

refactor: promote steer injection to a first-class pydantic-ai capability - #828

Open
mpfaffenberger wants to merge 2 commits into
mainfrom
feature/steer-injection-capability
Open

refactor: promote steer injection to a first-class pydantic-ai capability#828
mpfaffenberger wants to merge 2 commits into
mainfrom
feature/steer-injection-capability

Conversation

@mpfaffenberger

Copy link
Copy Markdown
Owner

What

The Ctrl+T mid-turn steering injector (_steer_processor.py) was a closure-based history processor wrapped in ProcessHistory. This PR rewrites it as SteerInjection, a genuine AbstractCapability subclass in pydantic-ai-harness house style — dataclass, injectable drain/mirror seams, get_serialization_name() -> None (not spec-serializable: seams take callables).

Module renamed _steer_processor.py_steering.py to match its promotion.

Why

  • Everything-is-a-capability: the builder already composes capabilities=[...]; steering was the odd one out hiding behind the generic ProcessHistory adapter.
  • Testable seams: the capability takes drain and mirror callables instead of reaching for globals + agent._message_history from inside a closure — unit tests no longer need the global pause controller.
  • Same seam, same ordering: SteerInjection.before_model_request is the exact hook ProcessHistory uses internally, so compaction-then-steering registration order is byte-for-byte preserved (a fresh steer still can't be compacted away on the same call).

Feature parity checklist

  • drains ONLY now-mode steers (queue-mode stays with the runtime's between-turns loop — no double-inject)
  • carries the in-effect instructions onto injected requests (claude-code OAuth stealth-reject footgun documented and preserved)
  • attachments resolve via resolve_steer_content (steer with a pasted screenshot Just Works)
  • one discrete ModelRequest per steer, appended AFTER existing messages
  • injections mirrored into agent._message_history for durability across the turn boundary
  • same observability emit_info per steer

Testing

  • New tests/agents/test_steering_capability.py — 10 contract tests on the capability directly (no-op on empty queue, ordering, instruction carry, mirror behavior, now-vs-queue isolation, native-async hook)
  • Updated wiring guard + smoke tests in test_runtime_pause_leakage.py
  • tests/agents/ + tests/plugins/test_steer_queue.py: 423 passed
  • ruff check + ruff format clean

Do not merge yet — under agent review.

…lity

The Ctrl+T mid-turn steering injector was a closure-based history
processor wrapped in ProcessHistory. Rewrite it as SteerInjection, a
proper AbstractCapability subclass in harness house style (dataclass,
injectable drain/mirror seams, get_serialization_name -> None).

It overrides before_model_request directly -- the exact seam
ProcessHistory uses -- so registration order against the compaction
capability is unchanged: compaction first, then steering, so a fresh
steer can never be compacted away on the same call.

Feature parity preserved:
- drains ONLY now-mode steers (queue-mode stays with the runtime loop)
- carries in-effect instructions onto injected requests
- resolves attachments via resolve_steer_content
- one discrete ModelRequest per steer, appended after existing messages
- mirrors injections into agent._message_history for durability
- emits the same observability message per steer

New unit suite tests the capability contracts directly; the wiring
guard and smoke tests in test_runtime_pause_leakage.py updated to the
capability seam.
…lity chain

Reviewer follow-up: the unit suite exercised the hook through a
duck-typed context. Add a TestModel-backed Agent.run() test proving the
framework dispatches before_model_request and that the injected steer
persists into result.all_messages().
This was referenced Aug 21, 2026
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