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.
This commit is contained in:
Nick Hollon
2026-05-27 18:53:30 -04:00
parent b36d8ffaf7
commit df2b4cfdc4
2 changed files with 49 additions and 2 deletions

View File

@@ -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

View File

@@ -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