PatchToolCallsMiddleware: Ensuring Tool Call Consistency in DeepAgents
The PatchToolCallsMiddleware is a built-in agent middleware that guarantees message history consistency by ensuring every tool call emitted by an AIMessage is paired with a corresponding ToolMessage, automatically inserting synthetic placeholders when tool responses are missing.
The langchain-ai/deepagents framework relies on strict message sequencing to manage complex agent interactions across sub-agents and the main orchestrator. When large language models invoke external tools, the resulting conversation history must maintain a balanced sequence of AIMessage tool calls and ToolMessage responses to satisfy LangGraph runtime expectations. The PatchToolCallsMiddleware serves as a critical safeguard in this execution pipeline, preventing state management errors caused by orphaned or dangling tool calls.
What is PatchToolCallsMiddleware?
PatchToolCallsMiddleware is a state-management middleware defined in deepagents/middleware/patch_tool_calls.py. It implements the before_agent hook, which executes immediately before the agent begins its next reasoning turn. Its sole responsibility is to audit the existing messages list for incomplete tool call cycles and repair any inconsistencies by injecting synthetic completion messages.
When an LLM decides to invoke a tool, it appends a ToolCall entry to its AIMessage. Under normal operation, the framework later receives a ToolMessage containing the tool’s result, identified by a matching tool_call_id. However, if the tool response is missing—due to user interruption, tool crashes, or runtime discards—the message stream ends with a dangling tool call that violates the "tool call-result" contract expected by downstream middleware and the LangGraph runtime.
How PatchToolCallsMiddleware Works
The middleware operates through a systematic validation process during the before_agent lifecycle phase.
Scanning for Unmatched Tool Calls
During the before_agent execution, the middleware iterates through the messages list in deepagents/middleware/patch_tool_calls.py. For every AIMessage containing a tool_calls array, it verifies whether a subsequent ToolMessage with a matching tool_call_id exists later in the conversation history.
Synthetic Message Injection
If a matching ToolMessage is absent, the middleware constructs a synthetic placeholder explaining the cancellation:
tool_msg = (
f"Tool call {tool_call['name']} with id {tool_call['id']} was "
"cancelled - another message came in before it could be completed."
)
ToolMessage(content=tool_msg, name=tool_call["name"], tool_call_id=tool_call["id"])
This synthetic message is wrapped in an Overwrite container and returned as a state update, ensuring the agent's message history is replaced with the patched version before any downstream logic processes it.
Integration in the DeepAgents Pipeline
According to the source code in deepagents/graph.py, PatchToolCallsMiddleware is a standard component of both the general-purpose subagent stack and the main DeepAgent orchestrator. It is positioned strategically to catch inconsistencies after filesystem operations but before the next reasoning cycle.
In the general-purpose subagent configuration:
gp_middleware = [
TodoListMiddleware(),
FilesystemMiddleware(backend=backend),
create_summarization_middleware(model, backend),
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
PatchToolCallsMiddleware(),
]
And in the main DeepAgent stack:
deepagent_middleware = [
TodoListMiddleware(),
FilesystemMiddleware(backend=backend),
SubAgentMiddleware(backend=backend, subagents=all_subagents),
create_summarization_middleware(model, backend),
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
PatchToolCallsMiddleware(),
]
Because it is placed after FilesystemMiddleware and before any logic that consumes the message list, it acts as a universal safety net for all tool calls generated by the LLM, regardless of which sub-agent emitted them.
Usage Examples
Automatic Application via create_agent
When building agents through the high-level API, the middleware is typically included in the default stack, but you can explicitly declare it:
from deepagents import create_agent, PatchToolCallsMiddleware
# The middleware ensures tool call consistency automatically
agent = create_agent(
model,
tools=my_tools,
middleware=[PatchToolCallsMiddleware()], # Optional – added by default
)
Manual State Patching
For advanced use cases requiring manual state manipulation, you can invoke the middleware directly to repair stale conversation histories:
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
from langgraph.runtime import Runtime
from langchain.agents.middleware import AgentState
# Message list with a missing tool result (dangling tool call)
state = AgentState(messages=[
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content="Give me the weather."),
AIMessage(content="Sure", tool_calls=[ToolCall(id="42", name="get_weather", args={})]),
])
# Execute the patching logic before the next agent step
patched = PatchToolCallsMiddleware().before_agent(state, Runtime())
# patched["messages"] now contains a synthetic ToolMessage for id "42"
Unit Test Verification
The repository's test suite in libs/deepagents/tests/unit_tests/test_middleware.py demonstrates the expected patching behavior:
def test_missing_tool_call():
input_messages = [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content="Hello"),
AIMessage(
content="I'm doing well",
tool_calls=[ToolCall(id="123", name="get_events_for_days", args={})],
),
HumanMessage(content="What's the weather?"),
]
middleware = PatchToolCallsMiddleware()
state_update = middleware.before_agent({"messages": input_messages}, None)
patched = state_update["messages"].value
assert patched[3].type == "tool" # the inserted placeholder
Additional integration tests in libs/deepagents/tests/unit_tests/middleware/test_subagent_middleware_init.py verify that the middleware functions correctly within sub-agent initialization contexts.
Summary
- PatchToolCallsMiddleware ensures every
AIMessagecontainingtool_callshas a correspondingToolMessageresponse, preventing dangling tool calls from breaking the execution pipeline. - The middleware runs during the
before_agentphase indeepagents/middleware/patch_tool_calls.py, scanning message history and injecting synthetic cancellation messages when responses are missing. - It returns state updates wrapped in an Overwrite container to replace the agent's message list with the patched version.
- The component is integrated into both the general-purpose subagent and main DeepAgent middleware stacks in
deepagents/graph.py, positioned after filesystem operations to catch all tool call inconsistencies.
Frequently Asked Questions
What happens if a tool call is missing its response?
If an AIMessage contains a tool_calls entry but no subsequent ToolMessage with a matching tool_call_id exists, PatchToolCallsMiddleware automatically inserts a synthetic ToolMessage stating that the tool call was cancelled due to an intervening message. This prevents the LangGraph runtime from encountering an unbalanced message sequence that could cause state-management errors.
Where is PatchToolCallsMiddleware installed in the middleware stack?
According to deepagents/graph.py, the middleware is appended at the end of both the general-purpose subagent middleware list and the main DeepAgent middleware list. This placement ensures it executes after FilesystemMiddleware and SubAgentMiddleware but before the next agent reasoning turn, catching any inconsistencies introduced by sub-agent operations or filesystem interactions.
Can I use PatchToolCallsMiddleware with custom agent implementations?
Yes. You can import PatchToolCallsMiddleware from deepagents.middleware.patch_tool_calls and instantiate it directly in custom middleware stacks. The middleware expects standard LangGraph AgentState objects containing a messages key and can be invoked manually via the before_agent method, as demonstrated in the manual state patching example above.
How does the middleware handle multiple missing tool calls?
The implementation scans the entire message history and processes every AIMessage with tool_calls independently. If multiple tool calls lack corresponding ToolMessage responses, PatchToolCallsMiddleware generates a synthetic placeholder for each missing response, ensuring the final message list maintains complete pairing for every tool invocation ID before the agent continues execution.
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 →