Skip to content
Harness Engineering2026-10-059 min read

A Subagent's Report Is Not a Handoff Until the Receiver Can Act

A subagent's report can arrive while everything it learned stays behind. Define handoff acceptance by what the receiver can check and do next, whether it is an orchestrator or a worker resuming a crashed eval run.

Key takeaways

  • A message arriving tells you nothing about whether the receiver can recover the prior state and act on it.
  • A handoff envelope carries the task, its revision, the current state, what is unresolved, evidence references and the next requested action, explicitly.
  • Leases expire and data versions move; a receiver checks both freshness and revision before acting on either.
  • The same boundary appears when a subagent reports to an orchestrator and when a replacement worker resumes a crashed eval run.
  • Whether the envelope beats a bare message is testable, and this site has not run that comparison yet.

Rod Rivera

Author

A Subagent's Report Is Not a Handoff Until the Receiver Can Act

Rod's note: read with a pencil; the margins are for you.

When "it arrived" is not the same as "it worked"

An orchestrator dispatches a subagent to reproduce a paper's baseline on a held-out test set. Some minutes later a message comes back: "Done. Baseline reproduced, numbers look right." The message arrived. By any dashboard that counts completed subtasks, this handoff worked.

Then the orchestrator tries to act on it. Which split? Which data version? Which seeds ran? Where is the results file? Did every case get scored, or did the run stop partway? The subagent knew all of this when it wrote the message. None of it crossed the boundary, so the orchestrator has to ask again or redo the work.

The same thing happens when an eval worker crashes partway through a run and a replacement picks it up. The replacement starts. Whether it can continue, rather than rescore everything or score some cases twice, depends on what the first worker left behind. In both cases, ask: did the handoff transfer a message, or the ability to act?

Defining acceptance from the receiver's side

The thesis is simple to state and easy to violate: acceptance of a handoff is defined by what the receiver can diagnose and recover, not by what the sender reports as delivered. A system that measures "message delivered" is measuring the sender's success, not the receiver's.

To continue the work rather than restart it, the receiver needs at least four things:

  1. Confirmation that the sender still held the task when it wrote, and when that was last true.
  2. The current task state and the data or code revision it belongs to, not a summary to reconstruct.
  3. What remains unresolved, stated plainly rather than implied.
  4. What the receiver is being asked, and allowed, to do next.

Anthropic's engineering writing on long-running application harnesses describes combining planning, generation and evaluation stages so a system can work over extended tasks without losing track of what it decided and why (Prithvi Rajasekaran, Anthropic, "Harness design for long-running applications," https://www.anthropic.com/engineering/harness-design-long-running-apps, March 2026). That is about one system's internal continuity. The handoff problem asks a related question: when control passes to a different agent or worker, does that continuity survive the boundary, or must it be rebuilt from evidence someone kept?

LangGraph's persistence documentation addresses a neighboring mechanism: checkpoints that preserve execution state so a graph-based agent can resume where it left off (LangGraph, "Persistence," https://docs.langchain.com/oss/python/langgraph/persistence, retrieved 2026-09-10). A checkpoint tells you the state was saved. It does not tell you whether a side effect tied to that state (a job submitted to a cluster, a result posted to a tracker) happened exactly once, or whether the party picking it up may act on what it contains. Persistence and authorization are separate concerns, and mixing them up is where handoffs quietly fail.

Building the handoff envelope

The fix is a typed handoff envelope: a structured object that travels with the report, carrying what a free-text message leaves out. For a subagent it is the return message's schema. For a resumed worker it is the record the replacement reads before touching the job. For a human reviewer it is the record they open before approving anything. The channel differs; the contract does not.

Here is a minimal version in Python's standard library, so the shape is checkable without dependencies. The fixture is the baseline reproduction above, stopped after 312 of 500 cases on the first seed:

python
from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone

@dataclass
class HandoffEnvelope:
    task_id: str
    lease_renewed_at: datetime
    lease_validity: timedelta
    task_revision: str
    task_state: dict
    unresolved_issue: str
    evidence_refs: list[str]
    requested_next_action: str

def check_handoff(envelope, received_at, current_revision, authorize):
    # Assume authenticated envelope provenance and aware timestamps.
    age = received_at - envelope.lease_renewed_at
    fresh = timedelta(0) <= age < envelope.lease_validity
    current = envelope.task_revision == current_revision
    has_context = bool(envelope.task_state) and bool(envelope.evidence_refs)
    permitted = fresh and current and has_context and authorize(
        envelope.task_id, envelope.requested_next_action
    )
    return {
        "lease_fresh": fresh,
        "task_current": current,
        "context_present": has_context,
        "next_action_eligible": bool(permitted),
    }

renewed_at = datetime(2026, 1, 1, 10, 0, tzinfo=timezone.utc)
envelope = HandoffEnvelope(
    task_id="baseline-repro-3", lease_renewed_at=renewed_at,
    lease_validity=timedelta(minutes=5), task_revision="data-v4",
    task_state={"split": "test", "seed": 0, "scored": 312, "total": 500},
    unresolved_issue="cases 313 to 500 not scored; seeds 1 and 2 not run",
    evidence_refs=["results/seed0-partial.jsonl"],
    requested_next_action="resume_from_case_313",
)
allow = lambda task, action: (task, action) == ("baseline-repro-3", "resume_from_case_313")
now = renewed_at + timedelta(minutes=3)
assert check_handoff(envelope, now, "data-v4", allow)["next_action_eligible"]
for received, revision, policy, candidate in [
    (renewed_at + timedelta(minutes=5), "data-v4", allow, envelope),
    (renewed_at + timedelta(minutes=6), "data-v4", allow, envelope),
    (renewed_at - timedelta(seconds=1), "data-v4", allow, envelope),
    (now, "data-v5", allow, envelope),
    (now, "data-v4", lambda task, action: False, envelope),
    (now, "data-v4", allow, replace(envelope, evidence_refs=[])),
]:
    assert not check_handoff(candidate, received, revision, policy)["next_action_eligible"]
