Skip to content

Commit b9b6048

Browse files
author
Owen Kaplan
committed
feat: mark _AgentAsTool as private; take Agent instead of AgentBase in the constructor
1 parent 7cf436c commit b9b6048

9 files changed

Lines changed: 94 additions & 101 deletions

File tree

src/strands/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""A framework for building, deploying, and managing AI agents."""
22

33
from . import agent, models, telemetry, types
4-
from .agent._agent_as_tool import AgentAsTool
4+
from .agent._agent_as_tool import _AgentAsTool
55
from .agent.agent import Agent
66
from .agent.base import AgentBase
77
from .event_loop._retry import ModelRetryStrategy
@@ -12,7 +12,7 @@
1212

1313
__all__ = [
1414
"Agent",
15-
"AgentAsTool",
15+
"_AgentAsTool",
1616
"AgentBase",
1717
"AgentSkills",
1818
"agent",

src/strands/agent/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import Any
1111

1212
from ..event_loop._retry import ModelRetryStrategy
13-
from ._agent_as_tool import AgentAsTool
13+
from ._agent_as_tool import _AgentAsTool
1414
from .agent import Agent
1515
from .agent_result import AgentResult
1616
from .base import AgentBase
@@ -25,7 +25,7 @@
2525
"Agent",
2626
"AgentBase",
2727
"AgentResult",
28-
"AgentAsTool",
28+
"_AgentAsTool",
2929
"ConversationManager",
3030
"NullConversationManager",
3131
"SlidingWindowConversationManager",

src/strands/agent/_agent_as_tool.py

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""Agent-as-tool adapter.
22
3-
This module provides the AgentAsTool class that wraps an Agent (or any AgentBase) as a tool
3+
This module provides the _AgentAsTool class that wraps an Agent as a tool
44
so it can be passed to another agent's tool list.
55
"""
66

7+
from __future__ import annotations
8+
79
import copy
810
import logging
911
import threading
10-
from typing import Any
12+
from typing import TYPE_CHECKING, Any
1113

1214
from typing_extensions import override
1315

@@ -16,12 +18,14 @@
1618
from ..types.content import Messages
1719
from ..types.interrupt import InterruptResponseContent
1820
from ..types.tools import AgentTool, ToolGenerator, ToolSpec, ToolUse
19-
from .base import AgentBase
21+
22+
if TYPE_CHECKING:
23+
from .agent import Agent
2024

2125
logger = logging.getLogger(__name__)
2226

2327

