From df2b4cfdc471d1d809d375af0ed02d3d7d66a4aa Mon Sep 17 00:00:00 2001 From: Nick Hollon Date: Wed, 27 May 2026 18:53:30 -0400 Subject: [PATCH] feat(core): wire subagent_name through @tool decorator @tool now accepts subagent_name= and passes it to the underlying tool object. Tested with both static-string and callable values. --- libs/core/langchain_core/tools/convert.py | 18 +++++++++++-- libs/core/tests/unit_tests/test_tools.py | 33 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/libs/core/langchain_core/tools/convert.py b/libs/core/langchain_core/tools/convert.py index 48c518a2982..ed0a844ac6f 100644 --- a/libs/core/langchain_core/tools/convert.py +++ b/libs/core/langchain_core/tools/convert.py @@ -2,7 +2,7 @@ import inspect from collections.abc import Callable -from typing import Any, Literal, cast, get_type_hints, overload +from typing import TYPE_CHECKING, Any, Literal, cast, get_type_hints, overload from pydantic import BaseModel, Field, create_model @@ -12,6 +12,9 @@ from langchain_core.tools.base import ArgsSchema, BaseTool from langchain_core.tools.simple import Tool from langchain_core.tools.structured import StructuredTool +if TYPE_CHECKING: + from langchain_core.messages.tool import ToolCall + @overload def tool( @@ -24,6 +27,7 @@ def tool( parse_docstring: bool = False, error_on_invalid_docstring: bool = True, extras: dict[str, Any] | None = None, + subagent_name: str | Callable[["ToolCall"], str] | None = None, ) -> Callable[[Callable | Runnable], BaseTool]: ... @@ -40,6 +44,7 @@ def tool( parse_docstring: bool = False, error_on_invalid_docstring: bool = True, extras: dict[str, Any] | None = None, + subagent_name: str | Callable[["ToolCall"], str] | None = None, ) -> BaseTool: ... @@ -55,6 +60,7 @@ def tool( parse_docstring: bool = False, error_on_invalid_docstring: bool = True, extras: dict[str, Any] | None = None, + subagent_name: str | Callable[["ToolCall"], str] | None = None, ) -> BaseTool: ... @@ -70,6 +76,7 @@ def tool( parse_docstring: bool = False, error_on_invalid_docstring: bool = True, extras: dict[str, Any] | None = None, + subagent_name: str | Callable[["ToolCall"], str] | None = None, ) -> Callable[[Callable | Runnable], BaseTool]: ... @@ -85,6 +92,7 @@ def tool( parse_docstring: bool = False, error_on_invalid_docstring: bool = True, extras: dict[str, Any] | None = None, + subagent_name: str | Callable[["ToolCall"], str] | None = None, ) -> BaseTool | Callable[[Callable | Runnable], BaseTool]: """Convert Python functions and `Runnables` to LangChain tools. @@ -150,6 +158,9 @@ def tool( For example, Anthropic-specific fields like `cache_control`, `defer_loading`, or `input_examples`. + subagent_name: Optional name of the subagent that should handle this tool call + (a static string), or a callable that resolves the subagent name from the + parsed `ToolCall` at dispatch time. Raises: ValueError: If too many positional arguments are provided (e.g. violating the @@ -313,6 +324,7 @@ def tool( parse_docstring=parse_docstring, error_on_invalid_docstring=error_on_invalid_docstring, extras=extras, + subagent_name=subagent_name, ) # If someone doesn't want a schema applied, we must treat it as # a simple string->string function @@ -322,7 +334,7 @@ def tool( "description not provided and infer_schema is False." ) raise ValueError(msg) - return Tool( + result = Tool( name=tool_name, func=func, description=f"{tool_name} tool", @@ -331,6 +343,8 @@ def tool( response_format=response_format, extras=extras, ) + result.subagent_name = subagent_name + return result return _tool_factory diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index 77a9c185a76..f53a5801aa2 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -3780,3 +3780,36 @@ def test_basetool_subagent_name_accepts_callable() -> None: } assert callable(tool.subagent_name) assert tool.subagent_name(fake_call) == "researcher" + + +# --- @tool decorator subagent_name passthrough tests --- + + +def test_tool_decorator_passes_subagent_name_static() -> None: + @tool("call_weather", subagent_name="weather_agent") + def call_weather(city: str) -> str: + """Call the weather agent.""" + return f"weather in {city}" + + assert call_weather.subagent_name == "weather_agent" + + +def test_tool_decorator_passes_subagent_name_callable() -> None: + def resolver(call: ToolCall) -> str: + return str(call["args"]["target"]) + + @tool("dispatch", subagent_name=resolver) + def dispatch(target: str, query: str) -> str: + """Dispatch to a target.""" + return f"dispatched to {target}: {query}" + + assert dispatch.subagent_name is resolver + + +def test_tool_decorator_subagent_name_defaults_to_none() -> None: + @tool("plain") + def plain(x: int) -> str: + """A tool with no subagent_name declared.""" + return str(x) + + assert plain.subagent_name is None