Problem
When an agent has access to many tools (30+), sending all tool definitions on every model call creates significant context bloat. This can degrade model accuracy, increase latency, and waste tokens. The model performs better when it chooses from a smaller, relevant set of tools.
Solution
Use a wrap_model_call middleware to control which tools the model sees on each turn. Pass all tools in the tools= list when creating the agent (so the executor can run any of them), then use middleware to filter which ones the model actually sees at inference time.
The core API is request.override(tools=filtered_tools), which returns a new request with only the tools you specify.
Example
from langchain.tools import tool
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from deepagents import create_deep_agent
from typing import Callable
# All tools registered upfront
ALL_TOOLS = [tool_a, tool_b, tool_c, ...] # 45 tools
# A small set always visible to the model
CORE_TOOLS = {"search_tools", "read_file", "write_file"}
@tool
def search_tools(query: str) -> str:
"""Search for available tools by keyword. Use this when you need
a capability that isn't currently available."""
matches = [
f"- {t.name}: {t.description}"
for t in ALL_TOOLS
if query.lower() in t.name.lower()
or query.lower() in t.description.lower()
]
return "\n".join(matches) if matches else "No matching tools found."
@wrap_model_call
def progressive_disclosure(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
active = set(CORE_TOOLS)
# Scan message history for prior search_tools calls
for msg in request.state["messages"]:
if hasattr(msg, "tool_calls"):
for tc in msg.tool_calls:
if tc["name"] == "search_tools":
q = tc["args"].get("query", "").lower()
for t in ALL_TOOLS:
if q in t.name.lower() or q in t.description.lower():
active.add(t.name)
filtered = [t for t in request.tools if t.name in active]
return handler(request.override(tools=filtered))
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
tools=[search_tools, *ALL_TOOLS],
middleware=[progressive_disclosure],
)How it works
The model always sees a small core set of tools plus
search_toolsWhen the model needs a capability it doesn't have, it calls
search_toolswith a keywordThe middleware scans message history for prior
search_toolscalls and adds matching tools to subsequent model callsTools are "discovered" progressively, keeping each prompt compact
Notes
This pattern uses the same middleware system (
wrap_model_call+request.override) documented in the LangChain custom middleware guidecreate_deep_agentaccepts custom middleware via themiddleware=parameter; your middleware runs after the built-in stackFor simpler cases where tools can be categorized statically (e.g. by user role or conversation phase), you can skip
search_toolsentirely and filter based on state or runtime context. See Filtering pre-registered toolsIf you want to track discovered tools in graph state (instead of scanning message history), define a custom
state_schemawith adiscovered_toolsfield and update it frombefore_model. See Custom state schema