diff --git a/libs/core/langchain_core/tools/base.py b/libs/core/langchain_core/tools/base.py index f069771f39e..33177445756 100644 --- a/libs/core/langchain_core/tools/base.py +++ b/libs/core/langchain_core/tools/base.py @@ -530,6 +530,20 @@ class ChildTool(BaseTool): ``` """ + subagent_name: str | Callable[[ToolCall], str] | None = None + """Declared name of the subagent this tool dispatches to, if any. + + A static string declares a fixed dispatch target (one tool -> one subagent). + A callable resolves the name from the parsed `ToolCall` at dispatch time, + enabling registry-style dispatching (one tool -> many subagents based on + LLM-supplied args). + + Read by `ToolNode` at dispatch time and surfaced on the wire as + `LifecyclePayload.graphName`. Purely declarative: the framework stamps the + resolved value into runtime config metadata; consumers project it onto + typed surfaces. Setting this does not change tool execution behavior. + """ + def __init__(self, **kwargs: Any) -> None: """Initialize the tool. diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index 094aaf1b713..77a9c185a76 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -3741,3 +3741,42 @@ def test_tool_invoke_returns_list_of_mixin() -> None: assert isinstance(result, list) assert len(result) == 3 assert all(isinstance(m, ToolMessage) for m in result) + + +# --- subagent_name attribute tests --- + + +class _SubagentNameDummyTool(BaseTool): + """Minimal BaseTool subclass for subagent_name attribute tests.""" + + name: str = "dummy" + description: str = "A dummy tool used in subagent_name tests." + + def _run(self, *args: Any, **kwargs: Any) -> str: + return "dummy result" + + +def test_basetool_subagent_name_default_none() -> None: + tool = _SubagentNameDummyTool() + assert tool.subagent_name is None + + +def test_basetool_subagent_name_accepts_static_string() -> None: + tool = _SubagentNameDummyTool(subagent_name="weather_agent") + assert tool.subagent_name == "weather_agent" + + +def test_basetool_subagent_name_accepts_callable() -> None: + def resolver(call: ToolCall) -> str: + return str(call["args"]["subagent_type"]) + + tool = _SubagentNameDummyTool(subagent_name=resolver) + assert tool.subagent_name is resolver + fake_call: ToolCall = { + "name": "task", + "args": {"subagent_type": "researcher"}, + "id": "id-1", + "type": "tool_call", + } + assert callable(tool.subagent_name) + assert tool.subagent_name(fake_call) == "researcher"