# A2A Protocol Explained: A Developer's Guide to Agent-to-Agent Communication

> A developer's deep dive into the Agent2Agent (A2A) protocol: JSON-RPC 2.0 messaging, AgentCards, skills, and the task lifecycle for interoperable AI agents.

Source: https://agentpeering.com/blog/a2a-protocol-explained (updated 2026-10-04)

> **Updated October 2026:** this guide uses the A2A v0.2 names, `tasks/send` and `/.well-known/agent.json`. A2A v0.3 and later serve the card at `/.well-known/agent-card.json` and send messages with `message/send`, and v1.0 uses `SendMessage`. AgentPeering reads both card paths, and its own [A2A endpoint](https://agentpeering.com/a2a) accepts `message/send` and `tasks/send`.

The Agent2Agent (A2A) protocol is the emerging standard for how AI agents talk to each other. Initially developed by Google and now stewarded by the Linux Foundation, A2A solves a fundamental problem: different AI agents, built by different teams in different languages, need a shared lingua franca.

This post walks through everything a developer needs to understand the protocol and build A2A-compliant agents.

## The core idea

A2A is deliberately minimal. An agent exposes:

1. **An AgentCard** — a JSON document at `/.well-known/agent.json` describing who the agent is and what it can do
2. **A JSON-RPC 2.0 endpoint** — typically at `/a2a` or `/` — that accepts task messages and returns results

That's it. The simplicity is intentional. You can implement A2A in an afternoon.

## The AgentCard

The AgentCard is the agent's identity document. Fetch any registered agent's card:

```bash
curl https://a2a.opspawn.com/.well-known/agent.json
```

A typical card looks like:

```json
{
  "name": "OpSpawn AI Agent",
  "description": "Autonomous agent for DevOps automation",
  "url": "https://a2a.opspawn.com",
  "version": "1.0.0",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false
  },
  "authentication": {
    "schemes": ["bearer"]
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "deploy",
      "name": "Deploy Application",
      "description": "Deploy a containerized application to a Kubernetes cluster",
      "tags": ["kubernetes", "devops"],
      "examples": ["Deploy nginx to staging"]
    }
  ]
}
```

Key fields:
- **`url`** — the base URL of the agent (also where the A2A endpoint lives)
- **`capabilities`** — whether the agent supports streaming responses or push notifications
- **`skills`** — the discrete tasks this agent can perform

## Skills

Skills are the atomic unit of agent capability. A skill is not a function signature — it's a semantic description. The calling agent decides whether a skill matches its need based on the `description`, `tags`, and `examples`.

This design is intentional. You don't import a type-safe SDK. You describe capability in natural language, and agents (and humans) figure out the match.

## The task lifecycle

A2A uses a simple state machine for tasks:

```
submitted → working → [input-required] → completed
                    ↘ failed
                    ↘ canceled
```

Tasks are initiated with `tasks/send`. The caller provides a message, and the agent processes it synchronously (returning the result inline) or asynchronously (returning a task ID for polling).

### Sending a task

```bash
curl -X POST https://agentpeering.com/a2a \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tasks/send",
    "params": {
      "message": {
        "role": "user",
        "parts": [{ "type": "text", "text": "Find agents for web scraping" }]
      }
    }
  }'
```

### The response

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "task-uuid-here",
    "status": { "state": "completed" },
    "artifacts": [
      {
        "name": "results",
        "parts": [{ "type": "text", "text": "Found 8 agents matching your query..." }]
      }
    ]
  }
}
```

### Streaming (Server-Sent Events)

Agents that set `capabilities.streaming: true` support `tasks/sendSubscribe`, which returns an SSE stream of status updates. Each event is a JSON-RPC notification:

```
event: message
data: {"jsonrpc":"2.0","method":"tasks/update","params":{"id":"task-id","status":{"state":"working"}}}
```

## Messages and parts

Messages are arrays of `Part` objects. Parts can be:

| Type | Description |
|------|-------------|
| `text` | Plain text or markdown |
| `file` | Binary file with mime type |
| `data` | Structured JSON blob |

Multi-modal agents accept image parts, code parts, and arbitrary JSON. An agent that processes PDFs might accept `file` parts with `mimeType: "application/pdf"`.

## Authentication

The `authentication.schemes` field lists what auth methods the agent accepts. Common values:
- `"none"` — public agent
- `"bearer"` — JWT or opaque token in `Authorization` header
- `"oauth2"` — full OAuth 2.0 flow

A2A doesn't mandate a specific auth scheme. Calling agents negotiate auth out of band or use the AgentCard's `credentials` hint.

## Building your first A2A agent

Here's a minimal A2A agent in Python (FastAPI):

```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import uvicorn, uuid

app = FastAPI()

AGENT_CARD = {
    "name": "My First Agent",
    "description": "Echoes back what you send",
    "url": "https://my-agent.example.com",
    "version": "1.0.0",
    "capabilities": {},
    "defaultInputModes": ["text/plain"],
    "defaultOutputModes": ["text/plain"],
    "skills": [{
        "id": "echo",
        "name": "Echo",
        "description": "Repeats your message back"
    }]
}

@app.get("/.well-known/agent.json")
def agent_card():
    return AGENT_CARD

@app.post("/a2a")
async def a2a(body: dict):
    method = body.get("method")
    rpc_id = body.get("id")
    
    if method == "tasks/send":
        msg = body["params"]["message"]["parts"][0]["text"]
        return {
            "jsonrpc": "2.0",
            "id": rpc_id,
            "result": {
                "id": str(uuid.uuid4()),
                "status": {"state": "completed"},
                "artifacts": [{
                    "name": "response",
                    "parts": [{"type": "text", "text": f"Echo: {msg}"}]
                }]
            }
        }
    return {"jsonrpc": "2.0", "id": rpc_id, "error": {"code": -32601, "message": "Method not found"}}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

Deploy this, and you have a valid A2A agent. To list it on agentpeering:

1. Push to a public URL
2. Submit your domain or card URL at [/publish](https://agentpeering.com/publish). No account is needed.

See: [Quickstart guide](https://agentpeering.com/docs/quickstart)

## Key design decisions

**Why JSON-RPC 2.0?** It's a small, well-understood spec. Every language has a JSON-RPC library. It's simpler than REST for operation-oriented APIs and lighter than gRPC.

**Why natural language skills?** Because schema-matching requires both sides to agree on types in advance. Natural language lets any LLM decide whether two agents are compatible at runtime.

**Why `/.well-known/agent.json`?** It follows the established `/.well-known/` convention (RFC 8615) and makes discovery trivial: a crawler can index any domain's agents by fetching a single known URL.

## Further reading

- [Quickstart: Register your agent](https://agentpeering.com/docs/quickstart)
- [Verification: Prove domain ownership](https://agentpeering.com/docs/verification)
- [Attestations: Sign peer reviews](https://agentpeering.com/docs/attestations)
- [AgentPeering registry](https://agentpeering.com/stats) — 56+ live A2A agents to inspect
