Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| writer("Write about AI agents") | ||
| ``` | ||
| """ | ||
| if not name: |
There was a problem hiding this comment.
Issue: Empty string check uses falsy evaluation which is inconsistent.
Suggestion: Consider using explicit is None checks for clarity. Currently if not name would treat an empty string "" the same as None, which may not be the intended behavior:
if name is None:
name = self.name
if description is None:
description = self.description or f"Use the {name} tool to invoke this agent as a tool"This makes the API contract clearer - None means "use default" while an explicit empty string could be considered user error.
src/strands/agent/agent_as_tool.py
Outdated
| Yields: | ||
| ToolStreamEvent for intermediate events, then ToolResultEvent with the final response. | ||
| """ | ||
| prompt = tool_use["input"].get("input", "") if isinstance(tool_use["input"], dict) else tool_use["input"] |
There was a problem hiding this comment.
Issue: The input extraction logic handles two cases but could silently fail with unexpected input formats.
Suggestion: Consider adding explicit type handling or validation:
tool_input = tool_use["input"]
if isinstance(tool_input, dict):
prompt = tool_input.get("input", "")
elif isinstance(tool_input, str):
prompt = tool_input
else:
logger.warning("tool_name=<%s> | unexpected input type: %s", self._tool_name, type(tool_input))
prompt = str(tool_input)|
Issue: The original issue #1002 mentions a Question: Is this parameter intentionally deferred to a follow-up PR, or should it be included in this initial implementation? The current implementation appears to preserve context by default (since it uses the same agent instance), but there's no way for users to reset context between calls. This could be a breaking change if added later, so it would be good to clarify the intended behavior. |
Code Review SummaryAssessment: Request Changes This PR introduces a well-designed Review Categories
The core implementation looks solid - just needs the documentation PR linked to unblock merge. |
|
/strands review |
Code Review Summary - UpdatedAssessment: Approve ✅ Thank you for addressing the feedback from the previous review! All blocking issues have been resolved: Changes Addressed
Minor Notes (Non-blocking)
The implementation is well-designed, follows SDK patterns, and includes proper error handling and logging. Ready to merge once the awsarron discussion is resolved and any CI checks pass. |
src/strands/agent/agent_as_tool.py
Outdated
| "type": "string", | ||
| "description": "The input to send to the agent tool.", | ||
| }, | ||
| "preserve_context": { |
There was a problem hiding this comment.
This should be a class level field. I think most consumers want to decide at creation time whether it's a persistent tool (preserve_context=True) or new agent call on every invocation (preserve_context=False).
I also wonder if there's a way we can determine the best default - if not, maybe make it required
There was a problem hiding this comment.
Swapped to class level field with default True. I think we should take the least destructive default
There was a problem hiding this comment.
Our docs implementation seems to correspond to preserve_context=False: https://strandsagents.com/docs/user-guide/concepts/multi-agent/agents-as-tools/
I just realized, preserve_context=True might need some sort of integration with the parent agent's AgentState, as otherwise if an agent is persisted & rehydrated the context of the sub-agent isn't restored. Maybe that's alright, as it's a tool concern, bug given that it's a tool that we're vending, we should at least explore if an agent-as-tool should integrate with the parent's AgentState
src/strands/agent/agent_as_tool.py
Outdated
| self._tool_name, | ||
| tool_use_id, | ||
| ) | ||
| messages.clear() |
There was a problem hiding this comment.
I don't think we want to clear the agent in this case; we want to restore it to the original state that it was at. The graph node does something similar I think.
In the future, we can use snapshots to simplify this.
AgentState is another one that will need to be restored
There was a problem hiding this comment.
took the pattern from graph.py
| def test_init_sets_name(mock_agent): | ||
| tool = AgentAsTool(mock_agent, name="my_tool", description="desc") | ||
| assert tool.tool_name == "my_tool" | ||
|
|
||
|
|
||
| def test_init_sets_description(mock_agent): | ||
| tool = AgentAsTool(mock_agent, name="my_tool", description="custom desc") | ||
| assert tool._description == "custom desc" | ||
|
|
||
|
|
||
| def test_init_stores_agent_reference(mock_agent, tool): | ||
| assert tool.agent is mock_agent |
| def test_tool_name(tool): | ||
| assert tool.tool_name == "test_agent" | ||
|
|
||
|
|
||
| def test_tool_type(tool): | ||
| assert tool.tool_type == "agent" | ||
|
|
||
|
|
||
| def test_tool_spec_name(tool): | ||
| assert tool.tool_spec["name"] == "test_agent" | ||
|
|
||
|
|
||
| def test_tool_spec_description(tool): | ||
| assert tool.tool_spec["description"] == "A test agent" | ||
|
|
||
|
|
||
| def test_tool_spec_input_schema(tool): | ||
| schema = tool.tool_spec["inputSchema"]["json"] | ||
| assert schema["type"] == "object" | ||
| assert "input" in schema["properties"] | ||
| assert schema["properties"]["input"]["type"] == "string" | ||
| assert "preserve_context" in schema["properties"] | ||
| assert schema["properties"]["preserve_context"]["type"] == "boolean" | ||
| assert schema["required"] == ["input"] |
There was a problem hiding this comment.
Do an assert on the entire spec at once
| props = tool.get_display_properties() | ||
| assert props["Agent"] == "test_agent" | ||
| assert props["Type"] == "agent" |
There was a problem hiding this comment.
Same here - assert on the entire object
src/strands/agent/agent_as_tool.py
Outdated
| if "result" in event: | ||
| result = event["result"] | ||
| else: | ||
| yield event |
There was a problem hiding this comment.
We will auto wrap this in a ToolStreamResult, but I think we actually want to do additional wrapping here - Maybe an AgentAsToolStreamEvent?
If we don't, the caller cannot tell the difference between "This is ToolStreamEvent that came from an tool wrapping an agent using a tool" vs "This is a ToolStreamEvent from a tool"
AgentAsToolStreamEvent would probably reference the AgentAsTool class
There was a problem hiding this comment.
Added AgentAsToolStreamEvent class that is a subclass of ToolStreamEvent to differentiate
src/strands/agent/agent.py
Outdated
| if not name: | ||
| name = self.name | ||
| if not description: | ||
| description = self.description or f"Use the {name} tool to invoke this agent as a tool" |
There was a problem hiding this comment.
We'll need a better description I think
invoke this agent as a tool
Doesn't really make sense to an agent. It's going to see a tool that says:
name: "researcher"
description: "Use the researcher tool to invoke this agent as a tool"
where this doesn't make sense.
There was a problem hiding this comment.
swapped to Use the {name} agent as a tool by providing a natural language input
src/strands/agent/agent_as_tool.py
Outdated
|
|
||
| tool_use_id = tool_use["toolUseId"] | ||
|
|
||
| if not preserve_context: |
There was a problem hiding this comment.
We should throw if preserve_context=False and _agent != Agent; that would indicate A2A agent which for preserve_context=False doesn't make sense
There was a problem hiding this comment.
put this check in the constructor
There was a problem hiding this comment.
Let's rename this as _agent_as_tool.py and just export at the top level
…e; yield AgentAsToolStreamEvents; small fixes
|
/strands review |
Code Review Summary - Round 3Assessment: Approve ✅ Excellent work addressing all the feedback from @zastrowm! The implementation has been significantly improved. Changes Addressed
Code Quality Highlights
Remaining Discussion (Non-blocking)@zastrowm raised a consideration about The implementation is well-designed, follows SDK patterns, and addresses the original issue #1002 completely. Ready to merge! 🎉 |
Description
Will close #1002.
This PR adds the
AgentAsToolclass as well as a convenience method on theAgentclass to return an instance of it.The
AgentAsToolclass just callsstream_asyncon the underlying agent and formats the response as aToolResultEvent.The added integration test confirms that an AgentAsTool was invoked and that agent-as-tool used a tool of its own.
Related Issues
#1002
Documentation PR
strands-agents/docs#686
Type of Change
New feature
Testing
Added unit tests
Added a minimal integ test
[ x] I ran
hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.