AI · Automation · Engineering

How Agents Talk to Each Other in Production

By Lazar MilicevicOctober 22, 202611 min read
Rows of networked servers with glowing cables, representing how agents communicate in production systems

The first multi-agent pipeline I ran unattended did not fail because a model was dumb. It failed because one agent returned a paragraph where the next one expected a list, and the downstream agent confidently invented the missing structure. Nothing crashed. The pipeline just kept publishing slightly wrong things, which is the worst way for it to fail.

That experience changed how I think about multi-agent systems. The interesting engineering is not the prompts. It is the wires between the agents.

Four ways agents exchange information

Agents communicate through four patterns in practice: shared state, message queues, orchestrator-worker handoffs, and structured outputs at the boundary. Most real systems mix them, and each one fails in its own way.

Pattern What moves Where it breaks
Shared state A common record (DB row, graph state) all agents read and write Silent overwrites, stale reads, no clear owner of a field
Message queue Discrete jobs between decoupled workers Duplicate delivery, poison messages, unbounded retries
Orchestrator-worker A lead agent delegates, workers return results Lead becomes a bottleneck and a single point of confusion
Structured output Schema-validated payloads at each boundary Schemas that are too loose, or too strict and brittle

None of these is the right one. The right question is: for each hop in my system, who owns the data, who is allowed to change it, and what happens when the hop fails?

Shared state: easiest to build, easiest to corrupt

Shared state is where everyone starts, and for good reason. In my content pipelines, a single record per article carries the research brief, the draft, the optimization notes, and the publish status. Every agent reads what it needs and writes its part.

The failure mode is ownership ambiguity. If two agents can write the same field, you will eventually get a quiet overwrite, and no error will tell you. My rule is that every field has exactly one writer. The research agent writes the brief. The writer agent writes the draft. The optimizer does not edit the draft in place; it writes a separate set of suggestions that a later step applies deliberately.

Two other habits paid off:

  • Stage as an explicit field. Each record carries its current stage (researched, drafted, optimized, queued, published). An agent only picks up records in the stage it handles, and it advances the stage as its last write. That makes the state machine visible and makes stuck records trivial to find.
  • Append, don't overwrite, for anything you might need to debug. Keeping prior versions of a draft costs almost nothing and has saved me repeatedly when I needed to find out which step degraded the output.

Queues: decoupling that you pay for later

Moving work between agents through a queue gives you isolation and independent scaling. A slow research step no longer blocks a fast publishing step. It is also the pattern most likely to bite you when nobody is watching, because queues are very good at faithfully repeating your mistakes.

The failures I plan for explicitly:

  • Duplicate delivery. Most queues deliver at least once. Every consumer must be idempotent, which for an agent means a stable job ID and a check that says "I already did this." Without that, a retry publishes the same article twice.
  • Poison messages. One malformed job that makes an agent throw will be retried forever unless you cap attempts and route it to a dead-letter location for a human to look at.
  • Retry amplification. If an agent retries on failure and the queue also retries, you multiply attempts. Decide which layer owns retries and make the other one not retry.

I won't quote queue-specific limits here because they vary by service and change; check the documentation for whatever you run. The design principle is stable: bounded attempts, backoff between them, and a dead-letter path that someone actually reads.

Orchestrator-worker: the pattern that survives contact with reality

When the work is open-ended and the number of subtasks is not known in advance, an orchestrator-worker design is the one I reach for. Anthropic describes its multi-agent research system this way: a lead agent analyzes the query, develops a strategy, and spawns subagents that explore in parallel, each with its own context window (source). That per-worker context window is a large part of the value: each worker gets a clean, focused context instead of inheriting a bloated one.

The same source reports that a Claude Opus 4 lead with Claude Sonnet 4 subagents beat a single-agent Opus 4 by 90.2% on Anthropic's internal research eval. I treat that as evidence the architecture can work, not as a promise it will on your task. That number comes from their eval on their task, and I read it via a secondary summary, so go to the primary write-up before you quote it.

There is a distinction worth understanding about how control moves. In the OpenAI Agents SDK, a handoff routes the conversation to a specialist who becomes the active agent for the rest of the turn, while with agents-as-tools a manager agent keeps control and calls specialists (source). These are genuinely different communication contracts:

  • Handoff transfers ownership. The receiving agent gets the conversation history and takes over (source). Good when the specialist should talk to the user directly.
  • Agents-as-tools keeps one agent accountable for the final answer. Good when you need one place to enforce output quality and cost limits.

