Are dynamic tool lists allowed when using create_agent?

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 a ToolNode constructed from the tool list. ToolNode.__init__ immediately builds an internal name -> tool mapping (self._tools_by_name) and also precomputes injection metadata once (self._injected_args[tool_name] = ...) for runtime efficiency. That’s the practical “registration” moment. See langgraph/libs/prebuilt/langgraph/prebuilt/tool_node.py where _InjectedArgs is “built once during ToolNode initialization” and ToolNode.__init__ populates _tools_by_name / _injected_args.

Sources:

  • langgraph/libs/prebuilt/langgraph/prebuilt/tool_node.py (ToolNode init builds self._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_agent builds a graph-based runtime on LangGraph)
  • langchain/libs/langchain_v1/langchain/agents/factory.py (imports StateGraph + ToolNode, and constructs the agent graph)

2) Is it compile-time or runtime?

There are two different “tool” concerns:

  1. 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 call graph.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 ToolNode already copied the tools into its own mapping at initialization.
  2. 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 the tools passed to the agent. See the create_react_agent docstring in langgraph/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py describing .bind_tools() and the “subset” requirement.
    • In the newer LangChain create_agent, tool exposure can also be modified via middleware (see below).

Sources:

  • langgraph/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py (dynamic model section: bind tools; bound tools must be subset of tools parameter)
  • 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:

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=[...]) or middleware.tools), or
    • Implement wrap_tool_call to execute/override the dynamically-added tool.

Sources:

  • langchain/libs/langchain_v1/langchain/agents/factory.py (see DYNAMIC_TOOL_ERROR_TEMPLATE, especially the guidance: middleware modifying request.tools must either pre-register tools or handle them in wrap_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 in DYNAMIC_TOOL_ERROR_TEMPLATE.