DEV Community

Cover image for A2A Protocol Explained: Architecture, Alternatives, and a Hands-On Implementation
Chinnureddy
Chinnureddy

Posted on

A2A Protocol Explained: Architecture, Alternatives, and a Hands-On Implementation

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.

A diagram showing multiple AI agents communicating with each other in a workflow

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
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Done.

But real systems often look like:

research-agent.company.internal
coding-agent.company.internal
testing-agent.company.internal
review-agent.company.internal
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Agent B knows:

I received a generate_code task
Enter fullscreen mode Exit fullscreen mode

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  │
└───────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Code:

result = coding_agent.generate_code(
    "Build a FastAPI API"
)
Enter fullscreen mode Exit fullscreen mode

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     │
└──────────────────┘
Enter fullscreen mode Exit fullscreen mode

The Research Agent might call:

POST /generate-code
Enter fullscreen mode Exit fullscreen mode

with:

{
  "language": "python",
  "requirement": "Build a FastAPI API"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

we define one common communication endpoint:

POST /a2a
Enter fullscreen mode Exit fullscreen mode

The operation is inside the message.

{
  "type": "task.create",

  "task_id": "task-123",

  "capability": "generate_code",

  "input": {
    "language": "python"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It may take 30 seconds or several minutes.

Instead of waiting:

Agent A
   |
   | request
   v
Agent B
   |
   | wait...
   |
   | wait...
   |
   v
result
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Possible technologies include:

Kafka
NATS
RabbitMQ
Redis Streams
Cloud queues
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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       │
└──────────────────┘
Enter fullscreen mode Exit fullscreen mode

The user asks:

"Build a Python REST API for user authentication."
Enter fullscreen mode Exit fullscreen mode

The workflow is:

User
 |
 v
Research Agent
 |
 | create task
 v
Coding Agent
 |
 | code
 v
Testing Agent
 |
 | test result
 v
Research Agent
 |
 v
Final response
Enter fullscreen mode Exit fullscreen mode

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
        }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "passed": 10,
  "failed": 0
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The orchestrator knows:

class names
method names
argument formats
return formats
implementation details
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The request:

import httpx

response = httpx.post(
    "http://coding-agent/generate-code",
    json={
        "language": "python",
        "requirement": "Build authentication API"
    }
)

code = response.json()
Enter fullscreen mode Exit fullscreen mode

This works.

But now the Research Agent depends on:

URL
endpoint
request schema
response schema
authentication
error handling
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

Run:

uvicorn server:app --port 8001
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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')"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Agent A needs to understand Agent B's API.

After

Agent A
   |
   | common protocol
   v
Agent B
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's understand why.


22. Task Identity

{
  "task_id": "task-123"
}
Enter fullscreen mode Exit fullscreen mode

This allows the whole workflow to refer to the same operation.

task-123
   |
   +-- task.create
   +-- task.accept
   +-- task.progress
   +-- task.complete
Enter fullscreen mode Exit fullscreen mode

23. Message Identity

{
  "message_id": "msg-456"
}
Enter fullscreen mode Exit fullscreen mode

This identifies one communication event.

Useful for:

deduplication
logging
retries
debugging
tracing
Enter fullscreen mode Exit fullscreen mode

24. Capability Discovery

Instead of asking:

"What endpoint does this service have?"
Enter fullscreen mode Exit fullscreen mode

we can ask:

"What can this agent do?"
Enter fullscreen mode Exit fullscreen mode

Example:

{
  "agent_id": "coding-agent",

  "capabilities": [
    "generate_code",
    "review_code",
    "run_tests"
  ]
}
Enter fullscreen mode Exit fullscreen mode

This is particularly useful when agents are dynamically deployed.


25. Structured Output

Avoid:

{
  "output": "Everything looks good."
}
Enter fullscreen mode Exit fullscreen mode

Prefer:

{
  "output": {
    "status": "approved",

    "issues": [],

    "artifacts": []
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the caller knows what to do.


27. Retries

Consider:

Agent A
   |
   | task.create
   v
Agent B
   |
   | executes
   |
   X network failure
Enter fullscreen mode Exit fullscreen mode

Agent A may retry.

But if Agent B already executed the task, we don't want:

execute
execute
Enter fullscreen mode Exit fullscreen mode

We want idempotency.

For example:

message_id = msg-100
Enter fullscreen mode Exit fullscreen mode

Agent B stores:

msg-100 -> completed
Enter fullscreen mode Exit fullscreen mode

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 <-------------------------
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

we can stream:

task.progress
task.progress
task.progress
task.complete
Enter fullscreen mode Exit fullscreen mode

For example:

{
  "type": "task.progress",
  "task_id": "task-123",
  "progress": 50,
  "message": "Running integration tests"
}
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

Authentication might use:

mTLS
JWT
OAuth
API keys
workload identity
Enter fullscreen mode Exit fullscreen mode

Authorization then controls capabilities.

For example:

research-agent
    |
    +--> generate_code       allowed
    |
    +--> run_tests           allowed
    |
    +--> production_deploy   denied
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

33. A2A vs Orchestration

These are also different concepts.

An orchestrator decides:

What should happen next?
Enter fullscreen mode Exit fullscreen mode

A2A decides:

How do agents communicate that work?
Enter fullscreen mode Exit fullscreen mode

For example:

Orchestrator

1. Ask Research Agent
2. Ask Coding Agent
3. Ask Testing Agent
4. Ask Review Agent
Enter fullscreen mode Exit fullscreen mode

A2A handles the communication:

task.create
task.accept
task.complete
task.failed
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You probably don't need it when:

everything is one Python process
+
only two agents exist
+
communication is simple
+
you control everything
Enter fullscreen mode Exit fullscreen mode

In that situation:

agent_b.run(task)
Enter fullscreen mode Exit fullscreen mode

is perfectly good engineering.


35. A Practical Evolution Path

Don't jump directly into a distributed architecture.

Use this progression:

Stage 1
Function calls
Enter fullscreen mode Exit fullscreen mode


Stage 2
HTTP + JSON
Enter fullscreen mode Exit fullscreen mode


Stage 3
Common A2A message format
Enter fullscreen mode Exit fullscreen mode


Stage 4
Task persistence + async workers
Enter fullscreen mode Exit fullscreen mode


Stage 5
Discovery + security + observability
Enter fullscreen mode Exit fullscreen mode


Stage 6
Message broker + streaming + horizontal scaling
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With a common protocol:

       A2A
        |
   ┌────┼────┐
   |    |    |
   A    B    C
        |
        D
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

Layer 2 — Task

What work are we asking it to perform?
Enter fullscreen mode Exit fullscreen mode

Layer 3 — Message

How do we communicate that work?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

When agents become independent services, we can move to:

HTTP + JSON
Enter fullscreen mode Exit fullscreen mode

When multiple independent agents need a shared communication model, we introduce an A2A protocol:

task
message
capability
state
result
error
Enter fullscreen mode Exit fullscreen mode

Then, when the system grows, we add:

async execution
queues
workers
discovery
authentication
authorization
retries
streaming
observability
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
steal profile image
Chinnureddy

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