ARTICLE  ·  14 MIN READ  ·  MARCH 02, 2026

Chapter 15: Inter-Agent Communication (A2A)

MCP connects agents to tools. A2A connects agents to agents. The Agent2Agent protocol is the missing standard that lets any AI agent — regardless of framework — collaborate with any other.


Before You Start — Key Terms Explained

Interoperability: The ability of different systems — built by different companies, using different technologies — to work together without custom translation layers. USB is an interoperable standard. HTTP is an interoperable standard. A2A aims to be the interoperable standard for agent communication.

Protocol: A set of agreed-upon rules for communication. (Covered in Chapter 10 — MCP.) A2A is a protocol specifically for agent-to-agent communication over HTTP.

JSON-RPC 2.0: A protocol for calling functions remotely using JSON messages. Format: {"jsonrpc":"2.0","method":"sendTask","params":{...},"id":1}. A2A uses JSON-RPC 2.0 as its message format over HTTP/HTTPS.

Server-Sent Events (SSE): A web standard where a server maintains a persistent HTTP connection and pushes data to the client over time, without the client needing to make repeated requests. Used in A2A for streaming updates from agent to agent.

mTLS (Mutual TLS): A security protocol where BOTH sides of a connection authenticate themselves — not just the server proving its identity to the client, but the client also proving its identity to the server. Prevents unauthorized agents from connecting to your A2A server.

Opaque system: A system where the internal implementation is hidden from the caller. An A2A server is "opaque" to its client — the client knows what the server can do (from the Agent Card) but not how it does it. This is essential for framework independence.

Webhook: A URL that a server can call to notify another system of an event. In A2A, a client can register a webhook URL and the server will POST to it when a long-running task completes — no need to poll.

In Chapter 7, we built multi-agent systems where multiple agents collaborate. In Chapter 10, we standardized how agents connect to tools via MCP. But there’s a gap that neither pattern addresses:

What happens when an agent built in Google ADK needs to collaborate with an agent built in LangGraph, which needs to call an agent built in CrewAI?

Without a standard, each pair of collaborating agents needs a custom integration — agent A knows how to call agent B using B’s specific API format, agent B knows how to call C using C’s format, and so on. This is the same integration explosion problem that MCP solved for tools — but at the agent layer.

The table below shows why the problem gets serious fast:

Agents Framework pairs Custom integrations needed (without A2A)
5 10 pairs 10 integrations
10 45 pairs 45 integrations
50 1,225 pairs 1,225 integrations

Agent2Agent (A2A) is Google’s open protocol, launched in April 2025, that solves this. It defines a universal communication standard for agents — any agent that speaks A2A can work with any other A2A-compliant agent, regardless of whether they’re built with ADK, LangGraph, CrewAI, Azure AI Foundry, or any other framework.


A2A vs MCP: Two Complementary Protocols

Before going deeper into A2A, the most important conceptual clarification:

🔧
Model Context Protocol (MCP)
Agent ↔ Tools / External Systems
MCP defines how an agent connects to external tools and data sources. When an agent needs to query a database, call a weather API, read a file, or execute code — it uses MCP to communicate with those resources.
Agent → database (query)
Agent → file system (read/write)
Agent → REST API (call)
Agent → code interpreter (execute)
Analogy: How a person uses tools — a hammer, a computer, a calculator. The tools don't know about each other.
+
🤝
Agent2Agent Protocol (A2A)
Agent ↔ Agent
A2A defines how agents communicate with each other. When an orchestrating agent needs to delegate a task to a specialist agent — possibly built by a different company in a different framework — it uses A2A to discover and interact with that remote agent.
ADK agent → LangGraph agent (delegate task)
CrewAI agent → ADK agent (request analysis)
Orchestrator → specialist agents (parallel)
Any agent → any A2A-compliant agent
Analogy: How team members collaborate — a manager delegates to specialists. The specialists may have completely different backgrounds and use different methods.

The key insight: MCP and A2A are complementary. A single agent might use MCP to connect to its tools (databases, APIs, file systems) AND use A2A to collaborate with other agents. They solve different problems at different levels of the stack.


The Three Core Actors

Every A2A interaction involves three roles:

A2A ACTOR MODEL — three roles in every interaction
User
The human initiating the request. Provides the high-level goal. Doesn't need to know which agents will collaborate to achieve it.
A2A Client (Client Agent)
An AI agent acting on behalf of the user. Discovers remote agents via Agent Cards, formulates task requests, and coordinates the workflow. Could be ADK, LangGraph, CrewAI, or any framework.
→ HTTP →
A2A Server (Remote Agent)
An AI agent that exposes an HTTP endpoint for receiving tasks. Processes requests using its own internal logic (opaque to the client) and returns results. Can be any framework.

Why “opaque”? The client agent doesn’t know whether the server agent uses GPT-4, Gemini, or Llama. It doesn’t know if the server uses LangGraph, CrewAI, or a custom implementation. All it knows is the Agent Card — what tasks the server can handle, what inputs it accepts, and what outputs it produces. This opaqueness is the key that makes framework independence possible.


The Agent Card: A Digital Identity

The Agent Card is the cornerstone of A2A. It’s a JSON file that describes everything another agent needs to know to interact with yours — without you needing to share your internal implementation.

AGENT CARD EXPLORER — click any field to see what it means
{
  "name": "WeatherBot",
  "description": "Provides weather forecasts",
  "url": "http://weather.example.com/a2a",
  "version": "1.0.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "stateTransitionHistory": true
  },
  "authentication": {
    "schemes": ["apiKey"]
  },
  "skills": [
    {
      "id": "get_current_weather",
      "name": "Get Current Weather",
      "description": "Real-time weather for any location",
      "inputModes": ["text"],
      "outputModes": ["text"],
      "examples": ["What's the weather in Paris?"],
      "tags": ["weather", "current", "real-time"]
    }
  ]
}
Click any field name
to see what it means and why it matters for agent interoperability

Where is the Agent Card served? By convention, agents serve their Agent Card at /.well-known/agent.json on their domain. This is the Well-Known URI discovery method — any A2A client can find your agent’s capabilities by fetching https://your-agent-domain.com/.well-known/agent.json. No prior coordination needed.


The Four Interaction Mechanisms

A2A supports four distinct ways for agents to communicate, each suited to different latency and complexity requirements:

A2A INTERACTION MECHANISMS — select to compare
Synchronous Request/Response
Client sends a request and waits — blocking — until the server returns a complete response. Simple and predictable. Suitable for tasks that complete within seconds.
When to use: Quick lookups (exchange rates, weather, simple calculations) where the response is fast and the client can afford to wait.
Client → POST /a2a (sendTask) → blocks → Server processes → complete result → Client continues
{
  "jsonrpc": "2.0",
  "method": "sendTask",
  "id": "1",
  "params": {
    "id": "task-001",
    "sessionId": "session-001",
    "message": {
      "role": "user",
      "parts": [{"type": "text", "text": "Exchange rate USD to EUR?"}]
    },
    "acceptedOutputModes": ["text/plain"]
  }
}
Asynchronous Polling
Client sends request → server immediately returns "working" status + task ID → client goes off and does other work → client periodically polls for completion. Non-blocking for the client.
When to use: Tasks that take 10+ seconds (research, document generation, complex analysis) where blocking the client thread would be wasteful.
Client → sendTask → "working" + taskId → [client does other things] → Client polls getTask(taskId) → eventually "completed" + result
// Initial request → server returns: {"status": "working", "id": "task-001"}
// Later poll:
{
  "jsonrpc": "2.0",
  "method": "getTask",
  "params": {"id": "task-001"}
}
// Response: {"status": "completed", "result": {...}}
Streaming (Server-Sent Events)
Client uses sendTaskSubscribe to open a persistent HTTP connection. The server pushes incremental results as they become available — like watching a document being written in real time. No polling needed.
When to use: Long documents, live research, real-time analysis where showing progress matters more than waiting for final output. The agent declares "streaming": true in its Agent Card.
Client → sendTaskSubscribe → open SSE connection → Server pushes: chunk 1 → chunk 2 → chunk 3 → "completed"
{
  "jsonrpc": "2.0",
  "method": "sendTaskSubscribe",
  "id": "2",
  "params": {
    "id": "task-002",
    "message": {
      "parts": [{"type": "text", "text": "Write a market analysis..."}]
    }
  }
}
// Server sends SSE events:
// data: {"type": "progress", "content": "Researching market trends..."}
// data: {"type": "artifact", "content": "## Market Analysis\n\n..."}
// data: {"type": "completed"}
Push Notifications (Webhooks)
Client registers a webhook URL when submitting a task. The client disconnects entirely. When the server finishes (hours or days later), it POSTs the result to the webhook URL. No persistent connection, no polling.
When to use: Very long-running tasks (overnight batch processing, multi-day research) where maintaining any connection would be wasteful. The agent declares "pushNotifications": true in its Agent Card.
Client → sendTask + webhookUrl → Server: "acknowledged" → [hours pass] → Server → POST webhookUrl → result delivered
{
  "jsonrpc": "2.0",
  "method": "sendTask",
  "params": {
    "id": "task-003",
    "message": {"parts": [{"type": "text", "text": "Analyze all Q1 data..."}]},
    "pushNotification": {
      "url": "https://my-agent.com/webhook/task-003",
      "authentication": {"schemes": ["bearer"]}
    }
  }
}

The Task Lifecycle

Every piece of work in A2A is a Task — an asynchronous unit of work with a unique ID and a defined state machine:

graph LR
    SUB([submitted]) --> WRK[working]
    WRK -->|needs more info| INP[input-required]
    INP -->|client provides info| WRK
    WRK --> COMP([completed])
    WRK --> FAIL([failed])
    WRK --> CANC([cancelled])
    style SUB fill:#141b2d,stroke:#2698ba,color:#e0e0e0
    style WRK fill:#141b2d,stroke:#e6a817,color:#e0e0e0
    style INP fill:#141b2d,stroke:#c97af2,color:#e0e0e0
    style COMP fill:#141b2d,stroke:#4fc97e,color:#e0e0e0
    style FAIL fill:#141b2d,stroke:#ff6b6b,color:#e0e0e0
    style CANC fill:#141b2d,stroke:#4a5a6a,color:#e0e0e0

The input-required state is particularly important for multi-turn interactions. A remote agent might partially complete a task, determine it needs more information from the client, and pause — waiting for the client to provide the missing data before continuing. This enables rich back-and-forth collaboration between agents, not just one-shot requests.


The Code: Building an A2A Calendar Agent

The A2A samples repository provides a complete example of a calendar agent built with ADK that exposes itself as an A2A server. Let’s walk through the key components.

Part 1: The ADK Agent with Calendar Tools

import datetime
from google.adk.agents import LlmAgent
from google.adk.tools.google_api_tool import CalendarToolset

async def create_agent(client_id, client_secret) -> LlmAgent:
    """Constructs the ADK calendar agent with Google Calendar access."""
    # Initialize the Google Calendar tool with OAuth credentials
    toolset = CalendarToolset(client_id=client_id, client_secret=client_secret)

    return LlmAgent(
        model       = 'gemini-2.0-flash-001',
        name        = 'calendar_agent',
        description = "An agent that can help manage a user's calendar",
        instruction = f"""
You are an agent that can help manage a user's calendar.
Users will request information about their calendar or ask to make changes.
Use the provided tools for interacting with the Calendar API.

If not specified, assume the user wants their 'primary' calendar.
When using Calendar API tools, use well-formed RFC3339 timestamps.
Today is {datetime.datetime.now()}.
""",
        tools = await toolset.get_tools(),
    )

CalendarToolset: A pre-built ADK toolset that wraps the Google Calendar API as MCP-compatible tools. It handles OAuth 2.0 authentication flow, token management, and API call formatting. toolset.get_tools() returns a list of tool objects that the agent can call to read/write calendar data.

f"Today is {datetime.datetime.now()}" in the instruction: This is dynamic instruction injection — the current date is embedded at agent creation time. Without it, the LLM might calculate “tomorrow” relative to its training cutoff date rather than today. This small detail ensures all date calculations are anchored to real-world time.

Why async def create_agent()? The toolset.get_tools() call requires async execution (it may need to make network calls to fetch the tool schema from the Calendar API). The entire function is marked async to enable this. The await keyword pauses execution until get_tools() completes before constructing the agent.

Part 2: Defining the Agent Card and Starting the A2A Server

from a2a.types import AgentSkill, AgentCard, AgentCapabilities
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from google.adk.runners import Runner

Why these imports? A2A provides a Python SDK (a2a package) that handles the protocol details — JSON-RPC framing, task state management, SSE streaming, etc. Without it, you’d need to implement the full A2A protocol specification manually. The ADK Runner manages the agent’s execution lifecycle, session management, and tool call routing.

def main(host: str, port: int):
    # 1. Define the agent's skill (what it can do)
    skill = AgentSkill(
        id          = 'check_availability',
        name        = 'Check Availability',
        description = "Checks a user's availability for a time using their Google Calendar",
        tags         = ['calendar'],
        examples     = ['Am I free from 10am to 11am tomorrow?'],
    )

AgentSkill is the fine-grained capability descriptor. While the AgentCard describes the agent overall, each AgentSkill describes a specific capability. This enables precise routing: if an orchestrating agent needs to check calendar availability (and not, say, create events), it can specifically request the check_availability skill. This granularity makes multi-agent systems more precise.

    # 2. Define the Agent Card (the agent's public identity)
    agent_card = AgentCard(
        name                = 'Calendar Agent',
        description         = "An agent that can manage a user's calendar",
        url                 = f'http://{host}:{port}/',
        version             = '1.0.0',
        defaultInputModes   = ['text'],
        defaultOutputModes  = ['text'],
        capabilities        = AgentCapabilities(streaming=True),
        skills              = [skill],
    )

AgentCard is the agent’s published identity. This is what gets served at /.well-known/agent.json. Any A2A client — regardless of framework — can fetch this URL and immediately understand: what this agent does, where to send requests, what authentication is required, whether streaming is supported, and which specific skills are available. The Agent Card makes the agent self-describing and discoverable without any prior coordination.

    # 3. Create the ADK agent with credentials from environment
    adk_agent = asyncio.run(create_agent(
        client_id     = os.getenv('GOOGLE_CLIENT_ID'),
        client_secret = os.getenv('GOOGLE_CLIENT_SECRET'),
    ))

Why os.getenv() for credentials? Never hardcode API keys or OAuth secrets in source code — they’d be committed to version control and exposed. Environment variables are the standard way to pass secrets to running processes without embedding them in code. In production, these would be set via secrets management services (AWS Secrets Manager, Google Secret Manager, etc.).

    # 4. Wire the ADK agent to the A2A execution infrastructure
    runner = Runner(
        app_name        = agent_card.name,
        agent           = adk_agent,
        artifact_service = InMemoryArtifactService(),   # stores file outputs
        session_service  = InMemorySessionService(),    # tracks conversation state
        memory_service   = InMemoryMemoryService(),     # long-term memory
    )

    agent_executor = ADKAgentExecutor(runner, agent_card)

ADKAgentExecutor is the bridge between the A2A protocol layer and the ADK agent. When an A2A task request arrives (a JSON-RPC sendTask message), the executor: (1) extracts the message content, (2) calls runner.run() with the message, (3) collects the ADK agent’s responses and tool calls, and (4) formats them as A2A protocol responses. This adapter pattern means the ADK agent doesn’t need to know anything about the A2A protocol — the executor handles the translation.

    # 5. Create the A2A web application
    request_handler = DefaultRequestHandler(
        agent_executor = agent_executor,
        task_store     = InMemoryTaskStore()  # tracks task states
    )

    a2a_app = A2AStarletteApplication(
        agent_card    = agent_card,
        http_handler  = request_handler
    )

    # 6. Add OAuth callback route (needed for Google Calendar auth)
    routes = a2a_app.routes()
    routes.append(Route(
        path     = '/authenticate',
        methods  = ['GET'],
        endpoint = handle_auth,
    ))

    # 7. Start the HTTP server
    app = Starlette(routes=routes)
    uvicorn.run(app, host=host, port=port)

A2AStarletteApplication: Creates a Starlette web application (a Python async web framework) that: (1) serves the Agent Card at /.well-known/agent.json, (2) handles incoming A2A task requests, (3) manages task state, (4) sends streaming responses via SSE when streaming: true. The entire A2A HTTP server is encapsulated in this single object.

InMemoryTaskStore: Stores the state of all active tasks (submitted, working, completed, failed) in memory. In production, you’d replace this with a persistent store (database) so task state survives server restarts. For development, in-memory is sufficient.

uvicorn.run(): Starts the ASGI server that will serve your A2A agent over HTTP. Uvicorn is a fast Python async HTTP server. Your agent is now accessible at http://host:port/ — any A2A-compliant client can discover it via /.well-known/agent.json and send tasks to /.


The Complete A2A Flow Visualized

graph TD
    U([User: "Plan a birthday party for March 15"]) --> CA[Client Agent — Orchestrator]
    CA -->|GET /.well-known/agent.json| DISC[Agent Discovery]
    DISC -->|Agent Card: capabilities, skills| CA
    CA -->|POST /a2a sendTask| CALSERV[Calendar Agent A2A Server]
    CALSERV --> ADK[ADK Agent Executor]
    ADK -->|tool call| GCAL[Google Calendar API]
    GCAL -->|availability data| ADK
    ADK -->|SSE streaming| CALSERV
    CALSERV -->|incremental results| CA
    CA --> RESP([Final response to user])
    style CA fill:#141b2d,stroke:#2698ba,color:#e0e0e0
    style CALSERV fill:#141b2d,stroke:#4fc97e,color:#e0e0e0
    style ADK fill:#141b2d,stroke:#c97af2,color:#e0e0e0
    style GCAL fill:#141b2d,stroke:#e6a817,color:#e0e0e0

Security: How A2A Stays Safe

Security is built into A2A at multiple levels:

🔒

Mutual TLS (mTLS)

Both the A2A client and server authenticate themselves with TLS certificates. This prevents unauthorized agents from connecting and ensures both sides can trust the other's identity. Essential for enterprise deployments where you don't want random clients calling your agent.

🔑

Credential Handling

Authentication credentials (API keys, OAuth tokens) are passed via HTTP Authorization headers — never in the URL or request body. The Agent Card declares which authentication scheme is required, so clients know what to send before making any requests.

📋

Audit Logs

All inter-agent communications are logged — which agent sent what to which agent, when, and what was returned. This creates an immutable audit trail for accountability, debugging, security analysis, and compliance with regulations that require traceability of AI decisions.

🪪

Agent Card Security

Even though Agent Cards contain capability descriptions (not secrets), they should be access-controlled. In enterprise environments, not everyone should be able to discover which agents exist and what they can do. Secure discovery endpoints with network restrictions or access tokens.


Practical Applications

01

Multi-Framework Collaboration

An ADK orchestrator delegates research to a LangGraph specialist, analysis to a CrewAI analyst, and report generation to another ADK agent — all communicating through A2A regardless of framework differences.

02

Enterprise Workflow Automation

A master agent decomposes a complex business process: collect data → delegate to analyst agent → delegate to compliance agent → delegate to report agent. Each agent operates independently and communicates via A2A tasks.

03

Dynamic Capability Discovery

An orchestrator agent discovers available specialist agents at runtime by querying a registry of Agent Cards, selects the most appropriate one for each sub-task, and delegates — without any hardcoded agent references.

04

Cross-Organization Collaboration

Your company's AI agent collaborates with a partner company's AI agent via A2A. Neither side needs to expose internal implementation details — just the Agent Card interface. Inter-company AI integration becomes as easy as an API call.


At a Glance

WHAT

An open, HTTP-based standard for agent-to-agent communication. A2A defines how agents discover each other (Agent Cards), how they exchange tasks (JSON-RPC 2.0), and how results are returned (sync, polling, streaming, webhooks) — regardless of the framework each agent uses.

WHY

Without A2A, integrating N agents from M frameworks requires N×M custom integrations. A2A reduces this to N+M standard implementations. It's the USB standard for agent collaboration — write once, work with any compliant agent.

vs MCP

MCP = how an agent connects to tools and external resources. A2A = how agents connect to each other. Both are needed in production systems: MCP gives individual agents their capabilities; A2A lets capable agents collaborate.


Key Takeaways

  • A2A is the missing standard between MCP and multi-agent systems. MCP connects agents to tools. A2A connects agents to each other. Together, they form a complete ecosystem: agents with tool access (MCP) collaborating with other agents (A2A).

  • The Agent Card is the protocol’s foundation. It makes agents self-describing and discoverable — any A2A client can learn an agent’s capabilities, endpoint, authentication requirements, and skills just by fetching its Agent Card JSON. This is what makes true interoperability possible.

  • Four interaction patterns cover every latency profile. Synchronous for fast tasks, polling for medium tasks, streaming SSE for incremental results, webhooks for overnight tasks. The Agent Card declares which patterns are supported — clients choose accordingly.

  • The input-required state enables multi-turn agent conversations. A remote agent can pause mid-task, request more information from the client, and resume once the information is provided. This supports complex collaborative workflows, not just one-shot requests.

  • A2A is opaque by design. The client agent doesn’t know or care what framework the server agent uses, what LLM it calls, or how it processes requests. This opacity is what makes framework independence real — you can replace the server-side implementation entirely without changing any client.

  • The A2A Python SDK encapsulates all protocol complexity. A2AStarletteApplication, ADKAgentExecutor, DefaultRequestHandler, and InMemoryTaskStore handle the JSON-RPC framing, task state machine, SSE streaming, and HTTP routing. You implement the agent logic; the SDK handles the protocol.

  • Security must be explicit. Declare authentication requirements in the Agent Card. Use mTLS for production. Log all inter-agent communications. Control who can discover your Agent Card. These aren’t optional features — they’re prerequisites for deploying A2A in enterprise environments.

  • Industry adoption is accelerating. Atlassian, Box, LangChain, MongoDB, Salesforce, SAP, ServiceNow, Microsoft (Azure AI Foundry), Auth0 — the A2A ecosystem is growing fast. Agents you build today to the A2A standard will be composable with agents from any of these platforms.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Chapter 10: Contributing to AI Safety — Paths, Skills, and Getting Started
  • Chapter 9: AI Control — Safety Without Trusting the Model
  • Chapter 19: Evaluation and Monitoring
  • Chapter 18: Guardrails and Safety Patterns
  • Chapter 17: Reasoning Techniques