Skip to main content
C carlos.enredando.me CTO · Advisor · Builder
Mastering Agentic AI: When Agents Talk to Other Agents (A2A)
A network — photo by Gábor Szűts on Unsplash.

Mastering Agentic AI: When Agents Talk to Other Agents (A2A)

·1515 words·8 mins
Carlos Prados
Author
Carlos Prados
Telecommunications Engineer, Entrepreneur, CTO & CIO, Team Leader & Manager, IoT-M2M-Big Data Consultant, Pre-sales Engineer, Product-Service Manager & Strategist.

In my previous post, we tackled Knowledge Retrieval (RAG) — giving a single agent access to facts it wasn’t trained on. That makes one agent smarter. But a lot of real systems aren’t one agent with better knowledge; they’re several agents, each owning a slice of the problem, that need to work together.

The moment you have more than one agent, you have a communication problem. How does Agent A address Agent B? How do they agree on the shape of a message? And what happens when Agent B doesn’t live inside your process at all — it’s a service some other team (or some other company) runs behind an HTTP endpoint?

That’s the Inter-Agent Communication (A2A) pattern. It’s the plumbing that turns a pile of isolated agents into an actual system.

Pattern #15: Inter-Agent Communication (A2A)
#

The Problem
#

Multi-agent systems (the multi-agent pattern from chapter 7) only work if agents can talk. And “talk” is doing a lot of heavy lifting in that sentence.

Inside one process it’s easy to be sloppy: you call a function, pass a dict, read the result. But that couples everything. The caller has to know the callee’s internals, its state shape, its imports. Scale that to a dozen agents and you’ve built a monolith wearing an agent costume.

The harder case is when agents are remote. Now you need answers to questions that function calls hide from you:

  • Discovery: how does an agent know another agent exists, and what it can do?
  • Addressing: where do I send a request, and how is it authenticated?
  • Message format: what’s the on-the-wire contract, so two agents written by two different teams interoperate?
  • Interaction style: is this one-shot request/response, or a long-running streaming task?

Ad-hoc REST endpoints answer none of these in a standard way. That’s the gap A2A fills: a protocol — built on JSON-RPC over HTTP — for agents to advertise their capabilities and exchange structured messages, regardless of who built them.

The Solution
#

There are two layers worth separating: in-process coordination (multiple agents in one runtime) and the A2A wire protocol (agents talking across the network). The scripts in this chapter cover both.

1. In-process: sub-graphs talking through a coordinator. The cleanest way to keep multiple agents decoupled inside one runtime is to make each one a self-contained graph with its own state, and have a coordinator pass structured messages between them. Each sub-agent is a tiny compiled graph:

class CalendarState(TypedDict):
    request: str
    calendar_response: str


def calendar_handler(state: CalendarState) -> dict:
    llm = get_llm(temperature=0)
    prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a calendar management assistant. Handle scheduling requests. "
                   "Respond with what actions you would take (create/modify/check events)."),
        ("user", "{request}")
    ])
    chain = prompt | llm | StrOutputParser()
    return {"calendar_response": chain.invoke({"request": state["request"]})}


def build_calendar_subgraph():
    builder = StateGraph(CalendarState)
    builder.add_node("calendar_handler", calendar_handler)
    builder.add_edge(START, "calendar_handler")
    builder.add_edge("calendar_handler", END)
    return builder.compile()

The CalendarState is private to that sub-graph. The coordinator never reaches inside it — it hands over a request string and gets back a calendar_response string. That’s the contract, and it’s the only thing both sides need to agree on:

calendar_graph = build_calendar_subgraph()
task_graph = build_task_subgraph()


class CoordinatorState(TypedDict):
    user_request: str
    calendar_result: str
    task_result: str
    final_response: str


def dispatch_to_calendar(state: CoordinatorState) -> dict:
    """Sends the request to the Calendar sub-graph."""
    result = calendar_graph.invoke({"request": state["user_request"]})
    return {"calendar_result": result["calendar_response"]}


def dispatch_to_tasks(state: CoordinatorState) -> dict:
    """Sends the request to the Task Manager sub-graph."""
    result = task_graph.invoke({"request": state["user_request"]})
    return {"task_result": result["task_response"]}

The coordinator wires the two sub-agents to run concurrently, then merges their outputs in a synthesis step — the same fan-out/fan-in shape from the parallelization pattern:

def build_coordinator_graph():
    builder = StateGraph(CoordinatorState)
    builder.add_node("dispatch_to_calendar", dispatch_to_calendar)
    builder.add_node("dispatch_to_tasks", dispatch_to_tasks)
    builder.add_node("synthesize_responses", synthesize_responses)

    # Both sub-agents run in parallel
    builder.add_edge(START, "dispatch_to_calendar")
    builder.add_edge(START, "dispatch_to_tasks")

    # Both feed into synthesis
    builder.add_edge("dispatch_to_calendar", "synthesize_responses")
    builder.add_edge("dispatch_to_tasks", "synthesize_responses")

    builder.add_edge("synthesize_responses", END)
    return builder.compile()

