How to Configure HumanInTheLoopMiddleware for Granular Tool Approval Workflows
The HumanInTheLoopMiddleware intercepts tool calls by matching names against an interrupt_on dictionary, injecting a HumanMessage to pause execution until a human approves, modifies, or rejects the operation.
The langchain-ai/deepagents repository provides a robust middleware architecture for enforcing human oversight in autonomous agent workflows. By configuring the HumanInTheLoopMiddleware with granular interrupt_on mappings, you can specify exactly which tools require explicit approval before execution, creating auditable safety guardrails for sensitive operations like file editing or code execution.
Core Architecture and Configuration Parameters
The middleware operates on a simple interception model. When a tool call matches a key in the interrupt_on mapping, the middleware pauses the agent and emits a HumanMessage containing the tool's description and payload.
Key parameters you will configure:
interrupt_on: A dictionary where keys are tool names (as strings in the agent's tools list) and values are booleans indicating whether to trigger human approval. Example:{"edit_file": True, "execute": True}.default_interrupt_on: A global interrupt map applied to every sub-agent that does not provide its owninterrupt_onconfiguration. This is passed tocreate_subagentsorcreate_graphfor broad policy enforcement.
Where the Middleware is Wired
Understanding the injection points in the source code helps you choose the right configuration strategy for your architecture.
Sub-Agent Creation Layer
In libs/deepagents/deepagents/middleware/subagents.py, the middleware is appended to the default middleware list when default_interrupt_on is supplied:
# line 319
if default_interrupt_on:
general_purpose_middleware.append(
HumanInTheLoopMiddleware(interrupt_on=default_interrupt_on)
)
Individual sub-agents can override this global setting with their own interrupt_on argument:
# line 355
if interrupt_on:
_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
Graph-Level Integration
For top-level graph construction in libs/deepagents/deepagents/graph.py, the same pattern applies to both general-purpose and deep-agent graphs:
# line 207
gp_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
# line 283
deepagent_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
Reference implementations in the test suite demonstrate concrete usage patterns. In libs/acp/tests/test_agent.py, line 220 shows {"write_file_tool": True} and line 267 demonstrates {"edit_file": True}. The Nvidia deep agent example at examples/nvidia_deep_agent/src/agent.py line 98 illustrates enabling HITL specifically for the execute tool.
Implementing Granular Tool Approval
Follow these steps to configure fine-grained control over tool execution.
Step 1: Identify Protected Tools
Examine your tool instantiation to determine the exact names used in the agent's tools list. Common examples include edit_file, execute, and write_file as defined by tools like EditFileTool() or ExecuteCodeTool().
Step 2: Create the interrupt_on Mapping
Define a dictionary mapping tool names to boolean values. Set the value to True for any tool requiring human approval.
interrupt_config = {
"edit_file": True, # Pause before any file modification
"execute": True, # Pause before code execution
"write_file": True, # Pause before creating new files
}
Step 3: Attach Middleware at the Appropriate Layer
You can apply HITL globally or per sub-agent. For global application across all sub-agents that don't specify their own config:
from deepagents.deepagents.middleware.subagents import create_subagents
subagents = create_subagents(
default_model=my_model,
default_tools=[EditFileTool(), ExecuteCodeTool()],
default_interrupt_on=interrupt_config, # Global policy
)
For granular, per-sub-agent control:
custom_subagents = create_subagents(
subagents=[
{
"name": "file-editor",
"description": "Handles safe file edits",
"tools": [EditFileTool()],
"interrupt_on": {"edit_file": True}, # Only this sub-agent gets HITL
},
{
"name": "executor",
"description": "Runs code snippets",
"tools": [ExecuteCodeTool()],
"interrupt_on": {"execute": True},
},
],
default_interrupt_on=None, # No global HITL
)
Step 4: Handle HumanMessage Responses
When a guarded tool is invoked, the middleware injects a HumanMessage into the agent's state. In libs/deepagents/deepagents/middleware/subagents.py lines 24-28, the implementation adds:
subagent_state["messages"] = [HumanMessage(content=description)]
Your UI or downstream consumer must listen for these messages, present the tool description and arguments to the human operator, collect the approval response, and feed it back into the agent's state. Once approved, the middleware converts the response to a ToolMessage and proceeds with execution.
Complete Implementation Example
This example demonstrates a multi-sub-agent system where each tool type has distinct approval requirements:
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from deepagents.deepagents.middleware.subagents import create_subagents
from deepagents.tools import EditFileTool, ExecuteCodeTool, WriteFileTool
# Define the toolset
tools = [EditFileTool(), ExecuteCodeTool(), WriteFileTool()]
# Build sub-agents with specific HITL configurations
subagents = create_subagents(
default_model=my_model,
default_tools=tools,
default_interrupt_on=None, # No global policy
subagents=[
{
"name": "editor",
"description": "Edits existing files",
"tools": [EditFileTool()],
"interrupt_on": {"edit_file": True},
},
{
"name": "runner",
"description": "Executes code snippets",
"tools": [ExecuteCodeTool()],
"interrupt_on": {"execute": True},
},
{
"name": "writer",
"description": "Creates new files",
"tools": [WriteFileTool()],
"interrupt_on": {"write_file": True},
},
],
)
# Top-level agent with HITL on the task delegation tool itself
agent = create_agent(
model=my_model,
system_prompt="You are an autonomous assistant. Use the `task` tool for sub-tasks.",
tools=[subagents["task"]],
middleware=[HumanInTheLoopMiddleware(interrupt_on={"task": True})],
)
How Interrupts Work Under the Hood
When HumanInTheLoopMiddleware intercepts a guarded tool call, it performs the following sequence:
- Interception: The middleware wraps the tool's
invokemethod, checking if the tool name exists ininterrupt_onwith a truthy value. - Injection: It creates a
HumanMessagecontaining the tool description and intended arguments, injecting this into the agent's message history. - Pause: The agent state halts, waiting for external input.
- Resolution: Upon receiving human approval, the middleware replaces the placeholder with a
ToolMessagecontaining the approved arguments, allowing the original tool to execute with the verified parameters.
This mechanism ensures that no guarded tool executes without explicit human confirmation, while maintaining the full context of the conversation for the operator's review.
Summary
interrupt_ondictionaries map tool names to boolean flags, determining which tools require human approval.- Configure global HITL via
default_interrupt_onincreate_subagentsor per-sub-agent overrides via individualinterrupt_onarguments. - The middleware resides in
libs/deepagents/deepagents/middleware/subagents.py(lines 319, 355) andlibs/deepagents/deepagents/graph.py(lines 207, 283). - When triggered, the middleware injects a
HumanMessage(as seen in line 27 ofsubagents.py) and pauses execution until receiving a response. - Concrete examples exist in
libs/acp/tests/test_agent.pyandexamples/nvidia_deep_agent/src/agent.py.
Frequently Asked Questions
What is the difference between interrupt_on and default_interrupt_on?
The default_interrupt_on parameter applies a global HITL configuration to all sub-agents that do not specify their own interrupt_on mapping, as implemented in libs/deepagents/deepagents/middleware/subagents.py at line 319. The interrupt_on parameter, used when defining individual sub-agents (line 355), overrides this global setting for that specific agent only, enabling granular control where some agents require approval while others operate autonomously.
How do I configure HITL for only specific sub-agents while leaving others unrestricted?
Pass default_interrupt_on=None to create_subagents, then explicitly define interrupt_on mappings only for the sub-agents requiring oversight. According to the source code at line 355, the middleware appends only when interrupt_on is truthy, so sub-agents without this key will execute without human interruption.
What message type does the middleware inject when interrupting a tool call?
The middleware injects a HumanMessage object containing the tool's description. In libs/deepagents/deepagents/middleware/subagents.py at lines 24-28, the code sets subagent_state["messages"] = [HumanMessage(content=description)], which the upstream agent or UI receives as a signal to pause and prompt the human operator for approval.
Can I use HumanInTheLoopMiddleware with top-level graph agents rather than just sub-agents?
Yes. The libs/deepagents/deepagents/graph.py file shows the middleware being appended to both gp_middleware (line 207) and deepagent_middleware (line 283) when building graph-level agents. You can pass the interrupt_on configuration directly to the graph builder functions to enforce HITL at the top level.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →