This section adds manual approval to high-risk tools: reading emails can be executed automatically, sending emails must be paused and wait for human decision-making, and when resuming, the tool can be approved, rejected, or edited to adjust Use.
Agent can adjust Use tools, but not all tools should be executed automatically. The risk of reading emails is low, while sending emails will have external effects, so it is more suitable to add manual approval.
| tool | Whether to interrupt | Reason |
|---|---|---|
read_email | no | Only read the email content in state, no external side effects are generated Use |
send_email | yes | An email will be sent, which is an external action that requires confirmation. |
Rather than having people involved in every step, Human-in-the-loop only pauses before high-risk actions are performed.
Notebook Define two tools first. read_email reads the original text of the email from state; send_email simulates sending an email.
@tool
def read_email(runtime: ToolRuntime) -> str:
"""Read an email from the given address."""
return runtime.state["email"]
@tool
def send_email(body: str) -> str:
"""Send an email to the given address with the given subject and body."""
return "Email sent"The email body is placed in EmailState.email so tools can read it through runtime.state["email"].
The key Configure of HumanInTheLoopMiddleware is interrupt_on. It declares exactly which tool calls need to be paused.
from langchain.agents.middleware import HumanInTheLoopMiddleware
class EmailState(AgentState):
email: str
humanInTheLoopMiddleware = HumanInTheLoopMiddleware(
interrupt_on={
"read_email": False,
"send_email": True,
},
description_prefix="Tool execution requires approval",
)
agent = create_agent(
model=deepseek_model(),
tools=[read_email, send_email],
state_schema=EmailState,
checkpointer=InMemorySaver(),
middleware=[humanInTheLoopMiddleware],
)Use the same one after interruptionthread_idResume execution, so the Agent needs checkpointer to save the state when paused.
User requested "read emails and reply immediately". The Agent can read the email first, but when it prepares the Usesend_emailwill return when__interrupt__, waiting for human decision.
config = {"configurable": {"thread_id": "1"}}
message = [HumanMessage(
content="Please read my email and send a response immediately. Send the reply now in the same thread."
)]
response = agent.invoke(
{
"messages": message,
"email": "Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John.",
},
config=config,
)
print(response)The interrupt occurs before the tool is executed. At this time, you can check the body of the email that the model intends to send, and then decide whether to allow it.
Passed in when resuming executionCommand(resume=...). The course shows three types of Decision: approving the original tool to adjust Use, rejecting and giving feedback to the model, and editing tool parameters before executing.
from langgraph.types import Command
response = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config,
)
response = agent.invoke(
Command(resume={
"decisions": [{
"type": "reject",
"message": "No please sign off - Your merciful leader, Seán.",
}]
}),
config=config,
)
response = agent.invoke(
Command(resume={
"decisions": [{
"type": "edit",
"edited_action": {
"name": "send_email",
"args": {"body": "This is the last straw, you're fired!"},
},
}]
}),
config=config,
)| Decision | Meaning |
|---|---|
| approve | Execute tool adjustment Use according to the original plan of the model |
| reject | The tool is not executed and the Put rejection Reason is returned to the Agent. |
| edit | Execute after modifying tool name or parameters |
interrupt_onAllows you to control whether to pause based on tool granularity.Command(resume=...)Use must be the samethread_id。