Agentic AI · Module 18 of 41 · 9 concepts · about 15 min

Agent Communication

How agents inside one system exchange information: message passing, shared context and shared state, structured messages, hand-offs and result aggregation; and the four problems to design against: context explosion, contradiction, duplicated work and overhead.

At work: Where multi-agent systems are won or lost. The agents are rarely the problem; what they tell each other, and what gets lost on the way, usually is.

Three ways agents share information

What it is

Agents in one system exchange information in three ways:

  • Message passing: one agent sends another a message (a brief, a report, a question). Each keeps its own private context.
  • Shared context: agents read the same conversation history. Everyone sees everything said.
  • Shared state: agents read and write a structured record (plan, facts, task statuses) held outside any one context.

Why it matters

The choice decides how much each agent knows and how much it costs to tell it.

  • Message passing keeps contexts small and isolates mistakes, and anything not put in the message is lost.
  • Shared context loses nothing and grows without limit.
  • Shared state gives a common picture in compact form, and needs rules about who may write what.
  • Most good systems combine message passing with a small shared state.

In the real world: A framework's default 'group chat' mode shared one transcript among six agents; by turn thirty each call carried forty thousand tokens of other agents' chatter. Switching to briefs and a shared task table cut cost by five-sixths.

Visual example

Channels

The middle column is the easiest to build and the first to break.

Message passing, shared context and shared state
message passingshared contextshared state
Each agent seesOnly what it is sentEverythingA structured summary
Context sizeSmallGrows without limitSmall
Information lostWhatever was not sentNothingWhatever was not recorded
A mistake spreadsOnly if passed onTo everyone at onceTo readers of that field
NeedsGood briefs and reportsAggressive trimmingOwnership rules

Defaulting to shared context because it is easiest to build: just give every agent the full transcript. It works with two agents for ten turns and collapses with five agents over a hundred.

Practise it: Design an AI Customer Support Agent

Structured messages and agent protocols

What it is

A structured message has named, typed fields instead of free prose. An agent protocol is the agreed set of message types and the rules for exchanging them. A minimal internal protocol defines:

  • an envelope: who sent it, to whom, which task, a message id, a timestamp;
  • a type: request, result, question, error, status update;
  • a body with a schema for that type;
  • the rules: every request gets exactly one result or error; questions go to the sender; results carry their sources.

Why it matters

Prose between agents fails the same way prose between a model and code fails.

  • The receiver must guess what is a finding and what is a caveat.
  • Code cannot check, route or count free text.
  • With typed messages you can validate a report before another agent reads it, log every exchange, and replay one.
  • The envelope's task id is what lets a trace follow one task across several agents.

In the real world: After adding a required not_done field to worker reports, a supervisor stopped presenting half-finished research as complete; the workers had always known, and now had somewhere to say so.

from typing import Literal
from pydantic import BaseModel

class Envelope(BaseModel):
    message_id: str
    task_id: str                    # ties every message to one task, across agents
    sender: str
    recipient: str
    type: Literal["request", "result", "question", "error", "status"]

class Result(BaseModel):
    envelope: Envelope
    status: Literal["complete", "partial", "failed"]
    findings: list[str]
    sources: list[str]              # every finding must be traceable
    not_done: str                   # what was asked and not achieved, and why
    tokens_used: int

def send(message: BaseModel) -> None:
    validate(message)               # reject malformed messages before anyone reads them
    log_message(message)            # every exchange is recorded for the trace
    inbox[message.envelope.recipient].put(message)

A status field with only 'done'. Workers then report success on partial work because there is no other option. Give them 'partial' and 'failed', and a not_done field they must fill.

Task handoffs and delegation

What it is

Two ways one agent involves another. They differ in who stays responsible:

  • Delegation: 'do this for me and report back'. The delegating agent keeps ownership and waits for the result. The worker returns control.
  • Hand-off: 'this is yours now'. Ownership moves. The receiving agent finishes the task and answers the user; the sender is done.

Both need a hand-off package: the goal, what is known, what has been done (with ids), what remains, and any constraints.

Why it matters

Confusing the two causes dropped tasks.

  • After a delegation, someone is waiting for a result; if none comes, they notice.
  • After a hand-off, nobody is waiting. If the receiver drops it, the task is simply gone.
  • So hand-offs need an explicit acknowledgement, and exactly one owner must be recorded at all times.
  • Actions already taken must travel with the task, or the next agent repeats them.

In the real world: A support system's most common duplicate action disappeared when hand-off packages began to carry actions_taken; idempotency keys on the tools caught the rest.

Visual example

Ownership

The question is who is responsible afterwards.

Delegation versus hand-off
delegationhand-off
OwnershipStays with the senderMoves to the receiver
Sender afterwardsWaits for the resultIs finished
Who answers the userThe senderThe receiver
If the receiver failsThe sender noticesNobody notices, unless acknowledged
Typical useSupervisor to workerTriage to specialist
class Handoff(BaseModel):
    task_id: str
    from_agent: str
    to_agent: str
    goal: str                       # the user's request, in their words
    established: list[str]          # facts found, with ids
    actions_taken: list[str]        # e.g. "refund_delivery_fee(88213) -> ₹25 refunded"
    remaining: str
    constraints: list[str]          # e.g. "customer asked not to be phoned"

def hand_off(package: Handoff) -> None:
    set_owner(package.task_id, package.to_agent)        # exactly one owner, recorded
    send(package)
    if not acknowledged(package.task_id, within_seconds=30):
        set_owner(package.task_id, "human_queue")       # an unacknowledged task is not lost

Handing off without the list of actions already taken. The billing agent refunds the fee, hands to delivery, and the delivery agent, seeing a late order with no refund in its context, refunds it again.

Result aggregation

What it is

Result aggregation is combining several agents' outputs into one. The methods:

  • Concatenate: put the parts side by side. Suits independent sections.
  • Merge and deduplicate: combine lists, removing repeats. Suits findings.
  • Synthesise: a model writes one coherent answer from the parts.
  • Vote or reconcile: where agents answered the same question, take the majority or resolve the difference.
  • Reduce in stages: combine in pairs, then pairs of pairs, when there are too many results for one context (map-reduce).

Why it matters

Aggregation is where a multi-agent system's output is actually made, and where it most often goes wrong.

  • A synthesiser can introduce claims that were in no worker's report.
  • Conflicts between reports can be silently smoothed over.
  • Gaps (what a worker could not find) tend to vanish from the final answer.
  • Do the mechanical parts in code (merging, deduplicating, counting) and leave only the writing to a model.

In the real world: A research product's final reports contradicted their own sources until aggregation was changed to list conflicts explicitly; the reports got slightly less polished and far more trusted.

Visual example

Aggregation

Code does the counting and comparing. The model only writes.

From several reports to one answer

  1. CollectFour typed reports
  2. MergeCombine, deduplicate, in code
  3. in code, side by side
    Detect conflictsDifferent figures flagged
    Carry gaps'not_done' kept
  4. SynthesiseModel writes from merged findings
def aggregate(results: list[Result]) -> dict:
    """Mechanical work in code. Only the final writing goes to a model."""
    failed = [r for r in results if r.status == "failed"]
    findings = dedupe([f for r in results for f in r.findings])
    conflicts = find_conflicts(findings)                 # e.g. two different revenue figures
    gaps = [r.not_done for r in results if r.not_done]

    answer = llm(SYNTHESIS_PROMPT.format(
        findings=findings, conflicts=conflicts, gaps=gaps,
        rule="Use only the findings listed. State every conflict and gap explicitly.",
    ))
    return {"answer": answer, "sources": dedupe([s for r in results for s in r.sources]),
            "incomplete": bool(failed or gaps)}

Asking a model to 'combine these reports into a summary' with no instruction about conflicts or gaps. It writes a smooth paragraph, picks one of two contradictory figures without saying so, and omits what was not found.

