We Ripped Out handoff() After Every Agent Did Its Job Perfectly — And a Fixed Ticket Still Reopened Itself
Day 11 — Why handoff() Is an Anti-Pattern in Production Multi-Agent Systems
A handoff() call has never once caused an incident I've seen — right up until the day it does, and by then there's no stack trace pointing at it. Handoffs demo beautifully: agent A finishes its part, calls handoff(), agent B picks up clean, like a relay baton changing hands. In production, that exact moment is where the unstated context that made agent A's work correct quietly disappears. Nothing throws an error. Nothing looks wrong. That's the problem.
The bug that made us rip it out
We had a Swarm-style setup: a triage agent classified support tickets, then handed off to a resolution agent with full ownership transfer — new instructions, new tools, the ticket reassigned to a new owner. The resolution agent fixed the bug, marked the ticket 'resolved,' and handed off again to a verification agent. That agent had no idea what 'resolved' meant in this specific case — it didn't know 'resolved' meant 'patched behind a feature flag, pending manual rollout,' not 'deployed and confirmed.' So it ran its checks, saw the flag was off, decided the fix wasn't live, and reopened a ticket that had already been fixed. Nobody lied. Every agent did exactly what it was supposed to do with what it received. The bug wasn't in any one agent — it was structural: handoff() moved the ticket's data across, but not the reasoning that gave 'resolved' its meaning.
Naming the pattern precisely
Swarm-style handoff — and its equivalent in AutoGen's agent-to-agent transfer, or any framework built around a transfer_to_agent() primitive — does two things at once. It serializes context into whatever schema the target agent accepts, and it releases ownership: the sending agent exits the conversation entirely. The new agent runs on its own system prompt, its own tools, and treats whatever it received as ground truth. Nothing gets checked against the original intent, because the agent that held that intent is no longer in the room.
Why it looks right on paper
It maps cleanly onto how we already think about good design: separation of concerns, one owner per task, a tidy diagram with an arrow from box A to box B. It's easy to reason about, easy to test in isolation, and it dodges the god-object trap of one giant agent trying to do everything. Every argument you've ever made for microservices over a monolith applies here almost word for word. Which is exactly why it's seductive — and exactly why it's worth being suspicious of.
Where it breaks
The handoff boundary is a compression point. Whatever doesn't fit the schema — the 'why,' the constraint that was never written down because it only ever lived in agent A's reasoning trace — gets left behind. This is the same context/trust boundary problem from Day 10: every seam between agents is a place where information either gets rebuilt from scratch or gets trusted without a check. A clean handoff picks trust by default, because releasing ownership means nobody's left who remembers the reasoning well enough to challenge the next agent's conclusion. 'Resolved' stops being a claim you could interrogate and becomes just a string sitting in a field.
The fix: supervisor-retains-ownership
We replaced clean handoffs with a coordinating agent that delegates subtasks but never actually leaves the conversation. The supervisor holds the original context for the task's entire lifecycle — the ticket, the constraint, what 'done' is supposed to mean — and dispatches subagents for specific work the way you'd hand a task to a teammate, not the way you'd transfer ownership of a codebase and walk away. Critically: a subagent's 'done' is a claim, not a fact. The supervisor checks it against context it never let go of before it treats the task as closed. In our case, that meant the resolution agent reports back 'patched, behind flag X' to the supervisor — and the supervisor, which still remembers this ticket started life as 'customer-facing bug, needs verified rollout,' is the one that decides whether to close it. Not the next agent in a chain that's already forgotten why any of this mattered.
The human parallel
Nobody does a clean handoff for the systems that actually matter. When a senior engineer leaves the team that owns the billing pipeline or the auth system, you don't get a doc dump and a Slack goodbye — you get weeks of paired work, shadowing, and the departing engineer staying reachable through the first few incidents. Same reason our ticket bot broke: the risk was never in the code, it's in the tacit knowledge — why a weird edge case is handled the way it is, which alert is safe to ignore, what 'stable' actually means for this particular system. Clean handoffs are fine for low-stakes, fully-specified work. Anything with real judgment calls in it gets a gradual, overlapping transition, where the outgoing owner is still around to answer 'wait, why did you do it this way?' A supervisor agent is the multi-agent version of that overlap — except it doesn't end after a transition period. It's permanent.
The design rule
Before you write a handoff() call, ask one question: does the receiving agent need to trust the sending agent's definition of 'done,' or does it need to verify it? If the answer is trust — the task is narrow, fully specified, low-stakes — a handoff is fine, it's just delegation with extra ceremony. If the answer is verify — 'done' is a judgment call, success criteria are fuzzy, or a wrong 'done' actually costs something — skip the handoff. Use a supervisor that keeps the original context alive and treats every subagent's completion signal as a claim to check, not a fact to inherit.
Where this fits in the arc
Day 10 established that every boundary between agents is a context/trust boundary. Day 11 is the most common place teams get that boundary wrong: reaching for a clean handoff() because it looks like good software design, when the situation actually calls for a supervisor that never fully lets go. Tomorrow we go one level deeper — how you actually build verification between agents, so 'trust' stops being the default just because nobody bothered to write the check.
Extend your knowledge
- ▹Read OpenAI's Swarm framework docs/source for the handoff() implementation and note exactly what state gets carried across vs. dropped.
- ▹Look at AutoGen's GroupChat manager pattern — it's closer to supervisor-retains-ownership than to Swarm's handoff, worth comparing the two designs directly.
- ▹Audit one multi-agent flow you've built: find every point where one agent's 'done' or 'resolved' signal is consumed by another agent without being re-checked against the original task intent.
- ▹Revisit Day 10's context/trust boundary lesson alongside this one — Day 11 is a specific failure mode of that same general problem.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.