DEV Community

Cover image for Multi-Agent Handoffs: The Protocol That Stops Context Loss
Gabriel Anhaia
Gabriel Anhaia

Posted on

Multi-Agent Handoffs: The Protocol That Stops Context Loss


You split your agent into two. A research agent gathers the facts, then hands off to a writer agent that turns them into a report. Clean separation. Each one has a tight prompt and a small toolset. The demo works.

Then a real task comes in. The research agent runs twelve tool calls, reads six documents, and corrects itself twice along the way. To hand off, you pass the writer the entire message history. The writer now reads a transcript that includes two wrong turns the researcher already abandoned, the raw text of six documents, and twelve tool-call payloads. It picks up a fact from a turn the researcher had retracted. The report cites a number that was never true.

That is context loss, and it is counterintuitive because you passed more context, not less. The whole-transcript dump is the most common multi-agent handoff and it is the one that fails most reliably. The fix is a protocol: a structured payload that carries the conclusions, not the journey.

Why the transcript dump fails

A message history is a record of how an agent thought. It is full of dead ends, self-corrections, tool errors it recovered from, and intermediate reasoning that only made sense in the moment. That is fine for the agent that produced it. The next agent does not need the journey. It needs the destination.

Three things go wrong when you pass the raw transcript.

The receiving agent can't tell live facts from retracted ones. If turn 4 says "the deadline is March" and turn 9 says "correction, the deadline is May," both sentences are sitting in the history with equal weight. The model has no reliable signal that the second one wins.

The token cost compounds. Every handoff carries the full weight of the previous agent's work. Chain three agents and the third one is paying for the first one's tool dumps. Cost and latency climb with each hop for context that is mostly noise.

The receiving agent inherits the sender's framing. A transcript carries tone, assumptions, and half-formed plans. A writer agent reading a researcher's anxious back-and-forth tends to mirror it. You wanted a fresh perspective on clean inputs. You got a continuation of someone else's train of thought.

The handoff is an API call, not a conversation

Treat the boundary between two agents the way you'd treat the boundary between two services. Service A does not hand service B its stack trace and ask it to figure out the result. It returns a typed response. The handoff between agents is the same shape: a defined payload, validated at the seam, carrying exactly what the next stage needs to do its job.

That reframing does the heavy lifting. Once the handoff is a contract, you stop asking "how do I compress the transcript" and start asking "what does the next agent actually need." Those are different questions with very different answers.

A handoff schema

Here is the payload I'd define for a research-to-writer handoff. It is JSON, validated on both sides. The point is not these exact fields; it is that every field earns its place.

from dataclasses import dataclass, field


@dataclass
class Finding:
    claim: str          # one settled fact
    source: str         # where it came from
    confidence: str     # "high" | "medium" | "low"


@dataclass
class Handoff:
    task: str               # what the next agent must do
    findings: list[Finding] # settled conclusions only
    open_questions: list[str] = field(default_factory=list)
    constraints: list[str] = field(default_factory=list)
    artifacts: dict = field(default_factory=dict)
    from_agent: str = ""
    to_agent: str = ""
Enter fullscreen mode Exit fullscreen mode

Walk the fields.

task is the single instruction for the receiver, written by the sender. It states what the receiver does next, never what the sender already did. The sending agent knows the goal; it should state it plainly so the receiver does not have to infer it from context.

findings is the core. Each one is a settled claim, its source, and a confidence level. Retracted turns never make it here. The researcher's job, before handing off, is to decide what it actually believes. That decision happens once, in the agent that did the work, not repeatedly in every agent downstream.

open_questions is what the sender could not resolve. This is the honest part. A handoff that pretends everything is settled forces the receiver to either guess or rediscover the gap. Naming the gap lets the receiver route around it.

constraints carries the rules that must survive the hop: word limits, tone, a banned competitor name, a regulatory disclaimer. These get lost first in a transcript dump because they were stated once, early, and buried.

