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 own interrupt_on configuration. This is passed to create_subagents or create_graph for 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:

  1. Interception: The middleware wraps the tool's invoke method, checking if the tool name exists in interrupt_on with a truthy value.
  2. Injection: It creates a HumanMessage containing the tool description and intended arguments, injecting this into the agent's message history.
  3. Pause: The agent state halts, waiting for external input.
  4. Resolution: Upon receiving human approval, the middleware replaces the placeholder with a ToolMessage containing 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

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →