A2A
A2A
https://github.com/a2aproject/A2A
An open protocol enabling communication and interoperability between opaque agentic applications.
The Agent2Agent (A2A) protocol addresses a critical challenge in the AI landscape: enabling gen AI agents, built on diverse frameworks by different companies running on separate servers, to communicate and collaborate effectively - as agents, not just as tools. A2A aims to provide a common language for agents, fostering a more interconnected, powerful, and innovative AI ecosystem.
With A2A, agents can:
- Discover each other's capabilities.
- Negotiate interaction modalities (text, forms, media).
- Securely collaborate on long running tasks.
- Operate without exposing their internal state, memory, or tools.
As AI agents become more prevalent, their ability to interoperate is crucial for building complex, multi-functional applications. A2A aims to:
- Break Down Silos: Connect agents across different ecosystems.
- Enable Complex Collaboration: Allow specialized agents to work together on tasks that a single agent cannot handle alone.
- Promote Open Standards: Foster a community-driven approach to agent communication, encouraging innovation and broad adoption.
- Preserve Opacity: Allow agents to collaborate without needing to share internal memory, proprietary logic, or specific tool implementations, enhancing security and protecting intellectual property.
- Standardized Communication: JSON-RPC 2.0 over HTTP(S).
- Agent Discovery: Via "Agent Cards" detailing capabilities and connection info.
- Flexible Interaction: Supports synchronous request/response, streaming (SSE), and asynchronous push notifications.
- Rich Data Exchange: Handles text, files, and structured JSON data.
- Enterprise-Ready: Designed with security, authentication, and observability in mind.
https://a2a-protocol.org/latest/topics/agent-discovery/#2-curated-registries-catalog-based-discovery
https://github.com/fanqingsong/a2a-samples
https://github.com/a2aproject/a2a-python
https://github.com/a2aproject/a2a-inspector
Build Multi-Agent Systems using A2A SDK
https://github.com/fanqingsong/a2a-samples
# pylint: disable=logging-fstring-interpolation import asyncio import json import logging import os import uuid from typing import Any import httpx from a2a.client import A2ACardResolver from a2a.types import ( AgentCard, MessageSendParams, Part, SendMessageRequest, SendMessageResponse, SendMessageSuccessResponse, Task, ) from dotenv import load_dotenv from google.adk import Agent from google.adk.agents.callback_context import CallbackContext from google.adk.agents.readonly_context import ReadonlyContext from google.adk.tools.tool_context import ToolContext from remote_agent_connection import ( RemoteAgentConnections, TaskUpdateCallback, ) logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) load_dotenv() def convert_part(part: Part, tool_context: ToolContext): """Convert a part to text. Only text parts are supported.""" if part.type == 'text': return part.text return f'Unknown type: {part.type}' def convert_parts(parts: list[Part], tool_context: ToolContext): """Convert parts to text.""" rval = [] for p in parts: rval.append(convert_part(p, tool_context)) return rval def create_send_message_payload( text: str, task_id: str | None = None, context_id: str | None = None ) -> dict[str, Any]: """Helper function to create the payload for sending a task.""" payload: dict[str, Any] = { 'message': { 'role': 'user', 'parts': [{'type': 'text', 'text': text}], 'messageId': uuid.uuid4().hex, }, } if task_id: payload['message']['taskId'] = task_id if context_id: payload['message']['contextId'] = context_id return payload class RoutingAgent: """The Routing agent. This is the agent responsible for choosing which remote seller agents to send tasks to and coordinate their work. """ def __init__( self, task_callback: TaskUpdateCallback | None = None, ): self.task_callback = task_callback self.remote_agent_connections: dict[str, RemoteAgentConnections] = {} self.cards: dict[str, AgentCard] = {} self.agents: str = '' async def _async_init_components( self, remote_agent_addresses: list[str] ) -> None: """Asynchronous part of initialization.""" # Use a single httpx.AsyncClient for all card resolutions for efficiency async with httpx.AsyncClient(timeout=30) as client: for address in remote_agent_addresses: card_resolver = A2ACardResolver( client, address ) # Constructor is sync try: card = ( await card_resolver.get_agent_card() ) # get_agent_card is async remote_connection = RemoteAgentConnections( agent_card=card, agent_url=address ) self.remote_agent_connections[card.name] = remote_connection self.cards[card.name] = card except httpx.ConnectError as e: logger.debug( 'ERROR: Failed to get agent card from %s: %s', address, e, ) except Exception as e: # Catch other potential errors logger.debug( 'ERROR: Failed to initialize connection for %s: %s', address, e, ) # Populate self.agents using the logic from original __init__ (via list_remote_agents) agent_info = [] for agent_detail_dict in self.list_remote_agents(): agent_info.append(json.dumps(agent_detail_dict)) self.agents = '\n'.join(agent_info) @classmethod async def create( cls, remote_agent_addresses: list[str], task_callback: TaskUpdateCallback | None = None, ) -> 'RoutingAgent': """Create and asynchronously initialize an instance of the RoutingAgent.""" instance = cls(task_callback) await instance._async_init_components(remote_agent_addresses) return instance def create_agent(self) -> Agent: """Create an instance of the RoutingAgent.""" gemini_model = os.getenv('GEMINI_MODEL', 'gemini-2.5-flash') return Agent( model=gemini_model, name='Routing_agent', instruction=self.root_instruction, before_model_callback=self.before_model_callback, description=( 'This Routing agent orchestrates the decomposition of the user asking for weather forecast or airbnb accommodation' ), tools=[ self.send_message, ], ) def root_instruction(self, context: ReadonlyContext) -> str: """Generate the root instruction for the RoutingAgent.""" current_agent = self.check_active_agent(context) return f""" **Role:** You are an expert Routing Delegator. Your primary function is to accurately delegate user inquiries regarding weather or accommodations to the appropriate specialized remote agents. **Core Directives:** * **Task Delegation:** Utilize the `send_message` function to assign actionable tasks to remote agents. * **Contextual Awareness for Remote Agents:** If a remote agent repeatedly requests user confirmation, assume it lacks access to the full conversation history. In such cases, enrich the task description with all necessary contextual information relevant to that specific agent. * **Autonomous Agent Engagement:** Never seek user permission before engaging with remote agents. If multiple agents are required to fulfill a request, connect with them directly without requesting user preference or confirmation. * **Transparent Communication:** Always present the complete and detailed response from the remote agent to the user. * **User Confirmation Relay:** If a remote agent asks for confirmation, and the user has not already provided it, relay this confirmation request to the user. * **Focused Information Sharing:** Provide remote agents with only relevant contextual information. Avoid extraneous details. * **No Redundant Confirmations:** Do not ask remote agents for confirmation of information or actions. * **Tool Reliance:** Strictly rely on available tools to address user requests. Do not generate responses based on assumptions. If information is insufficient, request clarification from the user. * **Prioritize Recent Interaction:** Focus primarily on the most recent parts of the conversation when processing requests. * **Active Agent Prioritization:** If an active agent is already engaged, route subsequent related requests to that agent using the appropriate task update tool. **Agent Roster:** * Available Agents: `{self.agents}` * Currently Active Seller Agent: `{current_agent['active_agent']}` """ def check_active_agent(self, context: ReadonlyContext): state = context.state if ( 'session_id' in state and 'session_active' in state and state['session_active'] and 'active_agent' in state ): return {'active_agent': f'{state["active_agent"]}'} return {'active_agent': 'None'} def before_model_callback( self, callback_context: CallbackContext, llm_request, ): state = callback_context.state if 'session_active' not in state or not state['session_active']: if 'session_id' not in state: state['session_id'] = str(uuid.uuid4()) state['session_active'] = True def list_remote_agents(self): """List the available remote agents you can use to delegate the task.""" if not self.cards: return [] remote_agent_info = [] for card in self.cards.values(): logger.debug( 'Found agent card: %s', card.model_dump(exclude_none=True) ) logger.debug('=' * 100) remote_agent_info.append( {'name': card.name, 'description': card.description} ) return remote_agent_info async def send_message( self, agent_name: str, task: str, tool_context: ToolContext, ): """Sends a task to remote seller agent. This will send a message to the remote agent named agent_name. Args: agent_name: The name of the agent to send the task to. task: The comprehensive conversation context summary and goal to be achieved regarding user inquiry and purchase request. tool_context: The tool context this method runs in. Yields: A dictionary of JSON data. """ if agent_name not in self.remote_agent_connections: raise ValueError(f'Agent {agent_name} not found') state = tool_context.state state['active_agent'] = agent_name client = self.remote_agent_connections[agent_name] if not client: raise ValueError(f'Client not available for {agent_name}') task_id = state['task_id'] if 'task_id' in state else None if 'context_id' in state: context_id = state['context_id'] else: context_id = str(uuid.uuid4()) message_id = '' metadata = {} if 'input_message_metadata' in state: metadata.update(**state['input_message_metadata']) if 'message_id' in state['input_message_metadata']: message_id = state['input_message_metadata']['message_id'] if not message_id: message_id = str(uuid.uuid4()) payload = { 'message': { 'role': 'user', 'parts': [ {'type': 'text', 'text': task} ], # Use the 'task' argument here 'messageId': message_id, }, } if task_id: payload['message']['taskId'] = task_id if context_id: payload['message']['contextId'] = context_id message_request = SendMessageRequest( id=message_id, params=MessageSendParams.model_validate(payload) ) send_response: SendMessageResponse = await client.send_message( message_request=message_request ) logger.debug( 'send_response', send_response.model_dump_json(exclude_none=True, indent=2), ) if not isinstance(send_response.root, SendMessageSuccessResponse): logger.debug('received non-success response. Aborting get task ') return None if not isinstance(send_response.root.result, Task): logger.debug('received non-task response. Aborting get task ') return None task = send_response.root.result return task def _get_initialized_routing_agent_sync() -> Agent: """Synchronously creates and initializes the RoutingAgent.""" async def _async_main() -> Agent: routing_agent_instance = await RoutingAgent.create( remote_agent_addresses=[ os.getenv('AIR_AGENT_URL', 'http://localhost:10002'), os.getenv('WEA_AGENT_URL', 'http://localhost:10001'), ] ) return routing_agent_instance.create_agent() try: return asyncio.run(_async_main()) except RuntimeError as e: if 'asyncio.run() cannot be called from a running event loop' in str(e): logger.debug( 'Warning: Could not initialize RoutingAgent with asyncio.run(): %s. ' 'This can happen if an event loop is already running (e.g., in Jupyter). ' 'Consider initializing RoutingAgent within an async function in your application.', e, ) raise root_agent = _get_initialized_routing_agent_sync()