artifacts is for large blobs by reference, not by value. A document the writer needs goes in as an ID or a URL, not 4,000 tokens of inlined text. The receiver fetches it if and when it needs it.

Building the payload

The sending agent produces the handoff as its final act. The natural way is to make it a tool call: the agent's last move is to call submit_handoff with the structured arguments, which gives you schema validation for free.

def submit_handoff(args: dict) -> Handoff:
    findings = [
        Finding(**f) for f in args.get("findings", [])
    ]
    handoff = Handoff(
        task=args["task"],
        findings=findings,
        open_questions=args.get("open_questions", []),
        constraints=args.get("constraints", []),
        artifacts=args.get("artifacts", {}),
        from_agent="researcher",
        to_agent="writer",
    )
    validate(handoff)
    return handoff


def validate(h: Handoff) -> None:
    if not h.task:
        raise ValueError("handoff missing task")
    if not h.findings and not h.open_questions:
        raise ValueError("handoff carries no content")
    for f in h.findings:
        if f.confidence not in {"high", "medium", "low"}:
            raise ValueError(f"bad confidence: {f.confidence}")
Enter fullscreen mode Exit fullscreen mode

The validation is the guard rail. If the researcher tries to hand off with no findings and no open questions, that is a bug worth catching at the seam, not three turns later in the writer's output. You want the handoff to fail loudly when it is empty, the same way a typed API response fails when a required field is missing.

Receiving the payload

The receiving agent does not get the sender's transcript. It gets a fresh message list, seeded with its own system prompt and the handoff rendered into a clean brief.

def render_brief(h: Handoff) -> str:
    lines = [f"Task: {h.task}", "", "Findings:"]
    for f in h.findings:
        lines.append(
            f"- ({f.confidence}) {f.claim} [src: {f.source}]"
        )
    if h.open_questions:
        lines.append("")
        lines.append("Open questions (unresolved):")
        for q in h.open_questions:
            lines.append(f"- {q}")
    if h.constraints:
        lines.append("")
        lines.append("Constraints:")
        for c in h.constraints:
            lines.append(f"- {c}")
    return "\n".join(lines)


def start_writer(h: Handoff, system_prompt: str) -> list:
    return [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": render_brief(h)},
    ]
Enter fullscreen mode Exit fullscreen mode

The writer wakes up with a clean context window. It sees settled facts tagged with confidence, the questions that are still open, and the rules it must follow. It never sees the twelve tool calls or the two retracted turns. The confidence tags matter: a writer can hedge a low-confidence finding and state a high-confidence one plainly, which is exactly the judgment you want it to make.

Where the transcript still belongs

Drop the raw transcript from the handoff, keep it in your traces. When the writer cites a wrong number, you want to walk back to the researcher's session and see where it came from. That is an observability concern, and it lives in your tracing backend keyed by session ID, not in the next agent's context window.

So the rule is split by audience. The next agent gets the structured payload. Your debugging tools get the full history. The two never mix, and neither one carries the other's weight.

What this buys you

Each agent gets a context window scoped to its job. Token cost stops compounding across hops because every handoff carries conclusions, not journeys. The contract fails loudly at the seam when an agent produces a bad payload, instead of failing quietly three stages downstream where the cause is hard to find. And each agent gets a genuinely fresh start on clean inputs, which is the entire reason you split them up in the first place.

The schema is small on purpose. Start with task, findings, open_questions, and constraints. Add fields when a real handoff needs them, not before. A handoff protocol that grows one field at a time stays a contract. One that starts by passing everything is just the transcript dump wearing a struct.

Next move

Find the handoff in your multi-agent system that passes the most context. Look at what the receiving agent actually uses from it. The gap between what you pass and what gets used is your payload. Write the schema for that gap, validate it on both sides, and cut the transcript.


If this was useful

The AI Agents Pocket Guide covers multi-agent coordination patterns of this flavour: typed handoffs, when to split an agent versus keep it whole, and how to scope context per stage. The chapter on orchestration pairs directly with the schema in this post.

AI Agents Pocket Guide

Top comments (0)