Agent-to-Agent (A2A) Protocol
Section titled “Agent-to-Agent (A2A) Protocol”Strands Agents supports the Agent-to-Agent (A2A) protocol, enabling seamless communication between AI agents across different platforms and implementations.
What is Agent-to-Agent (A2A)?
Section titled “What is Agent-to-Agent (A2A)?”The Agent-to-Agent protocol is an open standard that defines how AI agents can discover, communicate, and collaborate with each other.
Use Cases
Section titled “Use Cases”A2A protocol support enables several powerful use cases:
- Multi-Agent Workflows: Chain multiple specialized agents together
- Agent Marketplaces: Discover and use agents from different providers
- Cross-Platform Integration: Connect Strands agents with other A2A-compatible systems
- Distributed AI Systems: Build scalable, distributed agent architectures
Learn more about the A2A protocol:
!!! tip “Complete Examples Available” Check out the Native A2A Support samples for complete, ready-to-run client, server and tool implementations.
Installation
Section titled “Installation”To use A2A functionality with Strands, install the package with the A2A extra:
pip install 'strands-agents[a2a]'This installs the core Strands SDK along with the necessary A2A protocol dependencies.
Creating an A2A Server
Section titled “Creating an A2A Server”Basic Server Setup
Section titled “Basic Server Setup”Create a Strands agent and expose it as an A2A server:
import loggingfrom strands_tools.calculator import calculatorfrom strands import Agentfrom strands.multiagent.a2a import A2AServer
logging.basicConfig(level=logging.INFO)
# Create a Strands agentstrands_agent = Agent( name="Calculator Agent", description="A calculator agent that can perform basic arithmetic operations.", tools=[calculator], callback_handler=None)
# Create A2A server (streaming enabled by default)a2a_server = A2AServer(agent=strands_agent)
# Start the servera2a_server.serve()NOTE: the server supports both
SendMessageRequestandSendStreamingMessageRequestclient requests!
Server Configuration Options
Section titled “Server Configuration Options”The A2AServer constructor accepts several configuration options:
agent: The Strands agent to wrap with A2A compatibilityhost: Hostname or IP address to bind to (default: “127.0.0.1”)port: Port to bind to (default: 9000)version: Version of the agent (default: “0.0.1”)skills: Custom list of agent skills (default: auto-generated from tools)http_url: Public HTTP URL where this agent will be accessible (optional, enables path-based mounting)serve_at_root: Forces server to serve at root path regardless of http_url path (default: False)task_store: Custom task storage implementation (defaults to InMemoryTaskStore)queue_manager: Custom message queue management (optional)push_config_store: Custom push notification configuration storage (optional)push_sender: Custom push notification sender implementation (optional)
Advanced Server Customization
Section titled “Advanced Server Customization”The A2AServer provides access to the underlying FastAPI or Starlette application objects allowing you to further customize server behavior.
from contextlib import asynccontextmanagerfrom strands import Agentfrom strands.multiagent.a2a import A2AServerimport uvicorn
# Create your agent and A2A serveragent = Agent(name="My Agent", description="A customizable agent", callback_handler=None)a2a_server = A2AServer(agent=agent)
@asynccontextmanagerasync def lifespan(app: FastAPI): """Manage application lifespan with proper error handling.""" # Startup tasks yield # Application runs here # Shutdown tasks
# Access the underlying FastAPI app# Allows passing keyword arguments to FastAPI constructor for further customizationfastapi_app = a2a_server.to_fastapi_app(app_kwargs={"lifespan": lifespan})# Add custom middleware, routes, or configurationfastapi_app.add_middleware(...)
# Or access the Starlette app# Allows passing keyword arguments to FastAPI constructor for further customizationstarlette_app = a2a_server.to_starlette_app(app_kwargs={"lifespan": lifespan})# Customize as needed
# You can then serve the customized app directlyuvicorn.run(fastapi_app, host="127.0.0.1", port=9000)Configurable Request Handler Components
Section titled “Configurable Request Handler Components”The A2AServer supports configurable request handler components for advanced customization:
from strands import Agentfrom strands.multiagent.a2a import A2AServerfrom a2a.server.tasks import TaskStore, PushNotificationConfigStore, PushNotificationSenderfrom a2a.server.events import QueueManager
# Custom task storage implementationclass CustomTaskStore(TaskStore): # Implementation details... pass
# Custom queue managerclass CustomQueueManager(QueueManager): # Implementation details... pass
# Create agent with custom componentsagent = Agent(name="My Agent", description="A customizable agent", callback_handler=None)
a2a_server = A2AServer( agent=agent, task_store=CustomTaskStore(), queue_manager=CustomQueueManager(), push_config_store=MyPushConfigStore(), push_sender=MyPushSender())Interface Requirements:
Custom implementations must follow these interfaces:
task_store: Must implementTaskStoreinterface froma2a.server.tasksqueue_manager: Must implementQueueManagerinterface froma2a.server.eventspush_config_store: Must implementPushNotificationConfigStoreinterface froma2a.server.taskspush_sender: Must implementPushNotificationSenderinterface froma2a.server.tasks
Path-Based Mounting for Containerized Deployments
Section titled “Path-Based Mounting for Containerized Deployments”The A2AServer supports automatic path-based mounting for deployment scenarios involving load balancers or reverse proxies. This allows you to deploy agents behind load balancers with different path prefixes.
from strands import Agentfrom strands.multiagent.a2a import A2AServer
# Create an agentagent = Agent( name="Calculator Agent", description="A calculator agent", callback_handler=None)
# Deploy with path-based mounting# The agent will be accessible at http://my-alb.amazonaws.com/calculator/a2a_server = A2AServer( agent=agent, http_url="http://my-alb.amazonaws.com/calculator")
# For load balancers that strip path prefixes, use serve_at_root=Truea2a_server_with_root = A2AServer( agent=agent, http_url="http://my-alb.amazonaws.com/calculator", serve_at_root=True # Serves at root even though URL has /calculator path)This flexibility allows you to:
- Add custom middleware
- Implement additional API endpoints
- Deploy agents behind load balancers with different path prefixes
- Configure custom task storage and event handling components
A2A Client Examples
Section titled “A2A Client Examples”Synchronous Client
Section titled “Synchronous Client”Here’s how to create a client that communicates with an A2A server synchronously:
import asyncioimport loggingfrom uuid import uuid4
import httpxfrom a2a.client import A2ACardResolver, ClientConfig, ClientFactoryfrom a2a.types import Message, Part, Role, TextPart
logging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 300 # set request timeout to 5 minutes
def create_message(*, role: Role = Role.user, text: str) -> Message: return Message( kind="message", role=role, parts=[Part(TextPart(kind="text", text=text))], message_id=uuid4().hex, )
async def send_sync_message(message: str, base_url: str = "http://127.0.0.1:9000"): async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as httpx_client: # Get agent card resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url) agent_card = await resolver.get_agent_card()
# Create client using factory config = ClientConfig( httpx_client=httpx_client, streaming=False, # Use non-streaming mode for sync response ) factory = ClientFactory(config) client = factory.create(agent_card)
# Create and send message msg = create_message(text=message)
# With streaming=False, this will yield exactly one result async for event in client.send_message(msg): if isinstance(event, Message): logger.info(event.model_dump_json(exclude_none=True, indent=2)) return event elif isinstance(event, tuple) and len(event) == 2: # (Task, UpdateEvent) tuple task, update_event = event logger.info(f"Task: {task.model_dump_json(exclude_none=True, indent=2)}") if update_event: logger.info(f"Update: {update_event.model_dump_json(exclude_none=True, indent=2)}") return task else: # Fallback for other response types logger.info(f"Response: {str(event)}") return event
# Usageasyncio.run(send_sync_message("what is 101 * 11"))Streaming Client
Section titled “Streaming Client”For streaming responses, use the streaming client:
import asyncioimport loggingfrom uuid import uuid4
import httpxfrom a2a.client import A2ACardResolver, ClientConfig, ClientFactoryfrom a2a.types import Message, Part, Role, TextPart
logging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 300 # set request timeout to 5 minutes
def create_message(*, role: Role = Role.user, text: str) -> Message: return Message( kind="message", role=role, parts=[Part(TextPart(kind="text", text=text))], message_id=uuid4().hex, )
async def send_streaming_message(message: str, base_url: str = "http://127.0.0.1:9000"): async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as httpx_client: # Get agent card resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url) agent_card = await resolver.get_agent_card()
# Create client using factory config = ClientConfig( httpx_client=httpx_client, streaming=True, # Use streaming mode ) factory = ClientFactory(config) client = factory.create(agent_card)
# Create and send message msg = create_message(text=message)
async for event in client.send_message(msg): if isinstance(event, Message): logger.info(event.model_dump_json(exclude_none=True, indent=2)) elif isinstance(event, tuple) and len(event) == 2: # (Task, UpdateEvent) tuple task, update_event = event logger.info(f"Task: {task.model_dump_json(exclude_none=True, indent=2)}") if update_event: logger.info(f"Update: {update_event.model_dump_json(exclude_none=True, indent=2)}") else: # Fallback for other response types logger.info(f"Response: {str(event)}")
# Usageasyncio.run(send_streaming_message("what is 101 * 11"))Strands A2A Tool
Section titled “Strands A2A Tool”Installation
Section titled “Installation”To use the A2A client tool, install strands-agents-tools with the A2A extra:
pip install 'strands-agents-tools[a2a_client]'Strands provides this tool for discovering and interacting with A2A agents without manually writing client code:
import asyncioimport loggingfrom strands import Agentfrom strands_tools.a2a_client import A2AClientToolProvider
logging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)
# Create A2A client tool provider with known agent URLs# Assuming you have an A2A server running on 127.0.0.1:9000# known_agent_urls is optionalprovider = A2AClientToolProvider(known_agent_urls=["http://127.0.0.1:9000"])
# Create agent with A2A client toolsagent = Agent(tools=provider.tools)
# The agent can now discover and interact with A2A servers# Standard usageresponse = agent("pick an agent and make a sample call")logger.info(response)
# Alternative Async usage# async def main():# response = await agent.invoke_async("pick an agent and make a sample call")# logger.info(response)# asyncio.run(main())The A2A client tool provides three main capabilities:
- Agent Discovery: Automatically discover available A2A agents and their capabilities
- Protocol Communication: Send messages to A2A agents using the standardized protocol
- Natural Language Interface: Interact with remote agents using natural language commands
A2A Agent as a Tool
Section titled “A2A Agent as a Tool”A2A agents can be wrapped as tools within your agent’s toolkit, similar to the Agents as Tools pattern but leveraging the A2A protocol for cross-platform communication.
You can use a class-based approach to discover agent cards upfront and avoid repeated discovery calls:
import asynciofrom uuid import uuid4import httpxfrom a2a.client import A2ACardResolver, ClientConfig, ClientFactoryfrom a2a.types import Message, Part, Role, TextPartfrom strands import Agent, tool
class A2AAgentTool: def __init__(self, agent_url: str, agent_name: str): self.agent_url = agent_url self.agent_name = agent_name self.agent_card = None self.client = None
async with httpx.AsyncClient(timeout=300) as httpx_client: resolver = A2ACardResolver(httpx_client=httpx_client, base_url=self.agent_url) self.agent_card = await resolver.get_agent_card()
config = ClientConfig(httpx_client=httpx_client, streaming=False) factory = ClientFactory(config) self.client = factory.create(self.agent_card)
@tool async def call_agent(self, message: str) -> str: """ Send a message to the A2A agent.
Args: message: The message to send to the agent
Returns: Response from the A2A agent """ try: msg = Message( kind="message", role=Role.user, parts=[Part(TextPart(kind="text", text=message))], message_id=uuid4().hex, )
async for event in self.client.send_message(msg): if isinstance(event, Message): response_text = "" for part in event.parts: if hasattr(part, 'text'): response_text += part.text return response_text
return f"No response received from {self.agent_name}"
except Exception as e: return f"Error contacting {self.agent_name}: {str(e)}"
# Usageresearch_agent = A2AAgentTool("http://research-agent.example.com:9000", "Research Agent")calculator_agent = A2AAgentTool("http://calculator-agent.example.com:9000", "Calculator Agent")
orchestrator = Agent( tools=[research_agent.call_agent, calculator_agent.call_agent])Troubleshooting
Section titled “Troubleshooting”If you encounter bugs or need to request features for A2A support:
- Check the A2A documentation for protocol-specific issues
- Report Strands-specific issues on GitHub
- Include relevant error messages and code samples in your reports