Goal
A small adapter so a Pydantic AI app emits a CHAP human-decision record whenever a human approves, edits, or denies a deferred tool call. A merged adapter is strong evidence CHAP is usable in real agent stacks, and Pydantic AI's typed, approval-gated tools map almost one-to-one onto CHAP's record.
Where it hooks
Pydantic AI handles human-in-the-loop through deferred tools. A tool marked requires_approval=True, or gated by an ApprovalRequiredToolset, ends the run with a DeferredToolRequests output whose approvals list holds the pending ToolCallParts. The caller resolves them into DeferredToolResults using ToolApproved or ToolDenied, either after the run or inline via a handle_deferred_tool_calls hook. The adapter records each decision at that resolution point.
Mapping (Pydantic AI to CHAP)
- the pending
ToolCallPart (tool name, validated args, tool_call_id) is the referenced artifact the human acts on
ToolApproved() with no change is an approve, decision kept
ToolApproved(override_args=...) is a refining override; original args versus override_args is the diff
ToolDenied(message=...) is a reject or escalate, and the message is the rationale
DeferredToolResults.metadata[tool_call_id] carries the rationale and the CHAP tags
- records hash-chain, with an optional transparency-log anchor via the audit-scitt profile
Scope
Out of scope
Changing Pydantic AI itself, or anything in MCP core. This is application-layer and reads from the existing deferred-tools API.
Pointers
CHAP spec and README in this repo; the protocol-neutral record shape (link the shape issue once it exists); the conformance harness; Pydantic AI's deferred-tools and human-in-the-loop docs at ai.pydantic.dev.
Goal
A small adapter so a Pydantic AI app emits a CHAP human-decision record whenever a human approves, edits, or denies a deferred tool call. A merged adapter is strong evidence CHAP is usable in real agent stacks, and Pydantic AI's typed, approval-gated tools map almost one-to-one onto CHAP's record.
Where it hooks
Pydantic AI handles human-in-the-loop through deferred tools. A tool marked
requires_approval=True, or gated by anApprovalRequiredToolset, ends the run with aDeferredToolRequestsoutput whoseapprovalslist holds the pendingToolCallParts. The caller resolves them intoDeferredToolResultsusingToolApprovedorToolDenied, either after the run or inline via ahandle_deferred_tool_callshook. The adapter records each decision at that resolution point.Mapping (Pydantic AI to CHAP)
ToolCallPart(tool name, validated args,tool_call_id) is the referenced artifact the human acts onToolApproved()with no change is an approve, decision keptToolApproved(override_args=...)is a refining override; original args versusoverride_argsis the diffToolDenied(message=...)is a reject or escalate, and the message is the rationaleDeferredToolResults.metadata[tool_call_id]carries the rationale and the CHAP tagsScope
handle_deferred_tool_callshook, that emits one CHAP record per decisionOut of scope
Changing Pydantic AI itself, or anything in MCP core. This is application-layer and reads from the existing deferred-tools API.
Pointers
CHAP spec and README in this repo; the protocol-neutral record shape (link the shape issue once it exists); the conformance harness; Pydantic AI's deferred-tools and human-in-the-loop docs at ai.pydantic.dev.