Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions docs/human_in_the_loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,22 @@ For `ComputerTool`, use `on_safety_check` to accept or reject safety checks retu
## How the approval flow works

1. When the model emits a tool call, the runner evaluates its approval rule (`needs_approval`, `require_approval`, or the hosted MCP equivalent).
2. If an approval decision for that tool call is already stored in the [`RunContextWrapper`][agents.run_context.RunContextWrapper], the runner proceeds without prompting. Per-call approvals are scoped to the specific call ID; pass `always_approve=True` or `always_reject=True` to persist the same decision for future calls to the same tool identity during the rest of the run.
2. If an approval decision for that tool call is already stored in the [`RunContextWrapper`][agents.run_context.RunContextWrapper], the runner proceeds without prompting. Per-call approvals are scoped to the specific call ID; pass `always_approve=True` or `always_reject=True` to persist the same decision for future calls to the same tool identity.
3. If the approval rule requires approval and no decision for that tool call is stored, execution pauses, and `RunResult.interruptions` (or `RunResultStreaming.interruptions`) contains [`ToolApprovalItem`][agents.items.ToolApprovalItem] entries with details such as `agent.name`, `tool_name`, and `arguments`. This includes approvals raised after a handoff or inside nested `Agent.as_tool()` executions.
4. Convert the result to a `RunState` with `result.to_state()`, call `state.approve(...)` or `state.reject(...)`, and then resume with `Runner.run(agent, state)` or `Runner.run_streamed(agent, state)`, where `agent` is the original top-level agent for the run.
5. The resumed run continues where it left off and will re-enter this flow if new approvals are needed.

Sticky decisions created with `always_approve=True` or `always_reject=True` are stored in the run state, so they survive `state.to_string()` / `RunState.from_string(...)` and `state.to_json()` / `RunState.from_json(...)` when you resume the same paused run later.
Sticky decisions created with `always_approve=True` or `always_reject=True` remain active while the approval-bearing `RunContextWrapper` is reused, including across separate `Runner.run(...)` or `Runner.run_streamed(...)` calls. Passing a plain application context object to a new run creates a fresh wrapper; reusing that object alone does not carry approvals forward. `RunState` also stores sticky decisions, so they survive `state.to_string()` / `RunState.from_string(...)` and `state.to_json()` / `RunState.from_json(...)` when you resume the paused run later.

For function tools, sticky decisions are scoped to the agent that requested approval. A different agent's function tool does not inherit a decision merely because the tool has the same name.

!!! warning "Persistent shell and file-edit decisions are not agent-scoped"

For local [`ShellTool`][agents.tool.ShellTool] and [`ApplyPatchTool`][agents.tool.ApplyPatchTool], sticky decisions match the tool type and name, not the owning agent, executor, or editor. When agents share approval state, an `always_approve=True` decision can apply to another agent's tool of the same type and name after a handoff, even when the executors or editors have different permissions. The default names are `shell` and `apply_patch`, respectively. The SDK does not prompt again or re-evaluate the destination tool's `needs_approval` policy or `on_approval` callback when that sticky approval applies. `always_reject=True` decisions are shared in the same way.
Comment thread
seratch marked this conversation as resolved.

When executors or editors have different permissions, use per-call decisions: call `state.approve(interruption)` or `state.reject(interruption)`, leaving `always_approve` and `always_reject` at their default `False` values. To require a human decision for every new shell or file-edit call, also set `needs_approval=True` and omit `on_approval`. A per-call decision does not clear an existing sticky decision.

When starting a new run with different execution permissions, use a fresh `RunContextWrapper` rather than reusing a wrapper that holds approvals. Resuming an existing `RunState` preserves its approvals, even if you pass a new context object or wrapper; a context override does not reset approval state.

For approval requests from [`HostedMCPTool`][agents.tool.HostedMCPTool], the Agents SDK identifies a sticky tool decision by the combination of `server_label` and tool name. An always-approve decision for `lookup_account` on one hosted MCP server does not approve a tool with the same name on another server. The Agents SDK persists an always-approve or always-reject decision only when the hosted MCP approval request includes both non-empty identity fields.

Expand Down Expand Up @@ -182,10 +192,10 @@ To use streaming in a run that may pause for approvals, call `Runner.run_streame
- **Streaming approvals**: `examples/agent_patterns/human_in_the_loop_stream.py` shows how to drain `stream_events()` and then approve pending tool calls before resuming with `Runner.run_streamed(agent, state)`.
- **Custom rejection text**: `examples/agent_patterns/human_in_the_loop_custom_rejection.py` shows how to combine run-level `tool_error_formatter` with per-call `rejection_message` overrides when approvals are rejected.
- **Agent as tool approvals**: `Agent.as_tool(..., needs_approval=...)` applies the same interruption flow when delegated agent tasks need review. Nested interruptions still surface on the outer run, so resume the original top-level agent rather than the nested one.
- **Local shell and apply_patch tools**: `ShellTool` and `ApplyPatchTool` also support `needs_approval`. Use `state.approve(interruption, always_approve=True)` or `state.reject(..., always_reject=True)` to cache the decision for future calls to that tool during the rest of the run. To resolve approval inside a callback, provide `on_approval`; `examples/tools/shell.py` demonstrates an interactive callback that prompts the operator by default. For an automatic policy that rejects every shell call, set `needs_approval=True` and `on_approval=lambda _context, _item: {"approve": False, "reason": "Disabled by policy"}` on `ShellTool`. To let the application review a paused run instead, handle interruptions (see `examples/tools/shell_human_in_the_loop.py`). Hosted shell environments do not support `needs_approval` or `on_approval`; see the [tools guide](tools.md).
- **Local shell and apply_patch tools**: `ShellTool` and `ApplyPatchTool` also support `needs_approval`. Use `state.approve(interruption)` or `state.reject(interruption)` for per-call decisions. The `always_approve=True` and `always_reject=True` options persist decisions in the `RunContextWrapper` and `RunState`; review the [approval scope warning](#how-the-approval-flow-works) before using these options. To resolve approval inside a callback, provide `on_approval`; `examples/tools/shell.py` demonstrates an interactive callback that prompts the operator by default. To automatically reject shell calls that have no existing approval decision, set `needs_approval=True` and `on_approval=lambda _context, _item: {"approve": False, "reason": "Disabled by policy"}` on `ShellTool`. To let the application review a paused run instead, handle interruptions (see `examples/tools/shell_human_in_the_loop.py`). Hosted shell environments do not support `needs_approval` or `on_approval`; see the [tools guide](tools.md).
- **Local MCP servers**: Use `require_approval` on `MCPServerStdio` / `MCPServerSse` / `MCPServerStreamableHttp` to gate MCP tool calls (see `examples/mcp/get_all_mcp_tools_example/main.py` and `examples/mcp/tool_filter_example/main.py`).
- **Hosted MCP servers**: Set `tool_config={"require_approval": "always"}` on `HostedMCPTool` to force HITL, optionally providing `on_approval_request` to auto-approve or reject (see `examples/hosted_mcp/human_in_the_loop.py` and `examples/hosted_mcp/on_approval.py`). Use `"never"` for trusted servers (`examples/hosted_mcp/simple.py`).
- **Sessions and memory**: Pass a session to `Runner.run` so approvals and conversation history survive multiple turns. SQLite and OpenAI Conversations session variants are in `examples/memory/memory_session_hitl_example.py` and `examples/memory/openai_session_hitl_example.py`.
- **Sessions and memory**: Pass a session to `Runner.run` to preserve conversation history across turns, and use `RunState` to preserve approval decisions when resuming a paused run. SQLite and OpenAI Conversations session variants are in `examples/memory/memory_session_hitl_example.py` and `examples/memory/openai_session_hitl_example.py`.
- **Realtime agents**: The realtime demo exposes WebSocket messages that approve or reject tool calls via `approve_tool_call` / `reject_tool_call` on the `RealtimeSession` (see `examples/realtime/app/server.py` for the server-side handlers and [Realtime guide](realtime/guide.md#tool-approvals) for the API surface).

## Long-running approvals
Expand Down
2 changes: 2 additions & 0 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ For commands or file edits that require review, set `needs_approval=True` on the

To decide immediately in application code, set both `needs_approval` and `on_approval`. The SDK invokes `on_approval` only when the call requires approval and has no existing approval decision; setting the callback alone does not enable approval. See `examples/tools/shell.py` for a CLI prompt and `examples/tools/shell_human_in_the_loop.py` for manual interruption handling. The `examples/tools/apply_patch.py` example instead prompts inside its editor before changing files.

Persistent decisions for local `ShellTool` and `ApplyPatchTool` can carry across handoffs to another agent's tool of the same type and name, or across separate runs that reuse the same `RunContextWrapper`. Use per-call decisions when executor or editor permissions differ, and a fresh wrapper when starting a new run with different execution permissions; see the [approval scope warning](human_in_the_loop.md#how-the-approval-flow-works) for details, including why per-call decisions and context overrides during resume do not clear stored approvals.

Keep `needs_approval=False` when your application intentionally authorizes automatic execution, for example through an executor that enforces your sandbox policy. Hosted shell environments do not support the SDK's local `needs_approval` or `on_approval` settings.

### ComputerTool and the Responses computer tool
Expand Down
Loading