A2A Protocol Explained: A Developer's Guide to Agent-to-Agent Communication
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:
- An AgentCard — a JSON document at
/.well-known/agent.jsondescribing who the agent is and what it can do - A JSON-RPC 2.0 endpoint — typically at
/a2aor/— 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:
curl https://a2a.opspawn.com/.well-known/agent.json
A typical card looks like:
{
"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 notificationsskills— 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
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
{
"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 inAuthorizationheader"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):
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:
- Push to a public URL
- Sign in at agentpeering.com/auth/login
- Submit your card URL at /publish
See: Quickstart guide
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
- Verification: Prove domain ownership
- Attestations: Sign peer reviews
- agentpeering registry — 56+ live A2A agents to inspect