Agentic Middleware
Middleware is how you compose cross-cutting agent behavior in LangChain v1+. It plugs into create_agent(...) (and is the underlying implementation of DeepAgents). For any production agent, the question is "which middlewares" — not "do I need middleware".
The model
from langchain.agents import create_agent
from langchain.agents.middleware import (
SummarizationMiddleware,
ModelRetryMiddleware,
ModelFallbackMiddleware,
ModelCallLimitMiddleware,
ToolRetryMiddleware,
PIIMiddleware,
)
agent = create_agent(
model="claude-sonnet-4-6",
tools=[...],
middleware=[
ModelRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0),
ModelFallbackMiddleware("openai:gpt-4o-mini"),
ModelCallLimitMiddleware(run_limit=50),
ToolRetryMiddleware(max_retries=3, backoff_factor=2.0),
SummarizationMiddleware(model="claude-haiku-4-5", trigger=("tokens", 4000), keep=("messages", 20)),
PIIMiddleware("email", strategy="redact", apply_to_input=True),
],
)
create_agent returns a compiled LangGraph. All Runnable semantics apply: .invoke, .ainvoke, .stream, .astream, langgraph dev, langgraph build, etc.
Lifecycle hooks (for custom middleware)
Every middleware can override one or more of these:
| Hook |
Fires |
Return |
before_agent(state, runtime) |
Once, before the loop starts |
dict to merge into state, or None |
before_model(state, runtime) |
Before each model call |
dict to merge into state, or None |
wrap_model_call(request, handler) |
Wraps the model call |
call handler(request) → ModelResponse; return it (possibly modified) |
after_model(state, runtime) |
After each model response |
dict to merge into state, or None |
wrap_tool_call(request, handler) |
Wraps each tool call |
call handler(request) → ToolMessage | Command; return it (possibly modified) |
after_agent(state, runtime) |
Once, after the loop ends |
dict to merge into state, or None |
Node-style hooks (before_* / after_*) run sequentially. Wrap-style hooks compose like Python decorators — first middleware in the list is the outermost wrapper.
Built-in middlewares (provider-agnostic)
Import from langchain.agents.middleware:
| Middleware |
Purpose |
Constructor |
SummarizationMiddleware |
Auto-summarize long conversations to stay under token limits |
(model, trigger=("tokens", N), keep=("messages", N)) |
HumanInTheLoopMiddleware |
Pause for human approve/edit/reject on sensitive tool calls |
(interrupt_on={"tool_name": {"allowed_decisions": [...]}}) — requires a checkpointer |
ModelCallLimitMiddleware |
Cap model calls per run / per thread (cost containment, infinite-loop guard) |
(thread_limit, run_limit, exit_behavior="end") |
ToolCallLimitMiddleware |
Cap tool calls globally or per-tool |
(thread_limit, run_limit) or (tool_name, thread_limit, run_limit) |
ModelRetryMiddleware |
Retry transient model failures with exponential backoff |
(max_retries, backoff_factor, initial_delay) |
ToolRetryMiddleware |
Retry transient tool failures with exponential backoff |
same args |
ModelFallbackMiddleware |
Fall back to alternative models on primary failure |
("model-1", "model-2", ...) |
LLMToolSelectorMiddleware |
Use a small LLM to pick which tools to expose to the main model |
(model, max_tools, always_include=[...]) |
PIIMiddleware |
Detect & redact / mask / block PII |
("email"|"credit_card"|..., strategy="redact"|"mask"|"block", apply_to_input=True) |
ContextEditingMiddleware |
Drop old tool outputs from context to free tokens |
(edits=[ClearToolUsesEdit(trigger, keep)]) |
TodoListMiddleware |
Adds the write_todos planning tool to the agent |
() |
LLMToolEmulator |
Replace tool execution with LLM-generated outputs (testing) |
() — never use in production |
ShellToolMiddleware |
Persistent shell session as a tool, with execution policy |
(workspace_root, execution_policy) |
FilesystemFileSearchMiddleware |
Glob + Grep tools over a filesystem |
(root_path, use_ripgrep=True) |
DeepAgents-specific (import from deepagents.middleware):
| Middleware |
Purpose |
FilesystemMiddleware |
Virtual or backed filesystem for the agent (read/write/edit/ls/glob/grep) |
SubAgentMiddleware |
Adds the task tool with named sub-agents |
create_deep_agent(...) is a thin wrapper over create_agent(...) that pre-installs FilesystemMiddleware + SubAgentMiddleware + TodoListMiddleware. You can compose additional middlewares on top.
Production middleware stack (start here)
For any production agent, this is the default stack to copy and tune:
middleware=[
# Cost containment (set BEFORE retries — limits the multiplier)
ModelCallLimitMiddleware(run_limit=50),
ToolCallLimitMiddleware(run_limit=200),
# Resilience to transient failures
ModelRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0),
ToolRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0),
# Provider-level resilience
ModelFallbackMiddleware("openai:gpt-4o-mini"),
# Long-conversation hygiene
SummarizationMiddleware(model="claude-haiku-4-5", trigger=("tokens", 8000), keep=("messages", 20)),
# Privacy (only if user input may contain PII)
PIIMiddleware("email", strategy="redact", apply_to_input=True),
PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),
]
Add HumanInTheLoopMiddleware for any tool that touches money, sends external messages, or makes irreversible changes. Requires a checkpointer (InMemorySaver for dev, PostgresSaver for production — see the deploy skill).
Custom middleware
Inherit from AgentMiddleware:
from typing import Any, Callable
from langchain.agents.middleware import (
AgentMiddleware, AgentState, ModelRequest, ModelResponse,
)
from langchain.tools.tool_node import ToolCallRequest
from langchain.messages import ToolMessage
from langgraph.types import Command
class TokenBudgetMiddleware(AgentMiddleware):
"""Hard-cap total tokens across the run. Halts the agent when exceeded."""
def __init__(self, budget: int) -> None:
self.budget = budget
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
used = request.state.get("tokens_used", 0)
if used >= self.budget:
# short-circuit: return a synthetic "stop" response without calling the model
return ModelResponse(
messages=[{"role": "assistant", "content": "Token budget exceeded."}],
command=Command(goto="__end__"),
)
response = handler(request)
# ... extract token count from response.usage and add to state
return response
If you need extra fields in state, declare them on a subclass of AgentState and set state_schema = MyState on the middleware class.
Hard rules
- Order matters. Limits before retries (so retries don't burn through your budget). Privacy redaction before logging. Summarization should run before the model call, not after.
- HumanInTheLoopMiddleware needs a checkpointer. Without one, interrupts have nothing to resume from.
LLMToolEmulator is a testing-only middleware. Never ship it.
- Retries cost money. A
max_retries=3 with backoff_factor=2 means up to 4 calls per failure. Set ModelCallLimitMiddleware BEFORE retries to cap the worst-case cost.
- Don't roll your own retry/fallback/limit. The built-ins handle the edge cases (jitter, retryable error classification, streaming-aware wrapping). Custom middleware is for app-specific concerns.
Skills to load alongside this one
langchain-agents-deploy — productionisation: durable execution, checkpointers, deployment.
langchain-agents-observability — tracing what middleware actually does at runtime.
langchain-agents-langgraph-code — when to drop down to raw StateGraph (rare, but real cases exist).
Source: cwijayasundara/agent_cli_langchain — distributed by TomeVault.
1---2name: langchain-agents-middleware3description: Use when building or productionising any agent — adding retries, fallbacks, summarization, human-in-the-loop, PII redaction, call limits, or custom hooks. Middleware is THE composition primitive for modern LangChain agents (v1+); covers built-ins plus the custom middleware authoring API.4---56# Agentic Middleware78Middleware is how you compose cross-cutting agent behavior in LangChain v1+. It plugs into `create_agent(...)` (and is the underlying implementation of DeepAgents). **For any production agent, the question is "which middlewares" — not "do I need middleware".**910## The model1112```python13from langchain.agents import create_agent14from langchain.agents.middleware import (15 SummarizationMiddleware,16 ModelRetryMiddleware,17 ModelFallbackMiddleware,18 ModelCallLimitMiddleware,19 ToolRetryMiddleware,20 PIIMiddleware,21)2223agent = create_agent(24 model="claude-sonnet-4-6",25 tools=[...],26 middleware=[27 ModelRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0),28 ModelFallbackMiddleware("openai:gpt-4o-mini"),29 ModelCallLimitMiddleware(run_limit=50),30 ToolRetryMiddleware(max_retries=3, backoff_factor=2.0),31 SummarizationMiddleware(model="claude-haiku-4-5", trigger=("tokens", 4000), keep=("messages", 20)),32 PIIMiddleware("email", strategy="redact", apply_to_input=True),33 ],34)35```3637`create_agent` returns a compiled LangGraph. All `Runnable` semantics apply: `.invoke`, `.ainvoke`, `.stream`, `.astream`, `langgraph dev`, `langgraph build`, etc.3839## Lifecycle hooks (for custom middleware)4041Every middleware can override one or more of these:4243| Hook | Fires | Return |44|---|---|---|45| `before_agent(state, runtime)` | Once, before the loop starts | dict to merge into state, or None |46| `before_model(state, runtime)` | Before each model call | dict to merge into state, or None |47| `wrap_model_call(request, handler)` | Wraps the model call | call `handler(request)` → `ModelResponse`; return it (possibly modified) |48| `after_model(state, runtime)` | After each model response | dict to merge into state, or None |49| `wrap_tool_call(request, handler)` | Wraps each tool call | call `handler(request)` → `ToolMessage \| Command`; return it (possibly modified) |50| `after_agent(state, runtime)` | Once, after the loop ends | dict to merge into state, or None |5152Node-style hooks (`before_*` / `after_*`) run sequentially. Wrap-style hooks compose like Python decorators — **first middleware in the list is the outermost wrapper**.5354## Built-in middlewares (provider-agnostic)5556Import from `langchain.agents.middleware`:5758| Middleware | Purpose | Constructor |59|---|---|---|60| `SummarizationMiddleware` | Auto-summarize long conversations to stay under token limits | `(model, trigger=("tokens", N), keep=("messages", N))` |61| `HumanInTheLoopMiddleware` | Pause for human approve/edit/reject on sensitive tool calls | `(interrupt_on={"tool_name": {"allowed_decisions": [...]}})` — **requires a checkpointer** |62| `ModelCallLimitMiddleware` | Cap model calls per run / per thread (cost containment, infinite-loop guard) | `(thread_limit, run_limit, exit_behavior="end")` |63| `ToolCallLimitMiddleware` | Cap tool calls globally or per-tool | `(thread_limit, run_limit)` or `(tool_name, thread_limit, run_limit)` |64| `ModelRetryMiddleware` | Retry transient model failures with exponential backoff | `(max_retries, backoff_factor, initial_delay)` |65| `ToolRetryMiddleware` | Retry transient tool failures with exponential backoff | same args |66| `ModelFallbackMiddleware` | Fall back to alternative models on primary failure | `("model-1", "model-2", ...)` |67| `LLMToolSelectorMiddleware` | Use a small LLM to pick which tools to expose to the main model | `(model, max_tools, always_include=[...])` |68| `PIIMiddleware` | Detect & redact / mask / block PII | `("email"\|"credit_card"\|..., strategy="redact"\|"mask"\|"block", apply_to_input=True)` |69| `ContextEditingMiddleware` | Drop old tool outputs from context to free tokens | `(edits=[ClearToolUsesEdit(trigger, keep)])` |70| `TodoListMiddleware` | Adds the `write_todos` planning tool to the agent | `()` |71| `LLMToolEmulator` | Replace tool execution with LLM-generated outputs (testing) | `()` — never use in production |72| `ShellToolMiddleware` | Persistent shell session as a tool, with execution policy | `(workspace_root, execution_policy)` |73| `FilesystemFileSearchMiddleware` | Glob + Grep tools over a filesystem | `(root_path, use_ripgrep=True)` |7475DeepAgents-specific (import from `deepagents.middleware`):7677| Middleware | Purpose |78|---|---|79| `FilesystemMiddleware` | Virtual or backed filesystem for the agent (read/write/edit/ls/glob/grep) |80| `SubAgentMiddleware` | Adds the `task` tool with named sub-agents |8182`create_deep_agent(...)` is a thin wrapper over `create_agent(...)` that pre-installs `FilesystemMiddleware` + `SubAgentMiddleware` + `TodoListMiddleware`. You can compose additional middlewares on top.8384## Production middleware stack (start here)8586For any production agent, this is the default stack to copy and tune:8788```python89middleware=[90 # Cost containment (set BEFORE retries — limits the multiplier)91 ModelCallLimitMiddleware(run_limit=50),92 ToolCallLimitMiddleware(run_limit=200),9394 # Resilience to transient failures95 ModelRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0),96 ToolRetryMiddleware(max_retries=3, backoff_factor=2.0, initial_delay=1.0),9798 # Provider-level resilience99 ModelFallbackMiddleware("openai:gpt-4o-mini"),100101 # Long-conversation hygiene102 SummarizationMiddleware(model="claude-haiku-4-5", trigger=("tokens", 8000), keep=("messages", 20)),103104 # Privacy (only if user input may contain PII)105 PIIMiddleware("email", strategy="redact", apply_to_input=True),106 PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),107]108```109110Add `HumanInTheLoopMiddleware` for any tool that touches money, sends external messages, or makes irreversible changes. **Requires a checkpointer** (`InMemorySaver` for dev, `PostgresSaver` for production — see the deploy skill).111112## Custom middleware113114Inherit from `AgentMiddleware`:115116```python117from typing import Any, Callable118from langchain.agents.middleware import (119 AgentMiddleware, AgentState, ModelRequest, ModelResponse,120)121from langchain.tools.tool_node import ToolCallRequest122from langchain.messages import ToolMessage123from langgraph.types import Command124125126class TokenBudgetMiddleware(AgentMiddleware):127 """Hard-cap total tokens across the run. Halts the agent when exceeded."""128129 def __init__(self, budget: int) -> None:130 self.budget = budget131132 def wrap_model_call(133 self,134 request: ModelRequest,135 handler: Callable[[ModelRequest], ModelResponse],136 ) -> ModelResponse:137 used = request.state.get("tokens_used", 0)138 if used >= self.budget:139 # short-circuit: return a synthetic "stop" response without calling the model140 return ModelResponse(141 messages=[{"role": "assistant", "content": "Token budget exceeded."}],142 command=Command(goto="__end__"),143 )144 response = handler(request)145 # ... extract token count from response.usage and add to state146 return response147```148149If you need extra fields in state, declare them on a subclass of `AgentState` and set `state_schema = MyState` on the middleware class.150151## Hard rules152153- **Order matters.** Limits before retries (so retries don't burn through your budget). Privacy redaction before logging. Summarization should run *before* the model call, not after.154- **HumanInTheLoopMiddleware needs a checkpointer.** Without one, interrupts have nothing to resume from.155- **`LLMToolEmulator` is a testing-only middleware.** Never ship it.156- **Retries cost money.** A `max_retries=3` with `backoff_factor=2` means up to 4 calls per failure. Set `ModelCallLimitMiddleware` BEFORE retries to cap the worst-case cost.157- **Don't roll your own retry/fallback/limit.** The built-ins handle the edge cases (jitter, retryable error classification, streaming-aware wrapping). Custom middleware is for app-specific concerns.158159## Skills to load alongside this one160161- `langchain-agents-deploy` — productionisation: durable execution, checkpointers, deployment.162- `langchain-agents-observability` — tracing what middleware actually does at runtime.163- `langchain-agents-langgraph-code` — when to drop down to raw `StateGraph` (rare, but real cases exist).164165---166> Source: [cwijayasundara/agent_cli_langchain](https://github.com/cwijayasundara/agent_cli_langchain) — distributed by [TomeVault](https://tomevault.io).167<!-- tomevault:4.0:skill_md:2026-05-23 -->