Skip to content

Human in the Loop

The HumanInTheLoop intervention handler pauses agent execution before tool calls to request human approval. It provides a configurable, drop-in way to add human oversight without writing custom interrupt logic. Pass it to interventions and choose how you want to collect the human’s response.

The handler uses the confirm action to pause for human input. Under the hood it builds on the SDK’s interrupt mechanism, but abstracts away the manual interrupt/resume loop when you provide an ask option.

flowchart LR
A[Tool call] --> B{Allowed?}
B -->|Yes| C[Execute]
B -->|No| D{Trusted?}
D -->|Yes| C
D -->|No| E{Human approves?}
E -->|Yes| C
E -->|No| F[Cancel]

Without an ask option, the handler raises an interrupt and the agent pauses. The caller presents the interrupt to the user, collects their response, and resumes the agent. This is the same interrupt/resume pattern used throughout the SDK. For stateless deployments, combine with a session manager to persist state between requests.

agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop()],
)
# Agent pauses with stop_reason 'interrupt' when a tool needs approval
result = agent("Delete the temp files")
if result.stop_reason == "interrupt":
# Present the interrupt to the user (web UI, Slack, etc.)
print(result.interrupts[0].reason)
# Resume with the human's response
result = agent(
[
{
"interruptResponse": {
"interruptId": result.interrupts[0].id,
"response": "yes", # 'y', 'yes', or True -> approved
}
}
]
)

For CLI applications, pass ask="stdio" ask: 'stdio' to prompt the user inline via stdin. The agent blocks until the user responds, so no interrupt handling is needed on the caller side.

agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop(ask="stdio")],
)
agent("Delete the temp files")
# Terminal prompt:
# Tool "delete_files" requires human approval. Input: {...} (y/n):

For web UIs, Slack bots, or other custom interfaces, pass a function to ask. The function receives a prompt string describing the tool call and must return the user’s response.

async def ask(prompt: str) -> str:
# Your UI: Slack DM, web modal, push notification, etc.
return await ask_user_via_slack(prompt)
agent = Agent(
tools=[delete_files],
interventions=[HumanInTheLoop(ask=ask)],
)
agent("Delete the temp files")
ParameterTypeDefaultDescription
allowed_tools allowedTools list[str] string[] None undefined Tools that bypass approval. Supports "*" (all) and "!tool_name" (negation).
enable_trust enableTrust bool boolean False false When enabled, trust responses are remembered for the session.
evaluate_trust evaluateTrust FunctionAccepts "t" or "trust"Custom validator for trust responses. Only evaluated when trust is enabled.
evaluateFunctionAccepts True, "y", or "yes" true, 'y', or 'yes' Custom validator for approval responses.
ask Function or "stdio" Function or 'stdio' None undefined Pass a function for custom UIs, "stdio" for CLI prompting, or omit for interrupt/resume.

Use the allowed tools list to skip approval for safe, read-only tools:

agent = Agent(
tools=[read_file, delete_files],
interventions=[
HumanInTheLoop(
ask="stdio",
# Pattern syntax:
# "read_file" -> runs without approval
# "*" -> all tools run freely (disables handler)
# ["*", "!delete_files"] -> all except delete_files
allowed_tools=["read_file"],
),
],
)
agent("Read config.json then delete /tmp/old-logs")
# Only delete_files prompts; read_file executes immediately

When trust is enabled, a human can respond with 't' or 'trust' to approve the current tool call AND remember that decision for the rest of the session. Subsequent calls to the same tool skip the prompt entirely. Trust works in all modes: interrupt/resume, stdio, and custom callbacks.

Trust state is stored in agent.state agent.appState and persists across turns within a session but resets when the agent is re-created. Negated tools ("!tool_name") cannot be trusted and always prompt.

agent = Agent(
tools=[delete_files],
interventions=[
HumanInTheLoop(
ask="stdio",
enable_trust=True,
),
],
)
agent("Delete all log files in /tmp")
# First call: user responds 't' -> approved AND remembered
# Subsequent calls: no prompt needed for the session

By default, the handler accepts True, "y", or "yes" true, 'y', or 'yes' as approval. Use evaluate to define your own approval logic, for example requiring the user to type “confirm”:

agent = Agent(
tools=[delete_files],
interventions=[
HumanInTheLoop(
ask="stdio",
# Only approve if the user types "confirm"
evaluate=lambda response: isinstance(response, str) and response.lower() == "confirm",
),
],
)
agent("Delete the temp files")
# Prompt: Tool "delete_files" requires human approval. Input: {...}
# User must type "confirm" to approve (not just "y" or "yes")

Use HumanInTheLoop when you want tool-level approval gating with minimal code: it handles allow-lists, trust, and collection mode out of the box. Use raw interrupts when you need full control: custom interrupt shapes, multi-step interactions, or workflows beyond simple approve/deny.

  • Interventions: The intervention handler framework that HITL is built on
  • Interrupts: Low-level interrupt/resume mechanism
  • Agent State: How trust decisions persist via agent.state agent.appState
  • Session Management: Persisting interrupt state across sessions