Multi-Agentmulti-agenta2aprotocolmessage-passingshared-state

Agent-to-Agent Communication

Agents coordinate through discovery, capability descriptions, task delegation and hand-off, choosing between shared state and message passing; A2A-style protocols cover agent↔agent the way MCP covers agent↔tool.

Interview question
Progress

The primitives

Whatever the topology, agents need a few primitives. Discovery: how does agent A learn that agent B exists and what it can do? Capability description: a machine-readable "card" (name, skills, input/output modes, auth requirements, endpoint). Delegation: sending a task with an id, input, and constraints. Hand-off: transferring ownership of an ongoing task or conversation. Status/streaming: progress events while a long task runs.

A2A-style protocols standardise these across vendors: an agent publishes a capability card at a well-known URL; a client sends a task; the server agent returns task state (submitted, working, input-required, completed, failed) and streams artifacts. The details differ between protocols; the primitives do not.

  • A2A vs MCP: A2A is agent↔agent — tasks, multi-turn, long-running, opaque reasoning on the other side. MCP is agent↔tool — single calls with schemas, synchronous results, no reasoning on the tool side.
  • An agent can be both: it consumes MCP servers for its tools and exposes an A2A-style card so other agents can delegate to it.
  • Neither protocol decides *whether* to delegate; that remains the calling agent's judgment and your eval's job.

Shared state vs message passing

Shared state (a blackboard: one document, database row or state object all agents read and write) is simple to reason about and lets any agent see everything. It couples agents to a schema, needs locking or versioning for concurrent writes, and turns every write into potential context for every reader — including injected content.

Message passing (each agent only sees messages addressed to it, typically via a queue) decouples agents, makes traces linear per agent, and bounds what each sees. It needs explicit routing, ordering guarantees and dead-letter handling, and "why did agent C do that" requires correlating messages across queues.

Rule of thumb: shared state for tightly coupled agents on one task in one process (a workflow graph's state object); message passing for independent agents, different processes or teams, and anything long-running.

Delegation as a message with a task id and explicit constraints
1type TaskMessage = {
2 taskId: string // idempotency key for the receiving agent
3 from: string; to: string
4 skill: 'summarize' | 'extract-entities'
5 input: { text: string }
6 constraints: { maxTokens: number; deadlineMs: number }
7 replyTo: string // queue name for the result/status events
8}
9
10await queue.publish('agents.extractor', {
11 taskId: crypto.randomUUID(), from: 'planner', to: 'extractor',
12 skill: 'extract-entities', input: { text: doc }, constraints: { maxTokens: 4000, deadlineMs: 20_000 },
13 replyTo: 'agents.planner.inbox',
14} satisfies TaskMessage)

Streaming to the UI

Users cannot wait 40 s for silence. Agents should emit a typed event stream — run.started, tool.called, tool.result, text.delta, handoff, approval.required, run.finished — that the UI renders progressively and that doubles as the trace. Server-sent events or WebSockets carry it; the important design choice is that events are structured, not free text, so the UI can render an approval button or a tool card (Human-in-the-Loop Overview).

The same event stream is what an orchestrating agent consumes from a delegated agent: progress, partial artifacts, and input-required states that need to bubble up to a human.

Delegation with streaming status
taskstatus / artifactsUIPlanner agentTask queueWorker agentEvent stream
UserLLMAgentToolDataDecisionHumanGuardrail

Key points

  • Primitives: discovery, capability card, delegation, hand-off, status streaming.
  • A2A-style = agent↔agent tasks; MCP = agent↔tool calls. An agent can do both.
  • Shared state is simple but coupled; message passing is decoupled but needs routing and ordering.
  • Every task carries an id (idempotency), constraints (budget, deadline) and a reply channel.
  • Emit structured events for UI and trace; free-text progress is useless to both.

When to use — and when not to

Use it when
  • Agents owned by different teams or running in different processes.
  • Long-running tasks that need progress and input-required states.
  • You want to swap a delegated agent without changing the caller.
Avoid it when
  • Agents in one process on one task — a workflow state object is enough.
  • Only one agent exists; a protocol between one party is overhead.
  • Latency-critical paths where a queue hop is unaffordable.

Failure modes

  • Messages without task ids → duplicate work after retries.
  • Shared state written by an agent that read injected content, poisoning all readers.
  • Unbounded queues with no deadlines; tasks run long after the user left.
  • Free-text status that neither the UI nor the trace can interpret.
  • Capability cards that overstate skills; the caller delegates to an agent that cannot do the job.

Tradeoffs

Complexity
low → high
Latency
low → high
Cost
low → high
Reliability
poor → strong
Debuggability
hard → easy

Message passing improves per-agent debuggability but cross-agent correlation needs trace ids on every message.