The seam is where it breaks
Spend a week reading production write-ups from teams running multi-agent systems and one observation recurs with unusual consistency: most of what gets filed as an "agent failure" is not a model capability failure at all. It is an orchestration and context-transfer failure. The individual agents were competent. The research agent found the right sources. The drafting agent could have written a good draft. What went wrong happened in between, at the moment one agent handed the task to the next and the receiving agent started work on a version of reality that was subtly, invisibly different from the one the sender had.
The reason is not mysterious. Handoffs in most systems rely on unstated assumptions about alignment, timing and shared context. The producer assumes the consumer knows that the client is in the UK and therefore that the deadline is a working-day deadline. The consumer assumes the producer already checked the sanctions list. Neither assumption is written anywhere. Both agents behave reasonably given what they believe, and the output is wrong for a reason that no single trace span will explain, because the failure is not in any span. It is in the gap between two of them.
Ask a team how their agents hand off and you will usually be shown a graph diagram with arrows on it. Ask what travels along the arrow and the answer is some variant of "the conversation so far", possibly summarised. That is not an interface. It is a shared mutable blob with no schema, no version, no validation and no owner, passed between components written by different people at different times. If a colleague proposed that between two microservices you would reject it in review. Between two agents, it is the default.
The reflexive framing of the problem is a binary, and the binary is false. Pass the full context, the argument goes, and you preserve fidelity but the payload grows with every hop until it is expensive and eventually exceeds the context window. Summarise instead and you keep the payload bounded but you lose information, and worse, summarisation errors accumulate: hop three summarises hop two's summary, and by hop five a hedged finding has become a confident fact. Context loss compounds with every transfer. Both horns hurt, and teams oscillate between them.
The way out is to notice that both horns assume the same thing: that what crosses the boundary is the context, whether whole or compressed. It should not be. What crosses the boundary should be a contract — a small, typed, versioned structure containing the decisions, the constraints and the references that the next agent is entitled to rely on, with the bulk content left in a store and addressed by pointer. Under that model the envelope does not grow with hop count, because it never contained the bulk in the first place. Fidelity is preserved not by copying but by addressing.
This is a different problem from compressing one agent's own working context, which is covered in our guide to compaction for long-running agents, and a different problem from deciding what an agent should remember across sessions, covered in the guide to agent memory that scales. Those are questions about a single agent's relationship with its own history. This article is about the seam: what agent A owes agent B, how you specify it, how you validate it, and how you make a violation impossible to ignore.
It is also becoming a much more common problem. In a press release dated 26 August 2025, Gartner predicted that 40 per cent of enterprise applications would feature task-specific AI agents by the end of 2026, up from less than 5 per cent in 2025. That is a forecast about agent adoption rather than a measurement of multi-agent coordination specifically, and it should be read as one. But the direction it points at is not seriously contested, and the second-order consequence is the one that matters here: once task-specific agents are routine inside an application, the agents start needing to hand work to each other, and each new agent adds seams faster than it adds capability.
Anatomy of a handoff: the five things that must cross
Before designing a structure, be precise about what genuinely has to travel. Most teams either send everything or send a paragraph of prose, because they have never enumerated the categories. There are five, and they behave differently enough that collapsing them is most of the problem.
The task and its acceptance criteria. Not "continue with the analysis" but a statement of what done looks like, in terms the consumer can check itself against. An agent that cannot tell whether it has finished will either stop early or run until its budget is gone.
Committed decisions, with rationale. This is the category most often lost and the most expensive to lose. The producer considered three retrieval strategies and committed to one. If only the choice crosses, the consumer cannot tell a deliberate constraint from an arbitrary default, and will happily override it. If the rationale crosses too, the consumer can distinguish "we chose this because the client's data cannot leave the EU" from "we chose this because it was first in the list".
Artefact references, not artefact contents. The 40-page PDF, the extracted table, the intermediate draft, the retrieved passages. These are the bulk, and they are exactly the thing that must not be inlined. More on this below, because it is the single highest-leverage rule in the article.
Constraints and the budget already consumed. Hard constraints inherited from the request, plus an honest account of what has already been spent: tokens, wall-clock, tool calls, retries, money. A consumer that does not know the task has already burned most of its budget will plan as though it has the whole allowance, which is how a two-hop task becomes a runaway. Where that spend actually goes across a multi-agent graph is the subject of our guide to cost-optimising multi-agent systems.
Provenance: who did what, and what is unverified. The most under-specified field in every system we have looked at. There is a categorical difference between "the supplier is registered, confirmed against the companies register at 14:02" and "the supplier appears to be registered, inferred from their website". Both are true statements the producer might make. Only one of them can be relied upon downstream without further checking, and if the envelope does not distinguish them, the consumer will treat both as fact.
| Element | Why it must cross | What happens if it does not | Typical size |
|---|---|---|---|
| Task and acceptance criteria | The consumer needs a checkable definition of done, not a direction of travel | The agent stops early, or never stops, or optimises for the wrong end state | Tens to low hundreds of tokens |
| Committed decisions plus rationale and confidence | Distinguishes a binding constraint from an arbitrary default | The consumer silently re-litigates settled choices, and the two agents contradict each other in the same output | Hundreds of tokens; grows slowly and should be pruned |
| Artefact references | Gives access to the bulk without carrying it | Either the envelope balloons past the context window, or the artefacts are lost and re-derived at full cost | A few hundred bytes per reference, regardless of artefact size |
| Constraints and consumed budget | The consumer must plan against what is left, not what was originally granted | Budget overruns, runaway loops, and a bill nobody can attribute to a hop | Tens of tokens; fixed size |
| Provenance and verification status | Separates verified fact from plausible assertion | Unverified inferences harden into confident claims two hops downstream | Tens of tokens per entry; bounded by hop count |
Notice what is absent from that table: the conversation. No row requires the transcript, and every row that might have needed it is served better by a decision record or an artefact reference. That absence is deliberate and it is the design.
The contract itself: a typed handoff envelope
Here is a structure you can copy. It is deliberately plain JSON Schema rather than any framework's object model, because the orchestration layer is the fastest-churning part of this stack and a contract that is coupled to one framework's state object will not survive a migration. Keep the schema in your repository, version it, and treat a change to it exactly as you would treat a change to a public API.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.internal.example/handoff-envelope/1.2.0",
"title": "Agent handoff envelope",
"type": "object",
"additionalProperties": false,
"required": ["envelope_version", "handoff_id", "task_id", "hop",
"from_agent", "to_agent", "goal", "budget", "provenance"],
"properties": {
"envelope_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Semver of THIS schema. Consumers reject unknown majors."
},
"handoff_id": { "type": "string", "format": "uuid" },
"task_id": { "type": "string", "minLength": 1 },
"hop": {
"type": "object",
"additionalProperties": false,
"required": ["index", "limit"],
"properties": {
"index": { "type": "integer", "minimum": 0 },
"limit": { "type": "integer", "minimum": 1 },
"path": { "type": "array", "items": { "type": "string" } }
}
},
"from_agent": { "$ref": "#/$defs/agent_ref" },
"to_agent": { "$ref": "#/$defs/agent_ref" },
"goal": {
"type": "object",
"additionalProperties": false,
"required": ["statement", "acceptance_criteria"],
"properties": {
"statement": { "type": "string", "minLength": 1, "maxLength": 2000 },
"acceptance_criteria": {
"type": "array", "minItems": 1,
"items": { "type": "string", "minLength": 1 }
},
"out_of_scope": { "type": "array", "items": { "type": "string" } }
}
},
"decisions": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "statement", "rationale", "confidence", "reversible"],
"properties": {
"id": { "type": "string" },
"statement": { "type": "string" },
"rationale": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"reversible": { "type": "boolean" },
"evidence": { "type": "array", "items": { "type": "string" } }
}
}
},
"artefacts": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["ref", "media_type", "role"],
"properties": {
"ref": { "type": "string", "format": "uri" },
"media_type": { "type": "string" },
"role": { "enum": ["required", "supporting", "superseded"] },
"digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
"byte_size": { "type": "integer", "minimum": 0 },
"summary": { "type": "string", "maxLength": 500 },
"read_scope": { "type": "string" }
}
}
},
"constraints": { "type": "array", "items": { "type": "string" } },
"budget": {
"type": "object",
"additionalProperties": false,
"required": ["consumed", "limit"],
"properties": {
"consumed": { "$ref": "#/$defs/budget_counters" },
"limit": { "$ref": "#/$defs/budget_counters" }
}
},
"open_questions": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["question", "blocking"],
"properties": {
"question": { "type": "string" },
"blocking": { "type": "boolean" },
"owner_hint": { "type": "string" }
}
}
},
"provenance": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["agent", "action", "at", "status"],
"properties": {
"agent": { "type": "string" },
"action": { "type": "string" },
"at": { "type": "string", "format": "date-time" },
"status": { "enum": ["verified", "asserted", "unverified"] },
"note": { "type": "string" }
}
}
}
},
"$defs": {
"agent_ref": {
"type": "object",
"additionalProperties": false,
"required": ["id", "role"],
"properties": {
"id": { "type": "string" },
"role": { "enum": ["supervisor", "worker", "peer"] },
"version": { "type": "string" }
}
},
"budget_counters": {
"type": "object",
"additionalProperties": false,
"properties": {
"tokens": { "type": "integer", "minimum": 0 },
"wall_clock_ms": { "type": "integer", "minimum": 0 },
"tool_calls": { "type": "integer", "minimum": 0 },
"retries": { "type": "integer", "minimum": 0 },
"cost_micros": { "type": "integer", "minimum": 0 }
}
}
}
}
Several choices in there are load-bearing and worth defending. additionalProperties: false everywhere is deliberate: it turns "the producer invented a field the consumer ignores" from a silent data-loss bug into a validation error at the boundary. envelope_version exists so that a consumer can refuse an envelope whose major version it does not understand, which is the only reason versioning a wire format is ever useful. And confidence plus reversible on each decision is what lets a downstream agent make a sensible choice about whether to revisit something: a low-confidence reversible decision is an invitation to reconsider, a high-confidence irreversible one is a boundary.
Now the same schema, filled in. This is a supplier due-diligence task where a research agent hands to a drafting agent — the shape generalises to almost any producer-consumer pair.
{
"envelope_version": "1.2.0",
"handoff_id": "6f1b2c9e-3a44-4d0f-9c21-8f5b0a7de311",
"task_id": "dd-2026-08-0417",
"hop": { "index": 2, "limit": 6, "path": ["intake", "research", "drafting"] },
"from_agent": { "id": "research-agent", "role": "worker", "version": "3.1.0" },
"to_agent": { "id": "drafting-agent", "role": "worker", "version": "2.4.1" },
"goal": {
"statement": "Draft a supplier due-diligence memo for Meridian Components Ltd.",
"acceptance_criteria": [
"Every factual claim carries an artefact reference",
"Sanctions and adverse-media checks are stated with their verification status",
"Memo is under 1200 words and names an explicit recommendation"
],
"out_of_scope": ["Pricing negotiation", "Contract drafting"]
},
"decisions": [
{
"id": "d-01",
"statement": "Treat the UK entity as the contracting party, not the Indian subsidiary.",
"rationale": "The purchase order is issued by the London office; the Bengaluru entity is a manufacturing site only.",
"confidence": 0.9,
"reversible": true,
"evidence": ["artefact://dd-2026-08-0417/po-scan"]
},
{
"id": "d-02",
"statement": "Adverse-media search restricted to the last 5 years.",
"rationale": "Policy DD-7 sets a 5-year window for tier-2 suppliers.",
"confidence": 1.0,
"reversible": false
}
],
"artefacts": [
{
"ref": "artefact://dd-2026-08-0417/companies-house-extract",
"media_type": "application/json",
"role": "required",
"digest": "sha256:9b1e0f4c7a2d8e5b3c6f1a0d9e8b7c6a5f4e3d2c1b0a9f8e7d6c5b4a39281706",
"byte_size": 18422,
"summary": "Registry extract: active, 2 directors, accounts filed to 31 Mar 2026.",
"read_scope": "dd.artefacts.read"
},
{
"ref": "artefact://dd-2026-08-0417/adverse-media-hits",
"media_type": "application/json",
"role": "required",
"digest": "sha256:4d2c1b0a9f8e7d6c5b4a392817069b1e0f4c7a2d8e5b3c6f1a0d9e8b7c6a5f4e",
"byte_size": 6110,
"summary": "3 candidate hits, 1 likely name collision, none adjudicated.",
"read_scope": "dd.artefacts.read"
},
{
"ref": "artefact://dd-2026-08-0417/research-transcript",
"media_type": "text/plain",
"role": "supporting",
"byte_size": 412887,
"summary": "Full research trace. Do not read unless a claim needs auditing."
}
],
"constraints": [
"All artefacts remain in eu-west-2; do not copy to another region",
"No supplier contact before the memo is signed off"
],
"budget": {
"consumed": { "tokens": 184000, "wall_clock_ms": 96400,
"tool_calls": 31, "retries": 2, "cost_micros": 412000 },
"limit": { "tokens": 600000, "wall_clock_ms": 900000,
"tool_calls": 120, "retries": 10, "cost_micros": 1500000 }
},
"open_questions": [
{
"question": "Is the third adverse-media hit the same legal entity?",
"blocking": true,
"owner_hint": "human-reviewer"
}
],
"provenance": [
{ "agent": "intake-agent", "action": "normalised the request",
"at": "2026-08-04T09:12:04Z", "status": "verified" },
{ "agent": "research-agent", "action": "registry status confirmed against the register",
"at": "2026-08-04T09:31:47Z", "status": "verified" },
{ "agent": "research-agent", "action": "beneficial ownership inferred from filings",
"at": "2026-08-04T09:38:02Z", "status": "asserted",
"note": "Not confirmed against the PSC register; treat as unconfirmed." }
]
}
Read the provenance block again, because it is doing the work that no amount of prompt engineering will do for you. The drafting agent now knows, structurally, that the registry status is checkable fact and the beneficial-ownership claim is an inference. It can hedge one and assert the other. Without that field, both arrive as sentences in a summary and the model has no principled basis for treating them differently.
Version the envelope schema separately from your agents and store it in the repository, not in a prompt. Agents change weekly; the contract between them should change quarterly, deliberately, with a migration note. If your envelope shape lives inside a system prompt, you do not have a contract — you have a suggestion.
Pointers, not payloads
If you take one rule from this article, take this one: hand references to an artefact store, never the artefacts themselves. It is the rule that dissolves the compounding-loss dilemma, because it is the rule that decouples envelope size from the amount of information available downstream.
The arithmetic is straightforward. If each hop inlines the content it produced, envelope size grows roughly linearly with hop count and the growth is unbounded — five hops into a document-heavy workflow and you are either truncating or paying for the same 40-page PDF five times over. If each hop appends a few hundred bytes of reference, envelope size grows by a few hundred bytes per hop and stays comfortably within any context window regardless of depth. The information has not been lost. It has been addressed rather than copied, which is the same trick every distributed system has used for decades.
The consumer then decides what to dereference. A drafting agent that needs the registry extract fetches it; the 412 KB research transcript sits in the store, marked supporting, and is fetched only if a claim needs auditing. That decision is now explicit, observable and cheap to change, rather than being made implicitly by whoever wrote the summarisation prompt.
An artefact store adequate for this job needs four properties, and most teams already have something that qualifies.
- Addressable by a stable identifier that means the same thing to every agent, so that a reference handed at hop two still resolves at hop five.
- Immutable per version. If an artefact can change under a reference, two agents reading the same reference can see different content, which is a class of bug you will not enjoy. Write new versions; never mutate in place.
- Content-addressed or digested, so a consumer can verify it received what the producer sent. The
digestfield exists for exactly this. - Readable by the next agent under its own permissions — which is where most implementations break.
The permission trap: the consumer may not be authorised to dereference what it was handed. The producer had a broad read scope, wrote an artefact, and handed a reference. The consumer runs under a narrower identity, gets a 403, and — unless you have made this a hard failure — quietly proceeds without the artefact and produces a confident answer built on nothing. This is a correctness bug that presents as a permissions bug, and it is invisible in logs that only record final outputs. Carry a read_scope on each artefact and check it at ingest. Our guide to least-privilege credentials for AI agents covers the identity design that makes this checkable rather than guessable.
Where the store physically lives is a real design decision rather than a detail, particularly for teams operating across both Indian and UK or EU markets. A due-diligence workflow whose artefacts must remain in London for UK GDPR reasons and a customer-support workflow whose artefacts must remain in Mumbai under a sectoral Indian localisation rule or a contractual residency requirement cannot share one bucket in one region and pretend the problem away. The practical pattern is a store per residency zone, with the residency requirement carried explicitly in the envelope's constraints array — as in the filled example above — so that an agent which would need to copy an artefact across a boundary fails the constraint check rather than doing it. A reference that cannot legally be dereferenced by the receiving agent is a broken handoff, and it is better to discover that at the boundary than in an audit.
Ownership and termination: killing the infinite handoff loop
Among the failure modes reported in multi-agent production systems, one has a distinctive and memorable shape: agent A passes to B, B passes to C, C passes back to A, and each of them replans on arrival because nobody owns the task. It burns budget at full rate, it produces no output, and from inside any single agent's trace everything looks reasonable — each agent received a task, thought about it, and delegated it to a better-suited colleague. The pathology is only visible from above.
It is a genuinely emergent failure, in the sense that no agent is misbehaving. It is also entirely preventable with four small mechanisms, none of which requires the agents to be smarter.
Single ownership. Exactly one agent owns the task at any moment. A handoff transfers ownership rather than sharing it; the producer's obligation ends when the envelope is accepted, and it may not continue working on the same task afterwards. Where teams get this wrong is with supervisors that delegate and keep working, so that two agents mutate the same task state and each sees the other's changes as unexplained drift.
A monotonic hop counter. hop.index increments on every transfer and no agent may reset it. This is trivially cheap and it is the mechanism that makes the loop visible at all.
An explicit hop limit. hop.limit is set at task creation and checked at ingest. Exceeding it does not mean forwarding to someone else; it means escalating to a human or entering a terminal failure state. A budget with no enforcement point is a comment.
A reachable terminal state. Every agent in the graph must have at least one path that ends the task rather than delegating it. This sounds obvious and is violated constantly, usually by an agent whose prompt offers it three delegation options and no way to say "this is done" or "this cannot be done".
| Mechanism | Catches what | Cost | Failure mode when it misfires |
|---|---|---|---|
| Single-owner invariant | Two agents working the same task and overwriting each other's state | An ownership field plus a check on write; negligible runtime cost | Over-strict locking stalls legitimate parallel sub-tasks that should have been separate task IDs in the first place |
| Monotonic hop counter | Nothing on its own — it is the instrument that makes every other mechanism enforceable | One integer in the envelope | Reset by a well-meaning retry wrapper, which silently disables the hop limit |
| Hop limit with escalation | The infinite handoff loop, and slower variants that merely waste half the budget | One comparison at ingest, plus an escalation path you must actually build | Set too low, legitimate deep tasks escalate to humans and the queue fills with false alarms |
| Budget carried in the envelope | Runaway spend where each agent plans against the original allowance | Counters the producer must populate honestly; needs discipline | Under-reported consumption makes the limit meaningless while looking healthy on a dashboard |
| Reachable terminal state per agent | Agents that structurally cannot finish and therefore always delegate | A graph review, plus an explicit terminal option in each agent's action space | An over-eager terminal option makes agents give up early, trading a loop for a silent no-answer |
| Loop signature detection | A-to-B-to-A cycles that stay under the hop limit but repeat the same state | A hash of goal plus decision set per hop, compared against hop.path |
Legitimate iterative refinement is flagged as a loop because the signature is too coarse |
One nuance worth stating: handoffs are not symmetric, and a single contract shape applied uniformly is a mistake. A supervisor-to-worker handoff is a delegation — the supervisor retains accountability, the worker returns a result, and the envelope should carry tight acceptance criteria and a small budget slice. A peer-to-peer handoff is a transfer of accountability, and the envelope needs richer provenance and a fuller decision record because there is no one holding the thread. Use the role field on from_agent and to_agent to make the distinction explicit, and let your validation apply different required-field sets to each.
Most guides here carry a Verified Builder byline. Want your name on the next one?
AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.
Become a Verified Builder →Validate at the boundary, and fail loudly
A contract that is not enforced is documentation. The enforcement point is ingest: the moment the consumer receives an envelope and before it does any work. Three rules govern it.
Schema-validate first. Run the envelope against the schema and collect every error, not just the first. A consumer that reports one missing field, gets a repair, and then reports the next one turns a single round trip into four.
Reject rather than best-effort parse. This is the rule teams find hardest, because rejection feels brittle and improvisation feels robust. It is exactly backwards. A consumer that notices a missing constraint, assumes a reasonable default and proceeds has converted a loud, local, cheap failure into a quiet, distant, expensive one. The wrong answer surfaces three hops later with no trace of the assumption that caused it. Rejection at the boundary names the field and the hop, and it takes minutes to debug.
Make the rejection observable. Emit a structured rejection — handoff ID, hop index, producer, the list of violated fields — as a first-class event, not a log line. Route it back to the producer for one bounded repair attempt, and escalate on the second failure rather than looping. Repair attempts must not increment towards infinity, or you have replaced the handoff loop with a validation loop.
import json
from jsonschema import Draft202012Validator # any 2020-12 validator will do
with open("handoff-envelope.schema.json") as fh:
VALIDATOR = Draft202012Validator(json.load(fh))
SUPPORTED_MAJOR = 1
class HandoffRejected(Exception):
def __init__(self, handoff_id, hop_index, reasons):
self.handoff_id = handoff_id
self.hop_index = hop_index
self.reasons = reasons
super().__init__(
f"handoff {handoff_id} at hop {hop_index} rejected: "
+ "; ".join(reasons)
)
def accept(envelope, *, me, artefact_store):
"""Validate an inbound envelope. Return it, or raise. Never repair silently."""
reasons = [
f"{'/'.join(str(p) for p in err.path) or '<root>'}: {err.message}"
for err in VALIDATOR.iter_errors(envelope)
]
# Semantic checks the schema cannot express. Only meaningful once the
# structure is known-good, so gate them behind the schema pass.
if not reasons:
major = int(envelope["envelope_version"].split(".")[0])
if major != SUPPORTED_MAJOR:
reasons.append(
f"envelope_version: major {major} unsupported "
f"(this agent speaks {SUPPORTED_MAJOR}.x)"
)
if envelope["to_agent"]["id"] != me:
reasons.append("to_agent/id: envelope is addressed to another agent")
hop = envelope["hop"]
if hop["index"] >= hop["limit"]:
reasons.append(
f"hop/index: budget exhausted ({hop['index']}/{hop['limit']}) "
"— escalate, do not forward"
)
consumed = envelope["budget"]["consumed"]
limit = envelope["budget"]["limit"]
for counter, spent in consumed.items():
cap = limit.get(counter)
if cap is not None and spent >= cap:
reasons.append(f"budget/consumed/{counter}: at or over limit")
# The permission trap: can this agent actually read what it was handed?
for art in envelope.get("artefacts", []):
if art["role"] != "required":
continue
if not artefact_store.can_read(art["ref"], scope=art.get("read_scope")):
reasons.append(f"artefacts: cannot dereference required {art['ref']}")
blocking = [q["question"] for q in envelope.get("open_questions", [])
if q["blocking"]]
if blocking:
reasons.append("open_questions: blocking question(s) unresolved: "
+ "; ".join(blocking))
if reasons:
raise HandoffRejected(
envelope.get("handoff_id", "unknown"),
envelope.get("hop", {}).get("index", -1),
reasons,
)
return envelope
Two things that sketch does not do, on purpose. It does not fill in defaults, because a default supplied by the consumer is an assumption nobody agreed to. And it does not attempt a repair itself — repair belongs to the producer, which is the only party that knows why a field was omitted. The consumer's job at the boundary is a verdict, not a negotiation.
There is a related problem worth flagging: the producer has to emit a schema-valid envelope in the first place, and if the producer is a model rather than deterministic code, that is not free. Use constrained decoding or a structured-output mode for envelope generation rather than asking politely and hoping — the techniques are in our guide to reliable JSON from any LLM. Better still, have the agent emit only the semantic fields and let deterministic code assemble the envelope around them, so that hop, budget and provenance are never model-generated at all.
The "helpful consumer" anti-pattern. The envelope arrives without the residency constraint. The consumer notices, reasons that most tasks do not have one, proceeds, and copies an artefact to a convenient region. Nothing errors. The memo is good. Eight weeks later a compliance review asks why customer data left its jurisdiction, and the answer is buried in a model's chain of thought that nobody retained. Every silent default is a decision made by a component that lacked the authority to make it.
Measuring handoffs
You cannot improve a seam you are not measuring, and standard agent telemetry measures agents rather than the gaps between them. Five metrics cover it, and the last one is the most diagnostic.
Handoff latency. Measured from when agent A finishes to when agent B has successfully ingested the context — not to when B was invoked. One production guide suggests that exceeding roughly 30 seconds indicates either a bloated context window or agents struggling to parse the transfer instructions. Treat that as a single-source rule of thumb rather than a standard; the useful discipline is to set a threshold for your own system, watch the distribution, and investigate the tail.
Envelope size per hop. Plot it against hop index. A flat line means your pointer discipline is holding. An upward slope means something is being inlined, and it is usually either chat history that crept back in or an artefact summary that grew from one sentence to five paragraphs.
Hop count per task. A distribution, not an average. The mean will look fine while the ninety-fifth percentile quietly sits at the hop limit, which is your loop rate hiding in plain sight.
Ingest validation failure rate, by producer and by field. This tells you exactly which agent is producing malformed envelopes and exactly which field it keeps getting wrong. It is the single highest-value dashboard in a multi-agent system and it costs one counter with two labels.
Post-handoff replan rate. How often the consumer immediately redoes work the producer already did — re-running the same retrieval, re-deriving the same decision, re-reading the same artefact. This is the clearest signal that the contract is inadequate, because a consumer that trusted what it received would not have needed to. If replan rate is high while validation failure rate is low, your envelope is well-formed and semantically empty, which is the most common state for a first implementation.
| Metric | Where it lives in an OTEL trace | What a bad value usually means |
|---|---|---|
| Handoff latency | Gap between the producer's invoke_agent span ending and the consumer's beginning; record ingest completion as a span event |
Oversized envelope, slow artefact dereference, or a consumer struggling to parse the transfer |
| Envelope size per hop | A custom attribute on the consumer's invoke_agent span, recorded at ingest |
Payloads are being inlined somewhere; find the hop where the slope starts |
| Hop count per task | Depth of nested invoke_agent spans, or hop.index as an attribute on each |
A p95 pinned at the limit is a loop; a p50 above three suggests the graph is too granular |
| Ingest validation failure rate | Span status set to error on the consumer's ingest span, with the violated field as an attribute | One producer with a stale schema version, or a field nobody agreed on the meaning of |
| Post-handoff replan rate | Duplicate execute_tool spans across a producer-consumer pair within one task |
The envelope is well-formed but does not carry what the consumer actually needs to trust |
On the span shape itself: the OpenTelemetry GenAI semantic conventions give you a workable vocabulary, with a top-level invoke_agent span containing child chat spans per LLM call and execute_tool spans per tool invocation, all carrying gen_ai.* attributes. Be aware that as of the v1.41 conventions (mid-2026) most gen_ai.* attributes still carry Development stability badges, so attribute names can and do change between releases — pin the convention version you build against and expect a rename or two. The instrumentation mechanics, including sampling and cost attribution, are covered properly in our guide to instrumenting agents with OpenTelemetry; there is no need to repeat them here.
Replan rate in particular belongs alongside your trajectory evaluation rather than in a separate dashboard, because it is fundamentally a question about whether the agent's path was sensible given what it was handed. The framing is set out in the guide to evaluating AI agents on trajectory, tools and outcomes, and the natural extension is a handoff-level assertion: given this envelope, did the consumer's first three actions add information, or recover it?
Log the full envelope at every hop, keyed by task_id and hop.index, with artefact contents excluded. Because envelopes are bounded and structured, this is cheap to store and trivially diffable — and being able to run a diff between hop three and hop four is the fastest debugging tool you will have. Most handoff bugs are visible in seconds as a field that vanished or a confidence that inflated.
Common pitfalls
These recur often enough to be worth naming individually. Most of them are the default behaviour of a system nobody designed the seams for.
Passing raw chat history as "context". The most common and the most costly. History carries dead ends, abandoned hypotheses and self-corrections alongside conclusions, and the consumer has no reliable way to tell which is which. It also grows without bound. If the consumer needs the transcript, store it as an artefact and hand a reference, as in the filled example above.
Summarising with the same model that produced the error. If the producer misread a figure, its own summary will preserve the misreading with added confidence, because summarisation is not a verification step. Where a summary must be produced, generate it from structured decisions rather than from prose, or have a different component produce it against the artefacts.
Inlining artefacts. Covered above, and worth repeating because it creeps back in whenever someone adds a "just include the extracted table, it's small" special case. Small artefacts inlined at hop two are large artefacts inlined at hop six.
Unversioned envelopes. Without envelope_version, a producer upgraded on Tuesday and a consumer upgraded on Thursday will spend two days silently disagreeing about a field's meaning. Version it, and have consumers reject unknown majors.
No hop budget. The loop is not hypothetical and the mitigation is one integer and one comparison. There is no defensible reason to omit it.
Consumer-side improvisation. Every silent default is an unrecorded decision made by a component without the authority to make it. Reject instead.
Assuming shared tool permissions. The producer's read scope is not the consumer's. Check dereference capability at ingest, not at the moment of use, so the failure lands at the boundary where it is cheap to attribute.
Treating every handoff as symmetric. Supervisor-to-worker delegation and peer-to-peer transfer have different accountability semantics and need different required-field sets. One contract shape for both means one of them is over-specified and the other is under-specified.
"We spent a month tuning prompts because the second agent kept contradicting the first. Then we logged what was actually crossing between them and found it was a 9,000-token transcript with the decisions buried in the middle. We replaced it with about forty lines of structured decisions and artefact references, changed no prompts at all, and the contradictions stopped. The model was never the problem — we had simply never written down what it was supposed to receive."
— Anonymised composite, drawn from Verified Builder conversationsRetrofitting this onto a running system
None of the above requires a rewrite, and attempting one is the usual reason this work never happens. Three stages, each independently valuable, each shippable in days rather than quarters.
Stage one: observe the seam you already have. Change nothing about behaviour. Wrap each handoff so that whatever is currently passed gets logged with a task_id, a hop index and a byte count, and add a counter for hop depth per task. Within a week you will know your envelope size curve, your hop distribution and whether you have a loop problem. Most teams discover at this stage that their p95 hop count is far higher than anyone believed, and that a third of what crosses the boundary is transcript nobody reads. That finding alone usually justifies stage two.
Stage two: introduce the envelope alongside the existing payload. Populate the structured envelope and keep passing whatever you pass today, so that behaviour is unchanged and nothing can break. Validate the envelope at ingest but treat failures as warnings rather than rejections, and watch the validation failure rate by producer and field. This is where you discover that one agent has never once populated the rationale field and another emits confidence values of exactly 1.0 for everything. Fix those with the old payload still carrying the load, then move the artefacts into a store and switch the envelope's references to point at it.
Stage three: cut the old payload and turn validation into enforcement. Remove the legacy blob one producer-consumer pair at a time, starting with the pair that showed the lowest validation failure rate in stage two. Flip warnings to rejections for that pair, add the hop limit with a real escalation path, and only then move to the next pair. Retrofitting pair by pair means a regression is always attributable to one edge in the graph, which is the difference between a bad afternoon and a bad fortnight.
The end state is unglamorous and that is the point. A schema file in the repository, a validation function at each ingest, an artefact store per residency zone, five metrics on a dashboard, and a hop counter that no one is allowed to reset. There is no clever prompt in there and no framework dependency, which is why the design outlives both. The agents will be replaced; the seam will not.
It is also unusually legible proof of engineering judgement. Anyone can say they have built a multi-agent system. A versioned handoff schema with provenance semantics, a documented hop budget, and a dashboard showing replan rate falling quarter on quarter is a claim that verifies itself — and it is exactly the kind of work that is hard to see from a CV and obvious from a repository. If you have built one, put it somewhere the people hiring for agent and platform roles across Bengaluru, Chennai, London and Manchester can actually find it.