hi,
this is a result of my investigation:
1) How tools are “registered”
In LangGraph, tools aren’t registered in some global registry as part of graph compilation; they’re typically embedded into the graph as a node.
- In the standard agent loop created by
create_agent, the “tools” step is aToolNodeconstructed from the tool list.ToolNode.__init__immediately builds an internalname -> toolmapping (self._tools_by_name) and also precomputes injection metadata once (self._injected_args[tool_name] = ...) for runtime efficiency. That’s the practical “registration” moment. Seelanggraph/libs/prebuilt/langgraph/prebuilt/tool_node.pywhere_InjectedArgsis “built once during ToolNode initialization” andToolNode.__init__populates_tools_by_name/_injected_args.
Sources:
langgraph/libs/prebuilt/langgraph/prebuilt/tool_node.py(ToolNode init buildsself._tools_by_name; injected args “built once during ToolNode initialization”).
LangChain’s create_agent is explicitly graph-based and uses LangGraph under the hood.
Sources:
- LangChain Agents docs: Agents - Docs by LangChain (notes
create_agentbuilds a graph-based runtime on LangGraph) langchain/libs/langchain_v1/langchain/agents/factory.py(importsStateGraph+ToolNode, and constructs the agent graph)
2) Is it compile-time or runtime?
There are two different “tool” concerns:
-
What the graph can execute (the executor side)
- This is determined when you build the graph (when you create
ToolNode([...])/create_agent(..., tools=[...])). When you callgraph.compile(), it packages the already-constructed nodes; it doesn’t dynamically discover tools. - Practically: changing the Python list you originally passed later won’t update the compiled graph, because
ToolNodealready copied the tools into its own mapping at initialization.
- This is determined when you build the graph (when you create
-
What the model is allowed to call (the schema/binding side)
- Models that support tool calling need tool schemas bound (usually via
.bind_tools(...)). In the LangGraph prebuilt agent implementation, the model may be bound to tools up-front (static model) and must be a subset of thetoolspassed to the agent. See thecreate_react_agentdocstring inlanggraph/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.pydescribing.bind_tools()and the “subset” requirement. - In the newer LangChain
create_agent, tool exposure can also be modified via middleware (see below).
- Models that support tool calling need tool schemas bound (usually via
Sources:
langgraph/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py(dynamic model section: bind tools; bound tools must be subset oftoolsparameter)- LangChain Agents docs: Agents - Docs by LangChain
3) Can you modify the tool list at runtime?
Yes - but only if you distinguish “tools visible to the model” vs “tools executable by the graph”.
A) Runtime filtering of pre-registered tools (recommended)
If you know all tools ahead of time, register them once (pass them to create_agent(...)) and then filter which ones are exposed to the model per request via middleware that overrides request.tools.
This is explicitly documented as a supported pattern (“Filtering pre-registered tools”).
Source:
- LangChain Agents docs, “Dynamic tools” section: Agents - Docs by LangChain
B) Runtime addition of brand-new tools (possible, but you must also handle execution)
If middleware adds tools that were not included in create_agent(tools=[...]), the agent graph’s ToolNode won’t know how to execute them by default.
- The LangChain implementation even ships an explicit error template explaining this failure mode and the two fixes:
- Register tools at creation time (
create_agent(tools=[...])ormiddleware.tools), or - Implement
wrap_tool_callto execute/override the dynamically-added tool.
- Register tools at creation time (
Sources:
langchain/libs/langchain_v1/langchain/agents/factory.py(seeDYNAMIC_TOOL_ERROR_TEMPLATE, especially the guidance: middleware modifyingrequest.toolsmust either pre-register tools or handle them inwrap_tool_call)- LangChain Agents docs, “Runtime tool registration”: Agents - Docs by LangChain
Minimal sketch (conceptual):
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware
class DynamicToolMiddleware(AgentMiddleware):
def wrap_model_call(self, request, handler):
# expose the new tool to the model
return handler(request.override(tools=[*request.tools, my_dynamic_tool]))
def wrap_tool_call(self, request, handler):
# teach the graph how to execute it
if request.tool_call["name"] == my_dynamic_tool.name:
return handler(request.override(tool=my_dynamic_tool))
return handler(request)
agent = create_agent(model, tools=[some_static_tool], middleware=[DynamicToolMiddleware()])
C) If you’re using raw LangGraph StateGraph (no create_agent)
You can always build your own “tools node” callable that looks up tools from runtime.context / state and executes them dynamically. That’s a custom architecture choice; it’s not what ToolNode does out of the box.
Practical guidance / gotchas
- Compiled graphs are best treated as immutable: if the executable tool set changes, rebuild a new
ToolNode/ new graph. - If you dynamically change tool exposure to the model, keep the executor in sync:
- Filtering pre-registered tools is easy.
- Adding new tools requires
wrap_tool_call(or a custom tool execution node), otherwise you’ll hit the “unknown tools” error described inDYNAMIC_TOOL_ERROR_TEMPLATE.