本页还没有译文
本页 54 段还没译成日本語,下面显示的是英文原文。Human-in-the-loop
还没翻译译自英文(docs.langchain.com)本页 54 段都还没译
Some tool operations may be sensitive and require human approval before execution. Deep Agents support human-in-the-loop workflows through LangGraph's interrupt capabilities. You can configure which tools require approval using the interrupt_on parameter. When interrupt_on is set, HumanInTheLoopMiddleware is added to the Deep Agents stack. If a run is cancelled or interrupted before a tool returns a result, PatchToolCallsMiddleware in the same stack repairs the message history automatically.
graph LR
Agent[Agent] --> Check{Interrupt?}
Check --> |no| Execute[Execute]
Check --> |yes| Human{Human}
Human --> |approve| Execute
Human --> |edit| Execute
Human --> |reject| ToolMessage[ToolMessage]
Human --> |respond| ToolMessage
Execute --> Agent
ToolMessage --> Agent
classDef trigger fill:#F6FFDB,stroke:#6E8900,stroke-width:2px,color:#2E3900
classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710
classDef decision fill:#FDF3FF,stroke:#7E65AE,stroke-width:2px,color:#504B5F
classDef alert fill:#F8E8E6,stroke:#B27D75,stroke-width:2px,color:#634643
class Agent trigger
class Check,Human decision
class Execute process
class ToolMessage processBasic configuration
The interrupt_on parameter accepts a dictionary mapping tool names to interrupt configurations. Each tool can be configured with:
-
True: Enable interrupts with default behavior (approve, edit, reject, and respond allowed) -
False: Disable interrupts for this tool -
InterruptOnConfig: Custom configuration. Setallowed_decisionsto control review options. In Python, add an optionalwhenpredicate to interrupt only specific calls (see Conditional interrupts).
from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="openai:gpt-5.5",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="openrouter:z-ai/glm-5.2",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="fireworks:accounts/fireworks/models/glm-5p2",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="baseten:zai-org/GLM-5.2",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)from langchain.tools import tool
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
@tool
def remove_file(path: str) -> str:
"""Delete a file from the filesystem."""
return f"Deleted {path}"
@tool
def fetch_file(path: str) -> str:
"""Read a file from the filesystem."""
return f"Contents of {path}"
@tool
def notify_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
return f"Sent email to {to}"
# Checkpointer is REQUIRED for human-in-the-loop
checkpointer = MemorySaver()
agent = create_deep_agent(
model="ollama:north-mini-code-1.0",
tools=[remove_file, fetch_file, notify_email],
interrupt_on={
"remove_file": True, # Default: approve, edit, reject, respond
"fetch_file": False, # No interrupts needed
"notify_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
},
checkpointer=checkpointer, # Required!
)Decision types
The allowed_decisions list controls what actions a human can take when reviewing a tool call:
| Decision Type | Description | Example Use Case |
|---|---|---|
✅ approve | Execute the tool with the original arguments as proposed by the agent. | Send an email draft exactly as written |
✏️ edit | Modify the tool arguments before execution. | Change the recipient before sending an email |
❌ reject | Skip executing this tool call entirely and return rejection feedback to the agent. | Deny file deletion and explain why |
💬 respond | Return the human's message directly as a synthetic tool result, skipping execution, for "ask user" style tools. | Answer an "ask_user" prompt with a direct reply |
Use reject when the human denies a proposed action. Use respond only when the human is acting as the tool, such as answering an ask_user prompt. Do not use respond to deny side-effecting tools, because its message may be treated by the model as a successful tool result.
Tip
When editing tool arguments, make changes conservatively. Significant modifications to the original arguments may cause the model to re-evaluate its approach and potentially execute the tool multiple times or take unexpected actions.
You can customize which decisions are available for each tool:
interrupt_on = {
# Sensitive operations: allow all options
"delete_file": {"allowed_decisions": ["approve", "edit", "reject"]},
# Moderate risk: approval or rejection only
"write_file": {"allowed_decisions": ["approve", "reject"]},
# Must approve (no rejection allowed)
"critical_operation": {"allowed_decisions": ["approve"]},
}Conditional interrupts
By default, every tool call listed in interrupt_on pauses for review. To pause only some calls, add a when predicate to a tool's InterruptOnConfig. The predicate receives a ToolCallRequest and returns True to interrupt or False to auto-approve, so you can gate on the tool's arguments.
Note
Conditional interrupts require langchain>=1.3.3.
from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="openai:gpt-5.5",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="openrouter:z-ai/glm-5.2",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)
from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="fireworks:accounts/fireworks/models/glm-5p2",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)
from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="baseten:zai-org/GLM-5.2",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)
from deepagents import create_deep_agent
from langchain.agents.middleware import ToolCallRequest
from langgraph.checkpoint.memory import MemorySaver
def writes_outside_workspace(request: ToolCallRequest) -> bool:
"""Pause writes to paths outside the workspace directory."""
path = request.tool_call["args"].get("file_path", "")
return not path.startswith("/workspace/")
agent = create_deep_agent(
model="ollama:north-mini-code-1.0",
interrupt_on={
"write_file": {
"allowed_decisions": ["approve", "edit", "reject"],
"when": writes_outside_workspace,
},
},
checkpointer=MemorySaver(),
)
When the when predicate returns False, the call runs without interrupting. When it returns True, or when you omit when, the call pauses as usual. Calls that evaluate to False are never added to the interrupt batch, so a reviewer only sees the actions that need a decision.
See the LangChain human-in-the-loop documentation for additional configuration options and examples.
Handle interrupts
When an interrupt is triggered, the agent pauses execution and returns control. Check for interrupts in the result and handle them accordingly. If the user rejects an action, include a clear message that tells the agent the tool was not executed and what to do next.
from langchain_core.utils.uuid import uuid7
from langgraph.types import Command
# Create config with thread_id for state persistence
config = {"configurable": {"thread_id": str(uuid7())}}
# Invoke the agent
result = agent.invoke(
{"messages": [{"role": "user", "content": "Delete the file temp.txt"}]},
config=config
)
# Check if execution was interrupted
if result.get("__interrupt__"): # [!code highlight]
# Extract interrupt information
interrupt_value = result["__interrupt__"][0].value # [!code highlight]
action_requests = interrupt_value["action_requests"]
review_configs = interrupt_value["review_configs"]
# Create a lookup map from tool name to review config
config_map = {cfg["action_name"]: cfg for cfg in review_configs}
# Display the pending actions to the user
for action in action_requests:
review_config = config_map[action["name"]]
print(f"Tool: {action['name']}")
print(f"Arguments: {action['args']}")
print(f"Allowed decisions: {review_config['allowed_decisions']}")
# Get user decisions (one per action_request, in order)
decisions = [
{
"type": "reject",
"message": "User rejected deleting temp.txt. Do not retry deletion.",
}
]
# Resume execution with decisions
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config # Must use the same config!
)
# Process final result
print(result["messages"][-1].content) # [!code highlight]
Handle interrupts with streaming
For real-time UIs, use .stream() instead of .invoke() to display tokens and tool call progress while handling HITL interrupts. Interrupts appear as __interrupt__ entries in the updates stream. After the stream completes, collect human decisions and resume by streaming again with Command(resume=...).
Note
For a typical interrupt_on HITL pause, resume with a flat {"decisions": [...]} payload, the same shape as with .invoke(). When multiple interrupts are pending at once (for example, parallel branches), map each interrupt ID to its resume value instead. See Handling multiple interrupts.
from langchain_core.utils.uuid import uuid7
from langgraph.types import Command
config = {"configurable": {"thread_id": str(uuid7())}}
stream_input = {"messages": [{"role": "user", "content": "Delete the file temp.txt"}]}
while True:
pending_interrupt = None
for mode, data in agent.stream( # [!code highlight]
stream_input,
stream_mode=["messages", "updates"], # [!code highlight]
config=config,
):
if mode == "messages":
token, metadata = data
# Display tokens in real time
if token.content:
print(token.content, end="", flush=True)
elif mode == "updates" and isinstance(data, dict):
if "__interrupt__" in data: # [!code highlight]
pending_interrupt = data["__interrupt__"][0] # [!code highlight]
# If no interrupt occurred, the agent finished
if pending_interrupt is None:
break
interrupt_value = pending_interrupt.value
action_requests = interrupt_value["action_requests"]
review_configs = interrupt_value["review_configs"]
config_map = {cfg["action_name"]: cfg for cfg in review_configs}
for action in action_requests:
review = config_map[action["name"]]
print(f"\nTool: {action['name']}")
print(f"Arguments: {action['args']}")
print(f"Allowed: {review['allowed_decisions']}")
# Collect one decision per action_request, in order
decisions = [{"type": "approve"} for _ in action_requests]
stream_input = Command(resume={"decisions": decisions}) # [!code highlight]
print()
Tip
Combine "messages" and "updates" stream modes to show real-time LLM tokens to the user while still catching HITL interrupts. The "messages" mode streams tokens; the "updates" mode delivers interrupt payloads.
Multiple tool calls
When the agent calls multiple tools that require approval, all interrupts are batched together in a single interrupt. You must provide decisions for each one in order.
config = {"configurable": {"thread_id": str(uuid7())}}
result = agent.invoke(
{"messages": [{
"role": "user",
"content": "Delete temp.txt and send an email to admin@example.com"
}]},
config=config
)
if result.get("__interrupt__"): # [!code highlight]
interrupt_value = result["__interrupt__"][0].value # [!code highlight]
action_requests = interrupt_value["action_requests"]
# Two tools need approval
assert len(action_requests) == 2
# Provide decisions in the same order as action_requests
decisions = [
{"type": "approve"}, # First tool: delete_file
{
"type": "reject",
"message": "User rejected this action. Do not retry this tool call.",
} # Second tool: send_email
]
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config
)
Rejection messages
When a reviewer returns a reject decision, Deep Agents skip the tool call and send rejection feedback back to the agent. If you omit message, the default feedback tells the model that the tool was not executed and not to retry the same tool call unless the user asks.
For sensitive or side-effecting tools, pass a domain-specific message with the decision. Be explicit about whether the agent should abandon the action, ask a follow-up question, or try a safer alternative.
decisions = [
{
"type": "reject",
"message": "User rejected deleting this file. Do not retry deletion. Ask which file to archive instead.",
}
]Edit tool arguments
When "edit" is in the allowed decisions, you can modify the tool arguments before execution:
if result.get("__interrupt__"): # [!code highlight]
interrupt_value = result["__interrupt__"][0].value # [!code highlight]
action_request = interrupt_value["action_requests"][0]
# Original args from the agent
print(action_request["args"]) # {"to": "everyone@company.com", ...}
# User decides to edit the recipient
decisions = [{
"type": "edit",
"edited_action": {
"name": action_request["name"], # Must include the tool name
"args": {"to": "team@company.com", "subject": "...", "body": "..."}
}
}]
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config
)
Subagent interrupts
When using subagents, you can use interrupts on tool calls and within tool calls.
Interrupts on tool calls
Each subagent can have its own interrupt_on configuration that overrides the main agent's settings:
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
tools=[delete_file, read_file],
interrupt_on={
"delete_file": True,
"read_file": False,
},
subagents=[{
"name": "file-manager",
"description": "Manages file operations",
"system_prompt": "You are a file management assistant.",
"tools": [delete_file, read_file],
"interrupt_on": {
# Override: require approval for reads in this subagent
"delete_file": True,
"read_file": True, # Different from main agent!
}
}],
checkpointer=checkpointer
)
When a subagent triggers an interrupt, the handling is the same—check for __interrupt__ on the result and resume with Command.
When streaming, subagent interrupts also surface as __interrupt__ entries in the parent updates stream. You do not need subgraphs=True for interrupt delivery. Enable subgraphs=True if you also want to stream subagent tokens and progress events. See Handle interrupts with streaming for the full pattern.
Interrupts within tool calls
Subagent tools can call interrupt() directly to pause execution and await approval:
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langchain.messages import HumanMessage
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command, interrupt
from deepagents.graph import create_deep_agent
from deepagents.middleware.subagents import CompiledSubAgent
@tool(description="Request human approval before proceeding with an action.")
def request_approval(action_description: str) -> str:
"""Request human approval using the interrupt() primitive."""
# interrupt() pauses execution and returns the value passed to Command(resume=...)
approval = interrupt({
"type": "approval_request",
"action": action_description,
"message": f"Please approve or reject: {action_description}",
})
if approval.get("approved"):
return f"Action '{action_description}' was APPROVED. Proceeding..."
else:
return f"Action '{action_description}' was REJECTED. Reason: {approval.get('reason', 'No reason provided')}"
def main():
checkpointer = InMemorySaver()
model = ChatAnthropic(
model_name="claude-sonnet-4-6",
max_tokens=4096,
)
compiled_subagent = create_agent(
model=model,
tools=[request_approval],
name="approval-agent",
)
parent_agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
checkpointer=checkpointer,
subagents=[
CompiledSubAgent(
name="approval-agent",
description="An agent that can request approvals",
runnable=compiled_subagent,
)
],
)
thread_id = "test_interrupt_directly"
config = {"configurable": {"thread_id": thread_id}}
print("Invoking agent - sub-agent will use request_approval tool...")
result = parent_agent.invoke(
{
"messages": [
HumanMessage(
content="Use the task tool to launch the approval-agent sub-agent. "
"Tell it to use the request_approval tool to request approval for 'deploying to production'."
)
]
},
config=config
)
# Check for interrupt
if result.get("__interrupt__"): # [!code highlight]
interrupt_value = result["__interrupt__"][0].value # [!code highlight]
print(f"\nInterrupt received!")
print(f" Type: {interrupt_value.get('type')}")
print(f" Action: {interrupt_value.get('action')}")
print(f" Message: {interrupt_value.get('message')}")
print("\nResuming with Command(resume={'approved': True})...")
result2 = parent_agent.invoke(
Command(resume={"approved": True}),
config=config
)
if "__interrupt__" not in result2: # [!code highlight]
print("\nExecution completed!")
# Find the tool response
tool_msgs = [m for m in result2.get("messages", []) if m.type == "tool"] # [!code highlight]
if tool_msgs:
print(f" Tool result: {tool_msgs[-1].content}")
else:
print("\nAnother interrupt occurred")
else:
print("\n No interrupt - the model may not have called request_approval")
if __name__ == "__main__":
main()
When run, this produces the following output:
Invoking agent - sub-agent will use request_approval tool...
Interrupt received!
Type: approval_request
Action: deploying to production
Message: Please approve or reject: deploying to production
Resuming with Command(resume={'approved': True})...
Execution completed!
Tool result: Great! The approval request has been processed. The action **"deploying to production"** was **APPROVED**. You can now proceed with the production deployment.
Filesystem permission interrupts
Note
Filesystem permission interrupts require deepagents>=0.6.8.
Beyond interrupt_on, you can pause the built-in filesystem tools by marking a permission rule with mode="interrupt". When the agent calls write_file or edit_file on a path that matches an interrupt-mode rule, create_deep_agent raises the same human-in-the-loop interrupt as a configured tool, using the filesystem tool's name as the action name.
from deepagents import FilesystemPermission, create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_deep_agent(
model=model,
permissions=[
FilesystemPermission(
operations=["write"],
paths=["/secrets/**"],
mode="interrupt",
),
],
checkpointer=MemorySaver(), # Required to pause and resume
)
Handle and resume the interrupt the same way as a tool-call interrupt: run until it pauses, inspect the request, then resume with a decision.
from langgraph.types import Command
config = {"configurable": {"thread_id": "fs-thread-1"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Save the API key to /secrets/key.txt"}]},
config=config,
)
if result.get("__interrupt__"):
action = result["__interrupt__"][0].value["action_requests"][0]
print(f"Approve {action['name']} on {action['args']}?")
# Resume with the human decision (approve, edit, or reject).
result = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config, # Same thread ID
)
Filesystem-permission interrupts merge with any interrupt_on you pass, so a single review step can cover both custom tools and protected filesystem paths.
Best practices
Always use a checkpointer
Human-in-the-loop requires a checkpointer to persist agent state between the interrupt and resume:
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
tools=[...],
interrupt_on={...},
checkpointer=checkpointer # Required for HITL
)
Use the same thread ID
When resuming, you must use the same config with the same thread_id:
# First call
config = {"configurable": {"thread_id": "my-thread"}}
result = agent.invoke(input, config=config)
# Resume (use same config)
result = agent.invoke(Command(resume={...}), config=config)
Match decision order to actions
The decisions list must match the order of action_requests:
if result.get("__interrupt__"): # [!code highlight]
interrupt_value = result["__interrupt__"][0].value # [!code highlight]
action_requests = interrupt_value["action_requests"]
# Create one decision per action, in order
decisions = []
for action in action_requests:
decision = get_user_decision(action) # Your logic
decisions.append(decision)
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config
)
Tailor configurations by risk
Configure different tools based on their risk level:
interrupt_on = {
# High risk: full control (approve, edit, reject)
"delete_file": {"allowed_decisions": ["approve", "edit", "reject"]},
"send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
# Medium risk: no editing allowed
"write_file": {"allowed_decisions": ["approve", "reject"]},
# Low risk: no interrupts
"read_file": False,
"ls": False,
}