MCP gives your agent hands. A2A gives your agents colleagues. The Agent2Agent protocol (A2A) is the open standard for one agent to discover another, hand it work, and track that work to completion. Its first stable specification, v1.0.0, was tagged on March 12, 2026. A v1.0.1 bug-fix release followed on May 28. On August 17, 2026, A2A became a hosted project of the Agentic AI Foundation (AAIF). The AAIF is the Linux Foundation body that already hosts MCP, so the agent-to-tool and agent-to-agent protocols now have the same neutral steward.
This post is the production guide. It covers what v1.0 actually changed, what a spec-conformant Agent Card looks like, the full task state machine, and a worker plus orchestrator in Python. We ran both pieces of code against the official a2a-sdk 1.3.0. It closes with the gotchas that bite in week three, and the decision rule for when A2A is worth its weight.
What is A2A and how does it differ from MCP?
A2A standardizes how independent agents work together as peers. An orchestrator discovers a worker through its Agent Card, sends it a message, and tracks the resulting task until it reaches a terminal state. MCP standardizes how one agent connects to tools and data. The two protocols stack. An A2A worker typically uses MCP internally to call its own tools, then exposes a higher-level skill to other agents over A2A.
Google launched A2A in April 2025. It donated A2A to the Linux Foundation in June 2025, and IBM’s Agent Communication Protocol (ACP) merged into it that August. By the protocol’s first anniversary, more than 150 organizations supported A2A, according to the Linux Foundation. Microsoft (Azure AI Foundry, Copilot Studio), AWS (Bedrock AgentCore) and Google Cloud had all integrated it. The specification lives at a2a-protocol.org. The Linux Foundation lists official SDKs for Python, JavaScript, Java, Go and .NET.
Three primitives carry most of the weight.
Agent Cards. An Agent Card is a JSON document served at https://{host}/.well-known/agent-card.json. It declares the agent’s identity, its endpoints and bindings, its capabilities (streaming, push notifications, an authenticated extended card), its skills and its security schemes. Discovery needs only a hostname, in the same way openid-configuration works for identity providers.
Messages and tasks. A client sends a Message, which has a role and an array of parts. The agent can answer directly with a Message for trivial interactions. For anything stateful it creates a Task, which has an ID, a contextId that groups related tasks into one conversation, a status, a history and artifacts. The orchestrator learns about progress by polling, streaming, or receiving push notifications.
The task state machine. Every task carries a TaskState. The orchestrator does not have to invent its own lifecycle. It reacts to the worker’s.
Compare this with where multi-agent systems were a year earlier, when every team was rolling custom JSON over POST. A2A turns coordination into infrastructure.
What changed in v1.0?
v1.0 is the first release that promises stability. It is also the release that broke the most things relative to v0.3, so read the official change list before upgrading an existing integration. These are the changes that matter for production code:
- Operations were renamed to PascalCase.
message/sendbecameSendMessage,message/streambecameSendStreamingMessage,tasks/getbecameGetTask,tasks/cancelbecameCancelTaskandtasks/resubscribebecameSubscribeToTask.ListTasksand four push-notification-config operations complete the set. - Enums use prefixed SCREAMING_SNAKE_CASE.
"input-required"is nowTASK_STATE_INPUT_REQUIRED, and"user"is nowROLE_USER. String-matching on v0.3 values silently fails against a v1.0 worker. - Endpoints moved into
supportedInterfaces[]. The oldurl,preferredTransportandadditionalInterfacesfields are replaced by one array. Each entry carries its ownurl,protocolBindingandprotocolVersion. - Parts were unified. The separate
TextPart,FilePartandDataParttypes are gone. APartnow holds exactly one oftext,raw,urlordata, plus optionalmediaTypeandfilename. - Stream events lost their
kinddiscriminator. AStreamResponsewraps exactly one oftask,message,statusUpdateorartifactUpdate. Thefinalflag is also gone; the stream simply closes. - Errors follow
google.rpc.Status. Every A2A-specific error carries agoogle.rpc.ErrorInfodetail withdomain: "a2a-protocol.org"and a machine-readablereason, such asTASK_NOT_FOUND. - Version negotiation is explicit. Clients send an
A2A-Versionheader (for example1.0). Agents must treat a missing header as0.3, and must returnVersionNotSupportedErrorfor versions they don’t serve. - Agent Cards can be signed. The new
signatures[]field carries a JWS (RFC 7515) over the card, canonicalized with JCS (RFC 8785). - OAuth was modernized. The implicit and password flows are deprecated. The device code flow and PKCE were added.
- New capabilities: multi-tenancy via a
tenantfield, and cursor-based pagination forListTasks.
Two things that often get attributed to v1.0 are older than it. The well-known path moved from agent.json to agent-card.json in v0.3.0 (July 2025). The input-required and rejected states also already existed in v0.3.
Why the AAIF move matters. The foundation launched on December 9, 2025. Anthropic, Block and OpenAI were its founding project contributors, donating MCP, goose and AGENTS.md respectively. Putting A2A under the same umbrella means no single vendor controls either protocol’s roadmap. For procurement, that is the signal that writing A2A into an RFP is a safe bet. For engineering, it means the wire format will change through a public spec process, not a vendor pivot.
The Agent Card
The Agent Card is the only thing an orchestrator needs to discover and call a worker. Below is a v1.0 card for a research worker. It was generated with the official SDK’s protobuf types, so the field names and nesting match the specification’s data model.
{
"name": "research-agent",
"description": "Researches a topic and returns a cited brief with sources.",
"supportedInterfaces": [
{
"url": "https://agents.example.com/research/a2a",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
},
{
"url": "https://agents.example.com/research/rest",
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0"
}
],
"provider": {
"url": "https://example.com",
"organization": "Example Corp"
},
"version": "2.3.0",
"documentationUrl": "https://agents.example.com/research/docs",
"capabilities": {
"streaming": true,
"pushNotifications": true,
"extendedAgentCard": false
},
"securitySchemes": {
"oauth": {
"oauth2SecurityScheme": {
"flows": {
"clientCredentials": {
"tokenUrl": "https://auth.example.com/oauth2/token",
"scopes": { "research:run": "Run research tasks" }
}
}
}
}
},
"securityRequirements": [
{ "schemes": { "oauth": { "list": ["research:run"] } } }
],
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/markdown", "application/pdf"],
"skills": [
{
"id": "research-brief",
"name": "Research brief",
"description": "Researches a topic across the web and internal sources and returns a cited brief.",
"tags": ["research", "citations"],
"examples": ["Brief me on A2A v1.0 adoption for a CFO audience"],
"outputModes": ["text/markdown", "application/pdf"]
}
]
}
What to get right:
versionis the agent’s version, not the protocol’s. The protocol version lives on eachsupportedInterfacesentry. Spec versions are negotiated onMajor.Minoronly. Patch numbers must not be used for negotiation.- List every binding you serve. A client picks the first interface it supports, in the order you list them. Put your preferred binding first.
- Declare security in two places.
securitySchemesdefines how to authenticate.securityRequirementsdefines which schemes and scopes are required, either card-wide or per skill. The sample card in the v1.0.1 spec text still uses an oldersecurityfield. The protobuf definition and the current spec usesecurityRequirements, so follow those. - Skills are the capability contract.
id,name,descriptionandtagsare required.examplesand per-skill input and output modes are what a planning model reads when deciding whether to delegate. - Cache it like any HTTP resource. The spec says card endpoints should send
Cache-Controlwith amax-ageand anETag. Clients should revalidate withIf-None-Matchrather than re-download. - Sign it if you cross trust boundaries. Clients should verify at least one signature before trusting a card that came from another organization.
The task lifecycle
TaskState has eight meaningful values, plus TASK_STATE_UNSPECIFIED. The useful way to group them is by who has to act next.
| State | Group | What the orchestrator should do |
|---|---|---|
TASK_STATE_SUBMITTED | Active | Wait. The worker has acknowledged the task. |
TASK_STATE_WORKING | Active | Wait, stream, or poll. |
TASK_STATE_INPUT_REQUIRED | Interrupted | Read status.message, get the answer, and send a follow-up message with the same taskId. |
TASK_STATE_AUTH_REQUIRED | Interrupted | Fulfil the authorization request out of band, then keep listening. |
TASK_STATE_COMPLETED | Terminal | Read the artifacts. |
TASK_STATE_FAILED | Terminal | The worker accepted the task and could not finish. Retry with backoff if your policy allows. |
TASK_STATE_CANCELED | Terminal | Done. Cancellation was acknowledged. |
TASK_STATE_REJECTED | Terminal | The worker refused the task. Do not retry the same request. |
Terminal tasks cannot accept further messages. A send to one returns UnsupportedOperationError. Subscribing to a terminal task is also an error, so check state with GetTask first.
The distinction that most often breaks production is REJECTED versus FAILED. A rejection means the worker decided not to take the work. A failure means it tried and could not finish. Retrying a rejection just generates more rejections, and possibly rate-limit penalties. Wire the two into different branches of your retry policy from day one.
AUTH_REQUIRED deserves its own note. The spec’s in-task authorization section lets a worker pause and hand an authorization request back to the client, which can in turn push it further up a chain of agents. The spec is explicit that the state transition is not itself an authorization. A credential obtained while a task is in AUTH_REQUIRED must not be assumed to cover later messages unless the implementation or an extension defines that.
A working worker and orchestrator
The code below uses a2a-sdk 1.3.0 (pip install "a2a-sdk[http-server]" uvicorn), the current release on PyPI as of November 2026. We ran the pair end to end against each other before publishing. The worker asks a clarifying question, pausing in INPUT_REQUIRED, then completes with an artifact.
The worker
# worker.py
import uvicorn
from starlette.applications import Starlette
from a2a.helpers import new_task_from_user_message, new_text_message, new_text_part
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
from a2a.types import (
AgentCapabilities, AgentCard, AgentInterface, AgentProvider, AgentSkill,
)
CARD = AgentCard(
name="research-agent",
description="Researches a topic and returns a cited brief.",
version="2.3.0",
provider=AgentProvider(organization="Example Corp", url="https://example.com"),
supported_interfaces=[
AgentInterface(
url="http://localhost:9999/a2a/jsonrpc",
protocol_binding="JSONRPC",
protocol_version="1.0",
)
],
capabilities=AgentCapabilities(streaming=True, push_notifications=False),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[
AgentSkill(
id="research-brief",
name="Research brief",
description="Produces a cited research brief on a topic.",
tags=["research", "citations"],
examples=["Brief me on A2A v1.0 adoption"],
)
],
)
class ResearchExecutor(AgentExecutor):
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
task = context.current_task
if task is None:
task = new_task_from_user_message(context.message)
await event_queue.enqueue_event(task)
updater = TaskUpdater(event_queue, task.id, task.context_id)
text = context.get_user_input()
if "audience:" not in text:
# Pause and ask the caller for more input. Returning hands control back.
await updater.requires_input(
new_text_message(
"Who is the audience? Reply with 'audience: <who>'.",
context_id=task.context_id,
task_id=task.id,
)
)
return
await updater.start_work()
# Real research (and MCP tool calls) would happen here.
await updater.add_artifact(parts=[new_text_part(f"Brief for: {text}")], name="brief")
await updater.complete()
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
await updater.cancel()
handler = DefaultRequestHandler(
agent_executor=ResearchExecutor(),
task_store=InMemoryTaskStore(),
agent_card=CARD,
)
app = Starlette(routes=[
*create_agent_card_routes(CARD), # serves /.well-known/agent-card.json
*create_jsonrpc_routes(handler, rpc_url="/a2a/jsonrpc"),
])
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=9999)
The orchestrator
# orchestrator.py
import asyncio
import uuid
from a2a.client import ClientConfig, create_client
from a2a.helpers import get_artifact_text, get_message_text
from a2a.types import Message, Part, Role, SendMessageRequest, Task, TaskState
INTERRUPTED = {TaskState.TASK_STATE_INPUT_REQUIRED, TaskState.TASK_STATE_AUTH_REQUIRED}
def user_message(text: str, task_id: str = "", context_id: str = "") -> Message:
return Message(
message_id=str(uuid.uuid4()), # keep this ID when retrying the same send
role=Role.ROLE_USER,
parts=[Part(text=text)],
task_id=task_id,
context_id=context_id,
)
async def send(client, msg: Message, task: Task | None = None) -> Task:
"""Stream one message and fold the events into a local Task snapshot.
Pass the existing snapshot on follow-ups: in our tests the stream for a
continued task opened with a status update, not a fresh Task.
"""
async for event in client.send_message(SendMessageRequest(message=msg)):
if event.HasField("task"):
task = event.task
elif event.HasField("status_update"):
task.status.CopyFrom(event.status_update.status)
elif event.HasField("artifact_update"):
task.artifacts.append(event.artifact_update.artifact)
if task is not None:
print("state:", TaskState.Name(task.status.state))
return task
async def main() -> None:
# Fetches /.well-known/agent-card.json and picks a supported binding.
client = await create_client("http://localhost:9999", ClientConfig(streaming=True))
async with client:
task = await send(client, user_message("A2A v1.0 adoption"))
if task.status.state in INTERRUPTED:
print("worker asks:", get_message_text(task.status.message))
task = await send(
client,
user_message("audience: platform team", task.id, task.context_id),
task,
)
state = task.status.state
if state == TaskState.TASK_STATE_COMPLETED:
for artifact in task.artifacts:
print(artifact.name, "->", get_artifact_text(artifact))
elif state == TaskState.TASK_STATE_REJECTED:
raise RuntimeError("worker refused the task; do not retry")
else:
raise RuntimeError(f"task ended in {TaskState.Name(state)}")
asyncio.run(main())
Output from our run:
state: TASK_STATE_SUBMITTED
state: TASK_STATE_INPUT_REQUIRED
worker asks: Who is the audience? Reply with 'audience: <who>'.
state: TASK_STATE_WORKING
state: TASK_STATE_WORKING
state: TASK_STATE_COMPLETED
brief -> Brief for: audience: platform team
Two details to note. First, the follow-up message reuses task.id and task.context_id. That is how A2A continues an interrupted task rather than starting a new one. Second, the artifact handling appends each artifact update. For workers that stream one artifact in chunks (append: true), merge parts by artifactId instead.
What it looks like on the wire
The SDK hides the transport, but you will debug at the wire level eventually. This is a real JSON-RPC exchange with the worker above. IDs are shortened and history is omitted.
POST /a2a/jsonrpc HTTP/1.1
Content-Type: application/json
A2A-Version: 1.0
{"jsonrpc": "2.0", "id": 1, "method": "SendMessage",
"params": {"message": {"messageId": "msg-1", "role": "ROLE_USER",
"parts": [{"text": "audience: CFO, topic A2A"}]}}}
{"jsonrpc": "2.0", "id": 1,
"result": {"task": {
"id": "8f5e40d2-…", "contextId": "b4fba817-…",
"status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-11-01T19:10:15.243764Z"},
"artifacts": [{"artifactId": "7b15eb6c-…", "name": "brief",
"parts": [{"text": "Brief for: audience: CFO, topic A2A"}]}]}}}
SendMessage blocks by default. The spec says it waits until the task is terminal or interrupted. Set configuration.returnImmediately: true to get the task back at once and track it yourself. The HTTP+JSON binding exposes the same operations as resource paths: POST /message:send, POST /message:stream, GET /tasks/{id}, GET /tasks, POST /tasks/{id}:cancel and POST /tasks/{id}:subscribe.
Polling, streaming, or push?
The spec defines three update mechanisms, and production systems usually need more than one.
- Polling (
GetTask) works over every binding and through every proxy. It suits infrequent updates and clients behind restrictive firewalls. - Streaming (
SendStreamingMessage,SubscribeToTask) delivers events over Server-Sent Events on the JSON-RPC and HTTP+JSON bindings, and as native streams on gRPC. It requirescapabilities.streaming: true. Events must arrive in the order generated. The stream closes when the task reaches a terminal or interrupted state. - Push notifications let the worker POST updates to a webhook you register with
CreateTaskPushNotificationConfig. They requirecapabilities.pushNotifications: true, and they are the right fit for server-to-server work that runs for hours.
Recovering a dropped stream. The spec does not define event replay. It defines re-subscription instead. SubscribeToTask must send the task’s current state as its first event. That closes the gap between “my connection dropped” and “what did I miss?” without any Last-Event-ID bookkeeping. Workers may also serve several concurrent streams for one task, so a reconnecting client doesn’t disturb other subscribers. The practical pattern is to stream while a human is watching, and to fall back to polling or push once a task is likely to sit idle.
Error handling
Every binding maps the same A2A error types, so retry logic can be written once.
| A2A error | JSON-RPC code | HTTP status | Retry? |
|---|---|---|---|
TaskNotFoundError | -32001 | 404 | No: the task is gone or invisible to you |
TaskNotCancelableError | -32002 | 400 | No: it already finished |
PushNotificationNotSupportedError | -32003 | 400 | No: use streaming or polling |
UnsupportedOperationError | -32004 | 400 | No: includes sends to terminal tasks |
ContentTypeNotSupportedError | -32005 | 400 | No: change the part’s media type |
InvalidAgentResponseError | -32006 | 500 | Maybe: the worker is misbehaving |
ExtendedAgentCardNotConfiguredError | -32007 | 400 | No |
ExtensionSupportRequiredError | -32008 | 400 | No: declare the extension and resend |
VersionNotSupportedError | -32009 | 400 | No: negotiate a supported version |
The retry column is our policy, not the spec’s. Transient transport failures, such as HTTP 503 or gRPC UNAVAILABLE, are the retryable class. Treat them with backoff and honour Retry-After.
Here is what our worker returned when we tried to cancel a completed task. The spec-mandated ErrorInfo detail is what your code should branch on:
{"jsonrpc": "2.0", "id": 2,
"error": {"code": -32002, "message": "Task cannot be canceled",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "TASK_NOT_CANCELABLE",
"domain": "a2a-protocol.org", "metadata": {}}]}}
Branch on reason, not on message. The message is human-readable text that implementations are free to reword.
How does A2A compare to MCP and raw HTTP RPC?
A2A, MCP and raw HTTP RPC sit at different layers of the agent stack. MCP standardizes how a single agent calls tools and reads data. A2A standardizes how independent agents delegate work to each other, with stateful tasks that can pause for input. Raw HTTP RPC standardizes nothing. Choose by what you are integrating.
| Dimension | A2A v1.0 | MCP | Raw HTTP RPC |
|---|---|---|---|
| Topology | Agent to agent (peers) | Agent to tool (client/server) | Anything |
| Discovery | /.well-known/agent-card.json | Capability negotiation, then tools/list | Out of band |
| State model | Tasks with an 8-state lifecycle | Per-call tools; async tasks in newer specs | Whatever you build |
| Pausing for input | INPUT_REQUIRED / AUTH_REQUIRED | Elicitation | Custom |
| Bindings | JSON-RPC 2.0, gRPC, HTTP+JSON | JSON-RPC 2.0 over stdio or HTTP | Custom |
| Auth declaration | securitySchemes in the Agent Card | OAuth-based authorization for HTTP transports | Custom |
| Duplicate protection | messageId (dedup is optional) | Not standardized | Custom |
| Governance | AAIF (since August 2026) | AAIF (since December 2025) | None |
The decision rule we use:
- Wiring an agent to a database, an API, a search index or a SaaS tool is MCP. The MCP architecture post covers this end to end.
- Wiring an agent to another agent is A2A, meaning one with its own model, context and reasoning. This is especially true if the work is asynchronous, multi-turn, or might pause.
- Wiring an agent to a single internal microservice that answers deterministically in one call is fine with raw HTTP. Do not adopt A2A for an agent that wraps
GET /weather.
The most common mistake is using MCP for inter-agent work. That forces a client/server topology onto a peer relationship. The second most common is using A2A for tool calls, which adds a task lifecycle to operations that finish in 50 ms. For when peer coordination justifies the complexity at all, see Multi-Agent Systems: Patterns That Work Beyond the Demo.
Production gotchas
Duplicate sends and messageId
SendMessage is the one call where a network retry can create duplicate work, such as a second research run, a second billing event, or a second outbound email. The spec’s guarantee is deliberately weak. Send Message “MAY be idempotent”, and agents “may utilize the messageId to detect duplicate messages.” Two rules follow.
On the orchestrator side, generate the messageId once per logical send and reuse it on every retry of that send. A fresh UUID per attempt defeats any dedup the worker does. On the worker side, if your skill has side effects, actually implement dedup on messageId. Don’t assume the client will never retry. CancelTask, by contrast, is defined as idempotent.
Agent Card caching and version drift
Cache the card and revalidate it with its ETag. Re-fetch it when a call fails in a way that suggests the worker changed underneath you, such as UnsupportedOperationError or ContentTypeNotSupportedError on a call that used to work. If you gate on a minimum agent version, compare parsed versions, not strings. As strings, "1.10.0" < "1.9.0".
from packaging.version import Version
def meets_minimum(card_version: str, minimum: str) -> bool:
return Version(card_version) >= Version(minimum)
Protocol versions are a separate axis. Always send A2A-Version: 1.0. A worker that receives no header must assume you speak 0.3.
The auth boundary between orchestrator and worker
The orchestrator usually holds broad privileges, because it talks to many workers, often across teams. Workers should not inherit them. The pattern that holds up is OAuth 2.0 token exchange (RFC 8693). The orchestrator authenticates as itself, then mints a downscoped token for each task, bound to the user and to the scopes the worker declares in its securityRequirements. The worker sees only that token. If the orchestrator is compromised, the blast radius is the set of tokens it minted recently, not the union of everything it can reach. This is the least-privilege principle from the agent governance post, applied at the inter-agent layer.
For AUTH_REQUIRED chains, follow the spec’s guidance on in-band credentials. Bind them to the agent that requested them, and encrypt anything sensitive. A credential that passes through three agents is otherwise visible to all three.
Long interrupted states
A task can sit in INPUT_REQUIRED for hours while a human thinks. Three things break if you do not design for that.
First, an SSE stream held open for hours is fragile, because load balancers and proxies close idle connections. Once a task is interrupted, switch to push notifications or slow polling. Re-subscribe only when the user responds.
Second, A2A does not define timeouts for interrupted tasks, so that policy is yours. A 24-hour input window is reasonable for a research workflow. A 30-second one is reasonable for chat. Enforce it in the worker by failing or canceling the task, and surface the question to the user well before that point.
Third, the resume path must not depend on the original orchestrator process. Persist the task ID, the contextId and the worker URL. Any orchestrator instance can then send the follow-up message.
Observability across the agent boundary
OpenTelemetry’s GenAI semantic conventions now live in their own repository, and their status is still Development, not stable. Pin the version you instrument against. They define an invoke_agent operation, whose span is named invoke_agent {gen_ai.agent.name}. They also define attributes including gen_ai.operation.name, gen_ai.provider.name, gen_ai.agent.id, gen_ai.agent.name and gen_ai.conversation.id. If you instrumented earlier, note that gen_ai.system is deprecated in favour of gen_ai.provider.name.
There is no task-ID attribute in the conventions. Mapping A2A’s contextId to gen_ai.conversation.id, and putting the task ID in a custom attribute, is our convention, not a standard. Propagate traceparent on every call across the boundary, and treat trace continuity as a launch blocker, the same posture we recommend for agent CI/CD.
The non-obvious part is interrupted tasks. When a worker enters INPUT_REQUIRED, its active span ends, because the work is suspended rather than running. Persist the trace context alongside the task ID, and re-inject it when the follow-up message arrives. Otherwise a task that pauses for 30 minutes splits into two unconnected traces.
When to reach for A2A and when not
A2A is a heavy hammer. It earns its weight when the work is asynchronous, multi-turn, or done by a peer with its own reasoning. It is overkill for synchronous tool calls, internal microservices, and one-shot model calls.
Reach for A2A when:
- Two or more agents, each with its own model, prompt and tools, need to delegate work to each other.
- The delegated work is long-running, or might pause for human input or authorization.
- The agents are owned by different teams, vendors or organizations. A standardized contract is exactly the integration boundary you want.
- You want to swap one vendor’s worker for another’s without rewriting the orchestrator.
Do not reach for A2A when:
- You are wiring a single agent to its tools. That is MCP.
- The “other agent” is really a deterministic microservice with no reasoning step.
- The work always completes in under a second and never pauses.
- You only have one agent. Build it on solid tool calling and MCP foundations, and add A2A when a second agent forces the question.
The honest version of the case for adoption: A2A v1.0 is the right protocol for the inter-agent layer, but most teams do not have an inter-agent layer yet. Adopting it before you need it is resume-driven architecture. Adopting it when you need it gives you agent fleets that compose, can be governed, and survive vendor churn. That is the same compounding posture we argue for in the multi-agent patterns post and in the defense-in-depth design behind CaMeL.
Where this is heading
The stack has settled. MCP handles the tool layer and A2A the agent layer, and both are hosted by the AAIF. The open questions have moved up a level. How do you run registries of A2A workers? How do you verify signed cards across organizations? How do you govern the trust boundary when the worker belongs to a vendor? Check the A2A releases page before you build. At the time of writing, v1.0.1 is the latest tagged specification.
For teams building today, the sequence that worked for MCP works here too. Pick one inter-agent boundary that is real, and model it as an A2A relationship. Publish the Agent Card and run the worker and orchestrator above against it. Only then generalize.
We build production multi-agent systems for growing businesses. If two of your agents need to coordinate, whether across teams, across vendors or across a human-in-the-loop pause, let’s talk.