| name | a2a-protocol-patterns |
| description | Agent-to-Agent (A2A) protocol — Google 2025, 150+ org backing. Agent Cards discovery, task lifecycle (submitted→working→completed), artifacts (text/structured/video), opaque task model. MCP vs A2A split. Auth: bearer/mTLS/signed. Sources: rohitg00/ai-engineering-from-scratch (Apache-2.0). |
/a2a-protocol-patterns
When to Use
- Cross-organization agent calls: your agent calls an agent at another company/system
- Building a discoverable agent endpoint that any A2A-compatible client can invoke
- Replacing bespoke agent-to-agent HTTP contracts with a standard protocol
- Multi-agent systems where subagents are independent services (LangGraph, CrewAI, AutoGen)
Do NOT use for
- Tool use within a single agent (use MCP — [[mcp-server-patterns]])
- Internal function calls between co-located agents in the same process
- Synchronous tool calls where task lifecycle overhead isn't worth it
MCP vs A2A — the split
MCP (Model Context Protocol) — agent ↔ tool
vertical: agent reads/writes to a tool server
stateless by default, JSON-RPC
A2A (Agent-to-Agent Protocol) — agent ↔ agent
horizontal peer: both sides are agents with reasoning
stateful (task lifecycle), async by default
Production pattern: use both.
Agent A —[A2A]→ Agent B —[MCP]→ tools on Agent B's side
Agent Card (discovery at /.well-known/agent.json)
interface AgentCard {
name: string;
version: string;
skills: string[];
endpoints: {
tasks: string;
};
auth: {
type: "bearer" | "mtls" | "signed";
scopes?: string[];
};
modalities: ("text" | "structured" | "image" | "video" | "audio")[];
description?: string;
}
const yamtamAgentCard: AgentCard = {
name: "yamtam-security-agent",
version: "1.0.0",
skills: ["red-team-check", "blue-team-fix", "leak-check"],
endpoints: { tasks: "https://agents.example.com/tasks" },
auth: { type: "bearer", scopes: ["agent:invoke"] },
modalities: ["text", "structured"],
};
Task lifecycle
submitted → working → completed
→ failed
→ canceled
interface A2ATask {
id: string;
skill: string;
input: Record<string, unknown>;
state: "submitted" | "working" | "completed" | "failed" | "canceled";
progress?: number;
artifacts?: Artifact[];
error?: string;
}
interface Artifact {
type: "text" | "structured" | "image" | "video" | "audio";
content: string;
mimeType?: string;
}
Minimal A2A server (Node.js / Express)
import express from "express";
import { randomUUID } from "crypto";
const app = express();
const tasks = new Map<string, A2ATask>();
app.get("/.well-known/agent.json", (req, res) => res.json(yamtamAgentCard));
app.post("/tasks", express.json(), async (req, res) => {
const task: A2ATask = {
id: randomUUID(),
skill: req.body.skill,
input: req.body.input,
state: "submitted",
};
tasks.set(task.id, task);
res.status(201).json({ task_id: task.id, state: task.state });
processTask(task);
});
app.get("/tasks/:id", (req, res) => {
const task = tasks.get(req.params.id);
if (!task) return res.status(404).json({ error: "not found" });
res.json(task);
});
app.get("/tasks/:id/events", (req, res) => {
const task = tasks.get(req.params.id);
if (!task) return res.status(404).end();
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
const send = (data: object) => res.write(`data: ${JSON.stringify(data)}\n\n`);
const interval = setInterval(() => {
const t = tasks.get(req.params.id)!;
send({ state: t.state, progress: t.progress });
if (t.state === "completed" || t.state === "failed") {
clearInterval(interval);
res.end();
}
}, 500);
});
async function processTask(task: A2ATask) {
task.state = "working";
task.progress = 0;
task.state = "completed";
task.artifacts = [{ type: "text", content: "Scan complete. 0 critical findings." }];
}
A2A client (discovery + task submission)
async function callAgent(agentBaseUrl: string, skill: string, input: object): Promise<Artifact[]> {
const card = await fetch(`${agentBaseUrl}/.well-known/agent.json`).then(r => r.json());
if (!card.skills.includes(skill)) throw new Error(`Agent does not support skill: ${skill}`);
const { task_id } = await fetch(card.endpoints.tasks, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${getToken()}` },
body: JSON.stringify({ skill, input }),
}).then(r => r.json());
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 1000));
const task: A2ATask = await fetch(`${agentBaseUrl}/tasks/${task_id}`).then(r => r.json());
if (task.state === "completed") return task.artifacts ?? [];
if (task.state === "failed") throw new Error(`Task failed: ${task.error}`);
}
throw new Error("Task timed out");
}
Auth patterns
Bearer token — OAuth2 or opaque; declare required scopes in Agent Card
mTLS — mutual TLS; both sides present certificates; for cross-org zero-trust
Signed request — HMAC-SHA256 over request body; lightweight; no PKI required
Anti-Fake-Pass Checklist
❌ No Agent Card → client can't discover capabilities; bespoke integration required
❌ Task state not persisted → server restart loses in-progress tasks
❌ No auth on /tasks endpoint → anyone can invoke any skill
❌ Synchronous /tasks POST that blocks until done → timeouts on long skills; use async + poll/SSE
❌ Using A2A for tool calls within one agent → MCP is the right layer; A2A overhead unwarranted
❌ Opaque lifecycle violated → prescribing HOW remote agent solves the task defeats the abstraction