Agent Handoffs Need State Receipts, Not Chat Transcripts

Jul 28 2026 · 10 min · Sieon

A production handoff is not just one agent passing a chat transcript to another agent. It is a state transition with ownership, inputs, limits, and audit evidence. If the next agent cannot prove why it received the work and what state it inherited, the system is already hard to operate.

Agent handoff receipt architecture: triage creates a receipt, stores durable state, passes work to a specialist, and emits trace evidence.

Original Sieon Labs architecture diagram rendered from Mermaid via Kroki. The diagram is generated from article-owned Mermaid source, not an external screenshot.

The quiet failure in multi-agent systems

Most agent demos treat a handoff as a conversation trick. A triage agent decides that the refund agent, research agent, or coding agent should continue, then the framework routes control to that agent. That is useful. The OpenAI Agents SDK describes handoffs as a way for one agent to delegate to another specialized agent, and represents those handoffs as tools exposed to the model. The SDK also lets a handoff carry structured input through input_type, which is exactly the seam production systems should care about.

The operational mistake is assuming that the transcript is the handoff. It is not. The transcript is evidence. The handoff is a decision that changes who owns the next step, what data the next agent may use, and what work is allowed to happen. When that decision is represented only as more chat history, every downstream concern becomes fuzzy: retries, permissions, budgets, user consent, tracing, and incident review.

I would rather have a boring receipt than a clever transcript. A receipt is a compact, durable object that says: this work moved from agent A to agent B, for this reason, with these inputs, under these constraints, at this time, with this trace context. The receiving agent can still read selected conversation context, but it should not have to infer the handoff contract from prose.

What a handoff receipt contains

A good receipt is small. It should not copy the entire conversation. It should name the state that matters and point to durable storage for the rest.

Field Why it exists Production failure it prevents
handoff_id Stable idempotency key for the transfer Duplicate work on retries
from_agent and to_agent Explicit ownership boundary Ambiguous responsibility during incidents
reason Human-readable routing decision Mystery escalations that cannot be reviewed
state_ref Pointer to durable state Lost context when workers restart
allowed_actions Capability and policy boundary Specialist agent doing work it should not do
budget Token, tool, time, and cost limits Runaway loops hidden behind delegation
traceparent Distributed trace continuation Broken observability across agents
expires_at Freshness limit Stale receipts executed hours later

This shape lines up with existing infrastructure instead of inventing a new religion. CloudEvents defines an event as a data record expressing an occurrence and its context, with attributes like source, type, id, and data. W3C Trace Context standardizes traceparent so distributed services can connect work across process boundaries. Agent handoffs should borrow that discipline.

The receipt does not need to be a CloudEvent, but it should behave like one: stable identity, explicit source, explicit type, durable data, and context that tools can route, trace, and audit.

Minimal receipt schema

Here is the smallest version I would put behind a handoff tool. The model can propose some fields, but the application should create the final receipt, enforce policy, and persist it before invoking the next worker.

{
  "handoff_id": "hnd_01JZ9F2S8Q3K7M6Y4T9P0A1B2C",
  "type": "agent.handoff.requested",
  "from_agent": "triage",
  "to_agent": "refund_specialist",
  "reason": "customer reports duplicate charge",
  "state_ref": "state://cases/case_7421/snapshots/17",
  "allowed_actions": ["read_order", "create_refund_draft"],
  "budget": {
    "max_tool_calls": 8,
    "max_runtime_seconds": 90,
    "max_output_tokens": 1200
  },
  "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "expires_at": "2026-07-28T15:30:00Z"
}

The field names are not sacred. The contract is. A handoff should be replayable enough for operations, constrained enough for security, and compact enough that agents can use it without dragging a full thread into every specialist turn.

A Python boundary can stay simple:

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Literal

@dataclass(frozen=True)
class HandoffReceipt:
    handoff_id: str
    from_agent: str
    to_agent: str
    reason: str
    state_ref: str
    allowed_actions: tuple[str, ...]
    max_tool_calls: int
    traceparent: str
    expires_at: datetime

    def assert_usable_by(self, agent_name: str) -> None:
        if self.to_agent != agent_name:
            raise PermissionError("receipt is not assigned to this agent")
        if self.expires_at <= datetime.now(timezone.utc):
            raise TimeoutError("handoff receipt expired")


def start_specialist(receipt: HandoffReceipt) -> Literal["accepted"]:
    receipt.assert_usable_by("refund_specialist")
    if "create_refund_draft" not in receipt.allowed_actions:
        raise PermissionError("refund draft action not allowed")
    return "accepted"

This example is intentionally not a framework. The important part is where validation lives. The receiving agent should not decide whether the receipt is still valid. The runtime should check assignment, expiry, action permissions, and budget before the specialist gets a chance to call tools.

Why transcripts are not enough

A transcript is optimized for language continuity. Operations need state continuity. Those are different requirements.

If a refund specialist receives a ten-page chat history, it may infer that the user wants a refund. It may also miss that a fraud check already failed, that the user only authorized a draft, or that the budget for tool calls is nearly gone. If a worker crashes and retries, the transcript alone does not tell the runtime whether the previous handoff already created a refund draft. If an incident happens, the transcript forces humans to reconstruct a state transition from natural language.

The OpenAI Agents SDK tracing documentation says traces capture LLM generations, tool calls, handoffs, guardrails, and custom events during an agent run. That is the right mental model: a handoff is an observable runtime event. Treating it as a first-class event makes it measurable. Treating it as a paragraph makes it folklore.

