梦兽编程
AI_SUITE

A2A Protocol: Universal Language for Cross-Agent Communication

Detailed explanation of A2A (Agent to Agent) protocol design, differences from MCP, and how to enable A2A support in ADK Go.

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:

  1. Bloated responsibilities: One Agent has to handle too many task types; prompts grow longer and context windows are wasted
  2. Iteration conflicts: Teams working on different features keep modifying the same Agent, causing frequent conflicts
  3. Performance bottlenecks: All requests go through the same execution path, so you cannot optimize for specific tasks
  4. Poor fault isolation: A bug in one feature can take down the entire Agent
  5. 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:

DimensionMonolithic AgentDistributed Agent (A2A)
ResponsibilitiesBloatedSingle responsibility, high cohesion
Iteration speedSlow (mutual interference)Fast (independent deployment)
PerformanceCannot optimize per taskEach Agent scales independently
Fault toleranceSingle point of failurePartial failures don’t affect the whole
Tech stackLocked to one languageFree combination of languages
ReusabilityLowHigh (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

DimensionA2AMCP
Communication directionBidirectional peer-to-peer (Agent ↔ Agent)One-way invocation (Agent → Tool)
Communication targetOther AgentsExternal tools/systems
Core semanticsTask negotiation, capability discovery, streaming resultsTool invocation, context passing
LifecycleLong-lived sessions, multi-turn interactionSingle call, immediate response
State managementStateful (Session preserved)Usually stateless
Error handlingComplex (retry, degradation, timeout negotiation)Simple (success/failure)
Typical scenariosMulti-Agent collaboration, task orchestrationFile I/O, database queries, API calls
Protocol layerApplication-layer protocolTool 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

FeatureA2AGeneric HTTP API
Design goalAgent-to-Agent collaborationGeneric data exchange
Semantic layerTask lifecycle, capability negotiationCRUD operations
State managementBuilt-in Task state machineUsually stateless
Streaming supportNative (SSE/WebSocket)Must be implemented separately
Error handlingStructured error codes + retry semanticsRelies on HTTP status codes
AuthenticationAgent-level (API Key / OAuth)User-level
DiscoveryAutomatic discovery via Agent CardManual config or Swagger
MultimodalityNative text/file/data supportRequires 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.

Frequently Asked Questions

Why do multiple Agents need A2A?

A single monolithic Agent faces bloated responsibilities, iteration conflicts, performance bottlenecks, and poor fault isolation.

How is A2A different from a generic RPC framework?

A2A is designed for Agents with built-in discovery, capability negotiation, task distribution, and result aggregation.

Are A2A and MCP competitors?

No, they complement each other: A2A is Agent-to-Agent, while MCP connects Agents to tools and data.

What are the benefits of distributed Agent architecture?

Single responsibility, independent deployment/scaling, better fault isolation, and language freedom.

How do I enable A2A support in ADK Go?

Use the google.golang.org/adk/a2a package to create a server or client and configure the Agent Card.