For unattended pipelines with no user in the loop, I lean toward the second. A single accountable orchestrator is easier to reason about, easier to cap, and easier to debug than a chain of agents each passing the baton.

One more detail from the SDK docs that matches my experience: nested handoff history, which summarizes prior history into segments, is an opt-in beta and off by default (source). Whatever your framework, be deliberate about how much context crosses an agent boundary. Passing everything is expensive and noisy. Passing too little makes the worker guess.

Structured outputs: the boundary is the contract

Free text between agents is a bug waiting for a bad day. The fix is boring and effective: every hop gets a schema, and the receiver validates before it acts.

The OpenAI Agents SDK bakes this into handoffs. The input_type schema is exposed to the model as the handoff tool's parameters, and the SDK validates the returned JSON locally before passing it to on_handoff (source). That is the right shape regardless of framework: define the contract, let the model fill it, validate it yourself, and only then proceed.

A minimal version of what I do at each boundary:

from pydantic import BaseModel, Field, ValidationError

class ResearchBrief(BaseModel):
    topic: str
    target_query: str
    key_points: list[str] = Field(min_length=3)
    sources: list[str]          # URLs, validated downstream
    confidence: float = Field(ge=0, le=1)

def accept(raw: dict) -> ResearchBrief | None:
    try:
        return ResearchBrief.model_validate(raw)
    except ValidationError as e:
        record_failure(stage="research", error=str(e))
        return None   # caller decides: retry once, then park the job

Lessons from running this unattended:

  1. Reject, don't repair. If validation fails, retry the upstream agent with the error message. Silently patching bad output upstream hides the real defect.
  2. Include a confidence or status field the downstream agent must respect. "I couldn't find enough sources" is a valid output. Without a slot for it, the model fills the slot with fiction.
  3. Keep schemas small. Every field is something a model can get wrong. I add a field only when a downstream step actually consumes it.
  4. Version the schema. When you change one, in-flight jobs still carry the old shape. Handle that explicitly instead of discovering it at 3 a.m.

If you are heading toward cross-system agent communication, the A2A spec formalizes some of the same ideas. A Task is the fundamental unit of work, identified by a unique ID and moving through a defined lifecycle (source), and the terminal states are completed, failed, canceled and rejected (source). Even if you never use A2A, that vocabulary is a good model for your internal jobs: every unit of work has an ID and an explicit end state. A related note on scope: MCP connects an agent to tools and data, while A2A connects agents to other agents (source). Keep those two layers separate in your head.

Loop prevention and cost control: the unsupervised tax

Two things go wrong when nobody is watching: agents that never stop, and bills that quietly balloon.

Loops. Agents that can call each other can ping-pong. LangGraph's recursion limit caps the number of super-steps in one execution and raises GraphRecursionError when hit (source). The docs also provide a RemainingSteps managed value so a graph can detect it is approaching the limit and exit gracefully, and they call that proactive approach recommended over catching the error externally (source). I agree. A guard that lets the agent wrap up with a partial, well-labeled result beats one that kills it mid-thought.

One caution: the default limit differs across versions and sources, so check the one in your installed version rather than trusting a number from a blog, including this one. Then set your own, lower, explicit limit sized to your real workflow. A pipeline that normally finishes in a handful of steps does not need a ceiling of a thousand.

My loop guards, in order of how often they've saved me:

  • Hard step budget per job, set well above normal usage and below "something is clearly wrong."
  • No-progress detection. If an agent produces output identical or near-identical to its previous attempt, stop and escalate instead of retrying.
  • Per-job wall-clock timeout independent of step count.

Cost. Anthropic reports that agents use about 4x the tokens of a chat and multi-agent systems about 15x (source). That is the number I keep in mind whenever someone proposes "just add another agent." The same analysis notes that token usage alone explained 80% of performance variance on BrowseComp (source), which cuts both ways: more tokens often buys better results, so cost and quality are coupled, and you have to budget deliberately rather than just cut.

