Skip to content

Commit 521c4d7

Browse files
feat: auto-wrap Agent instances passed in tools list (#1997)
Co-authored-by: agent-of-mkmeral <217235299+strands-agent@users.noreply.github.com> Co-authored-by: agent-of-mkmeral <agent-of-mkmeral@users.noreply.github.com>
1 parent 6a35add commit 521c4d7

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed

src/strands/agent/agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ def __init__(
153153
- Imported Python modules (e.g., from strands_tools import current_time)
154154
- Dictionaries with name/path keys (e.g., {"name": "tool_name", "path": "/path/to/tool.py"})
155155
- ToolProvider instances for managed tool collections
156-
- Functions decorated with `@strands.tool` decorator.
156+
- Functions decorated with `@strands.tool` decorator
157+
- Agent instances (auto-wrapped via `agent.as_tool()` with defaults)
157158
158159
If provided, only these tools will be available. If None, all tools will be available.
159160
system_prompt: System prompt to guide model behavior.

src/strands/tools/registry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from typing_extensions import TypedDict
2020

2121
from .._async import run_async
22+
from ..agent.base import AgentBase
2223
from ..tools.decorator import DecoratedFunctionTool
2324
from ..types.tools import AgentTool, ToolSpec
2425
from . import ToolProvider
@@ -62,6 +63,7 @@ def process_tools(self, tools: list[Any]) -> list[str]:
6263
3. A module for a module based tool
6364
4. Instances of AgentTool (@tool decorated functions)
6465
5. Dictionaries with name/path keys (deprecated)
66+
6. Agent instances with an ``as_tool()`` method (auto-wrapped)
6567
6668
6769
Returns:
@@ -140,6 +142,12 @@ async def get_tools() -> Sequence[AgentTool]:
140142
for provider_tool in provider_tools:
141143
self.register_tool(provider_tool)
142144
tool_names.append(provider_tool.tool_name)
145+
# Agent instances - auto-wrap with .as_tool() for convenience
146+
elif isinstance(tool, AgentBase) and hasattr(tool, "as_tool") and callable(tool.as_tool):
147+
wrapped_tool = tool.as_tool()
148+
self.register_tool(wrapped_tool)
149+
tool_names.append(wrapped_tool.tool_name)
150+
143151
else:
144152
logger.warning("tool=<%s> | unrecognized tool specification", tool)
145153

tests/strands/agent/test_agent_as_tool.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,49 @@ async def test_stream_releases_lock_after_error(tool, mock_agent, tool_use):
674674
pass
675675

676676
assert not tool._lock.locked()
677+
678+
679+
# --- Agent-as-tool sugar (passing agents directly in tools list) ---
680+
681+
682+
def test_agent_passed_directly_in_tools_list():
683+
"""Test that an Agent can be passed directly in another Agent's tools list."""
684+
from strands.agent.agent import Agent
685+
686+
sub_agent = Agent(name="research_agent", description="Does research", callback_handler=None)
687+
688+
# This should work without calling .as_tool() explicitly
689+
parent_agent = Agent(name="orchestrator", tools=[sub_agent], callback_handler=None)
690+
691+
assert "research_agent" in parent_agent.tool_names
692+
693+
694+
def test_multiple_agents_passed_directly_in_tools_list():
695+
"""Test that multiple Agents can be passed directly in another Agent's tools list."""
696+
from strands.agent.agent import Agent
697+
698+
agent_a = Agent(name="agent_a", callback_handler=None)
699+
agent_b = Agent(name="agent_b", callback_handler=None)
700+
701+
parent = Agent(name="parent", tools=[agent_a, agent_b], callback_handler=None)
702+
703+
assert "agent_a" in parent.tool_names
704+
assert "agent_b" in parent.tool_names
705+
706+
707+
def test_agent_mixed_with_regular_tools_in_tools_list():
708+
"""Test that Agents can be mixed with regular tools in the tools list."""
709+
from strands import tool as tool_decorator
710+
from strands.agent.agent import Agent
711+
712+
@tool_decorator
713+
def my_tool(x: str) -> str:
714+
"""A regular tool."""
715+
return x
716+
717+
sub_agent = Agent(name="helper_agent", callback_handler=None)
718+
719+
parent = Agent(name="parent", tools=[my_tool, sub_agent], callback_handler=None)
720+
721+
assert "my_tool" in parent.tool_names
722+
assert "helper_agent" in parent.tool_names

tests/strands/tools/test_registry.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,92 @@ def test_tool_registry_replace_non_dynamic_with_dynamic():
603603

604604
assert registry.registry["my_tool"] == new_tool
605605
assert registry.dynamic_tools["my_tool"] == new_tool
606+
607+
608+
# --- Agent-as-tool sugar ---
609+
610+
611+
def test_process_tools_with_agent_instance():
612+
"""Test that passing an Agent instance in tools list auto-wraps it with as_tool()."""
613+
from strands.agent.agent import Agent
614+
615+
sub_agent = Agent(name="research_agent", description="Finds information", callback_handler=None)
616+
617+
registry = ToolRegistry()
618+
tool_names = registry.process_tools([sub_agent])
619+
620+
assert "research_agent" in tool_names
621+
assert "research_agent" in registry.registry
622+
assert registry.registry["research_agent"].tool_type == "agent"
623+
624+
625+
def test_process_tools_with_agent_instance_uses_agent_name():
626+
"""Test that the auto-wrapped tool uses the agent's name."""
627+
from strands.agent.agent import Agent
628+
629+
sub_agent = Agent(name="my_custom_agent", callback_handler=None)
630+
631+
registry = ToolRegistry()
632+
registry.process_tools([sub_agent])
633+
634+
assert "my_custom_agent" in registry.registry
635+
spec = registry.registry["my_custom_agent"].tool_spec
636+
assert spec["name"] == "my_custom_agent"
637+
638+
639+
def test_process_tools_with_agent_instance_uses_agent_description():
640+
"""Test that the auto-wrapped tool uses the agent's description."""
641+
from strands.agent.agent import Agent
642+
643+
sub_agent = Agent(name="helper", description="A helpful assistant", callback_handler=None)
644+
645+
registry = ToolRegistry()
646+
registry.process_tools([sub_agent])
647+
648+
spec = registry.registry["helper"].tool_spec
649+
assert spec["description"] == "A helpful assistant"
650+
651+
652+
def test_process_tools_with_agent_in_nested_list():
653+
"""Test that Agent instances in nested iterables are auto-wrapped."""
654+
from strands.agent.agent import Agent
655+
656+
agent_a = Agent(name="agent_a", callback_handler=None)
657+
agent_b = Agent(name="agent_b", callback_handler=None)
658+
659+
registry = ToolRegistry()
660+
tool_names = sorted(registry.process_tools([[agent_a, agent_b]]))
661+
662+
assert tool_names == ["agent_a", "agent_b"]
663+
664+
665+
def test_process_tools_with_mixed_agents_and_tools():
666+
"""Test that Agent instances can be mixed with regular tools."""
667+
from strands.agent.agent import Agent
668+
669+
def function() -> str:
670+
return "done"
671+
672+
regular_tool = tool(name="regular_tool")(function)
673+
sub_agent = Agent(name="sub_agent", callback_handler=None)
674+
675+
registry = ToolRegistry()
676+
tool_names = sorted(registry.process_tools([regular_tool, sub_agent]))
677+
678+
assert tool_names == ["regular_tool", "sub_agent"]
679+
assert registry.registry["sub_agent"].tool_type == "agent"
680+
681+
682+
def test_process_tools_with_multiple_agents():
683+
"""Test that multiple Agent instances can be passed."""
684+
from strands.agent.agent import Agent
685+
686+
agent_1 = Agent(name="researcher", description="Does research", callback_handler=None)
687+
agent_2 = Agent(name="writer", description="Writes content", callback_handler=None)
688+
agent_3 = Agent(name="reviewer", description="Reviews work", callback_handler=None)
689+
690+
registry = ToolRegistry()
691+
tool_names = sorted(registry.process_tools([agent_1, agent_2, agent_3]))
692+
693+
assert tool_names == ["researcher", "reviewer", "writer"]
694+
assert all(registry.registry[name].tool_type == "agent" for name in tool_names)

0 commit comments

Comments
 (0)