Context Handoff: Passing State Between Agents Cleanly
Context handoff is the act of transferring ownership of the next subgoal from one agent to another with enough state to succeed - and no more. Clean handoffs use a packet outbound and a return contract inbound, optionally backed by a shared task store for large artifacts.
Dirty handoffs paste entire transcripts, drop critical ids, or duplicate the same facts until context windows melt.
Summary
Define a versioned packet schema (subgoal, constraints, inputs, success/failure contracts, budget), validate it at the boundary, run the specialist, validate the return, then merge artifacts into shared task state by reference.
Recipe
- List fields every specialist needs to start work (ids, policies, artifact refs).
- Write a packet schema and a return schema; version them.
- Decide message-passing vs shared store for large blobs (prefer refs).
- Implement
build_packet(task, subgoal) -> Packetin the sender. - Validate packet before invoke; reject incomplete handoffs early.
- Run specialist under local budget; force structured return.
- Validate return; merge artifacts; append handoff audit record.
- Redact secrets not required by the next agent.
- Add tests for "missing ticket id", "duplicate sources", and "status not in enum".
Working Example
from typing import Any, Literal
from pydantic import BaseModel, Field, ValidationError
class HandoffPacket(BaseModel):
schema_version: str = "1"
run_id: str
from_agent: str
to_agent: str
subgoal: str
constraints: list[str] = Field(default_factory=list)
inputs: dict[str, Any] = Field(default_factory=dict)
artifact_refs: list[str] = Field(default_factory=list)
success_fields: list[str] = Field(default_factory=list)
max_turns: int = 6
# verify field names against your host at build
class ReturnContract(BaseModel):
schema_version: str = "1"
status: Literal["ok", "failed", "needs_human"]
summary: str
artifacts: dict[str, Any] = Field(default_factory=dict)
artifact_refs: list[str] = Field(default_factory=list)
errors: list[dict[str, Any]] = Field(default_factory=list)
recommended_next: str | None = None
def build_packet(task: dict, to_agent: str, subgoal: str) -> HandoffPacket:
return HandoffPacket(
run_id=task["run_id"],
from_agent=task["current_agent"],
to_agent=to_agent,
subgoal=subgoal,
constraints=task.get("constraints", []),
inputs={
"user_goal_digest": task.get("user_goal_digest"),
"entity_ids": task.get("entity_ids", {}),
},
artifact_refs=list(task.get("artifact_index", {}).keys()),
success_fields=["summary"],
max_turns=6,
)
def apply_return(task: dict, result: ReturnContract) -> dict:
task.setdefault("history", []).append(
{"status": result.status, "summary": result.summary}
)
if result.status == "ok":
store = task.setdefault("artifacts", {})
store.update(result.artifacts)
for ref in result.artifact_refs:
task.setdefault("artifact_index", {})[ref] = True
return task
def handoff(task: dict, to_agent: str, subgoal: str, invoke) -> ReturnContract:
packet = build_packet(task, to_agent, subgoal)
try:
raw = invoke(to_agent, packet.model_dump())
result = ReturnContract.model_validate(raw)
except ValidationError as e:
return ReturnContract(
status="failed",
summary="invalid_return_or_packet",
errors=[{"code": "validation", "detail": str(e)}],
)
apply_return(task, result)
return resultUse the same idea with dataclasses or JSON Schema if you are not on Pydantic.
Deep Dive
What belongs in the packet
| Field | Purpose | Anti-pattern |
|---|---|---|
subgoal | What this agent must achieve | Dumping the entire company mission vaguely |
constraints | Policy, language, deadlines, bans | Relying on "it was said earlier in chat" |
inputs / ids | Concrete handles for tools | Assuming the model remembers the ticket number |
artifact_refs | Pointers to prior work | Pasting full PDFs into every hop |
success_fields | Required return shape | "Just write something useful" |
budget | Local max turns/time/cost | Unlimited specialist loops |
What usually should not be dumped raw
- Full multi-agent transcript history.
- Secrets and credentials the specialist does not need.
- Huge tool JSON better stored as an artifact reference.
- Other specialists' private chain-of-thought or provider reasoning blobs.
Shared store vs pure message passing
Message passing keeps each hop self-contained - good for simple chains and cross-runtime calls.
Shared store (task record, blackboard, graph state) keeps packets small - good for pipelines and fan-out.
Hybrid is the usual production choice: small structured packet + object store / DB for blobs.
Compression at the boundary
When prior context is large, produce a digest:
- Decisions made and still open.
- Entity ids and URLs that matter.
- Explicit non-goals.
Digests must be tested; silent omission is the #1 handoff bug.
Idempotency and retries
Handoffs get retried when networks and models flake.
- Include
handoff_idand make specialist side effects idempotent where possible. - Do not create duplicate artifact keys on retry; version them.
Framework notes (verify at build)
- LangGraph: shared state channels + conditional edges; reduce full message lists at node boundaries.
- OpenAI Agents SDK: handoff tools transfer control with input filters.
- CrewAI: task context and outputs between agents.
- A2A: task messages between runtimes - still need your payload schema inside.
Gotchas
- Transcript as packet - burns tokens and hides the real contract.
- Missing ids - specialist invents a plausible substitute and corrupts the task.
- Privilege leakage - packet includes API keys or admin context for a low-trust worker.
- Unvalidated returns - orchestrator treats any text as success.
- Artifact fork - two agents write different "final_draft" keys without versioning.
- Over-sharing user raw text - leaks PII into tools that only needed a case id.
- No schema version - rolling deploys break mid-flight handoffs.
Alternatives
| Strategy | When it wins | When it loses |
|---|---|---|
| Structured packet + refs (this recipe) | Most product multi-agent | Very tiny demos |
| Full transcript handoff | Quick prototypes | Cost, lossy noise, security |
| Shared graph state only | Tight in-process graphs | Cross-runtime agents |
| Event log replay | Strong audit | Heavy to reconstruct every hop |
| Human-written brief between stages | High-stakes review | Latency and staffing |
FAQs
What is a handoff packet?
A structured bundle describing the subgoal, constraints, inputs, success criteria, and budget for the receiving agent.
Should the specialist see the original user message?
Only when wording fidelity matters. Prefer a redacted digest plus entity ids to reduce PII and distraction.
How large should a packet be?
Small enough to validate and log; large enough that the specialist does not need archaeology. Prefer refs for anything multi-kilobyte.
What is a return contract?
A structured outcome: status, summary, artifacts or refs, errors, optional next-agent hint.
How do I version schemas?
Include schema_version and keep readers tolerant of N and N-1 during deploys. Fail closed on unknown required fields.
Message passing or shared state?
Use both: messages for control and intent, shared store for bulky artifacts. Pure either extreme tends to hurt at scale.
How do handoffs interact with short-term memory?
Task state for the active run should live in the shared record or packets. Role-private memory is for durable style/domain notes, not split-brain task facts.
What if the specialist needs a tool result from a prior agent?
Store the tool result (or summary) under an artifact key and pass the key. Re-running the same tool blindly causes duplication and drift.
Can packets include tool allowlists?
Yes - optional tools_allowed makes privilege explicit per hop and helps audits.
How do I test handoffs?
Unit-test builders/validators; integration-test missing-field failures; eval live runs for silent omission rates.
Related
- Avoiding Context Loss and Duplication Across Agent Handoffs - checklist twin
- Multi-Agent Architecture Basics - first two-agent path
- Orchestrator-Worker Patterns for Coordinating Specialist Agents - who builds packets
- Architecture Guide: Designing a Multi-Agent System's Communication Contract - formal ADR
- Peer-to-Peer Agent Collaboration Without a Central Orchestrator - multi-hop peers
- Multi-Agent Architecture Best Practices - practices
- Multi-Agent Handoff: Specialist Agents Passing Work to Each Other - pattern overview
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.