The win here is isolation. The calendar agent can be rewritten, given new tools, or swapped for a remote service — and the coordinator doesn’t change, because it only ever depended on the message contract, not the implementation.

2. Over the wire: the A2A protocol. That message contract is exactly what A2A standardizes for remote agents. It starts with discovery. Every A2A agent publishes an Agent Card — a JSON document describing who it is, where it lives, how to authenticate, and what skills it offers:

{
  "name": "WeatherBot",
  "description": "Provides accurate weather forecasts and historical data.",
  "url": "http://weather-service.example.com/a2a",
  "version": "1.0.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "stateTransitionHistory": true
  },
  "authentication": {
    "schemes": ["apiKey"]
  },
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "skills": [
    {
      "id": "get_current_weather",
      "name": "Get Current Weather",
      "description": "Retrieve real-time weather for any location.",
      "examples": ["What's the weather in Paris?", "Current conditions in Tokyo"],
      "tags": ["weather", "current", "real-time"]
    }
  ]
}

This is the same idea as an OpenAPI spec or an MCP server descriptor: a machine-readable manifest so a calling agent can find the endpoint, see that it does streaming but not pushNotifications, and decide whether it can use it — without a human reading docs.

Once you’ve discovered an agent, you talk to it over JSON-RPC. A2A defines two interaction styles. The first is a synchronous task — fire it, wait for the result:

sync_request = {
    "jsonrpc": "2.0",
    "id": "1",
    "method": "sendTask",
    "params": {
        "id": "task-001",
        "sessionId": "session-001",
        "message": {
            "role": "user",
            "parts": [
                {"type": "text", "text": "What is the exchange rate from USD to EUR?"}
            ]
        },
        "acceptedOutputModes": ["text/plain"],
        "historyLength": 5
    }
}

The second is a streaming subscription, for long-running work where you want incremental updates instead of blocking on one big response — same envelope, different method:

streaming_request = {
    "jsonrpc": "2.0",
    "id": "2",
    "method": "sendTaskSubscribe",
    "params": {
        "id": "task-002",
        "sessionId": "session-001",
        "message": {
            "role": "user",
            "parts": [
                {"type": "text", "text": "What's the exchange rate for JPY to GBP today?"}
            ]
        },
        "acceptedOutputModes": ["text/plain"],
        "historyLength": 5
    }
}

Notice the structure that repeats: a task has a stable id and a sessionId, the payload is a message with a role and a list of typed parts, and the caller declares acceptedOutputModes. That’s the contract two independently-built agents agree on — sendTask vs sendTaskSubscribe is the only thing that changes between request/response and streaming.

Why This Matters
#

A2A is what makes multi-agent systems an architecture instead of a tangle. Concretely:

  • Cross-team and cross-vendor agents: your scheduling agent calls a billing agent another team owns — or a third party’s weather agent — without sharing a codebase. The Agent Card is the only integration surface.
  • Independent deployment and scaling: each agent is its own service, deployed, versioned, and scaled on its own. Swap an implementation behind the same card and nobody upstream notices.
  • Long-running work: sendTaskSubscribe plus sessionId lets you model tasks that take minutes and stream progress, not just sub-second Q&A.
  • Discovery at runtime: a coordinator can fetch cards, read the skills and capabilities, and route dynamically instead of hard-coding who does what.

The cost is real, and it’s the cost of all distributed systems. You’ve traded a function call for a network hop: latency, partial failure, retries, auth, versioning of the card, and observability that now has to span process boundaries. A bug that used to be a stack trace is now a distributed trace. Don’t reach for A2A because it’s elegant — reach for it when agents genuinely need to be separate services owned by separate people. If your agents live happily in one process, the in-process coordinator above is simpler and faster, and you should stay there until you can’t.

Rule of thumb: use in-process sub-graphs while you own all the agents and they fit one runtime; adopt A2A the moment an agent needs to be discovered, deployed, or owned independently.


The Bigger Picture
#

This is post #15 in my series documenting Antonio Gulli’s Agentic Design Patterns. All credit for the conceptual work goes to him — I’m focused on producing clean, runnable Python implementations that you can clone, modify, and ship.

A2A sits in the same family as MCP (chapter 10): both are standardized contracts that let agents reach beyond their own process. MCP standardizes how an agent talks to tools and data; A2A standardizes how an agent talks to other agents. Together they’re the two halves of an interoperable agent ecosystem — and both lean hard on the guardrails we’ll cover later, because the moment you accept messages from agents you didn’t write, trust stops being free.

All the code from this post is in my repository: carlosprados/Agentic_Design_Patterns, specifically under 15_Inter_Agent_Communication_A2A/. The LangGraph coordinator runs with uv run and supports Gemini and Ollama via the shared get_llm() abstraction; the Agent Card and JSON-RPC examples are plain JSON you can adapt to any A2A server.


What’s Next
#

In the next post we’ll tackle Resource-Aware Optimization — teaching agents to reason about their own budget: which model to call, how many tokens to spend, when a cheap path is good enough and when the expensive one is worth it. Once your agents are talking to each other across a network, every message has a cost, and managing that cost deliberately is the difference between a demo and a system you can afford to run.

Stay tuned.