Skip to content

Commit 00f4db5

Browse files
chore(langchain_v1): remove support for ToolNode in create_agent (#33306)
Let's add a note to help w/ migration once we add the tool call retry middleware.
1 parent 62ccf7e commit 00f4db5

File tree

2 files changed

+39
-38
lines changed

2 files changed

+39
-38
lines changed

libs/langchain_v1/langchain/agents/factory.py

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def _handle_structured_output_error(
194194

195195
def create_agent( # noqa: PLR0915
196196
model: str | BaseChatModel,
197-
tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode | None = None,
197+
tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
198198
*,
199199
system_prompt: str | None = None,
200200
middleware: Sequence[AgentMiddleware[AgentState[ResponseT], ContextT]] = (),
@@ -218,8 +218,8 @@ def create_agent( # noqa: PLR0915
218218
Args:
219219
model: The language model for the agent. Can be a string identifier
220220
(e.g., ``"openai:gpt-4"``), a chat model instance (e.g., ``ChatOpenAI()``).
221-
tools: A list of tools or a ToolNode instance. If ``None`` or an empty list,
222-
the agent will consist of a single LLM node without tool calling.
221+
tools: A list of tools, dicts, or callables. If ``None`` or an empty list,
222+
the agent will consist of a model_request node without a tool calling loop.
223223
system_prompt: An optional system prompt for the LLM. If provided as a string,
224224
it will be converted to a SystemMessage and added to the beginning
225225
of the message list.
@@ -321,42 +321,24 @@ def check_weather(location: str) -> str:
321321

322322
# Setup tools
323323
tool_node: ToolNode | None = None
324-
if isinstance(tools, list):
325-
# Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)
326-
built_in_tools = [t for t in tools if isinstance(t, dict)]
327-
regular_tools = [t for t in tools if not isinstance(t, dict)]
328-
329-
# Tools that require client-side execution (must be in ToolNode)
330-
available_tools = middleware_tools + regular_tools
331-
332-
# Only create ToolNode if we have client-side tools
333-
tool_node = ToolNode(tools=available_tools) if available_tools else None
334-
335-
# Default tools for ModelRequest initialization
336-
# Use converted BaseTool instances from ToolNode (not raw callables)
337-
# Include built-ins and converted tools (can be changed dynamically by middleware)
338-
# Structured tools are NOT included - they're added dynamically based on response_format
339-
if tool_node:
340-
default_tools = list(tool_node.tools_by_name.values()) + built_in_tools
341-
else:
342-
default_tools = list(built_in_tools)
343-
elif isinstance(tools, ToolNode):
344-
tool_node = tools
345-
if tool_node:
346-
# Add middleware tools to existing ToolNode
347-
available_tools = list(tool_node.tools_by_name.values()) + middleware_tools
348-
tool_node = ToolNode(available_tools)
349-
350-
# default_tools includes all client-side tools (no built-ins or structured tools)
351-
default_tools = list(tool_node.tools_by_name.values())
352-
else:
353-
default_tools = []
354-
# No tools provided, only middleware_tools available
355-
elif middleware_tools:
356-
tool_node = ToolNode(middleware_tools)
357-
default_tools = list(tool_node.tools_by_name.values())
324+
# Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)
325+
built_in_tools = [t for t in tools if isinstance(t, dict)]
326+
regular_tools = [t for t in tools if not isinstance(t, dict)]
327+
328+
# Tools that require client-side execution (must be in ToolNode)
329+
available_tools = middleware_tools + regular_tools
330+
331+
# Only create ToolNode if we have client-side tools
332+
tool_node = ToolNode(tools=available_tools) if available_tools else None
333+
334+
# Default tools for ModelRequest initialization
335+
# Use converted BaseTool instances from ToolNode (not raw callables)
336+
# Include built-ins and converted tools (can be changed dynamically by middleware)
337+
# Structured tools are NOT included - they're added dynamically based on response_format
338+
if tool_node:
339+
default_tools = list(tool_node.tools_by_name.values()) + built_in_tools
358340
else:
359-
default_tools = []
341+
default_tools = list(built_in_tools)
360342

361343
# validate middleware
362344
assert len({m.name for m in middleware}) == len(middleware), ( # noqa: S101

libs/langchain_v1/tests/unit_tests/agents/test_middleware_tools.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from langchain.agents.middleware.types import AgentMiddleware, AgentState, ModelRequest
66
from langchain.agents.factory import create_agent
7+
from langchain.tools import ToolNode
78
from langchain_core.messages import HumanMessage, ToolMessage
89
from langchain_core.tools import tool
910
from .model import FakeToolCallingModel
@@ -301,3 +302,21 @@ class ToolProvidingMiddleware(AgentMiddleware):
301302
assert len(tool_messages) == 1
302303
assert tool_messages[0].name == "middleware_tool"
303304
assert "middleware" in tool_messages[0].content.lower()
305+
306+
307+
def test_tool_node_not_accepted() -> None:
308+
"""Test that passing a ToolNode instance to create_agent raises an error."""
309+
310+
@tool
311+
def some_tool(input: str) -> str:
312+
"""Some tool."""
313+
return "result"
314+
315+
tool_node = ToolNode([some_tool])
316+
317+
with pytest.raises(TypeError, match="'ToolNode' object is not iterable"):
318+
create_agent(
319+
model=FakeToolCallingModel(),
320+
tools=tool_node, # type: ignore[arg-type]
321+
system_prompt="You are a helpful assistant.",
322+
)

0 commit comments

Comments
 (0)