Skip to content

Adapter: record CHAP human-decision events from Pydantic AI tool approvals#5

Merged
brightbeamarsalan merged 2 commits into
mainfrom
adapter/pydantic-ai
Jul 2, 2026
Merged

Adapter: record CHAP human-decision events from Pydantic AI tool approvals#5
brightbeamarsalan merged 2 commits into
mainfrom
adapter/pydantic-ai

Conversation

@BrightbeamBogdanCastraveti

Copy link
Copy Markdown
Collaborator

Closes #2.

A small adapter mirroring chap-langgraph: when a Pydantic AI run pauses for tool approval, the human's decision becomes a CHAP envelope.

True / ToolApproved()             -> decide.approve
ToolApproved(override_args=...)    -> decide.override   (RFC 6902 diff of the args)
False / ToolDenied(message=...)   -> decide.reject

The pending ToolCallPart (tool, validated args, tool_call_id) is the artefact under review; the resolution is the human's decision on it. Targets the stable deferred_tool_results path rather than the inline hook while pydantic-ai/pydantic-ai#3959 settles.

Addressing the review

  • intent_preserved is not hardcoded. It defaults to true for an args edit but the resolver can override it, via results.metadata[tool_call_id] — the same channel that carries rationale and tags. Refine-vs-replace stays the human's signal.
  • Approver identity flows into every record. The Coordinator has no ambient actor: the decider is the envelope from, which it stamps onto decisions[].reviewer, the override artefact, and task history. The bridge takes a default reviewer but a per-decision approver overrides it; the participant type is derived from the URI scheme and the approver is joined just-in-time. The example uses real named approvers (alice approves/denies, sam signs the capped override).

Design notes

  • pydantic-ai is an optional dependency. The resolution objects are read structurally, so the bridge and its tests run without it installed.
  • comment is used for decide.approve/decide.reject and rationale for decide.override — the field each handler actually records.
  • A no-op override (override_args equal to the original args) records an approve, not an empty-diff override, so it doesn't show up as a phantom in the overrides view.

Verification

  • 17 unit tests pass with no pydantic-ai installed (structural stand-ins).
  • examples/01-approve-edit-deny.py runs all three decisions against real pydantic-ai 2.0 using TestModel (offline, no API key); resuming with the override confirms the edited args take effect. The emitted chain is hash-linked and accepted by a conformant Coordinator. Happy to add an explicit harness-binary run against a live server if you'd like that demonstrated literally rather than by construction.

Two spec things worth a look (not changed here)

  • decide.approve/decide.reject record the reviewer's note as comment, while decide.override uses rationale — two field names for "the why."
  • decide.* don't verify from is a joined member (only workspace/task existence and review_requested state), so a decision could currently name a non-member.

Files

packages/chap-pydantic-ai/
  pyproject.toml
  chap_pydantic_ai/__init__.py
  chap_pydantic_ai/py.typed
  examples/01-approve-edit-deny.py
  tests/test_bridge.py
  README.md

…c AI tool approvals

Adapter mirroring chap-langgraph: when a Pydantic AI run pauses for tool
approval, record the human's decision as a CHAP envelope.

  True / ToolApproved()            -> decide.approve
  ToolApproved(override_args=...)   -> decide.override (RFC 6902 diff of args)
  False / ToolDenied(message=...)  -> decide.reject

- pydantic-ai is an optional dependency; resolution objects are read
  structurally, so the bridge and its tests run without it installed.
- intent_preserved defaults to true on an args edit but is overridable
  via the metadata channel (refine vs replace stays the human's signal).
- approver identity flows per-decision; participant type follows the
  URI scheme; the approver is joined just-in-time.
- a no-op override (override_args equal to the original) records an
  approve, not a phantom override.

Closes #2.

@brightbeamarsalan brightbeamarsalan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed in full, and this is excellent work Bogdan, thank you. Approving.

The structure mirrors chap-langgraph cleanly, the bridge stays free of any pydantic-ai import, and the two things we discussed are handled exactly right: intent_preserved is the human's signal with a sensible default and an explicit override, and the per-decision approver is joined and recorded. Nice touch that an edit producing no real change records as decide.approve rather than an empty override. And the comment versus rationale split is right (comment for decide.approve / decide.reject, rationale for decide.override), which matches the spec.

It also lines up with the 0.2.6 authorisation rules by construction: each call becomes its own task whose review.request is addressed to that approver, so from is always in the review's to set and the approver is always a member. Well done.

GitHub shows no conflicts and a single commit, so no rebase needed. Two small things before merge:

  1. The example shows approve, a refining edit, and a deny, but not the replace-in-disguise case (intent_preserved=false) you said it would carry. The tests cover false; please add one step to the example so the runnable path teaches the refine-versus-replace distinction too.
  2. There's no CI on the repo yet, so could you run the adapter's pytest and the conformance harness locally against current main (0.2.6) and confirm both are green? The 0.2.6 authorisation tightening is enforced in core now, and the no-conflicts banner doesn't exercise it, so a local run on main is what proves the adapter still passes.

Add the example step and confirm the local 0.2.6 run, and I'll merge. Great work.

Add an intent_preserved=false step so the runnable example teaches the
refine-vs-replace distinction, and surface intent_preserved in the
printed chain.
@BrightbeamBogdanCastraveti

Copy link
Copy Markdown
Collaborator Author

Both asks handled — one is blocked on a repo setting, not the code.

1. Example — added a substituting-override step (intent_preserved=false, redirecting the payment) alongside the refining one, and the printed chain now shows intent_preserved, so the runnable path teaches refine-vs-replace, not just the tests. Committed locally as 71e1c39.

2. Local run against main (0.2.6) — both green:

  • adapter pytest: 17 passed
  • conformance harness against the 0.2.6 Python reference server: 23/23, including the new rv-07 (decide by non-member → -32011) and rv-08 (decide by member not in the reviewer set → -32011). The adapter satisfies the tightened decide.* rules by construction — approver joined first, review.request addressed to that approver — so from is always a member and always in the to set.

Blocker on pushing the example commit: the protection ruleset targets ~ALL branches with pull_request + no bypass actors, so commits can't be pushed to any existing branch — only branch creation is allowed. That's why the follow-up commit to this branch is rejected. Could you scope it to ~DEFAULT_BRANCH (or add a bypass / exclude adapter/**)? Then I'll push 71e1c39 and it's ready to merge. I'd rather not open a fresh branch/PR and lose your review here.

@brightbeamarsalan

Copy link
Copy Markdown
Collaborator

great thanks Bogdan, please proceed with the push now.

@brightbeamarsalan brightbeamarsalan merged commit c93bddc into main Jul 2, 2026
@brightbeamarsalan brightbeamarsalan deleted the adapter/pydantic-ai branch July 2, 2026 14:41
@BrightbeamBogdanCastraveti

Copy link
Copy Markdown
Collaborator Author

Pushed 71e1c39 — the substituting-override example step is on the branch now. Ready to merge whenever.

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.

Adapter: record CHAP human-decision events from Pydantic AI tool approvals

2 participants