Not every tool should just run
Lesson 6-7's tools executed immediately whenever the model called them. For anything destructive or costly, deleting a file, sending an email, issuing a refund, you often want a human to approve the call before it runs. requires_approval=True on a tool turns "call it" into "pause and ask."
@agent.tool_plain(requires_approval=True)def delete_file(path: str) -> str: """Delete a file at the given path.""" return f"deleted {path}"What happens when an approval-gated tool is called
Instead of running the tool, the agent's run ends early and result.output comes back as a DeferredToolRequests, holding the pending calls that need a decision, not your normal output_type. Declare that this can happen by including it in output_type.
from pydantic_ai import Agentfrom pydantic_ai.models.test import TestModelfrom pydantic_ai.tools import DeferredToolRequests, DeferredToolResults
agent = Agent("test", output_type=[str, DeferredToolRequests])
@agent.tool_plain(requires_approval=True)def delete_file(path: str) -> str: """Delete a file at the given path.
Args: path: The path of the file to delete. """ return f"deleted {'{'}path{'}'}"
def main() -> None: test_model = TestModel(call_tools=["delete_file"])
result = agent.run_sync("Please delete /tmp/foo.txt", model=test_model) print("First run output type:", type(result.output).__name__)
if not isinstance(result.output, DeferredToolRequests): print("No approval needed, final answer:", result.output) return
print("Pending approvals:") for call in result.output.approvals: print(f" {call.tool_name}({call.args})")
# A human (or a policy function) decides here. We approve every call. decisions = DeferredToolResults() for call in result.output.approvals: decisions.approvals[call.tool_call_id] = True
final = agent.run_sync( message_history=result.all_messages(), deferred_tool_results=decisions, model=test_model, ) print("\nAfter approval, final output:", final.output)This lesson uses TestModel (Lesson 14) rather than a live Gemini call, so the approval flow is deterministic and needs no API key: the point here is the approve/resume mechanics, not what a real model decides to call.
Resuming after a decision
Once a human, or your own policy code, approves or rejects each pending call, build a DeferredToolResults and run again, passing the prior message history back in. The agent picks up exactly where it left off: approved tools actually run, rejected ones don't, and the run continues toward a real final answer. This is the Pydantic AI equivalent of LangGraph's interrupt-and-resume pattern for human approval, just modeled as a distinct output type instead of a graph interrupt.
Checkpoint
requires_approval=Trueon a tool turns a call into a pending request instead of an immediate execution.output_typemust includeDeferredToolRequestsfor this to be a valid outcome of a run.DeferredToolResultsplus the priormessage_historyresumes the run, executing approved calls and skipping rejected ones.
If anything here still feels unclear, ask before moving to Lesson 21.