Workflows And Multi Agent Systems
Agent Communication
When agent A hands work to agent B, B does not know what A knows. Everything B needs must be transferred explicitly.
JrCodex·7 min read
Jr Codex Agentic AI Notes
Level: Advanced Prerequisites: Chapter 3: Multi-Agent Architectures Time to complete: ~20 minutes
Table of Contents
- The Context Transfer Problem
- Shared State vs Message Passing
- Designing a Handoff
- The Handoff Contract
- Returning Results Upward
- Communication Failure Modes
- Summary & Next Steps
1. The Context Transfer Problem
When agent A hands work to agent B, B does not know what A knows. Everything B needs must be transferred explicitly.
The Two Ways to Get It Wrong
─────────────────────────────────────────
UNDER-TRANSFER
"Research the competitor."
B does not know WHICH competitor, why, what
depth is wanted, or what A already found. It
guesses, and produces something unusable.
OVER-TRANSFER
A forwards its entire 30,000-token transcript.
B's context is now full before it starts, and
every advantage of separating them is gone.
The correct amount is a deliberate design
decision, not a default.
─────────────────────────────────────────
Why This Chapter Exists
─────────────────────────────────────────
Chapter 3 said the supervisor should see
summaries. This is the same problem in the other
direction — and it is where most multi-agent
systems actually fail.
Not in the architecture. In the handoffs.
─────────────────────────────────────────
2. Shared State vs Message Passing
The Two Models
─────────────────────────────────────────
SHARED STATE
All agents read and write one state object.
+ no serialisation between agents
+ naturally consistent
+ easy to checkpoint (Chapter 2)
- every agent can see everything, so context
isolation is LOST unless you filter on read
- concurrent writes need reducers (Module 5,
Chapter 3)
MESSAGE PASSING
Agents exchange explicit messages.
+ genuine isolation — B sees only what it was
sent
+ the interface is explicit and reviewable
- more plumbing
- state is distributed and harder to snapshot
─────────────────────────────────────────
# The practical hybrid: shared state for coordination, FILTERED views per agent.
class SharedState(TypedDict):
goal: str
findings: Annotated[list, operator.add] # accumulates across agents
plan: dict
agent_scratch: dict # NAMESPACED, private per agent
def view_for(state: SharedState, agent_name: str) -> dict:
"""Each agent gets what it needs — not the whole state."""
return {
"goal": state["goal"],
"findings": [f for f in state["findings"] if f["relevant_to"] == agent_name],
"my_scratch": state["agent_scratch"].get(agent_name, {}),
}The Recommendation
─────────────────────────────────────────
Store in shared state, so checkpointing and
observability stay simple.
READ through a filtered view, so context isolation
— Chapter 3's main reason for multi-agent —
survives.
Giving every agent the full state is the most
common way to build a multi-agent system that
costs more than a single agent and performs worse.
─────────────────────────────────────────
3. Designing a Handoff
from pydantic import BaseModel, Field
class Handoff(BaseModel):
"""The explicit contract between two agents."""
to_agent: str
objective: str = Field(description="ONE concrete outcome, checkable as done")
context: str = Field(description="What the receiver needs to know. FACTS only, "
"not the sender's reasoning.")
constraints: list[str] = Field(default_factory=list)
expected_output: str = Field(description="Shape and detail of the required result")
already_tried: list[str] = Field(default_factory=list,
description="Approaches that failed, so they are "
"not repeated")
budget_steps: int = 8def make_handoff(client, state, target_agent, subtask) -> Handoff:
return client.chat.completions.parse(
model="gpt-4o",
messages=[{"role": "user", "content":
f"Write a handoff brief for the {target_agent} agent.\n"
f"SUBTASK: {subtask}\n"
f"WHAT WE KNOW: {relevant_findings(state, target_agent)}\n"
f"WHAT FAILED SO FAR: {state['failures']}\n\n"
f"Include only what the receiver needs. Do not include your reasoning."}],
response_format=Handoff, temperature=0,
).choices[0].message.parsedThe Field That Earns Its Place
─────────────────────────────────────────
`already_tried`.
Without it, agent B independently repeats the
three searches agent A already ran and that
already failed. This is the single most common
waste in multi-agent systems, and it is invisible
unless you are reading traces.
It is Module 3's "DO NOT RETRY" scratchpad
section, crossing an agent boundary.
─────────────────────────────────────────
4. The Handoff Contract
The Five Questions Every Handoff Must Answer
─────────────────────────────────────────
1. WHAT is the outcome? objective
2. WHAT do I need to know? context
3. WHAT must I not do? constraints
4. WHAT shape is the answer? expected_output
5. WHAT is already ruled out? already_tried
A handoff missing any of these produces
predictable failures — and you can diagnose a bad
multi-agent run by checking which question the
handoff failed to answer.
─────────────────────────────────────────
The Test
─────────────────────────────────────────
"Could a competent colleague who has never seen
this task do the work from this brief alone?"
If not, the handoff is under-specified. If the
brief is longer than the work, you should not have
delegated it.
─────────────────────────────────────────
5. Returning Results Upward
The return path needs as much design as the outbound one.
class AgentResult(BaseModel):
status: Literal["completed", "partial", "blocked", "failed"]
findings: list[str] = Field(description="Facts discovered. Each independently useful.")
summary: str = Field(description="Under 150 words. What the supervisor needs.")
confidence: Literal["high", "medium", "low"]
sources: list[str] = Field(description="Tool calls or documents supporting each finding")
blocked_by: str | None = None
cost_cents: float = 0.0Why status Has Four Values
─────────────────────────────────────────
A boolean success flag loses the distinction that
matters most.
"partial" tells the supervisor to use what came
back AND route the remainder elsewhere.
"blocked" tells it that retrying will not help and
something must change.
"failed" tells it to try a different approach.
Collapsing these into success/failure makes the
supervisor's next decision a guess.
─────────────────────────────────────────
Confidence Must Propagate
─────────────────────────────────────────
A low-confidence finding that reaches the
supervisor as a bare statement becomes a
high-confidence input to the final answer.
Uncertainty laundering through summarisation is a
quiet, serious failure mode — the system as a
whole ends up more confident than any of its
parts.
Carry `confidence` through every layer, and have
the synthesiser state it.
─────────────────────────────────────────
6. Communication Failure Modes
The Five
─────────────────────────────────────────
TELEPHONE each summarisation drops
nuance; by layer three the
finding has changed meaning.
Fix: keep hierarchies shallow
(Chapter 3), carry sources.
UNCERTAINTY "possibly X" becomes "X".
LAUNDERING Fix: propagate confidence as a
structured field.
DUPLICATE WORK two agents research the same
thing. Fix: `already_tried`,
and shared findings.
ORPHANED A delegates, B fails silently,
SUBTASKS nobody notices. Fix: every
handoff must return an
AgentResult; treat a missing
one as a failure.
CONTEXT handoffs grow until every agent
BLOAT receives everything. Fix: cap
handoff size and enforce it.
─────────────────────────────────────────
def validate_handoff(h: Handoff, enc, max_tokens=1_500) -> Handoff:
"""Enforce the cap, rather than trusting the sender to be concise."""
size = len(enc.encode(h.context))
if size > max_tokens:
h.context = compress_to(h.context, max_tokens, enc)
log.warning("handoff to %s truncated from %d tokens", h.to_agent, size)
if not h.expected_output:
raise ValueError("handoff missing expected_output") # question 4 unanswered
return h7. Summary & Next Steps
Key Takeaways
- Multi-agent systems usually fail at the handoffs rather than the architecture: under-transfer produces guessing, over-transfer destroys the context isolation that motivated the split.
- Store coordination data in shared state for checkpointing, but read through per-agent filtered views so isolation survives.
- Every handoff must answer five questions — objective, context, constraints, expected output, and what is already ruled out — and
already_triedprevents the most common waste. - Results need a four-value status and a propagated confidence field; collapsing either makes the supervisor's next decision a guess and launders uncertainty into false confidence.
Concept Check
- Why is giving every agent the full shared state a self-defeating design?
- What does
already_triedprevent, and which single-agent mechanism is it the cross-agent version of? - Explain uncertainty laundering and why a boolean success flag contributes to it.
Next Chapter
→ Chapter 5: Multi-Agent Failure Modes
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index