Deep AgentsDeep Agents 0.7 · Python 3.11+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
28 small wins to finish your pathNext lesson →

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

python
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 result

A 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.

python
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}."
python
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)
python
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 result

The agent with the logger

python
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

ExampleAPI keytrip.py, continued
result = agent.invoke({"messages": [{"role": "user", "content": "Save the Paris hotel and flight prices to /trip/notes.md."}]})
print(result["messages"][-1].text)

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

HookRunsTypical use
@wrap_tool_callAround each tool callLogging, retries, blocking a call
@wrap_model_callAround each model callChanging the prompt or the model
before_agent / after_agentOnce per invokeSetup and cleanup
before_model / after_modelBefore or after each model callChecking 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.
Watch out. Do not keep counters on the middleware object itself. Subagents, parallel tools and parallel runs share it; the docs recommend keeping such values in the agent's state.
Try it yourself
  • 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 calling handler when the tool is write_file; import it with from langchain.messages import ToolMessage.
  • Add @wrap_model_call middleware that prints how many messages each model call receives.

This is what real progress feels like.