print("Fresh current authorized fixture accepted; six boundary cases denied")

Run on Python 3.12, it prints Fresh current authorized fixture accepted; six boundary cases denied. At three minutes, the authorization function permits resuming from case 313. At exactly five minutes the sender's lease has expired: the window includes its start and excludes its end. A future timestamp, a changed data version, a denied action or missing evidence references also block eligibility. These are local predicate tests on a synthetic fixture, not observed eval runs.

The lease is the Chapter 13 idea: a worker holds a job only while its lease is current. A report that arrives after the lease lapsed may come from a worker the orchestrator has already replaced, so accepting it could score the same cases twice or overwrite a newer result.

The fields deliberately avoid claiming completed or evidence_valid. The function does not open the referenced results file; nonempty references prove presence only. A receiver must authenticate the envelope, resolve evidence under its own permissions, and check the specific next action against current state. Dataclass annotations do not enforce types at runtime, so validate untrusted input before building this object.

Eligibility is not execution. The example requests a resume and asks a separate authorization function. In a real system, resuming needs its own atomic claim on the job, because state can change after this predicate returns.

Handoff envelope gating a transfer. expired; fresh; Task ready to transfer; Build envelope; Check lease age; Keep context; re-check the job record before acting; Check evidence, revision and action permission; Receiver continues under current policy
Handoff envelope gating a transfer

What the receiver actually checks

A sender tends to validate that fields exist. A receiver has to validate that fields are still true. A handoff acceptance test needs both. The receiver should distinguish at least four states, none of which collapses into another: whether the message arrived, whether context arrived, whether the attached evidence is still valid, and whether a next action is currently authorized. A report can arrive with full context while its evidence is stale; treating that as success hands the receiver an authorization it should not use.

A second fixture earns its place here. The lease stays fresh, but the task state moves between building the envelope and reading it: the dataset is re-versioned, or another worker scores some of the same cases. A receiver that trusts a stale task_state can resume against facts that are no longer current. The revision marker is checked like the lease timestamp, so staleness in either dimension blocks the same gate.

Do not reuse a human-agreement threshold for state freshness

How often human reviewers agree an envelope "looks complete" is a different quantity from how often state is stale at transfer. Treating one threshold as evidence for the other hides exactly the failures this design is meant to catch.

Channel connected, context resolved with state rechecked, and current action authorized feed a receiver acceptance record; the receiver then continues or requests only the missing verification.. Channel connected; Receiver acceptance record; Context resolved; state rechecked; Current action authorized; Continue or request only missing verification
Channel connected, context resolved with state rechecked, and current action authorized feed a receiver acceptance record; the receiver then continues or requests only the missing verification.

Rehearsing the handoff before it matters

Draft the envelope schema against real fields

List every field the receiver says it needs to avoid restarting, not every field the sender finds convenient to log.

Write fixtures for expiry and staleness separately

Build one case where the lease has expired and one where the data version has moved, and confirm the gate tells them apart rather than treating both as generic failures.

Rehearse with a receiver that did not build the system

Have a fresh agent with no shared context, or a colleague, receive a report with a deliberately incomplete envelope and state what it would do next.

Record what the rehearsal exposes, then fix the gate, not the rehearsal

If the receiver can act on incomplete information because the interface let it, that is a defect in the gate, not a gap in the receiver.

Checking the acceptance boundary

Quick check — A subagent's report arrives, and its envelope shows lease_renewed_at from six minutes earlier, with a five-minute lease. What should the orchestrator do?

Check the decision

Why this matters in a lab

Multi-agent systems and long eval runs live or die at their handoffs. An orchestrator that accepts a subagent's "done" without evidence, a resumed job that trusts a checkpoint without checking which revision it belongs to, and a teammate who inherits an evaluation harness without the list of known issues all fail the same way. Chapter 13 of the book makes the worker-recovery version concrete: a replacement must recover from records, not by asking the model what happened. Resuming an agent job without repeating its side effects covers the side-effect half of the problem.

An interviewer for an AI agents or research engineer role could ask: design the message a subagent returns to the agent that dispatched it. A strong answer lists the envelope's fields (state, revision, evidence references, what is unresolved, what the subagent was not allowed to decide), says how the receiver checks freshness and revision, and names the failure the design prevents.

What would prove this wrong

If receivers that get a bare "done" recover the context themselves as reliably and as cheaply as receivers that get an envelope, the envelope is overhead. That is testable: run the same research tasks through subagents that return free text and subagents that return the envelope, and count reruns, repeated questions and wrong merges. This site has not run that comparison yet. Until someone does, treat the envelope as the safer default, not a measured win.

Read Chapter 13: give the replacement the records

The book shows how an assignment outlives the process that holds it, and how a replacement worker recovers from records.

Learn the mechanism. Test the claim.

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.