Skip to content

Conversation

@mkmeral
Copy link
Contributor

@mkmeral mkmeral commented Jan 9, 2026

Description

Picking up from @awsarron's work in #1174 to address review feedback.

A2AAgent makes it simple to consume remote A2A agents and invoke them like any other Strands Agent. This PR addresses the open review comments from the original implementation.

Changes from original PR:

  • Changed logger.info to logger.debug for agent card discovery and message sending
  • Simplified factory creation with _create_default_factory() helper method
  • Fixed invoke_async to delegate to stream_async (ensures consistent behavior)
  • Added comprehensive docstring for A2AStreamEvent explaining when events are emitted
  • Expanded integration tests to cover sync, async, streaming, and custom client config scenarios
  • Added unit tests for __del__ cleanup behavior

Example usage (unchanged from original):

from strands.agent.a2a_agent import A2AAgent

a2a_agent = A2AAgent(endpoint="http://localhost:9000")
result = a2a_agent("Show me 10 ^ 6")
# AgentResult(stop_reason='end_turn', message={'role': 'assistant', 'content': [{'text': '10^6 = 1,000,000'}]}, ...)

Follow-ups:

Related Issues

#907

Resolves review comments from #1174

Documentation PR

TODO

Type of Change

New feature

Testing

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • [TODO] I have updated the documentation accordingly
  • [TODO] I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.


diff of a2a agent compared to original PR

diff --git a/src/strands/agent/a2a_agent.py b/src/strands/agent/a2a_agent.py
index 1f2e015..7c30269 100644
--- a/src/strands/agent/a2a_agent.py
+++ b/src/strands/agent/a2a_agent.py
@@ -88,9 +88,19 @@ class A2AAgent:
         if not self.description and self._agent_card.description:
             self.description = self._agent_card.description
 
-        logger.info("agent=<%s>, endpoint=<%s> | discovered agent card", self.name, self.endpoint)
+        logger.debug("agent=<%s>, endpoint=<%s> | discovered agent card", self.name, self.endpoint)
         return self._agent_card
 
+    def _create_default_factory(self) -> ClientFactory:
+        """Create default A2A client factory with non-streaming config.
+
+        Returns:
+            Configured ClientFactory instance.
+        """
+        httpx_client = self._get_httpx_client()
+        config = ClientConfig(httpx_client=httpx_client, streaming=False)
+        return ClientFactory(config)
+
     async def _get_a2a_client(self) -> Client:
         """Get or create the A2A client for this agent.
 
@@ -99,16 +109,7 @@ class A2AAgent:
         """
         if self._a2a_client is None:
             agent_card = await self._get_agent_card()
-
-            if self._a2a_client_factory is not None:
-                # Use provided factory
-                factory = self._a2a_client_factory
-            else:
-                # Create default factory
-                httpx_client = self._get_httpx_client()
-                config = ClientConfig(httpx_client=httpx_client, streaming=False)
-                factory = ClientFactory(config)
-
+            factory = self._a2a_client_factory or self._create_default_factory()
             self._a2a_client = factory.create(agent_card)
         return self._a2a_client
 
@@ -130,7 +131,7 @@ class A2AAgent:
         client = await self._get_a2a_client()
         message = convert_input_to_message(prompt)
 
-        logger.info("agent=<%s>, endpoint=<%s> | sending message", self.name, self.endpoint)
+        logger.debug("agent=<%s>, endpoint=<%s> | sending message", self.name, self.endpoint)
         return client.send_message(message)
 
     def _is_complete_event(self, event: A2AResponse) -> bool:
@@ -174,6 +175,8 @@ class A2AAgent:
     ) -> AgentResult:
         """Asynchronously invoke the remote A2A agent.
 
+        Delegates to stream_async and returns the final result.
+
         Args:
             prompt: Input to the agent (string, message list, or content blocks).
             **kwargs: Additional arguments (ignored).
@@ -185,10 +188,15 @@ class A2AAgent:
             ValueError: If prompt is None.
             RuntimeError: If no response received from agent.
         """
-        async for event in await self._send_message(prompt):
-            return convert_response_to_agent_result(event)
+        result: AgentResult | None = None
+        async for event in self.stream_async(prompt, **kwargs):
+            if "result" in event:
+                result = event["result"]
+
+        if result is None:
+            raise RuntimeError("No response received from A2A agent")
 
-        raise RuntimeError("No response received from A2A agent")
+        return result
 
     def __call__(
         self,
@@ -244,7 +252,7 @@ class A2AAgent:
             yield AgentResultEvent(result)
 
     def __del__(self) -> None:
-        """Clean up resources when agent is garbage collected."""
+        """Best-effort cleanup on garbage collection."""
         if self._owns_client and self._httpx_client is not None:
             try:
                 client = self._httpx_client


REV2

  • Moved to streaming=True by default with the client. Otherwise streaming would require both custom client and using stream async. Right now both stream async and invoke async would work normally.

awsarron and others added 12 commits November 18, 2025 14:09
- Fix invoke_async to delegate to stream_async (prevents returning first incomplete event)
- Add async context manager support (__aenter__/__aexit__) and explicit aclose() method
- Improve __del__ cleanup to handle event loop edge cases
- Change logger.info to logger.debug for consistency with project standards
- Simplify factory creation with _create_default_factory() helper method
- Add comprehensive documentation to A2AStreamEvent
- Improve test fixture pattern with pytest fixture for subprocess management
- Add comprehensive e2e tests for invoke_async, stream_async, and context manager

Addresses PR strands-agents#1174 review comments:
- Comment strands-agents#2: Critical bug - invoke_async now waits for complete events
- Comment strands-agents#5: Code duplication - invoke_async delegates to stream_async
- Comment strands-agents#6: Async cleanup - proper async context manager pattern
- Comment strands-agents#3: Logging level - changed to debug
- Comment strands-agents#4: Factory simplification - extracted helper method
- Comment strands-agents#12: Documentation - documented A2AStreamEvent behavior
- Comment strands-agents#9: Test fixture - using pytest fixture pattern
- Comment strands-agents#10: Test coverage - added comprehensive e2e tests
@codecov
Copy link

codecov bot commented Jan 9, 2026

Codecov Report

❌ Patch coverage is 91.27907% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/strands/multiagent/a2a/_converters.py 85.96% 0 Missing and 8 partials ⚠️
src/strands/agent/__init__.py 33.33% 4 Missing ⚠️
src/strands/agent/a2a_agent.py 97.05% 0 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@dbschmigelski dbschmigelski self-requested a review January 9, 2026 20:14
@cagataycali

This comment was marked as spam.

cagataycali

This comment was marked as off-topic.

cagataycali

This comment was marked as off-topic.

@cagataycali

This comment was marked as off-topic.

dbschmigelski
dbschmigelski previously approved these changes Jan 22, 2026
@github-actions github-actions bot added size/xl and removed size/xl labels Jan 28, 2026
zastrowm
zastrowm previously approved these changes Jan 28, 2026
dbschmigelski
dbschmigelski previously approved these changes Jan 28, 2026
@mkmeral mkmeral enabled auto-merge (squash) January 28, 2026 17:08
@github-actions github-actions bot added size/xl and removed size/xl labels Jan 28, 2026
Tests that create real httpx.AsyncClient instances now explicitly clear
the client reference to prevent __del__ from attempting async cleanup
during garbage collection, which can deadlock on Windows due to
ProactorEventLoop + ThreadPoolExecutor issues.

Refs:
- python/cpython#83413
- aio-libs/aiohttp#4324
@mkmeral mkmeral dismissed stale reviews from dbschmigelski and zastrowm via bfb51a5 January 28, 2026 21:17
@github-actions github-actions bot added size/xl and removed size/xl labels Jan 28, 2026
@github-actions github-actions bot added size/xl and removed size/xl labels Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants