DEV Community

Diven Rastdus
Diven Rastdus

Posted on

The A2A Protocol: How Google Wants AI Agents to Talk to Each Other

The A2A Protocol: How Google Wants AI Agents to Talk to Each Other

Google released the Agent-to-Agent (A2A) protocol in April 2025. It's an open standard for AI agents to discover each other, negotiate capabilities, and delegate tasks. If you've heard of MCP (Model Context Protocol), A2A is the layer above it: MCP connects agents to tools, A2A connects agents to other agents.

I built a 3-agent healthcare system using A2A. Here's what it actually looks like in practice.

Why A2A exists

Imagine you have three AI agents:

  1. A data collection agent that pulls patient records from a FHIR server
  2. A clinical analysis agent that checks drug interactions and flags risks
  3. An orchestrator that coordinates the workflow

Without a protocol, you'd hardcode HTTP calls between them, invent your own message format, and handle errors ad hoc. Every new agent requires custom integration code. This doesn't scale.

A2A standardizes three things:

  • Discovery: agents publish "agent cards" describing what they can do
  • Communication: JSON-RPC messages over HTTP
  • Task lifecycle: a shared state machine (submitted, working, completed, failed, canceled)

Agent Cards: the service registry

Every A2A agent publishes an agent card at /.well-known/agent.json. This is how other agents discover capabilities.

{
  "name": "MedRecon Clinical Analysis Agent",
  "description": "Analyzes medication lists for drug interactions, allergy conflicts, and dose validation",
  "url": "https://clinical-agent.example.com",
  "version": "1.0.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "skills": [
    {
      "id": "drug-interaction-check",
      "name": "Drug Interaction Check",
      "description": "Checks a medication list against 48 curated interaction pairs and OpenFDA",
      "tags": ["healthcare", "pharmacology", "safety"],
      "examples": [
        "Check interactions for metformin, lisinopril, and warfarin",
        "Are there conflicts between these medications?"
      ]
    },
    {
      "id": "allergy-safety-check",
      "name": "Allergy Safety Check",
      "description": "Cross-references medications against patient allergy records",
      "tags": ["healthcare", "allergies", "safety"]
    }
  ],
  "authentication": {
    "schemes": ["bearer"]
  }
}
Enter fullscreen mode Exit fullscreen mode

This is REST-native. No service mesh, no gRPC definitions, no WSDL. An agent reads another agent's card and knows what skills are available. The examples field is particularly useful: it gives the calling agent natural-language examples of how to invoke each skill.

The message protocol

A2A uses JSON-RPC 2.0 over HTTP. There are five core methods:

tasks/send      - Send a task to an agent
tasks/get       - Check task status
tasks/cancel    - Cancel a running task
tasks/pushNotificationConfig/set - Subscribe to updates
tasks/sendSubscribe - Send task and stream updates via SSE
Enter fullscreen mode Exit fullscreen mode

A task request looks like this:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "tasks/send",
  "params": {
    "id": "task-abc-123",
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "text",
          "text": "Check drug interactions for: metformin 500mg, warfarin 5mg, aspirin 81mg"
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The response:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "id": "task-abc-123",
    "status": {
      "state": "completed",
      "message": {
        "role": "agent",
        "parts": [
          {
            "type": "text",
            "text": "Found 1 critical interaction: warfarin + aspirin increases bleeding risk..."
          }
        ]
      }
    },
    "artifacts": [
      {
        "name": "interaction-report",
        "parts": [
          {
            "type": "data",
            "data": { "interactions_found": 1, "severity": "high" }
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Key concepts:

  • Messages carry conversation (like chat messages with roles)
  • Artifacts carry structured output (data, files, reports)
  • Parts are multimodal (text, data, file references)

Task lifecycle

Every task moves through defined states:

submitted -> working -> completed
                    \-> failed
                    \-> canceled (via tasks/cancel)
                    \-> input-required (agent needs more info)
Enter fullscreen mode Exit fullscreen mode

The input-required state is interesting. If an agent needs clarification, it transitions the task to input-required with a message explaining what it needs. The calling agent can then send another tasks/send with the same task ID to provide the missing information.

// Check if agent needs more input
const result = await a2aClient.sendTask(agentUrl, {
  id: taskId,
  message: { role: "user", parts: [{ type: "text", text: prompt }] }
});

if (result.status.state === "input-required") {
  // Agent needs more info -- send follow-up
  const followUp = await a2aClient.sendTask(agentUrl, {
    id: taskId, // same task ID continues the conversation
    message: {
      role: "user",
      parts: [{ type: "text", text: result.status.message.parts[0].text }]
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Building an A2A server

Here's the minimal implementation. An A2A server is just an HTTP server that handles JSON-RPC:

import express from "express";

const app = express();
app.use(express.json());

// Serve agent card
app.get("/.well-known/agent.json", (req, res) => {
  res.json({
    name: "My Agent",
    url: "https://my-agent.example.com",
    version: "1.0.0",
    capabilities: { streaming: false },
    skills: [
      {
        id: "summarize",
        name: "Summarize Text",
        description: "Summarizes long text into key points",
      },
    ],
  });
});

// Handle JSON-RPC requests
app.post("/", async (req, res) => {
  const { method, params, id } = req.body;

  switch (method) {
    case "tasks/send": {
      const userMessage = params.message.parts
        .filter((p) => p.type === "text")
        .map((p) => p.text)
        .join("\n");

      // Do the actual work (call an LLM, query a DB, whatever)
      const result = await processTask(userMessage);

      return res.json({
        jsonrpc: "2.0",
        id,
        result: {
          id: params.id,
          status: {
            state: "completed",
            message: {
              role: "agent",
              parts: [{ type: "text", text: result }],
            },
          },
        },
      });
    }

    case "tasks/get": {
      // Return stored task status
      const task = taskStore.get(params.id);
      return res.json({ jsonrpc: "2.0", id, result: task });
    }

    default:
      return res.json({
        jsonrpc: "2.0",
        id,
        error: { code: -32601, message: `Method not found: ${method}` },
      });
  }
});
Enter fullscreen mode Exit fullscreen mode

That's it. The protocol is thin by design. Most of the complexity lives in your agent's actual logic, not in the communication layer.

A2A vs MCP: different layers, not competitors

This confuses a lot of people. Here's the simplest way to think about it:

MCP = how an agent talks to tools (databases, APIs, file systems). It's the "hands" of the agent.

A2A = how an agent talks to other agents. It's the "mouth" of the agent.

In my healthcare system:

  • Each agent uses MCP internally to call tools (FHIR queries, drug interaction databases, allergy checks)
  • The orchestrator uses A2A to delegate tasks to the data collection and clinical analysis agents
Orchestrator
  |-- A2A --> Data Collection Agent
  |              |-- MCP --> FHIR Server (get_patient, get_medications)
  |
  |-- A2A --> Clinical Analysis Agent
                 |-- MCP --> Drug Interaction DB (check_interactions)
                 |-- MCP --> OpenFDA API (check_allergies)
Enter fullscreen mode Exit fullscreen mode

They complement each other. You could use both in the same system (and probably should).

Streaming with SSE

For long-running tasks, use tasks/sendSubscribe instead of tasks/send. The server returns a Server-Sent Events stream:

// Client side
const response = await fetch(agentUrl, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: "req-stream",
    method: "tasks/sendSubscribe",
    params: {
      id: taskId,
      message: { role: "user", parts: [{ type: "text", text: prompt }] },
    },
  }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value);
  // Parse SSE events
  for (const line of chunk.split("\n")) {
    if (line.startsWith("data: ")) {
      const event = JSON.parse(line.slice(6));
      console.log(`State: ${event.result.status.state}`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The server sends status updates as the task progresses: working, then intermediate results, then completed. This is how you build real-time UIs on top of multi-agent systems.

What I learned building with A2A

Agent cards should be specific. Generic descriptions like "processes data" don't help the orchestrator choose the right agent. Include concrete examples and list the exact input/output format.

Task IDs are your correlation key. In a multi-agent system, the orchestrator fires tasks to multiple agents. The task ID is how you correlate responses. Use UUIDs, not sequential integers.

Error handling is your problem. A2A defines the failed state but doesn't prescribe retry logic. If the clinical analysis agent fails mid-way, the orchestrator decides whether to retry, skip, or fail the whole workflow. Build this into the orchestrator, not the agents.

Auth is underspecified. The agent card has an authentication field, but the actual auth flow (API keys, OAuth, mutual TLS) is left to the implementer. For internal services, bearer tokens work fine. For cross-org agent communication, this will need more standardization.

Start simple. You don't need streaming, push notifications, or multi-turn conversations on day one. tasks/send with synchronous responses handles 90% of use cases. Add complexity only when the response times justify it.

When to use A2A

A2A makes sense when:

  • You have multiple specialized agents that need to collaborate
  • You want loose coupling (agents can be replaced independently)
  • You're building a system where new agents can be added dynamically
  • You need a standard way for agents from different teams/orgs to communicate

It doesn't make sense when:

  • You have a single agent calling a few tools (just use MCP)
  • All your "agents" run in the same process (use function calls)
  • You need sub-millisecond latency between components (use direct function calls or gRPC)

Resources


I build multi-agent systems for production use. If you're working with A2A or MCP, I'm at astraedus.dev.

Top comments (0)