feat: InterruptedSubagentNotes capability (capability series, round 12) - #840
Open
mpfaffenberger wants to merge 2 commits into
Open
feat: InterruptedSubagentNotes capability (capability series, round 12)#840mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
…es capability The run-start eager append in _run_signals.inject_interrupted_subagent_notes becomes a first-class pydantic-ai capability on the before_model_request seam, delivered per-turn via a ContextVar observation built at the exact old call site (drain + emit timing unchanged). Injection-time mirror plus a custody-boundary fallback in the run task's finally preserve the old persistence guarantees across retries, cancels, and crashes.
Review findings (PR #840 pass 1): installing None was a no-op, so a nested run_with_mcp task inherited the outer turn's observation through the ContextVar and could consume its one-shot injection state (empty outer anchor or identical prompts). None installs now shadow; the prompt anchor stays as defense in depth for runs that bypass run_with_mcp. Adds regression tests for both stealing sequences plus a multimodal [prompt, *attachments] anchor pin.
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
Twelfth entry in the capability-conversion series (#828–#836, #838, #839). After round 11 declared the candidate well "genuinely dry at the seams", this round found the feature that was hiding in
_run_signals.pythe whole time: interrupted-sub-agent notes — the run-start injection that tells the model a Ctrl+C'd delegation existed and where to resume it.Previously
inject_interrupted_subagent_noteseagerly appended the notes toagent._message_historyat run start. Now the notes ride a first-class pydantic-ai capability,InterruptedSubagentNotes, on thebefore_model_requestseam.How
New
code_puppy/agents/_interrupt_notes.pybuild_interrupt_note_observation(agent)runs at the exact old call site (run start, never nested) — drain timing,emit_infotext/count, and the no-_message_historyearly-out are byte-identical. It packages the notes into a per-turnInterruptNoteObservationinstead of mutating history.InterruptedSubagentNotes(stateless dataclass, shared across turns) resolves the observation from a ContextVar installed aroundcreate_task(Deliver stream rendering via a StreamRendering capability #835/refactor: promote first-turn prompt preparation to a PromptPreparation capability #839 pattern) and splices the notes immediately before the turn's own user request — the exact position the eager append produced. The splice is anchored on the turn's built prompt payload (observation.turn_prompt), so a nested run inheriting the ambient observation can't steal the injection.before_model_request's returned messages feed the run's state — injected messages persist into subsequent requests of the same run and intoresult.all_messages(). So success-path custody is automatic.agent._message_history(the steer-processor pattern) keeps the notes acrossstreaming_retryre-entries, which re-seed from that list.mirror_uninjectedis the custody-boundary fallback in the run task'sfinally(before the interrupted-tool-call prune, matching the existing boundary order): a turn that dies before any model request still persists the notes for the next turn — exactly what the eager append gave._builder.py: capability wired before the compactionProcessHistory, so compaction sees the notes exactly as it saw the old eager append. Ordering pinned by test._run_signals.py: old injector removed; fixed a pre-existingall__→__all__typo while editing that list (the module's exports were silently never declared).Sub-agent site deliberately untouched: the notes belong to the main conversation; injecting them into a sub-agent's transcript while mirroring into the main agent's history would be wrong on both ends. Pinned by test.
Bounded divergences (documented in the module docstring)
model_selecthook fires before any model request, so it now sees pre-note history. Notes are a few short user messages; nothing in tree routes models off them.agent._message_historyholds no notes (the old path baked them eagerly). Model-visible bytes are unaffected; every exit path converges via mirror or fallback.Tests
19 contract tests in
tests/agents/test_interrupt_notes_capability.py: exact note-text/emit parity, splice position end-to-end through a realAgent+FunctionModel, single-injection across multi-step tool runs, empty-prompt append-at-end parity, nested-run anchor guard, ContextVar shadowing, mirror idempotence, multimodal [prompt, *attachments] anchor pinning, and production-shaped runs throughrun_with_mcp(happy path, cancel-before-request custody, nested-run no-drain, and both nested-run note-stealing sequences from review pass 1).Full suite: 7616 passed, 0 failed (10 skipped, 1 xpassed).
Notes
Do not merge yet — same review protocol as the rest of the series. Like its eleven siblings, this grazes the shared
capabilities=[...]block in_builder.py; whichever lands last eats a trivial rebase.Review
Reviewed by code-puppy clone (session
pr-840-review-f76d4d-52543e), two passes: REQUEST_CHANGES on pass 1 (real find:install_interrupt_note_observation(None)was a no-op, so nested runs inherited the outer observation through the ContextVar and could steal the one-shot injection — fixed at the root inca39aad8by making None installs shadow, plus regression tests for both exact stealing sequences). Final verdict pass 2: APPROVE, zero findings.