ExamplenotebookintermediateRunnablehuman-approval
Tool Requires Approval
Runnable example (intermediate) for notebook using python-dotenv.
Key Facts
- Level
- intermediate
- Runtime
- Notebook • Python Dotenv
- Pattern
- Inspectable flow with visible system boundaries
- Interaction
- Runnable sandbox • Notebook
- Updated
- 14 March 2026
Navigate this example
Library
Browse examplesReopen the wider library to compare adjacent patterns and linked learning paths.Interaction
Run sandbox nowTry the interaction directly in this example’s guided sandbox surface.Source
Open full sourceRead the real implementation, highlighted checkpoints, and runtime requirements.MCP
Call via MCPUse the same resource inside agents, deterministic exports, and MCP setup flows.
Linked principles
Model context
Model-agnosticLocal-viableWrapped tool calling acceptableLow reasoning requirementOrchestration compensates
Human approval gate is structurally enforced before any tool executes. Model quality is not load-bearing for the safety property.
1-tool-requires-approval.ipynb
json
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "0cba1839",
"metadata": {},
"outputs": [],
"source": [
"from pydantic_ai import (\n",
" Agent,\n",
" ApprovalRequired,\n",
" DeferredToolRequests,\n",
" DeferredToolResults,\n",
" RunContext,\n",
" ToolDenied,\n",
")\n",
"from dotenv import load_dotenv\n",
"import nest_asyncio"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "1f256e9c",
"metadata": {},
"outputs": [],
"source": [
"load_dotenv()\n",
"nest_asyncio.apply()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "97388b83",
"metadata": {},
"outputs": [],
"source": [
"agent = Agent(\"openai:gpt-5.1-chat-latest\", output_type=[str, DeferredToolRequests])\n",
"\n",
"PROTECTED_FILES = {\".env\"}"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "ad0a17a0",
"metadata": {},
"outputs": [],
"source": [
"@agent.tool\n",
"def update_file(ctx: RunContext, path: str, content: str) -> str:\n",
" if path in PROTECTED_FILES and not ctx.tool_call_approved:\n",
" raise ApprovalRequired(metadata={'reason': 'protected'}) \n",
" return f'File {path!r} updated: {content!r}'\n",
"\n",
"@agent.tool_plain(requires_approval=True)\n",
"def delete_file(path: str) -> str:\n",
" return f'File {path!r} deleted'"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "9a74b4de",
"metadata": {},
"outputs": [],
"source": [
"result = agent.run_sync('Delete `__init__.py`, write `Hello, world!` to `README.md`, and clear `.env`')\n",
"messages = result.all_messages()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "9f2c35fd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ModelRequest(parts=[UserPromptPart(content='Delete `__init__.py`, write `Hello, world!` to `README.md`, and clear `.env`', timestamp=datetime.datetime(2025, 11, 22, 14, 25, 30, 520960, tzinfo=datetime.timezone.utc))], run_id='5189df96-6d33-4184-a945-2a2558d0886f')\n",
"ModelResponse(parts=[ToolCallPart(tool_name='delete_file', args='{\"path\": \"__init__.py\"}', tool_call_id='call_ucEXOOM7waIQA5472KWXzlWc'), ToolCallPart(tool_name='update_file', args='{\"path\": \"README.md\", \"content\": \"Hello, world!\"}', tool_call_id='call_FY4MDOfSyHbRaQUoXYfqbOOd'), ToolCallPart(tool_name='update_file', args='{\"path\": \".env\", \"content\": \"\"}', tool_call_id='call_QZ0Khsmk0hYMPkNBE6EcNmkm')], usage=RequestUsage(input_tokens=159, output_tokens=83, details={'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}), model_name='gpt-5.1-chat-latest', timestamp=datetime.datetime(2025, 11, 22, 14, 25, 31, tzinfo=TzInfo(0)), provider_name='openai', provider_details={'finish_reason': 'tool_calls'}, provider_response_id='chatcmpl-CeiqJWZzkuF3rfO1cmM6INeF2QMOG', finish_reason='tool_call', run_id='5189df96-6d33-4184-a945-2a2558d0886f')\n",
"ModelRequest(parts=[ToolReturnPart(tool_name='update_file', content=\"File 'README.md' updated: 'Hello, world!'\", tool_call_id='call_FY4MDOfSyHbRaQUoXYfqbOOd', timestamp=datetime.datetime(2025, 11, 22, 14, 25, 33, 406069, tzinfo=datetime.timezone.utc))], run_id='5189df96-6d33-4184-a945-2a2558d0886f')\n"
]
}
],
"source": [
"for message in messages:\n",
" print(message)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "8c0876f5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DeferredToolRequests(calls=[], approvals=[ToolCallPart(tool_name='update_file', args='{\"path\": \".env\", \"content\": \"\"}', tool_call_id='call_QZ0Khsmk0hYMPkNBE6EcNmkm'), ToolCallPart(tool_name='delete_file', args='{\"path\": \"__init__.py\"}', tool_call_id='call_ucEXOOM7waIQA5472KWXzlWc')], metadata={'call_QZ0Khsmk0hYMPkNBE6EcNmkm': {'reason': 'protected'}})\n"
]
}
],
"source": [
"assert isinstance(result.output, DeferredToolRequests)\n",
"requests = result.output\n",
"print(requests)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "bcd881c4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DeferredToolResults(calls={}, approvals={})\n"
]
}
],
"source": [
"results = DeferredToolResults()\n",
"print(results)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "589a459f",
"metadata": {},
"outputs": [],
"source": [
"for call in requests.approvals:\n",
" result = False\n",
" if call.tool_name == 'update_file':\n",
" # Approve all updates\n",
" result = True\n",
" elif call.tool_name == 'delete_file':\n",
" # deny all deletes\n",
" result = ToolDenied('Deleting files is not allowed')\n",
"\n",
" results.approvals[call.tool_call_id] = result"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "dc556ad5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DeferredToolResults(calls={}, approvals={'call_QZ0Khsmk0hYMPkNBE6EcNmkm': True, 'call_ucEXOOM7waIQA5472KWXzlWc': ToolDenied(message='Deleting files is not allowed', kind='tool-denied')})\n"
]
}
],
"source": [
"print(results)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "2370e4fc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The requested operations are complete, with one exception:\n",
"\n",
"- `__init__.py` could not be deleted (the system reports that deleting files is not allowed).\n",
"- `README.md` now contains: Hello, world!\n",
"- `.env` has been cleared.\n",
"\n",
"If you want to modify `__init__.py` instead of deleting it, just let me know.\n"
]
}
],
"source": [
"result = agent.run_sync(message_history=messages, deferred_tool_results=results)\n",
"print(result.output)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "cbb85877",
"metadata": {},
"outputs": [],
"source": [
"import json"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "02b3a1a8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'parts': [{'content': 'Delete `__init__.py`, write `Hello, world!` to `README.md`, and clear `.env`',\n",
" 'timestamp': '2025-11-22T14:25:30.520960Z',\n",
" 'part_kind': 'user-prompt'}],\n",
" 'instructions': None,\n",
" 'kind': 'request',\n",
" 'run_id': '5189df96-6d33-4184-a945-2a2558d0886f',\n",
" 'metadata': None},\n",
" {'parts': [{'tool_name': 'delete_file',\n",
" 'args': '{\"path\": \"__init__.py\"}',\n",
" 'tool_call_id': 'call_ucEXOOM7waIQA5472KWXzlWc',\n",
" 'id': None,\n",
" 'provider_details': None,\n",
" 'part_kind': 'tool-call'},\n",
" {'tool_name': 'update_file',\n",
" 'args': '{\"path\": \"README.md\", \"content\": \"Hello, world!\"}',\n",
" 'tool_call_id': 'call_FY4MDOfSyHbRaQUoXYfqbOOd',\n",
" 'id': None,\n",
" 'provider_details': None,\n",
" 'part_kind': 'tool-call'},\n",
" {'tool_name': 'update_file',\n",
" 'args': '{\"path\": \".env\", \"content\": \"\"}',\n",
" 'tool_call_id': 'call_QZ0Khsmk0hYMPkNBE6EcNmkm',\n",
" 'id': None,\n",
" 'provider_details': None,\n",
" 'part_kind': 'tool-call'}],\n",
" 'usage': {'input_tokens': 159,\n",
" 'cache_write_tokens': 0,\n",
" 'cache_read_tokens': 0,\n",
" 'output_tokens': 83,\n",
" 'input_audio_tokens': 0,\n",
" 'cache_audio_read_tokens': 0,\n",
" 'output_audio_tokens': 0,\n",
" 'details': {'accepted_prediction_tokens': 0,\n",
" 'audio_tokens': 0,\n",
" 'reasoning_tokens': 0,\n",
" 'rejected_prediction_tokens': 0}},\n",
" 'model_name': 'gpt-5.1-chat-latest',\n",
" 'timestamp': '2025-11-22T14:25:31Z',\n",
" 'kind': 'response',\n",
" 'provider_name': 'openai',\n",
" 'provider_details': {'finish_reason': 'tool_calls'},\n",
" 'provider_response_id': 'chatcmpl-CeiqJWZzkuF3rfO1cmM6INeF2QMOG',\n",
" 'finish_reason': 'tool_call',\n",
" 'run_id': '5189df96-6d33-4184-a945-2a2558d0886f',\n",
" 'metadata': None},\n",
" {'parts': [{'tool_name': 'update_file',\n",
" 'content': \"File 'README.md' updated: 'Hello, world!'\",\n",
" 'tool_call_id': 'call_FY4MDOfSyHbRaQUoXYfqbOOd',\n",
" 'metadata': None,\n",
" 'timestamp': '2025-11-22T14:25:33.406069Z',\n",
" 'part_kind': 'tool-return'}],\n",
" 'instructions': None,\n",
" 'kind': 'request',\n",
" 'run_id': '5189df96-6d33-4184-a945-2a2558d0886f',\n",
" 'metadata': None},\n",
" {'parts': [{'tool_name': 'update_file',\n",
" 'content': \"File '.env' updated: ''\",\n",
" 'tool_call_id': 'call_QZ0Khsmk0hYMPkNBE6EcNmkm',\n",
" 'metadata': None,\n",
" 'timestamp': '2025-11-22T14:29:48.692282Z',\n",
" 'part_kind': 'tool-return'},\n",
" {'tool_name': 'delete_file',\n",
" 'content': 'Deleting files is not allowed',\n",
" 'tool_call_id': 'call_ucEXOOM7waIQA5472KWXzlWc',\n",
" 'metadata': None,\n",
" 'timestamp': '2025-11-22T14:29:48.691698Z',\n",
" 'part_kind': 'tool-return'}],\n",
" 'instructions': None,\n",
" 'kind': 'request',\n",
" 'run_id': '1f50cb76-6787-4e40-9d8b-60b2f66a58d5',\n",
" 'metadata': None},\n",
" {'parts': [{'content': 'The requested operations are complete, with one exception:\\n\\n- `__init__.py` could not be deleted (the system reports that deleting files is not allowed).\\n- `README.md` now contains: Hello, world!\\n- `.env` has been cleared.\\n\\nIf you want to modify `__init__.py` instead of deleting it, just let me know.',\n",
" 'id': None,\n",
" 'provider_details': None,\n",
" 'part_kind': 'text'}],\n",
" 'usage': {'input_tokens': 279,\n",
" 'cache_write_tokens': 0,\n",
" 'cache_read_tokens': 0,\n",
" 'output_tokens': 76,\n",
" 'input_audio_tokens': 0,\n",
" 'cache_audio_read_tokens': 0,\n",
" 'output_audio_tokens': 0,\n",
" 'details': {'accepted_prediction_tokens': 0,\n",
" 'audio_tokens': 0,\n",
" 'reasoning_tokens': 0,\n",
" 'rejected_prediction_tokens': 0}},\n",
" 'model_name': 'gpt-5.1-chat-latest',\n",
" 'timestamp': '2025-11-22T14:29:49Z',\n",
" 'kind': 'response',\n",
" 'provider_name': 'openai',\n",
" 'provider_details': {'finish_reason': 'stop'},\n",
" 'provider_response_id': 'chatcmpl-CeiuTz1CWDMpGsDbHh0Ri4oAqNc55',\n",
" 'finish_reason': 'stop',\n",
" 'run_id': '1f50cb76-6787-4e40-9d8b-60b2f66a58d5',\n",
" 'metadata': None}]"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"json.loads(result.all_messages_json())"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Related principles
- P4trustApply progressive disclosure to system agencyProvide the minimum information necessary by default, while enabling users to inspect additional detail when confidence, understanding, or intervention is required.Open principle →
- P7trustEstablish trust through inspectabilityUsers should be able to examine how a result was produced when confidence, accountability, or decision quality is important.Open principle →
- P8trustMake hand-offs, approvals, and blockers explicitWhen the system cannot proceed, the reason should be immediately visible, along with any action required from the user or another dependency.Open principle →
- P9orchestrationRepresent delegated work as a system, not merely as a conversationWhere work involves multiple steps, agents, dependencies, or concurrent activities, it should be represented as a structured system rather than solely as a message stream.Open principle →