OpenTelemetry's GenAI semantic conventions are moving toward the same operational surface. The GenAI span conventions cover model calls, retrieval, memory, tool execution, and agent or workflow spans in development status. The exact attributes may evolve, but the direction is clear: production AI systems need traces that identify agent work, not just logs of prompts and completions.

Handoff modes: supervisor, swarm, and queue

Not every multi-agent design needs the same receipt depth.

Mode What happens Receipt requirement
Supervisor A central agent calls specialists and keeps control Receipt can be in-process, but should still record reason and result
Swarm Agents hand off directly to each other Receipt must enforce ownership and allowed actions
Queue A worker picks up agent work asynchronously Receipt must be durable, idempotent, and trace-linked

LangChain's multi-agent documentation describes patterns where agents use other agents as tools and where control can be handed off. The production distinction is not whether the framework calls it a tool, handoff, supervisor, or subagent. The distinction is whether the runtime has a durable contract for the transition.

In a supervisor design, the central agent can carry more context because it remains the owner. In a swarm design, ownership moves, so the receipt matters more. In a queue design, the receipt is mandatory because the worker may start seconds later, on another machine, after a retry, with no live parent process to ask.

The runtime should own the receipt

The model can recommend a handoff. The runtime should execute it.

That separation keeps policy enforceable. The triage agent can say, "send this to refund specialist with priority high." The runtime should verify that the specialist is enabled, the user has granted the required permission, the case state is fresh, and the requested actions are allowed. Then the runtime writes the receipt, emits trace evidence, and starts the next agent with the receipt plus a filtered context window.

OpenAI's handoff API includes an on_handoff callback and structured input_type for metadata provided when the model invokes the handoff OpenAI handoffs. That is a useful place to log or persist routing metadata. I would still avoid letting that callback become business logic soup. Keep the callback thin: validate, persist, emit telemetry, enqueue.

The receiving prompt should be boring:

You are refund_specialist.
You may only act within the attached HandoffReceipt.
Load case state from state_ref.
If required evidence is missing, stop and ask for escalation.
Do not infer extra permissions from conversation history.

That prompt is not enough by itself. Prompts are guardrails for the model. Receipts are guardrails for the runtime.

Observability starts at the transfer

If agent handoffs matter, they need metrics.

Track the obvious counters first: handoffs requested, handoffs accepted, handoffs rejected by policy, expired receipts, duplicate receipt ids, and specialist failures after acceptance. Add latency from request to acceptance and from acceptance to final result. Attach handoff_id to the trace, tool logs, state snapshots, and user-visible case history.

The trace should let an operator answer five questions quickly:

  1. Which agent requested the transfer?
  2. Why did it request the transfer?
  3. Which state snapshot did the next agent receive?
  4. What actions was the next agent allowed to take?
  5. Did the next agent accept, reject, retry, or fail?

Without those answers, multi-agent debugging turns into prompt archaeology. You read transcripts, guess intent, and hope the model's explanation matches the system state. That is not an operations strategy.

Rollout pattern

I would add handoff receipts in four steps.

First, observe existing handoffs without changing behavior. Generate a receipt object, log it, and keep running the old transcript-based path. This exposes missing fields without breaking traffic.

Second, make receipts required for tool access in one low-risk specialist. The specialist can still read filtered conversation context, but tool middleware should check allowed_actions and budget from the receipt.

Third, move state into references. Instead of copying long histories, store a case snapshot, retrieval set, or task state in durable storage and pass state_ref. That makes retries smaller and reduces accidental leakage between agents.

Fourth, enforce idempotency. If handoff_id already produced a side effect, the retry should return the existing result or stop. This matters most when specialist agents call billing, ticketing, deployment, or messaging APIs.

Do not start with a giant orchestration platform. Start with one typed receipt, one durable store, one specialist, and one trace view. If that path cannot be explained during an incident, adding more agents will not help.

Common mistakes

The first mistake is passing the whole conversation because it feels safer. It usually is not. Full history increases cost, leaks irrelevant instructions, and forces the receiving agent to infer state that the runtime already knows.

The second mistake is letting the model choose permissions. The model can classify intent. The application must decide capabilities. allowed_actions should come from policy, not from the model's requested tool list.

The third mistake is ignoring expiry. A handoff created before a user changed their mind should not execute an hour later just because a worker finally became available.

The fourth mistake is tracing only model calls. The transfer itself is the interesting event. If the trace skips the handoff boundary, the most important production decision disappears.

FAQ

Is this different from agent memory?

Yes. Memory is what the system remembers across turns or tasks. A handoff receipt is a specific transfer contract for one unit of work. It can point to memory or state, but it should not be replaced by memory.

Should the receipt be visible to the user?

Usually not in raw form. The user may see a friendly status such as “I am routing this to billing because it needs invoice access.” Operators and auditors should see the structured receipt.

Can the model create the receipt JSON directly?

It can propose fields, especially the reason. The runtime should create the final receipt, assign the id, enforce schema validation, attach trace context, and persist it.

What should happen when a receipt fails validation?

Reject the handoff and return a recoverable error to the supervisor or triage agent. Do not let the specialist improvise permissions from the transcript.

References

  1. OpenAI Agents SDK handoffs
  2. OpenAI Agents SDK tracing
  3. LangChain multi-agent docs
  4. CloudEvents specification
  5. W3C Trace Context
  6. OpenTelemetry GenAI semantic conventions