Context explosion

What it is

Context explosion is the runaway growth of what each agent must read. It has three causes:

  • Shared transcripts: every agent's every message is added to everyone's context.
  • Verbose reports: workers return their full working instead of a summary.
  • Forwarding: the supervisor passes worker A's whole report to worker B 'for context'.

Why it matters

Cost and quality both degrade quickly.

  • With a shared transcript, each new message is paid for by every agent on every later turn.
  • Long contexts bury the instructions each agent actually needs.
  • The fix is the same discipline as for a single agent, applied at the boundaries: bounded reports, references instead of copies, and private contexts.

In the real world: A data-analysis team's agents passed whole query results to each other; saving results as artifacts and passing references cut the supervisor's context from 60,000 tokens to 4,000.

WORKER_RULES = """Return a report of at most 200 words.
- Findings as short bullets, each with its source.
- Do NOT include your search history, reasoning or the pages you read.
- If you produced something large (a table, a file), save it with save_artifact
  and return only its reference id."""

def save_artifact(content: str) -> str:
    """Bulk goes to storage. Contexts carry a reference, not the content."""
    ref = f"artifact-{uuid4().hex[:8]}"
    storage.put(ref, content)
    return ref                      # another agent reads it with read_artifact(ref) if needed

Fixing explosion by giving agents a larger context window. The growth is multiplicative; a bigger window only delays the failure and raises the bill.

Contradictory agents and duplicated work

What it is

Two failures of coordination:

  • Contradictory agents: two agents reach different conclusions, or take conflicting actions, because each saw different information or made different assumptions.
  • Duplicated work: two agents do the same thing, because briefs overlapped or neither knew what the other had done.

Why it matters

Both come from agents acting on partial views.

  • Each agent's decision implies assumptions the others cannot see.
  • Contradiction is at least visible if you look for it. Duplication is silent and shows up only on the bill, or as a double refund.
  • Fixes: briefs with explicit scope and 'not your job' lines; a shared record of decisions and actions; one owner per task; conflict detection before aggregation.

In the real world: Two sub-agents building halves of a report used different date ranges, and the totals did not reconcile; a 'decisions' section in every brief, written before the split, fixed it.

def record_action(task_id: str, agent: str, action: str, key: str) -> bool:
    """A shared ledger of actions. Returns False if someone already did this."""
    return db.insert_if_absent("actions", key=key,
                               row={"task_id": task_id, "agent": agent, "action": action})

def refund_once(task_id: str, order_id: int) -> str:
    """Before any write, every agent checks the ledger."""
    if not record_action(task_id, "billing", "refund_delivery_fee", key=f"{task_id}:refund_fee:{order_id}"):
        return "Already done by another agent. Do not repeat."
    return refund_delivery_fee(order_id)

DECISIONS = "Shared decisions so far (follow these; do not re-decide):\n{decisions}"
# e.g. "- Currency is INR throughout.\n- 'Late' means more than 30 minutes."

Parallel workers each making the same judgement call independently: one treats 'recent' as 30 days, another as 90. Settle shared definitions once, in the brief or the shared state, before fanning out.

Practise it: Design an AI Customer Support Agent

Communication overhead

What it is

Communication overhead is the cost of agents talking, in tokens, time and failure points. It grows with the number of agents and with how freely they connect:

  • in a supervisor design with n workers there are n channels;
  • if every agent can talk to every other, there are n × (n − 1) / 2 channels;
  • each message costs tokens to write and tokens to read, adds a delay, and is a place for misunderstanding.

Why it matters

Past a small team, coordinating can cost more than the work.

  • Each channel needs a format, a log and a failure rule.
  • Free conversation between agents rarely pays for itself: they are polite, repetitive and agree readily.
  • Rule of thumb: the fewest agents, the fewest channels, and messages only at task boundaries.

In the real world: A trace review found two agents exchanging eleven messages to agree on a file name; the team replaced the conversation with one structured request and one result.

