Multi-agent communication
Conversation protocols and handoffs
Structured handoffs vs free-form chat.
What goes in the message
Once you have transport (direct call, shared state, pub/sub), the next question is the message content. Two extremes:
- Pass the whole transcript. Agent A forwards its entire message history to agent B so B has full context. This is what every "let's just chain agents" tutorial defaults to.
- Pass a structured handoff. Agent A produces a small, typed payload that B is expected to act on, nothing more.
The first is easy and slowly poisons quality. The second is harder to design and almost always pays off. This lesson is about how to design the second.
Why "forward everything" goes wrong
If a code agent has been deep in a 12-turn conversation about a Python bug and then hands off to an ops agent for a deploy check, forwarding all 12 turns means:
- The ops agent's context now contains thousands of code-specific tokens it does not need.
- The ops system prompt is competing for the model's attention with a long history about Python.
- Tool selection in the ops agent is biased toward code-shaped reasoning because that is what is "warm."
- Token cost roughly doubles for the ops turn.
This is exactly the context pollution problem from Module 1, except now you have engineered it by treating the transcript as a baton.
Structured handoffs
A handoff is a small, well-typed payload that carries the minimum necessary information from one agent to the next. The structure forces the sending agent to summarize, which forces the receiving agent to start fresh.
A reasonable handoff schema:
@dataclass
class Handoff:
from_agent: str
to_agent: str
intent: str # one sentence: what should the next agent do?
context: str # a paragraph of summarized findings
artifacts: dict # named pieces of data the next agent needs
return_to: str | None # which agent gets the result, if anyA real handoff:
Handoff(
from_agent="code-agent",
to_agent="ops-agent",
intent="Verify whether commit a3f1 is the cause of the staging crash",
context="Found that retries.py was modified in a3f1 to remove the timeout. The change matches the timing of the crashes.",
artifacts={"commit_sha": "a3f1", "modified_file": "retries.py"},
return_to="code-agent",
)The receiving agent's prompt template knows how to read this:
You are the ops agent. Another agent has handed off work to you.
INTENT: {handoff.intent}
CONTEXT FROM PREVIOUS AGENT: {handoff.context}
ARTIFACTS: {handoff.artifacts}
Use your tools to address the intent. When done, return your findings.The ops agent never sees the original conversation. It sees a focused brief.
Designing the schema
Three rules for handoff schemas:
Make the intent a sentence, not a paragraph
The intent field is the single most important part of the handoff. If the sending agent cannot summarize the request in a sentence, it has not actually decided what it wants. Force the sentence; the rest of the payload supports it.
Limit the context to what the next agent needs to act, not to understand
The receiving agent does not need the history of how you got here. It needs the current best understanding of the situation. Keep the context section a paragraph. If you find it growing past 200 tokens, you are leaking history into the handoff.
Use typed artifacts for anything the next agent will quote or query
If the ops agent needs the exact commit SHA, do not embed it in prose; put it in artifacts. Typed artifacts are easier for the next agent to reference correctly and survive a model that paraphrases prose poorly.
Handoff anti-patterns
Two patterns to avoid:
"Pass the whole conversation as context"
This is the lazy default. It works for the first handoff in a small system. It scales badly: every handoff makes the next agent's context larger, which makes its handoff larger, which makes the agent after that even more polluted.
"Pass nothing, the next agent will figure it out"
The opposite mistake. The orchestrator hands off a single sentence with no context, and the next agent has to redo investigative work to understand the situation. Now you are paying for the same investigation twice.
The handoff schema is the dial between these two. You want enough context to act, no more.
Free-form vs structured handoffs
| Property | Free-form (forward transcript) | Structured handoff |
|---|---|---|
| Implementation effort | None | Real |
| Context cleanliness for next agent | Bad | Good |
| Token cost across hops | Grows monotonically | Bounded |
| Tool selection accuracy in next agent | Drops over hops | Stable |
| Debuggability | Conversation as a wall of text | Clear boundaries between agents |
| Easy to add a new agent | Yes | Schema may need extending |
The right answer for most systems is structured handoffs with a single schema shared across agents. The implementation cost is a few hours; the quality dividend pays out forever.
Multi-turn handoffs
Some workflows need the calling agent to come back with follow-up questions, not just receive a final answer. The schema's return_to field handles this: the worker finishes, the orchestrator routes the result back to the original caller, the caller decides whether to ask again.
def orchestrator_loop(user_request):
code_result = dispatch(Handoff(
from_agent="orchestrator",
to_agent="code-agent",
intent="Find the cause of the bug",
context=user_request,
artifacts={},
return_to="orchestrator",
))
ops_result = dispatch(Handoff(
from_agent="orchestrator",
to_agent="ops-agent",
intent="Confirm whether deploy timing aligns with the crash",
context=f"Code agent found: {code_result.summary}",
artifacts={"commit_sha": code_result.commit_sha},
return_to="orchestrator",
))
return synthesize(code_result, ops_result)The orchestrator is the only agent that sees both findings. The code agent and ops agent each operate in clean contexts. This is the supervisor/worker pattern from Module 3, ahead of schedule, but the communication shape is identical to what we just built.
Treat the handoff as an API
The handoff schema is the API between your agents. Version it. Validate it. If a worker receives a handoff with missing fields, return a typed error rather than guessing. The same engineering discipline you would use for a service-to-service API applies here. Agents are services that happen to use natural language internally.
Key takeaway
The transport (Module 2 last lesson) decides how the message gets there. The protocol decides what is in it. Forward-the-whole-transcript is the easy default that quietly degrades quality at every hop. A small typed handoff schema forces summarization, keeps each agent's context clean, and turns multi-agent into a real architecture instead of a chain of polluted monologues. The next module (orchestration topologies) is about which agents talk to which, in what shape.
Done with this lesson?