Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/oss/langchain/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,110 @@ There are two approaches depending on whether tools are known ahead of time:
</Tab>
</Tabs>

#### How dynamic tools work

Understanding how tools are registered and executed helps avoid common pitfalls when working with dynamic tools.

##### Tradeoffs

Dynamic tool selection improves accuracy and reduces prompt size, but introduces tradeoffs you should be aware of:

- **Model may call removed tools.** If a tool was available in earlier turns but is removed in a later turn, the model may still attempt to call it based on conversation history. Handle this with [tool error handling](#tool-error-handling) middleware, or include a brief system message explaining which tools are currently available.
- **Changing tools invalidates the prompt cache.** Most providers (OpenAI, Anthropic, Google) cache the prompt prefix — including tool definitions — to reduce latency and cost on subsequent calls. When the tool list changes between calls, the cached prefix no longer matches and the provider must reprocess the full prompt. This is an unavoidable cost of dynamic tool selection. To minimize cache invalidation:
- Keep a stable set of "core" tools that are always present and place them first in the list.
- Append or remove tools at the end of the list rather than reordering, since prefix-based caches match from the beginning.
- Prefer filtering over frequent additions/removals when possible.
- **Fewer tools generally means better accuracy.** Models perform better with a focused set of relevant tools. If you have many tools, dynamic selection can significantly improve tool-calling accuracy by reducing the number of options the model must choose from.

##### Tool registration vs. tool exposure

`create_agent` uses [LangGraph](/oss/langgraph/overview) under the hood. When you call `create_agent(tools=[...])`, two things happen:

1. **Tool schemas are bound to the model** — the model learns what tools are available and how to call them.
2. **Tools are registered for execution** — a `ToolNode` is created with an internal mapping of tool names to tool implementations. This mapping is built once at initialization.

Middleware can modify **which tools the model sees** (step 1) on each request, but the `ToolNode` executor (step 2) only knows about tools that were registered at creation time. This is why runtime-registered tools require `wrap_tool_call` — the executor needs to be told how to run tools it doesn't know about.

##### Why `wrap_tool_call` is required for runtime-registered tools

If middleware adds a tool via `wrap_model_call` that wasn't passed to `create_agent(tools=[...])`, the model may call it, but the `ToolNode` won't know how to execute it. This produces an error like:

```text
ValueError: Middleware returned unknown tool names: ['my_dynamic_tool']

Available client-side tools: ['search', 'get_weather']

To fix this issue:
1. Ensure the tools are passed to create_agent() via the 'tools' parameter
2. If using custom middleware with tools, ensure they're registered via middleware.tools attribute
3. Verify that tool names in ModelRequest.tools match the actual tool.name values
```

To fix this, implement `wrap_tool_call` in your middleware to handle execution of the dynamically added tools, as shown in the [Runtime tool registration](#runtime-tool-registration) example above.

##### Create an agent with no static tools

You can create an agent with an empty tool list and add all tools dynamically via middleware. Both `wrap_model_call` and `wrap_tool_call` are required:

:::python

```python
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ToolCallRequest

class AllDynamicToolsMiddleware(AgentMiddleware):
def wrap_model_call(self, request: ModelRequest, handler):
dynamic_tools = fetch_tools_for_user(request.runtime)
return handler(request.override(tools=dynamic_tools))

def wrap_tool_call(self, request: ToolCallRequest, handler):
dynamic_tools = fetch_tools_for_user(request.runtime)
tool_map = {t.name: t for t in dynamic_tools}
tool = tool_map.get(request.tool_call["name"])
if tool:
return handler(request.override(tool=tool))
return handler(request)

agent = create_agent(
model="gpt-4.1",
tools=[],
middleware=[AllDynamicToolsMiddleware()],
)
```

:::

:::js

```typescript
import { createAgent, createMiddleware } from "langchain";

const allDynamicToolsMiddleware = createMiddleware({
name: "AllDynamicTools",
wrapModelCall: (request, handler) => {
const dynamicTools = fetchToolsForUser(request.runtime);
return handler({ ...request, tools: dynamicTools });
},
wrapToolCall: (request, handler) => {
const dynamicTools = fetchToolsForUser(request.runtime);
const toolMap = Object.fromEntries(dynamicTools.map((t: any) => [t.name, t]));
const tool = toolMap[request.toolCall.name];
if (tool) {
return handler({ ...request, tool });
}
return handler(request);
},
});

const agent = createAgent({
model: "gpt-4.1",
tools: [],
middleware: [allDynamicToolsMiddleware],
});
```

:::

<Tip>
To learn more about tools, see [Tools](/oss/langchain/tools).
</Tip>
Expand Down
10 changes: 7 additions & 3 deletions src/oss/langchain/middleware/custom.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1186,9 +1186,13 @@ Use @[`SystemMessage.concat`] to preserve cache control metadata or structured c
Select relevant tools at runtime to improve performance and accuracy. This section covers filtering pre-registered tools. For registering tools that are discovered at runtime (e.g., from MCP servers), see [Runtime tool registration](/oss/langchain/agents#dynamic-tools).

**Benefits:**
- **Shorter prompts** - Reduce complexity by exposing only relevant tools
- **Better accuracy** - Models choose correctly from fewer options
- **Permission control** - Dynamically filter tools based on user access
- **Shorter prompts**: Reduce complexity by exposing only relevant tools
- **Better accuracy**: Models choose correctly from fewer options
- **Permission control**: Dynamically filter tools based on user access

<Warning>
Changing the tool list between calls invalidates the provider's prompt cache (tool definitions are part of the cached prefix), which increases latency and cost. The model may also attempt to call tools that were available in earlier turns but have since been removed. See [Tradeoffs](/oss/langchain/agents#tradeoffs) for mitigation strategies.
</Warning>

:::python

Expand Down
Loading