Visual example

Overhead

Through a supervisor the count grows in a line. Fully connected, it grows much faster.

Channels to manage as the team grows
agentschannels via a supervisorchannels if all talk to all
211
323
5410
8728

Letting agents 'collaborate' in open-ended chat. Count the tokens: often more is spent on greetings, restating and agreeing than on the task.

Interview questions: agent communication

What it is

The questions interviewers ask about agent communication, from the first screening call to the senior round. Cover the answer, say yours aloud, then compare.

Why it matters

Reading an idea and explaining it are different skills.

  • Each answer below is the two or three sentences an interviewer is listening for.
  • The reasoning behind each one is in the cards above. If an answer surprises you, go back to that card.

In the real world: A candidate is shown a trace of two agents and asked why the customer was refunded twice; the answer is in the hand-off message, which did not say the first refund had happened.

Q: How do agents communicate?

By message passing with private contexts, by sharing a conversation history, or by reading and writing structured shared state. Most systems use typed messages plus a small shared state; shared transcripts do not scale.

Q: Why structured messages?

So code can validate, route, log and count them, and so the receiver need not guess what is a finding, a caveat or a failure. An envelope with a task id lets a trace follow a task across agents.

Q: Delegation versus hand-off?

In delegation the sender keeps ownership and waits for a result. In a hand-off ownership moves and the receiver finishes the task. Hand-offs need an acknowledgement, a single recorded owner, and the list of actions already taken.

Q: What is context explosion and how do you prevent it?

Unbounded growth in what each agent reads, from shared transcripts, verbose reports and forwarding. Prevent it with private contexts, bounded structured reports, and references to stored artifacts instead of copies.

Q: How do you handle contradictory agents?

Detect conflicts in code before synthesis, resolve by evidence or a designated judge, state unresolved conflicts explicitly, and reduce them at source by settling shared definitions before work fans out.

Q: How do you prevent duplicated work?

Non-overlapping briefs with explicit scope, one owner per task, a shared ledger of actions checked before any write, and idempotency keys as the final safeguard.

Q: How do you aggregate results?

Merge, deduplicate and detect conflicts in code; carry each worker's stated gaps; have a model write only from the merged findings with an instruction to state conflicts and gaps.

Q: What is the cost of adding an agent?

Another prompt and tool set to pay for, more channels to define and log, more hand-offs where information is lost, and another trace to read. Overhead grows faster than headcount if agents talk freely.

Describing agents 'collaborating' or 'discussing' without saying what message, in what format, on which channel.

Exercise: design an internal agent protocol

What it is

A short build that uses this module's ideas. Write it yourself before looking anything up; the checks at the end tell you whether it works.

Why it matters

An idea you have only read fades in a week.

  • Building it once, even in fifty lines, is what makes it yours.
  • Interviewers can tell the difference between someone who has read about a loop and someone who has debugged one.

In the real world: A platform team publishes its internal message schema as a small library; every new agent in the company imports it, and cross-agent traces work on the first day.

Your task. Replace free-text messages between two agents with a typed protocol, a hand-off package and a shared action ledger, and show a duplicate action being prevented.

Steps

  1. Start with a triage agent that hands off to a billing agent by passing the raw conversation.
  2. Reproduce the bug: triage refunds a fee, hands off, and billing refunds it again.
  3. Define Envelope, Handoff and Result as typed models, and validate every message before delivery.
  4. Add actions_taken to the hand-off and a shared ledger checked before every write.
  5. Require an acknowledgement within a time limit, with a fallback to a human queue.
  6. Log every message with its task id and print the full exchange for one task.

You are done when

  • The double refund no longer happens, and you can show which of your two safeguards stopped it.
  • A malformed message is rejected before the receiving agent sees it.
  • An unacknowledged hand-off ends up in the human queue, not lost.
  • The printed exchange lets a stranger follow the task without reading either agent's full context.

Stretch. Add a third agent and count the channels. Then restructure so all messages go through one coordinator and count again.