refactor: promote first-turn prompt preparation to a PromptPreparation capability - #839
Open
mpfaffenberger wants to merge 2 commits into
Open
refactor: promote first-turn prompt preparation to a PromptPreparation capability#839mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
…n capability The claude-code-style system-prompt fold (real system prompt delivered inside the first user message because the provider pins its own instructions string) was baked into the prompt argument by _runtime._should_prepend_system_prompt before pydantic_agent.run() ever saw it. Promote it to a first-class pydantic-ai capability: - code_puppy/agents/_prompt_preparation.py: build_prompt_observation() computes the raw -> prepared substitution once per turn at the exact old call site (identical hook arguments, timing, and call count); PromptPreparation swaps it onto the conversation's first user message at before_model_request (send side) and mirrors the swap into recorded history at after_run (persist side). Per-turn state rides a ContextVar installed around the run task, so retries and follow-up runs within the turn keep folding even from checkpointed raw history, and nested sub-agent runs shadow the outer observation. - Both construction sites list PromptPreparation FIRST so compaction sees the folded first message exactly as it did when the fold was baked into the prompt string. - Core mirrors at its state-custody boundaries (main run-task finally, sub-agent partial-session save) so bytes at rest stay byte-identical to the old behaviour on every exit path, including cancellation. - Degenerate empty-prompt folds are still baked eagerly (payload building drops empty prompts, so there is no user part to anchor the request-time swap on); resumed sub-agent sessions keep the eager pass-through, which is an identity transform under the prepend flag contract. 17 contract tests, including wire + history parity against the old baked-prompt behaviour through a real Agent.run().
…iew findings 1-2) - test_checkpoint_resume_rerun_still_folds now asserts the exact post-run state: recorded history mirrored by after_run, caller's checkpoint list untouched (pydantic-ai copies supplied history) -- which is precisely why the core custody-boundary mirror exists. - New production-shaped tests replicating the two core custody boundaries: the main run-task finally (mirror then prune) and the sub-agent partial-save except block.
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.
Eleventh in the capability series (#828–#836, #838). This one converts the last feature still smuggling itself past
pydantic_agent.run()inside the prompt argument: the first-turn system-prompt fold for claude-code-style models.The feature
Providers like claude-code OAuth pin their own instruction string, so code_puppy delivers the real system prompt (+ puppy rules) folded into the first user message. Previously
_runtime._should_prepend_system_prompt(and the twinprepare_prompt_for_modelcall insubagent_invocation) baked that fold into the prompt string before the run started — invisible to the capability system, and baked into recorded history as a side effect.The conversion
New
code_puppy/agents/_prompt_preparation.py:build_prompt_observation()computes theraw -> preparedsubstitution once per turn, at the exact old call site — identical hook arguments (prepare_model_prompt/get_model_system_prompt), identical timing, identical call count (non-qualifying turns fire zero hooks, as before).PromptPreparation(statelessAbstractCapability):before_model_request— swaps the conversation's first user messageraw -> prepared(send side; fresh copies, positional + content-guarded, attachments payloads handled),after_run— mirrors the same swap into the recorded history in place (persist side), so bytes at rest match the old baked-in prompt.PromptPreparationsits first in bothcapabilities=[...]blocks so compaction sees the folded first message exactly as it did when the fold arrived pre-baked (pinned by test).finallyand the sub-agent partial-session save — so cancellation/crash checkpoints also persist the prepared form (idempotent; refactor: promote steer injection to a first-class pydantic-ai capability #828's injectable-mirror precedent).Feature-parity notes (the honest bits)
prepare_prompt_for_model(prepend_system_to_user=False)leaves the user prompt untouched under the plugin contract (verified against the claude_code_oauth plugin), so the eager pass-through stays — an identity transform either way.initial_prompt=) now pass the prepared string explicitly, matching what the old code saved.Tests
19 contract tests in
tests/agents/test_prompt_preparation_capability.py, including wire + stored-history parity against the old baked-prompt behaviour through a realAgent.run(), checkpoint-resume folding, compaction-ordering, and nested-observation shadowing.Full suite: 7616 passed, 0 failed (28 skipped, 1 xpassed).
Review
code-puppy clone, two passes: APPROVE pass 1 with 2 non-blocking findings + 1 nit (both findings applied in
e5623438: exact checkpoint-copy semantics pinned, production-shaped custody-boundary tests for the main-task finally and the sub-agent partial save; full_run_with_mcp_implintegration test declined — would mostly test the mocks, YAGNI). Final verdict pass 2: APPROVE, zero new findings.