What I do about it:

  • Per-job token and cost ceilings, enforced in code, with a graceful stop rather than a crash.
  • Cheaper models for narrow workers, stronger models only where judgment matters, mirroring the lead/subagent split above.
  • Cap fan-out. Parallel workers multiply spend. Set a maximum number of subagents per task.
  • Log cost per stage per job. You cannot cut what you cannot see. Once I could see which stage dominated, optimization was obvious.

Why this is mostly a systems problem

The MAST study (Cemri et al.) identifies 14 failure modes in multi-agent LLM systems, clustered into three categories: system design issues, inter-agent misalignment, and task verification (source). The authors argue many failures come from system design and interaction issues, not just model limitations, which calls for structural redesign rather than superficial fixes (source).

That matches what I've seen. When my pipelines misbehaved, swapping to a smarter model rarely helped. Tightening a schema, assigning a single owner to a field, or adding a verification step did. I'd note that the statistics on this study vary across the pages that report it, so I'm only relying on the structure of the taxonomy here, not any percentages.

What I'd do

If I were starting a multi-agent system today for something that has to run unattended:

  1. Start with one agent. Add a second only when a single context demonstrably can't hold the task. Every agent you add is another hop that can fail and another multiplier on cost.
  2. One owner per field, one explicit stage per record. Make the state machine visible.
  3. Schema at every boundary, validate on receipt, reject instead of repair.
  4. Prefer an accountable orchestrator over chained handoffs when there's no human in the loop.
  5. Idempotent consumers, bounded retries, a dead-letter path someone reads.
  6. Set your own loop and cost ceilings explicitly. Never inherit a framework default for something running while you sleep.
  7. Add a verification step whose only job is to check the previous agent's output against the brief. It's the cheapest quality gain I know.

Multi-agent systems are less about intelligence and more about discipline at the seams. If you're wrestling with agents that work in the demo and wobble in production, I'm happy to compare notes: lazar-milicevic.com/#contact, or browse the rest of the blog for more on running autonomous systems for real.

Frequently asked questions

How do AI agents communicate with each other in production?

In practice I see four patterns: shared state, message queues, orchestrator-worker handoffs, and structured outputs at the boundary between agents. Most real systems mix them, and each fails differently. Shared state breaks through silent overwrites, queues through duplicate delivery and poison messages, orchestrators through bottlenecks, and structured outputs through schemas that are too loose or too brittle. For every hop I ask who owns the data, who may change it, and what happens when the hop fails.

What is the orchestrator-worker pattern in multi-agent systems?

In the orchestrator-worker pattern, a lead agent analyzes the task, plans a strategy, and delegates subtasks to worker agents that return results. Anthropic describes its multi-agent research system this way, with subagents exploring in parallel, each in its own context window. That separate context is a big part of the value, because each worker starts with a clean, focused context instead of a bloated one. I use it when the work is open-ended and the number of subtasks isn't known in advance.

What is the difference between a handoff and agents-as-tools?

They are different communication contracts. In a handoff, the conversation is routed to a specialist agent that becomes the active agent and takes over, receiving the conversation history. With agents-as-tools, a manager agent keeps control and calls specialists like functions, so one agent stays accountable for the final answer. For unattended pipelines with no user in the loop, I lean toward agents-as-tools because a single orchestrator is easier to cap, reason about, and debug.

How do you prevent agents from overwriting each other's work in shared state?

I enforce a rule that every field has exactly one writer, so no two agents can quietly overwrite the same data. The research agent writes the brief, the writer writes the draft, and the optimizer writes separate suggestions that a later step applies deliberately. I also store an explicit stage field that each agent advances as its last write, which makes the state machine visible and stuck records easy to find. Appending prior versions instead of overwriting helps me trace which step degraded the output.

How do you handle failures when agents communicate through message queues?

I plan for three failures explicitly: duplicate delivery, poison messages, and retry amplification. Because most queues deliver at least once, every consumer must be idempotent, using a stable job ID and a check that the work was already done. I cap retry attempts, add backoff, and route failing jobs to a dead-letter location that a human actually reads. I also decide which layer owns retries and disable retries in the other, so attempts don't multiply.

Lazar Milicevic

Lazar Milićević

Senior Technical Engineer. I build AI automation, GenAI/LLM systems and cloud architecture — autonomous systems that run while you sleep. Founder of BizFlowAI.

Building something hard with AI or automation? I am open to talk.

Get in touch

← All posts