24-
class AgentAsTool(AgentTool):
28+
class _AgentAsTool(AgentTool):
2529
"""Adapter that exposes an Agent as a tool for use by other agents.
2630
2731
The tool accepts a single ``input`` string parameter, invokes the wrapped
@@ -30,18 +34,14 @@ class AgentAsTool(AgentTool):
3034
Example:
3135
```python
3236
from strands import Agent
33-
from strands.agent import AgentAsTool
3437
3538
researcher = Agent(name="researcher", description="Finds information")
3639
37-
# Use directly
38-
tool = AgentAsTool(researcher, name="researcher", description="Finds information")
39-
40-
# Or via convenience method
40+
# Use via convenience method (default: fresh conversation each call)
4141
tool = researcher.as_tool()
4242
43-
# Start each invocation with a fresh conversation
44-
tool = researcher.as_tool(preserve_context=False)
43+
# Preserve context across invocations
44+
tool = researcher.as_tool(preserve_context=True)
4545
4646
writer = Agent(name="writer", tools=[tool])
4747
writer("Write about AI agents")
@@ -50,36 +50,35 @@ class AgentAsTool(AgentTool):
5050

5151
def __init__(
5252
self,
53-
agent: AgentBase,
53+
agent: Agent,
5454
*,
5555
name: str,
56-
description: str,
56+
description: str | None = None,
5757
preserve_context: bool = False,
5858
) -> None:
5959
r"""Initialize the agent-as-tool adapter.
6060
6161
Args:
6262
agent: The agent to wrap as a tool.
6363
name: Tool name. Must match the pattern ``[a-zA-Z0-9_\\-]{1,64}``.
64-
description: Tool description.
64+
description: Tool description. Defaults to the agent's description, or a
65+
generic description if the agent has no description set.
6566
preserve_context: Whether to preserve the agent's conversation history across
6667
invocations. When False, the agent's messages and state are reset to the
6768
values they had at construction time before each call, ensuring every
6869
invocation starts from the same baseline regardless of any external
69-
interactions with the agent. Defaults to False. Only effective when the
70-
wrapped agent exposes a mutable ``messages`` list and/or an ``AgentState``
71-
(e.g. ``strands.agent.Agent``).
70+
interactions with the agent. Defaults to False.
7271
"""
7372
super().__init__()
7473
self._agent = agent
7574
self._tool_name = name
76-
self._description = description
75+
self._description = (
76+
description or agent.description or f"Use the {name} agent as a tool by providing a natural language input"
77+
)
7778
self._preserve_context = preserve_context
7879

7980
# When preserve_context=False, we snapshot the agent's initial state so we can
8081
# restore it before each invocation. This mirrors GraphNode.reset_executor_state().
81-
# We require an Agent instance for this since AgentBase doesn't guarantee
82-
# messages/state attributes.
8382
self._initial_messages: Messages = []
8483
self._initial_state: AgentState = AgentState()
8584
# Serialize access so _reset_agent_state + stream_async are atomic.
@@ -88,15 +87,17 @@ def __init__(
8887
self._lock = threading.Lock()
8988

9089
if not preserve_context:
91-
from .agent import Agent
92-
93-
if not isinstance(agent, Agent):
94-
raise TypeError(f"preserve_context=False requires an Agent instance, got {type(agent).__name__}")
90+
if getattr(agent, "_session_manager", None) is not None:
91+
raise ValueError(
92+
"preserve_context=False cannot be used with an agent that has a session manager. "
93+
"The session manager persists conversation history externally, which conflicts with "
94+
"resetting the agent's state between invocations."
95+
)
9596
self._initial_messages = copy.deepcopy(agent.messages)
9697
self._initial_state = AgentState(agent.state.get())
9798

9899
@property
99-
def agent(self) -> AgentBase:
100+
def agent(self) -> Agent:
100101
"""The wrapped agent instance."""
101102
return self._agent
102103

@@ -259,12 +260,6 @@ def _reset_agent_state(self, tool_use_id: str) -> None:
259260
Args:
260261
tool_use_id: Tool use ID for logging context.
261262
"""
262-
from .agent import Agent
263-
264-
# isinstance narrows the type for mypy; __init__ guarantees this when preserve_context=False
265-
if not isinstance(self._agent, Agent):
266-
return
267-
268263
logger.debug(
269264
"tool_name=<%s>, tool_use_id=<%s> | resetting agent to initial state",
270265
self._tool_name,
@@ -275,8 +270,7 @@ def _reset_agent_state(self, tool_use_id: str) -> None:
275270

276271
def _is_sub_agent_interrupted(self) -> bool:
277272
"""Check whether the wrapped agent is in an activated interrupt state."""
278-
interrupt_state = getattr(self._agent, "_interrupt_state", None)
279-
return interrupt_state is not None and interrupt_state.activated
273+
return self._agent._interrupt_state.activated
280274

281275
def _build_interrupt_responses(self) -> list[InterruptResponseContent]:
282276
"""Build interrupt response payloads from the sub-agent's interrupt state.
@@ -288,13 +282,9 @@ def _build_interrupt_responses(self) -> list[InterruptResponseContent]:
288282
Returns:
289283
List of interrupt response content blocks for resuming the sub-agent.
290284
"""
291-
interrupt_state = getattr(self._agent, "_interrupt_state", None)
292-
if interrupt_state is None:
293-
return []
294-
295285
return [
296286
{"interruptResponse": {"interruptId": interrupt.id, "response": interrupt.response}}
297-
for interrupt in interrupt_state.interrupts.values()
287+
for interrupt in self._agent._interrupt_state.interrupts.values()
298288
if interrupt.response is not None
299289
]
300290

src/strands/agent/agent.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from ..types.content import ContentBlock, Message, Messages, SystemContentBlock
6363
from ..types.exceptions import ConcurrencyException, ContextWindowOverflowException
6464
from ..types.traces import AttributeValue
65-
from ._agent_as_tool import AgentAsTool
65+
from ._agent_as_tool import _AgentAsTool
6666
from .agent_result import AgentResult
6767
from .base import AgentBase
6868
from .conversation_manager import (
@@ -618,21 +618,22 @@ def as_tool(
618618
name: str | None = None,
619619
description: str | None = None,
620620
preserve_context: bool = False,
621-
) -> AgentAsTool:
621+
) -> _AgentAsTool:
622622
r"""Convert this agent into a tool for use by another agent.
623623
624624
Args:
625625
name: Tool name. Must match the pattern ``[a-zA-Z0-9_\\-]{1,64}``.
626626
Defaults to the agent's name.
627-
description: Tool description. Defaults to the agent's description.
627+
description: Tool description. Defaults to the agent's description, or a
628+
generic description if the agent has no description set.
628629
preserve_context: Whether to preserve the agent's conversation history across
629630
invocations. When False, the agent's messages and state are reset to the
630631
values they had at construction time before each call, ensuring every
631632
invocation starts from the same baseline regardless of any external
632633
interactions with the agent. Defaults to False.
633634
634635
Returns:
635-
An AgentAsTool wrapping this agent.
636+
An _AgentAsTool wrapping this agent.
636637
637638
Example:
638639
```python
@@ -643,9 +644,7 @@ def as_tool(
643644
"""
644645
if not name:
645646
name = self.name
646-
if not description:
647-
description = self.description or f"Use the {name} agent as a tool by providing a natural language input"
648-
return AgentAsTool(self, name=name, description=description, preserve_context=preserve_context)
647+
return _AgentAsTool(self, name=name, description=description, preserve_context=preserve_context)
649648

650649
def cleanup(self) -> None:
651650
"""Clean up resources used by the agent.

src/strands/tools/executors/_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ async def _stream(
228228
if isinstance(event, ToolInterruptEvent):
229229
# Register any interrupts not already in the agent's state.
230230
# For normal hooks this is a no-op (already registered by _Interruptible.interrupt()).
231-
# For sub-agent interrupts propagated via AgentAsTool, this is where they get
231+
# For sub-agent interrupts propagated via _AgentAsTool, this is where they get
232232
# registered so that _interrupt_state.resume() can locate them by ID.
233233
for interrupt in event.interrupts:
234234
agent._interrupt_state.interrupts.setdefault(interrupt.id, interrupt)

src/strands/types/_events.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
if TYPE_CHECKING:
2323
from ..agent import AgentResult
24-
from ..agent._agent_as_tool import AgentAsTool
24+
from ..agent._agent_as_tool import _AgentAsTool
2525
from ..multiagent.base import MultiAgentResult, NodeResult
2626

2727

@@ -327,25 +327,25 @@ def tool_use_id(self) -> str:
327327
class AgentAsToolStreamEvent(ToolStreamEvent):
328328
"""Event emitted when an agent-as-tool yields intermediate events during execution.
329329
330-
Extends ToolStreamEvent with a reference to the originating AgentAsTool so callers
330+
Extends ToolStreamEvent with a reference to the originating _AgentAsTool so callers
331331
can distinguish sub-agent stream events from regular tool stream events and access
332332
the wrapped agent, tool name, description, etc.
333333
"""
334334

335-
def __init__(self, tool_use: ToolUse, tool_stream_data: Any, agent_as_tool: "AgentAsTool") -> None:
335+
def __init__(self, tool_use: ToolUse, tool_stream_data: Any, agent_as_tool: "_AgentAsTool") -> None:
336336
"""Initialize with tool streaming data and agent-tool reference.
337337
338338
Args:
339339
tool_use: The tool invocation producing the stream.
340340
tool_stream_data: The yielded event from the sub-agent execution.
341-
agent_as_tool: The AgentAsTool instance that produced this event.
341+
agent_as_tool: The _AgentAsTool instance that produced this event.
342342
"""
343343
super().__init__(tool_use, tool_stream_data)
344344
self._agent_as_tool = agent_as_tool
345345

346346
@property
347-
def agent_as_tool(self) -> "AgentAsTool":
348-
"""The AgentAsTool instance that produced this event."""
347+
def agent_as_tool(self) -> "_AgentAsTool":
348+
"""The _AgentAsTool instance that produced this event."""
349349
return self._agent_as_tool
350350

351351

tests/strands/agent/test_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import strands
1717
from strands import Agent, Plugin, ToolContext
18-
from strands.agent import AgentAsTool, AgentResult
18+
from strands.agent import AgentResult, _AgentAsTool
1919
from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
2020
from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
2121
from strands.agent.state import AgentState
@@ -2702,11 +2702,11 @@ def hook_callback(event: BeforeModelCallEvent):
27022702

27032703

27042704
def test_as_tool_returns_agent_tool():
2705-
"""Test that as_tool returns an AgentAsTool wrapping the agent."""
2705+
"""Test that as_tool returns an _AgentAsTool wrapping the agent."""
27062706
agent = Agent(name="researcher", description="Finds information")
27072707
tool = agent.as_tool()
27082708

2709-
assert isinstance(tool, AgentAsTool)
2709+
assert isinstance(tool, _AgentAsTool)
27102710
assert tool.agent is agent
27112711

27122712

0 commit comments

Comments
 (0)