Custom middleware: logging every tool call
Custom middleware is your own code that a deep agent runs around its model calls or tool calls; @wrap_tool_call turns a function into middleware that sees every tool call before and after it runs.
Last updated: 29 Sep, 2026 · Deep Agents 0.7
In the video's first deep agent the difference from a plain agent is the middleware hooks around the loop: tool-call patching, summarization, the to-do list (opt-in since 0.7). Deep Agents lets you add your own to the same stack with middleware=[...]. The docs' first example logs every tool call, which is also the quickest way to see what a deep agent does, including calls made by its built-in tools.
The wrap_tool_call syntax
from langchain.agents.middleware import wrap_tool_call
@wrap_tool_call
def my_middleware(request, handler):
# before: request.tool_call has the name and args
result = handler(request) # run the tool
# after: result is the ToolMessage
return resultA logger for tool calls
Start trip.py with search_travel, the catalog tool from Tools: a travel search the agent can call. Everything below goes in the same file, under it.
from langchain.tools import tool
CATALOG = {
"paris": {
"flight": ["Return flight Delhi to Paris: 42,000 rupees"],
"hotel": ["Seine Budget Inn, Latin Quarter: 5,200 rupees a night",
"Hotel Lumiere, Montmartre: 7,500 rupees a night",
"Le Grand Opera Hotel: 16,000 rupees a night"],
"sight": ["Eiffel Tower summit: 3,100 rupees", "Louvre Museum: 2,000 rupees",
"Seine river cruise: 1,500 rupees", "Versailles day trip: 2,600 rupees",
"Montmartre walking tour: free"],
"food": ["Cafe breakfast and bistro dinner: 3,000 rupees a day"],
},
}
@tool
def search_travel(city: str, kind: str) -> str:
"""Search the travel catalog. kind is "flight", "hotel", "sight" or "food". Prices are in rupees."""
entries = CATALOG.get(city.lower(), {}).get(kind)
return "\n".join(entries) if entries else f"The catalog has no {kind} entries for {city}."from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0, max_retries=6)from langchain.agents.middleware import wrap_tool_call
@wrap_tool_call
def log_tool_calls(request, handler):
"""Print every tool call and the size of its result."""
print(f"-> {request.tool_call['name']} {request.tool_call['args']}")
result = handler(request) # run the tool
print(f"<- {len(str(result.content))} characters back")
return resultThe agent with the logger
agent = create_deep_agent(
model=model,
tools=[search_travel],
middleware=[log_tool_calls],
system_prompt="You are a travel planner. Save findings to /trip/notes.md. Reply in one short sentence.",
)Watching a save to /trip/notes.md
result = agent.invoke({"messages": [{"role": "user", "content": "Save the Paris hotel and flight prices to /trip/notes.md."}]})
print(result["messages"][-1].text)-> search_travel {'city': 'Paris', 'kind': 'hotel'}
<- 145 characters back
-> search_travel {'city': 'Paris', 'kind': 'flight'}
<- 43 characters back
-> write_file {'content': 'Paris Travel Prices:\n\nHotels:\n- Seine Budget Inn, Latin Quarter: 5,200 rupees per night\n- Hotel Lumiere, Montmartre: 7,500 rupees per night\n- Le Grand Opera Hotel: 16,000 rupees per night\n\nFlight (Delhi → Paris): 42,000 rupees', 'file_path': '/trip/notes.md'}
<- 27 characters back
Saved.What the log shows
- Every call is logged, your tool and the built-in ones alike: two catalog searches, then
write_file. - The arrows show order: "->" before the tool runs with its arguments, "<-" after, with the size of what came back.
- The agent's reply printed last, after all the tools had run.
Middleware hooks compared
| Hook | Runs | Typical use |
|---|---|---|
@wrap_tool_call | Around each tool call | Logging, retries, blocking a call |
@wrap_model_call | Around each model call | Changing the prompt or the model |
before_agent / after_agent | Once per invoke | Setup and cleanup |
before_model / after_model | Before or after each model call | Checking or trimming messages |
Middleware you pass goes after the core built-ins (files, subagents, summarization, tool-call patching) and before memory and human-in-the-loop. An instance whose name matches a built-in one, such as FilesystemMiddleware in the offloading lesson, replaces it instead.
Where custom middleware fits
- Logging and cost tracking for every tool and model call.
- Blocking or rewriting a call that breaks a rule of your own.
- Adding a tool or a prompt section for every run.
Related
- Previous: Fault tolerance: call limits and tool retries
- Next: MCP tools with MCPAdapter
- Reference: Customize Deep Agents: custom middleware
- Print
request.tool_call["id"]as well and match it to the tool messages. - Return
ToolMessage(content="Writing is off today.", tool_call_id=request.tool_call["id"])instead of callinghandlerwhen the tool iswrite_file; import it withfrom langchain.messages import ToolMessage. - Add
@wrap_model_callmiddleware that prints how many messages each model call receives.
This is what real progress feels like.