Google’s Agent-to-Agent (A2A) protocol gives you a standard wire format for agents to discover each other, exchange tasks, and stream results – regardless of what framework each agent runs on. Instead of gluing agents together with custom REST endpoints and ad-hoc message schemas, A2A defines an open protocol with JSON-RPC transport, agent cards for capability discovery, and a task lifecycle that handles everything from submission to streaming partial results.
This guide walks you through building a multi-agent orchestrator in Python where a coordinator agent discovers specialist agents via their agent cards, delegates subtasks over A2A, and streams progress back to the caller using Server-Sent Events (SSE).
Install the SDK
The official a2a-sdk package from Google requires Python 3.10+. Install it with the HTTP server extras so you get Starlette and Uvicorn support out of the box:
| |
This pulls in the core types (AgentCard, AgentSkill, AgentCapabilities), the server framework (A2AStarletteApplication, DefaultRequestHandler), the client (A2AClient, A2ACardResolver), and utility functions for building messages and artifacts.
Agent Cards: Declaring What Your Agent Can Do
Every A2A agent publishes an Agent Card – a JSON document that describes its name, version, skills, supported input/output modes, and endpoint URL. Clients fetch this card from a well-known path (/.well-known/agent.json) before sending any tasks.
Here is a card for a research agent that can summarize topics:
| |
The capabilities field tells clients this agent supports streaming responses. The skills list is how the orchestrator decides which agent to route a given subtask to – match the task description against skill tags and descriptions.
Implement a Specialist Agent
Each specialist agent needs an AgentExecutor subclass. This is where you put the actual logic – call an LLM, run a tool, query a database. The executor receives a RequestContext with the incoming message and an EventQueue where you push status updates and final results.
Here is a full research agent that calls OpenAI to generate a summary:
| |
The TaskUpdater is a helper that wraps EventQueue and handles the boilerplate of creating TaskStatusUpdateEvent and TaskArtifactUpdateEvent objects. You call start_work() to signal the “working” state, add_artifact() to push results, and complete() to mark the task as done.
Run the Agent as an A2A Server
Wrap the executor in a Starlette app and serve it with Uvicorn:
| |
The DefaultRequestHandler manages the JSON-RPC layer, routes message/send and message/stream calls to your executor, and serves the agent card at /.well-known/agent.json. The InMemoryTaskStore tracks task state so clients can poll for status on long-running work.
You can spin up a second agent (say a code-review agent on port 8002) by writing another executor and card with different skills.
Build the Orchestrator Client
The orchestrator is itself an agent that discovers other agents and delegates work to them. Here is the core delegation function:
| |
The A2ACardResolver hits /.well-known/agent.json on the target agent, parses the card, and gives you a typed AgentCard object. Then A2AClient uses that card’s URL to send tasks via JSON-RPC.
Streaming Updates with SSE
For long-running tasks, you want partial progress instead of waiting for the final answer. Switch from send_message to send_message_streaming and iterate over the SSE chunks:
| |
On the server side, the agent executor pushes events to the EventQueue as they happen. The DefaultRequestHandler serializes these into SSE frames over the HTTP response. Status updates arrive as TaskStatusUpdateEvent (with state transitions like working or input-required), and results arrive as TaskArtifactUpdateEvent objects that can be chunked for large payloads.
Multi-Agent Orchestration
Now put it all together. The orchestrator maintains a registry of specialist agents, matches incoming requests to the right agent based on skill tags, and fans out subtasks:
| |
This is a minimal orchestrator. A production version would use an LLM to decompose complex requests into multiple subtasks, fan them out to different agents in parallel with asyncio.gather, and merge the results before returning to the caller.
Task Lifecycle
Every task in A2A goes through a state machine:
- submitted – the client sent the request, the server acknowledged it
- working – the agent executor is processing the task
- input-required – the agent needs clarification from the user (enables multi-turn)
- completed – the agent produced a final artifact
- failed – something went wrong
- canceled – the client or server aborted the task
Your executor controls these transitions through the TaskUpdater:
| |
Clients can also cancel a running task, which triggers the cancel() method on your executor.
Common Errors and Fixes
“Connection refused” when the client calls an agent
The agent server is not running or is on the wrong port. Double-check the url field in your AgentCard matches the host/port you passed to uvicorn.run(). Also verify no firewall rules are blocking the port.
“Agent card not found” (404 on /.well-known/agent.json)
You are hitting the wrong base URL. The A2AStarletteApplication serves the card automatically, but only if you use server.build() to create the ASGI app. If you wrote a custom Starlette app, you need to mount the well-known route yourself.
InvalidParamsError when sending a message
The message payload is malformed. Every message needs a role (either "user" or "agent"), a non-empty parts array, and a unique messageId. Check that you are wrapping text in the correct structure:
| |
Streaming hangs and never completes
Your executor is not calling updater.complete() (or updater.failed()) after the last artifact. The SSE stream stays open until a terminal state event arrives. Always pair add_artifact(last_chunk=True) with a complete() or failed() call.
UnsupportedOperationError on cancel
The default cancel implementation raises this error. If you need cancellation support, override the cancel() method in your executor and implement actual cleanup logic (like canceling an in-flight LLM call).
Import errors after installing a2a-sdk
Make sure you installed the extras: pip install "a2a-sdk[http-server]". The base package does not include Starlette, Uvicorn, or the HTTP server components. If you need gRPC support, add that extra too: pip install "a2a-sdk[http-server,grpc]".
Related Guides
- How to Build a Workflow Agent with Human Feedback Loops
- How to Build a Document QA Agent with PDF Parsing and Tool Use
- How to Build a Web Research Agent with LLMs and Search APIs
- How to Build a Monitoring Agent with Prometheus Alerts and LLM Diagnosis
- How to Build a Memory-Augmented Agent with Vector Search
- How to Build an Email Triage Agent with LLMs and IMAP
- How to Build a Data Analysis Agent with Code Execution
- How to Build a Browser Automation Agent with Playwright and LLMs
- How to Build a Tool-Calling Agent with Claude and MCP
- How to Build a SQL Query Agent with LLMs and Tool Calling