AI agents are becoming less like isolated chatbots and more like software components.
- One agent can research.
- Another can write code.
- Another can test the code.
- Another can deploy it.
That immediately creates a new engineering problem:
How should one agent communicate with another agent?
You can solve this with a simple function call.
You can solve it with REST APIs.
You can use queues or event streams.
Or you can define a proper Agent-to-Agent communication protocol.
This article explains the problem from the ground up, compares the alternatives, and builds one working A2A system step by step.
1. What Is Agent-to-Agent Communication?
At its simplest, Agent-to-Agent communication means:
Agent A
|
| request
v
Agent B
|
| result
v
Agent A
Agent A asks Agent B to perform some work.
For example:
Research Agent
"Find the best authentication approach for our API."
|
v
Coding Agent
"Implement JWT authentication."
|
v
Testing Agent
"Run the test suite."
|
v
Review Agent
"Review the implementation."
Each agent specializes in something.
Instead of making one giant agent do everything, we divide responsibility.
2. Why Do We Need A2A?
Suppose everything is inside one Python process.
We can simply write:
coding_agent.generate_code(request)
Done.
But real systems often look like:
research-agent.company.internal
coding-agent.company.internal
testing-agent.company.internal
review-agent.company.internal
Now agents are independent services.
We need to answer:
Who is sending the request?
Who should receive it?
What task is being requested?
What capability is required?
What input is being provided?
What is the task status?
Where is the result?
What happens if the request fails?
What happens if the request is duplicated?
How does the receiving agent authenticate the caller?
That is the real reason protocols become useful.
3. A2A Is a Communication Contract
A2A is easiest to understand as a contract.
It defines how agents exchange work.
For example:
{
"task_id": "task-123",
"sender": "research-agent",
"receiver": "coding-agent",
"capability": "generate_code",
"input": {
"language": "python",
"requirements": [
"FastAPI",
"JWT authentication"
]
}
}
The important part is not the JSON itself.
The important part is that both agents understand the same meaning.
Agent A knows:
I am asking for capability = generate_code
Agent B knows:
I received a generate_code task
That shared understanding is the protocol.
4. A2A Is Not the Same Thing as HTTP
This distinction is important.
HTTP is a transport.
A2A is the communication contract.
You can think of it as:
┌───────────────────────────┐
│ A2A Protocol │
│ │
│ Task │
│ Message │
│ Capability │
│ State │
│ Result │
│ Error │
└─────────────┬─────────────┘
|
v
┌───────────────────────────┐
│ Transport │
│ │
│ HTTP / WebSocket / Queue │
└───────────────────────────┘
Today you might use HTTP.
Tomorrow you may use a message broker.
The protocol concepts can remain the same.
5. What Are the Ways to Achieve Agent-to-Agent Communication?
There is no single architecture.
There are several levels.
Level 1 - Direct Function Calls
The easiest approach.
Agent A
|
| function call
v
Agent B
Code:
result = coding_agent.generate_code(
"Build a FastAPI API"
)
Advantages
Very simple.
Very fast.
Easy to debug.
Problems
Agents are tightly coupled.
They must exist in the same application or runtime.
You cannot easily deploy them independently.
6. Level 2 — REST API Between Agents
Now make the agents separate services.
┌──────────────────┐
│ Research Agent │
└────────┬─────────┘
|
| HTTP
v
┌──────────────────┐
│ Coding Agent │
└──────────────────┘
The Research Agent might call:
POST /generate-code
with:
{
"language": "python",
"requirement": "Build a FastAPI API"
}
This is already useful.
But now each agent needs its own API contract.
You might end up with:
POST /generate-code
POST /review-code
POST /run-tests
POST /deploy
POST /summarize
As the number of agents grows, integration becomes harder.
7. Level 3 — A Common A2A Endpoint
Instead of creating a different API contract for every capability:
/generate-code
/review-code
/run-tests
we define one common communication endpoint:
POST /a2a
The operation is inside the message.
{
"type": "task.create",
"task_id": "task-123",
"capability": "generate_code",
"input": {
"language": "python"
}
}
Now the endpoint stays stable.
The capability changes.
This gives us a much cleaner abstraction.
8. Level 4 — Asynchronous A2A
Some agent tasks take time.
For example:
LLM reasoning
+
database lookup
+
code generation
+
test execution
+
security scan
It may take 30 seconds or several minutes.
Instead of waiting:
Agent A
|
| request
v
Agent B
|
| wait...
|
| wait...
|
v
result
we can make the communication asynchronous.
Agent A
|
| task.create
v
Agent B
|
| task.accept
v
Agent A
...work happens...
Agent B
|
| task.complete
v
Agent A
This is much more suitable for production systems.
9. Level 5 — A2A + Message Broker
For larger systems:
Agent A
|
| publish
v
┌─────────────────┐
│ Message Broker │
└───────┬─────────┘
|
+─────────────+
| |
v v
Agent B Agent C
Possible technologies include:
Kafka
NATS
RabbitMQ
Redis Streams
Cloud queues
Now agents do not have to call each other directly.
They publish and consume events.
This provides better decoupling and scaling, but also introduces more infrastructure.
10. So What Is the Best Approach?
There is no universal answer.
A useful progression is:
Same process
↓
Function call
Separate services
↓
HTTP
Long-running tasks
↓
HTTP + Task Store + Worker
Large distributed system
↓
Broker / Event-driven architecture
The mistake is starting with the most complicated architecture.
Start with the smallest architecture that solves the actual problem.
11. One Real Use Case
Let's use one concrete example throughout this article.
We are building a software-development system.
There are three agents:
┌──────────────────┐
│ Research Agent │
│ │
│ Finds solution │
└────────┬─────────┘
|
v
┌──────────────────┐
│ Coding Agent │
│ │
│ Writes code │
└────────┬─────────┘
|
v
┌──────────────────┐
│ Testing Agent │
│ │
│ Runs tests │
└──────────────────┘
The user asks:
"Build a Python REST API for user authentication."
The workflow is:
User
|
v
Research Agent
|
| create task
v
Coding Agent
|
| code
v
Testing Agent
|
| test result
v
Research Agent
|
v
Final response
Now let's implement it.
12. Implementation 1 — Without A2A
First, let's solve the problem without A2A.
Everything runs inside one application.
class ResearchAgent:
def create_plan(self, requirement):
return {
"language": "python",
"requirement": requirement
}
class CodingAgent:
def generate_code(self, plan):
return {
"files": [
{
"name": "main.py",
"content": "print('FastAPI app')"
}
]
}
class TestingAgent:
def run_tests(self, code):
return {
"passed": 10,
"failed": 0
}
The orchestrator calls each agent:
research = ResearchAgent()
coding = CodingAgent()
testing = TestingAgent()
plan = research.create_plan(
"Build authentication API"
)
code = coding.generate_code(plan)
tests = testing.run_tests(code)
print(tests)
Output:
{
"passed": 10,
"failed": 0
}
This is perfectly valid.
There is nothing wrong with this architecture.
13. What Is the Problem Here?
Look at the coupling.
Orchestrator
|
+--> ResearchAgent
|
+--> CodingAgent
|
+--> TestingAgent
The orchestrator knows:
class names
method names
argument formats
return formats
implementation details
Now imagine the Coding Agent becomes a separate service.
Everything changes.
14. Implementation 2 — Without a Formal A2A Protocol
We can move the Coding Agent to another service.
Research Agent
|
| HTTP
v
Coding Service
The request:
import httpx
response = httpx.post(
"http://coding-agent/generate-code",
json={
"language": "python",
"requirement": "Build authentication API"
}
)
code = response.json()
This works.
But now the Research Agent depends on:
URL
endpoint
request schema
response schema
authentication
error handling
The Coding Agent has an API.
The Research Agent has to understand that API.
15. Implementation 3 — Introduce a Minimal A2A Protocol
Now define a shared message.
from pydantic import BaseModel
class A2AMessage(BaseModel):
message_id: str
task_id: str
type: str
sender: str
receiver: str
capability: str
input: dict
Example:
{
"message_id": "msg-100",
"task_id": "task-100",
"type": "task.create",
"sender": "research-agent",
"receiver": "coding-agent",
"capability": "generate_code",
"input": {
"language": "python",
"requirement": "Build authentication API"
}
}
Now every agent understands the same basic contract.
16. Create a Generic Agent
class Agent:
def __init__(self, agent_id):
self.agent_id = agent_id
self.capabilities = {}
def register(self, name, handler):
self.capabilities[name] = handler
async def execute(self, capability, input):
handler = self.capabilities.get(capability)
if not handler:
raise ValueError(
f"Capability '{capability}' not found"
)
return await handler(input)
Now register a coding capability.
coding_agent = Agent("coding-agent")
async def generate_code(input):
return {
"language": input["language"],
"artifacts": [
{
"type": "file",
"name": "main.py",
"content": "print('FastAPI')"
}
]
}
coding_agent.register(
"generate_code",
generate_code
)
17. Add the A2A API
Using FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.post("/a2a")
async def receive_message(
message: A2AMessage
):
result = await coding_agent.execute(
message.capability,
message.input
)
return {
"message_id": "msg-101",
"task_id": message.task_id,
"type": "task.complete",
"sender": "coding-agent",
"receiver": message.sender,
"output": result
}
Run:
uvicorn server:app --port 8001
Now the Coding Agent is an independent service.
18. Send a Real A2A Request
The Research Agent sends:
import httpx
message = {
"message_id": "msg-100",
"task_id": "task-100",
"type": "task.create",
"sender": "research-agent",
"receiver": "coding-agent",
"capability": "generate_code",
"input": {
"language": "python",
"requirement":
"Build authentication API"
}
}
response = httpx.post(
"http://localhost:8001/a2a",
json=message
)
print(response.json())
19. The Output
The Coding Agent returns:
{
"message_id": "msg-101",
"task_id": "task-100",
"type": "task.complete",
"sender": "coding-agent",
"receiver": "research-agent",
"output": {
"language": "python",
"artifacts": [
{
"type": "file",
"name": "main.py",
"content": "print('FastAPI')"
}
]
}
}
Now the communication is explicit:
Research Agent
|
| task.create
|
| task_id = task-100
v
Coding Agent
|
| task.complete
|
| task_id = task-100
v
Research Agent
20. What Did A2A Actually Improve?
It is important not to claim that A2A magically makes agents smarter.
It doesn't.
The model is still the model.
The improvement is in system architecture.
Before
Agent A
|
| custom API
v
Agent B
Agent A needs to understand Agent B's API.
After
Agent A
|
| common protocol
v
Agent B
Both understand the same communication contract.
This gives us:
Loose coupling
Agents don't need to know each other's internal implementation.
Interoperability
Different agents can communicate using the same contract.
Replaceability
You can replace the Coding Agent with another implementation as long as it supports the same protocol.
Async execution
Tasks can continue without keeping an HTTP request open.
Traceability
Every task can have a unique ID.
Extensibility
New capabilities can be introduced without creating a completely new communication architecture.
21. Important Features of a Good A2A Protocol
A production-quality protocol normally needs more than:
sender
receiver
input
output
Useful capabilities include:
Task identity
Message identity
Capability discovery
Task states
Structured results
Error handling
Retries
Timeouts
Authentication
Authorization
Idempotency
Streaming
Observability
Versioning
Artifact references
Let's understand why.
22. Task Identity
{
"task_id": "task-123"
}
This allows the whole workflow to refer to the same operation.
task-123
|
+-- task.create
+-- task.accept
+-- task.progress
+-- task.complete
23. Message Identity
{
"message_id": "msg-456"
}
This identifies one communication event.
Useful for:
deduplication
logging
retries
debugging
tracing
24. Capability Discovery
Instead of asking:
"What endpoint does this service have?"
we can ask:
"What can this agent do?"
Example:
{
"agent_id": "coding-agent",
"capabilities": [
"generate_code",
"review_code",
"run_tests"
]
}
This is particularly useful when agents are dynamically deployed.
25. Structured Output
Avoid:
{
"output": "Everything looks good."
}
Prefer:
{
"output": {
"status": "approved",
"issues": [],
"artifacts": []
}
}
Structured output allows another agent to consume the result without parsing natural language.
26. Error Handling
A protocol should define errors.
For example:
{
"type": "task.failed",
"task_id": "task-123",
"error": {
"code": "CAPABILITY_NOT_SUPPORTED",
"message":
"Agent cannot generate code",
"retryable": false
}
}
Now the caller knows what to do.
27. Retries
Consider:
Agent A
|
| task.create
v
Agent B
|
| executes
|
X network failure
Agent A may retry.
But if Agent B already executed the task, we don't want:
execute
execute
We want idempotency.
For example:
message_id = msg-100
Agent B stores:
msg-100 -> completed
A duplicate message can then return the existing result.
28. Async Execution
A useful production flow is:
task.create
Agent A ----------------------> Agent B
|
v
persist task
|
v
task.accept
|
v
execute task
|
v
task.complete
|
v
Agent A <-------------------------
Now the task can run for minutes without holding an HTTP connection open.
29. Streaming
Some agents generate incremental output.
Instead of waiting:
request
|
| ............
|
v
complete response
we can stream:
task.progress
task.progress
task.progress
task.complete
For example:
{
"type": "task.progress",
"task_id": "task-123",
"progress": 50,
"message": "Running integration tests"
}
This is useful for long-running workflows.
30. Security
A serious A2A system must answer:
Who is this agent?
What is it allowed to do?
What data can it access?
Which capabilities can it invoke?
Authentication might use:
mTLS
JWT
OAuth
API keys
workload identity
Authorization then controls capabilities.
For example:
research-agent
|
+--> generate_code allowed
|
+--> run_tests allowed
|
+--> production_deploy denied
31. Where A2A Fits
A useful architecture is:
User
|
v
Orchestrator
|
v
┌─────────────────┐
│ A2A Protocol │
└───────┬─────────┘
|
┌──────────┼──────────┐
| | |
v v v
Research Coding Testing
Agent Agent Agent
| | |
└──────────┼──────────┘
|
v
Tools
A2A is the communication layer between the agent components.
32. A2A vs Other Approaches
A2A isn't always the best answer.
| Approach | Best when |
|---|---|
| Function call | Agents are in one process |
| REST API | Agents are independent services |
| RPC/gRPC | Strong typed service-to-service communication |
| Message queue | Long-running asynchronous work |
| Event streaming | Large event-driven systems |
| Workflow engine | Complex deterministic workflows |
| A2A protocol | Independent agents need a common communication contract |
You may even combine them.
For example:
A2A
|
+-- HTTP for control
|
+-- queue for long-running work
|
+-- object storage for artifacts
33. A2A vs Orchestration
These are also different concepts.
An orchestrator decides:
What should happen next?
A2A decides:
How do agents communicate that work?
For example:
Orchestrator
1. Ask Research Agent
2. Ask Coding Agent
3. Ask Testing Agent
4. Ask Review Agent
A2A handles the communication:
task.create
task.accept
task.complete
task.failed
So they complement each other.
34. When Should You Use A2A?
Use a formal A2A-style protocol when:
agents are independently deployed
+
agents may come from different teams
+
agents have different capabilities
+
tasks are asynchronous
+
you need traceability
+
you need standardization
You probably don't need it when:
everything is one Python process
+
only two agents exist
+
communication is simple
+
you control everything
In that situation:
agent_b.run(task)
is perfectly good engineering.
35. A Practical Evolution Path
Don't jump directly into a distributed architecture.
Use this progression:
Stage 1
Function calls
↓
Stage 2
HTTP + JSON
↓
Stage 3
Common A2A message format
↓
Stage 4
Task persistence + async workers
↓
Stage 5
Discovery + security + observability
↓
Stage 6
Message broker + streaming + horizontal scaling
This keeps complexity proportional to your actual requirements.
36. The Real Value of A2A
A2A doesn't make an agent more intelligent.
Instead, it makes an agent system more composable.
Without a common protocol:
Agent A
↓ custom integration
Agent B
↓ another integration
Agent C
↓ another integration
Agent D
With a common protocol:
A2A
|
┌────┼────┐
| | |
A B C
|
D
Each agent only needs to understand the protocol.
That is the architectural win.
37. Final Mental Model
Think about A2A as three layers.
Layer 1 — Agent
What can this agent do?
Layer 2 — Task
What work are we asking it to perform?
Layer 3 — Message
How do we communicate that work?
So the complete flow is:
Agent A
|
| creates
v
Task
|
| encoded as
v
A2A Message
|
| transported by
v
HTTP / Queue / WebSocket
|
v
Agent B
|
| executes
v
Result
That is the core idea.
Conclusion
There is no magic behind Agent-to-Agent communication.
At the beginning, it can be as simple as:
coding_agent.generate_code(task)
When agents become independent services, we can move to:
HTTP + JSON
When multiple independent agents need a shared communication model, we introduce an A2A protocol:
task
message
capability
state
result
error
Then, when the system grows, we add:
async execution
queues
workers
discovery
authentication
authorization
retries
streaming
observability
The important architectural lesson is:
Don't start with a protocol because protocols sound sophisticated. Start with the communication problem. Introduce A2A when a common contract between independent agents provides real value.
The simplest implementation can be built in a few lines.
The same contract can later evolve into a distributed multi-agent platform.
That is what makes A2A useful: not that it makes agents smarter, but that it makes them easier to connect, replace, scale, observe, and compose.

Top comments (1)
Open for discussion! Feel free to share your thoughts, feedback, or experiences with A2A in the comments.