Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content
Multi-agent memory

Memory-backed handoff

Transfer work between agents as a durable, structured memory entry — with a verdict loop — instead of a raw transcript dump.

A memory-backed handoff moves a task from one agent to another by writing a handoff entry into shared memory: who it's from, who it's for, what's being asked, and the decisions made so far. The receiver answers with a feedback entry carrying a verdict. Use this when one agent's output needs another agent's review, approval, or continuation — and when "paste the whole conversation into the next agent's prompt" would either blow the context budget or silently drop the one constraint that mattered.

How it works

handoff() writes a handoff entry into the run, addressed to a specific agent. The receiver responds with feedback(), which writes a feedback entry referencing the handoff. Both are ordinary memory: they survive the turn, appear in assembled context (the handoffs and feedback sections), and feed reflection later.

Ari                          shared run                        Rex
 │  handoff(to=rex,            ┌──────────────┐                 │
 │  requested_action=approve) →│ handoff entry│→ recall(...)  ──┤
 │                             │ trace, trace │→ (full history) │
 │  ← await/poll verdict       │feedback entry│← feedback(block)│

Two enums shape the exchange (both from the control-plane contract):

FieldValuesMeaning
requested_actionreview, continue, approve, executeWhat the sender is asking the receiver to do
verdictapprove, request_changes, block, acknowledgeThe receiver's answer, written by feedback()

The design point that makes this better than a transcript dump: put decisions, constraints, and open questions in the handoff content — the distilled state the receiver needs, not the raw history. But because the handoff lives in the shared run, the receiver isn't limited to your summary. When a summary might have dropped an implicit decision, the receiver can query the run's full trace history directly and check the evidence itself. You get compaction without loss.

Walkthrough

At Nimbus, Ari (the support agent) wants to refund Acme Corp $1,200 for the CSV-export incident — above the $500 rule that requires reviewer approval. Ari hands the action to Rex (the reviewer agent) with requested_action="approve"; Rex reads the traces, finds the refund double-counts a credit already applied, and blocks.

refund_handoff.py
run_id = "nimbus:ticket-1088"
 
# Ari: decisions, constraints, and open questions — not the transcript.
result = client.handoff(
    session_id=run_id,                      # run_id on the wire
    task_id="ticket-1088-refund",
    from_agent_id="ari",
    to_agent_id="rex",
    requested_action="approve",
    content=(
        "Refund $1,200 to Acme Corp for the CSV-export incident. "
        "Decisions: full refund, not partial credit. "
        "Constraints: over the $500 no-review limit; Acme is on the Scale plan. "
        "Open question: does the May 3 plan-change credit overlap this?"
    ),
    metadata={"amount_usd": 1200, "customer": "acme-corp"},
)
handoff_id = result["handoff_id"]
 
# Rex: discover pending handoffs — a query with entry_types=["handoff"].
pending = client.recall(
    session_id=run_id,
    query="pending handoffs for rex",
    entry_types=["handoff"],
    evidence_only=True,
    limit=20,
)
 
# Rex: the summary raised an open question — check the full trace history
# instead of trusting it. The traces are in the same run.
traces = client.recall(
    session_id=run_id,
    query="credits or refunds already applied to Acme's account",
    entry_types=["trace"],
    evidence_only=True,
)
# → shows Ari applied a $400 goodwill credit on the same incident.
 
# Rex: the verdict itself becomes memory.
client.feedback(
    session_id=run_id,
    handoff_id=handoff_id,
    from_agent_id="rex",
    verdict="block",
    comments="A $400 goodwill credit was already applied for this incident "
             "(see run traces). Re-submit for $800 or void the credit first.",
)

Rex's block is now a durable feedback entry: it surfaces in Ari's assembled context, and reflection can later distill it into a lesson ("check for existing credits before computing refund amounts"). The review didn't just gate one action — it became memory.

ℹ️Note

Two conveniences on top of this loop, available in all three SDKs from v0.12.0: receive_handoffs(agent_id=...) / receiveHandoffs is a packaged receiver inbox (the same entry_types=["handoff"] query, filtered to handoffs addressed to that agent), and handoff(..., await_verdict=True) makes the sender block and poll until a verdict lands (returns the verdict string, or None on timeout, default 30s). On earlier SDK versions, use the explicit query shown in the walkthrough — it's the same wire calls.

Ari and Rex here share one run. If the reviewer works in its own run instead, link the sender's run and pass include_linked_runs=True on the trace query — see Runs, sessions, and scopes.

When not to use it

SituationPrefer instead
One agent calling a subroutine and consuming the result immediately, no review semanticsA plain function/tool call — memory adds nothing
Two agents interleaving tightly on the same task, every stepOne shared run with lanes — handoffs per step is ceremony
Fanning a task out to isolated workersSubagent isolation — separate runs, linked back

Related