As AI Agent systems grow more complex, a single Agent can no longer handle every business scenario. A modern Agent system is usually composed of multiple specialized Agents—some excel at data analysis, others at natural language processing, and some focus on external API calls. These Agents need an efficient, reliable, and secure communication mechanism, and that is exactly why A2A (Agent to Agent) was created.
A2A is not just another RPC framework. It is a communication protocol designed specifically for Agent scenarios, with built-in semantics for Agent discovery, capability negotiation, task distribution, and result aggregation. Understanding A2A’s design philosophy is the key to building scalable multi-Agent systems.
Why A2A: From Monolithic to Distributed Agent Systems
Limitations of a Monolithic Agent
In the early days of a system, a “do-everything Agent” seems like the simplest solution:
User request → [Universal Agent] → Result
But as the business grows, this architecture quickly runs into bottlenecks:
- Bloated responsibilities: One Agent has to handle too many task types; prompts grow longer and context windows are wasted
- Iteration conflicts: Teams working on different features keep modifying the same Agent, causing frequent conflicts
- Performance bottlenecks: All requests go through the same execution path, so you cannot optimize for specific tasks
- Poor fault isolation: A bug in one feature can take down the entire Agent
- Technology lock-in: Everything must be implemented in one language, missing the strengths of each language’s ecosystem
Distributed Agent Architecture
The distributed architecture supported by A2A splits the system into multiple specialized Agents:
┌─────────────────┐
│ Orchestrator │
│ Agent (dispatch)│
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Data Agent │ │ NLP Agent │ │ API Agent │
│ (data) │ │ (language) │ │ (external) │
└──────────────┘ └──────────────┘ └──────────────┘
▲ ▲ ▲
└──────────────────┼──────────────────┘
│
┌────────┴────────┐
│ A2A Protocol │
│ (Agent comms) │
└─────────────────┘
Key advantages:
| Dimension | Monolithic Agent | Distributed Agent (A2A) |
|---|---|---|
| Responsibilities | Bloated | Single responsibility, high cohesion |
| Iteration speed | Slow (mutual interference) | Fast (independent deployment) |
| Performance | Cannot optimize per task | Each Agent scales independently |
| Fault tolerance | Single point of failure | Partial failures don’t affect the whole |
| Tech stack | Locked to one language | Free combination of languages |
| Reusability | Low | High (Agent as a Service) |
A2A vs MCP: Complementary, Not Competitive
A2A and MCP (Model Context Protocol) are often confused, but they solve problems at different layers:
Positioning Differences
┌─────────────────────────────────────────────────────────────┐
│ Agent System Architecture │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ A2A Layer │ │
│ │ Agent ↔ Agent Communication │ │
│ │ (task distribution, result aggregation, │ │
│ │ capability negotiation) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Agent A │ │ Agent B │ │ Agent C │ │
│ │ (Go) │ │ (Python) │ │ (Java) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MCP Layer │ │
│ │ Agent ↔ External Tool Communication │ │
│ │ (filesystem, database, API, browser) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ File Sys │ │ Database │ │ Ext. API │ │ Browser │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
Detailed Comparison
| Dimension | A2A | MCP |
|---|---|---|
| Communication direction | Bidirectional peer-to-peer (Agent ↔ Agent) | One-way invocation (Agent → Tool) |
| Communication target | Other Agents | External tools/systems |
| Core semantics | Task negotiation, capability discovery, streaming results | Tool invocation, context passing |
| Lifecycle | Long-lived sessions, multi-turn interaction | Single call, immediate response |
| State management | Stateful (Session preserved) | Usually stateless |
| Error handling | Complex (retry, degradation, timeout negotiation) | Simple (success/failure) |
| Typical scenarios | Multi-Agent collaboration, task orchestration | File I/O, database queries, API calls |
| Protocol layer | Application-layer protocol | Tool interface protocol |
A Real-World Collaboration Example
User: "Analyze this sales data, generate a report, and email it to the team"
Orchestrator Agent
│
├── A2A ──→ Data Agent (Go)
│ │
│ ├── MCP ──→ Read Excel file
│ ├── MCP ──→ Run data analysis
│ └── A2A ──→ Return analysis results
│
├── A2A ──→ Report Agent (Python)
│ │
│ ├── MCP ──→ Generate PDF report
│ └── A2A ──→ Return report URL
│
└── A2A ──→ Email Agent (Java)
│
├── MCP ──→ Send email
└── A2A ──→ Return send status
Orchestrator Agent ──→ Aggregates results, returns to user
In this flow:
- A2A handles task distribution and result collection between Agents
- MCP handles each Agent’s concrete interaction with external tools
Both are indispensable; together they form the complete Agent system communication stack.
A2A Protocol Core Concepts
Agent Card
Every Agent that exposes an A2A service needs to provide an Agent Card describing its capabilities, interface, and authentication method:
{
"name": "data-processor",
"version": "1.2.0",
"description": "Data processing expert supporting CSV/Excel/JSON analysis and conversion",
"url": "https://agent.example.com/a2a",
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"skills": [
{
"id": "csv-analysis",
"name": "CSV Data Analysis",
"description": "Analyze CSV files, return statistical summaries and visualization suggestions",
"inputModes": ["text", "file"],
"outputModes": ["text", "file"]
},
{
"id": "excel-conversion",
"name": "Excel Format Conversion",
"description": "Convert Excel to other formats (CSV/JSON/PDF)",
"inputModes": ["file"],
"outputModes": ["file"]
}
],
"authentication": {
"type": "apiKey",
"header": "X-API-Key"
}
}
Task
The basic unit of work in A2A is a Task, which has its own lifecycle:
Task lifecycle:
submitted → working → input-required → working → completed
↓ ↑
failed ←───────┘
cancelled
// A2A Task structure (simplified)
type Task struct {
ID string `json:"id"`
SessionID string `json:"sessionId"`
Status TaskStatus `json:"status"`
Input map[string]interface{} `json:"input"`
Output map[string]interface{} `json:"output,omitempty"`
Artifacts []Artifact `json:"artifacts,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type TaskStatus string
const (
TaskStatusSubmitted TaskStatus = "submitted"
TaskStatusWorking TaskStatus = "working"
TaskStatusInputRequired TaskStatus = "input-required"
TaskStatusCompleted TaskStatus = "completed"
TaskStatusFailed TaskStatus = "failed"
TaskStatusCancelled TaskStatus = "cancelled"
)
Message
The communication unit during Task execution:
type Message struct {
Role MessageRole `json:"role"` // user / agent
Parts []Part `json:"parts"` // message content parts
Timestamp time.Time `json:"timestamp"`
}
type MessageRole string
const (
RoleUser MessageRole = "user"
RoleAgent MessageRole = "agent"
)
type Part struct {
Type string `json:"type"` // text / file / data
Data interface{} `json:"data"`
}
Quick Start: Enabling A2A Support
Server Side: Exposing an Agent
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"google.golang.org/adk/a2a"
"google.golang.org/adk/agent"
)
func main() {
ctx := context.Background()
// Create the Agent
myAgent, err := agent.New(agent.Config{
Name: "data-processor",
Model: model,
Instruction: "You are a data processing expert...",
})
if err != nil {
log.Fatal(err)
}
// Create the A2A server
server, err := a2a.NewServer(ctx,
a2a.WithAgent(myAgent),
a2a.WithPort(8080),
a2a.WithAgentCard(a2a.AgentCard{
Name: "data-processor",
Version: "1.2.0",
Description: "Data processing expert supporting CSV/Excel/JSON",
URL: "http://localhost:8080/a2a",
Capabilities: a2a.Capabilities{
Streaming: true,
PushNotifications: false,
},
Skills: []a2a.Skill{
{
ID: "csv-analysis",
Name: "CSV Data Analysis",
Description: "Analyze CSV files, return statistical summaries",
InputModes: []string{"text", "file"},
OutputModes: []string{"text", "file"},
},
},
}),
a2a.WithAuth(a2a.APIKeyAuth{
Header: "X-API-Key",
Validator: func(key string) bool {
// Production: query the database or call an auth service
return key == "production-api-key" || key == "staging-api-key"
},
}),
a2a.WithRateLimit(100, time.Minute), // 100 requests per minute
a2a.WithCORS(a2a.CORSConfig{
AllowedOrigins: []string{"https://orchestrator.example.com"},
AllowedMethods: []string{"POST", "GET", "OPTIONS"},
}),
)
if err != nil {
log.Fatal(err)
}
// Start the server (with graceful shutdown support)
log.Println("A2A server starting on :8080")
if err := server.Serve(); err != nil {
log.Fatal(err)
}
}
Client Side: Calling an External Agent
package main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/adk/a2a/client"
)
func main() {
ctx := context.Background()
// Create the A2A client
c, err := a2aclient.New(ctx,
a2aclient.WithURL("http://data-agent:8080/a2a"),
a2aclient.WithAPIKey("production-api-key"),
a2aclient.WithTimeout(30*time.Second),
a2aclient.WithRetry(3, time.Second), // Retry up to 3 times on failure
)
if err != nil {
log.Fatal(err)
}
defer c.Close()
// 1. Get the Agent Card (capability discovery)
card, err := c.GetAgentCard(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Agent: %s v%s\n", card.Name, card.Version)
fmt.Printf("Skills: %d\n", len(card.Skills))
// 2. Send a task
task, err := c.SendTask(ctx, &a2a.Task{
Input: map[string]interface{}{
"skill": "csv-analysis",
"data": "file://uploads/sales.csv",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Task submitted: %s (status: %s)\n", task.ID, task.Status)
// 3. Poll task status (or use streaming updates)
for task.Status != a2a.TaskStatusCompleted && task.Status != a2a.TaskStatusFailed {
time.Sleep(time.Second)
task, err = c.GetTask(ctx, task.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Task status: %s\n", task.Status)
}
// 4. Get the result
if task.Status == a2a.TaskStatusCompleted {
fmt.Printf("Result: %v\n", task.Output)
} else {
fmt.Printf("Task failed: %v\n", task.Output)
}
}
A2A vs Generic HTTP API: The Essential Difference
| Feature | A2A | Generic HTTP API |
|---|---|---|
| Design goal | Agent-to-Agent collaboration | Generic data exchange |
| Semantic layer | Task lifecycle, capability negotiation | CRUD operations |
| State management | Built-in Task state machine | Usually stateless |
| Streaming support | Native (SSE/WebSocket) | Must be implemented separately |
| Error handling | Structured error codes + retry semantics | Relies on HTTP status codes |
| Authentication | Agent-level (API Key / OAuth) | User-level |
| Discovery | Automatic discovery via Agent Card | Manual config or Swagger |
| Multimodality | Native text/file/data support | Requires custom formats |
Key insight: A2A doesn’t replace HTTP APIs—it builds an Agent-collaboration semantic layer on top of HTTP. Think of A2A as “gRPC for the Agent world”: it defines standard message formats and interaction patterns so Agents built by different teams can collaborate seamlessly.
Production Considerations
Service Discovery
In a multi-Agent system, Agents need to discover each other dynamically:
// Service discovery with Consul
type AgentRegistry struct {
client *api.Client
}
func (r *AgentRegistry) RegisterAgent(card *a2a.AgentCard) error {
service := &api.AgentServiceRegistration{
ID: card.Name,
Name: "a2a-agent",
Tags: []string{"a2a", "v1"},
Port: 8080,
Address: "agent.internal",
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("%s/health", card.URL),
Interval: "10s",
Timeout: "5s",
},
Meta: map[string]string{
"agent_card_url": fmt.Sprintf("%s/agent.json", card.URL),
},
}
return r.client.Agent().ServiceRegister(service)
}
func (r *AgentRegistry) DiscoverAgents(skill string) ([]*a2a.AgentCard, error) {
services, _, err := r.client.Health().Service("a2a-agent", "", true, nil)
if err != nil {
return nil, err
}
var agents []*a2a.AgentCard
for _, svc := range services {
// Fetch the Agent Card
cardURL := svc.Service.Meta["agent_card_url"]
card, err := fetchAgentCard(cardURL)
if err != nil {
continue
}
// Filter Agents that provide the requested skill
for _, s := range card.Skills {
if s.ID == skill {
agents = append(agents, card)
break
}
}
}
return agents, nil
}
Circuit Breaking & Degradation
// Circuit breaking with gobreaker
type CircuitBreakerClient struct {
client *a2aclient.Client
breaker *gobreaker.CircuitBreaker
}
func NewCircuitBreakerClient(client *a2aclient.Client) *CircuitBreakerClient {
settings := gobreaker.Settings{
Name: "a2a-client",
MaxRequests: 100,
Interval: time.Minute,
Timeout: 5 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 10 && failureRatio >= 0.5
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
log.Printf("circuit breaker %s: %s -> %s", name, from, to)
},
}
return &CircuitBreakerClient{
client: client,
breaker: gobreaker.NewCircuitBreaker(settings),
}
}
func (c *CircuitBreakerClient) Call(ctx context.Context, req *a2a.Task) (*a2a.Task, error) {
result, err := c.breaker.Execute(func() (interface{}, error) {
return c.client.SendTask(ctx, req)
})
if err != nil {
return nil, err
}
return result.(*a2a.Task), nil
}
Deep Dive: Common Questions
Q: What’s the difference between A2A and an HTTP API?
A2A builds an Agent-collaboration semantic layer on top of HTTP:
- Task semantics: A2A Tasks have a full state machine (submitted → working → completed/failed); HTTP APIs are usually single request-response
- Capability discovery: A2A automatically discovers other Agents’ capabilities through the Agent Card; HTTP APIs require manually maintained interface documentation
- Streaming interaction: A2A natively supports streaming results (SSE), ideal for Agents’ incremental output
- Multimodality: A2A’s message format natively supports text, files, and structured data
Q: Can I use A2A and MCP together?
Highly recommended—they solve problems at different layers:
Agent A (Orchestrator)
│
├── A2A ──→ Agent B (Data Processing) ──→ MCP ──→ Database
│
├── A2A ──→ Agent C (NLP) ──→ MCP ──→ File System
│
└── A2A ──→ Agent D (API) ──→ MCP ──→ External REST API
- A2A: the orchestration layer between Agents
- MCP: the tool layer between Agents and the outside world
They complement each other and together form a complete Agent system communication architecture.
Next Steps
Now that you understand the A2A concept, let’s look at how to expose a Go Agent for external calls.
← Cloud Run / GKE Deployment | Exposing →
Follow “Mengshou Programming” on WeChat for more hands-on Go ADK tutorials—weekly updates on practical Go / AI programming content.
