The triage agent says it found the problem and passes the case to a specialist. The second agent receives one short sentence, opens the same tools, and asks again for the identifier the first agent already confirmed.
That is the handoff problem. The failure is not only in the model that answered. It is at the boundary between two executions: what was actually completed, which evidence supports it, what the next agent may do, and how it should react when the packet is incomplete.
A useful handoff is a small contract, not a copy of the conversation. It should carry the next objective, confirmed state, evidence references, bounded authority, acceptance checks, and an explicit way to fail. The receiving agent validates that contract before continuing.
This article presents a design model and an illustrative TypeScript schema. The code was not run against a provider or a multi-agent runtime. The research combines the current OpenAI Agents SDK handoff guide, the LangChain handoff documentation, and recent public discussions. For the broader architecture, see multi-agent orchestration for coding agents with TypeScript.
The short answer
- Use a handoff when another agent should own the next step, instead of replying as a tool of the current agent.
- Pass confirmed state and verifiable evidence references, not the full transcript as the source of truth.
- Limit the receiver's tools and authority. The second agent should not inherit every permission from the first.
- Reject missing, stale, or inconsistent packets before starting an external action.
When should an agent hand over control?
A handoff fits when a specialist should take over the conversation or the next stage. The OpenAI Agents SDK, "Handoffs", retrieved September 25, 2026, separates this case from the pattern where an agent calls another agent as a tool and remains responsible for the answer.
That choice changes who owns the next decision. In a manager pattern, the main agent calls a specialist, receives a result, and keeps control. In a handoff, triage stops owning the next execution. The receiver interprets the context, calls its own tools, and closes the stage.
Do not use a handoff just because two prompts exist. If the task is short, uses the same tools, and needs one central synthesis, a tool-using agent may be easier to operate. Add the boundary when responsibility, available data, permissions, or acceptance criteria genuinely change.
Before transferring control, answer four questions:
- What must the next agent produce?
- Which state is confirmed, and what evidence confirms it?
- Which tools and effects are outside its authority?
- What happens if it cannot continue?
If those answers still exist only in the triage prompt, the handoff is being treated as a message. Write the contract before choosing the framework.
What belongs in an agent handoff contract?
The minimum contract I use as an editorial model has six blocks: objective, state, evidence, authority, acceptance, and recovery. These names are not a universal standard. They are a compact way to expose decisions the receiver would otherwise have to guess.
The objective says which stage starts now. State records what already happened, beyond the original request. Evidence points to artifacts, results, or identifiers that can be checked. Authority lists what the receiver may do and what remains forbidden. Acceptance says when the stage is ready. Recovery gives rejection, blocked, or stale state a destination.
A schema can make that boundary concrete:
import { z } from "zod";
const HandoffContract = z.object({
version: z.literal(1),
taskId: z.string().min(1),
nextObjective: z.string().min(1),
confirmedState: z.enum(["ready", "blocked", "needs-review"]),
evidence: z.array(z.object({
kind: z.enum(["artifact", "check", "decision"]),
reference: z.string().min(1),
})).min(1),
authority: z.object({
allowedTools: z.array(z.string()),
forbiddenActions: z.array(z.string()),
}),
acceptance: z.array(z.string().min(1)).min(1),
recovery: z.object({
onReject: z.enum(["repair", "escalate", "stop"]),
resumeFrom: z.string().nullable(),
}),
});
type HandoffContract = z.infer<typeof HandoffContract>;
taskId stops the receiver from treating a packet from another run as the current one. version lets you change the contract without reading an old shape as a new one. confirmedState separates ready from blocked. The evidence list does not need huge logs. It can point to a versioned artifact, a test, a persisted decision, or a redacted result.
The schema above is illustrative and was not executed. In a real system, validation should also check that each reference belongs to the task, that the version is still valid, and that the requested authority does not exceed the receiver's policy.
How do you pass context without copying the full conversation?
Start with the contract, then filter the history. The OpenAI Agents SDK, "Handoffs" says that the receiver gets the full conversation history by default and that inputFilter can change what crosses the boundary. The same documentation separates inputType, which describes handoff arguments, from RunContext, which carries existing state and dependencies.
That distinction prevents a common mistake. A model-generated summary can explain why triage chose a specialist. It does not prove that a tool ran, that a file is at a particular version, or that an approval is still valid. The contract should point to verifiable state. History can explain reasoning, but it should not be the only source of truth.
For subgraph handoffs, the LangChain, "Handoffs", retrieved September 25, 2026, calls attention to message-sequence validity. A tool call and its response need to remain paired when that history moves to the next agent. Context filtering must preserve the units the runtime expects instead of cutting messages in the middle.
A filter policy can follow this order:
- Remove credentials, internal prompts, and data the receiver does not need.
- Remove old tool calls when they do not belong to the current task.
- Preserve the messages required by the provider's protocol.
- Add the validated contract as an object separate from the narrative text.
- Record which filter was applied so the transfer can be audited.
Do not turn inputType into a state bucket. The SDK documentation says this field is for metadata the model decides at handoff time, such as a reason, language, or priority. Application state, dependencies, and permissions should come from a runtime-controlled source.
How does the receiving agent validate the packet?
The receiver should validate before choosing its first tool. The validation should treat missing fields, version, task identity, state, authority, and evidence as input conditions. If one fails, the result should be an explainable rejection, not a guess about what triage meant.
A validation function can return a discriminated result:
type HandoffResult =
| { ok: true; contract: HandoffContract }
| { ok: false; reason: "invalid" | "stale" | "unauthorized" };
function acceptHandoff(
value: unknown,
expectedTaskId: string,
allowedTools: ReadonlySet<string>,
): HandoffResult {
const parsed = HandoffContract.safeParse(value);
if (!parsed.success || parsed.data.taskId !== expectedTaskId) {
return { ok: false, reason: "invalid" };
}
const hasUnknownTool = parsed.data.authority.allowedTools.some(
(tool) => !allowedTools.has(tool),
);
if (hasUnknownTool) {
return { ok: false, reason: "unauthorized" };
}
return { ok: true, contract: parsed.data };
}
This example performs only three checks. It validates the shape, checks the task, and prevents the packet from requesting a tool outside the receiver's allowlist. A runtime can also compare the state version, require references to exist, and reject confirmedState: "ready" when the recorded evidence is incomplete.
Authorization cannot depend on handoff prose. If the triage agent writes "you may publish" inside nextObjective, that remains an intention. Receiver policy must read structured authority and apply its own limits before calling a tool. The OpenAI Agents SDK, "Handoffs" also warns that isEnabled does not authorize values inside model-generated arguments. When authorization depends on received fields, check them at the start of onHandoff, before side effects.
What happens when a handoff is rejected?
A rejection needs to be an operational state. invalid means the structure fails the schema. stale means the task changed after the packet was created. unauthorized means the transfer requests a capability the receiver cannot use. Each case should have a different next step.
For invalid, return the error to triage with the missing or inconsistent fields. For stale, reload current state and create a new packet. For unauthorized, stop and escalate or reduce authority. Do not retry blindly. Repeating the same transfer only creates another chance to hide the cause.
If the handoff crosses a process, queue, or human approval, persist the contract and its version. Durable execution for AI agents covers state that must survive failures. A handoff inside one run can use runtime memory, but it should still have an observable rejection.
Also separate handoff from compaction. AI agent context compaction reduces the history one agent sends to a model. A handoff changes who owns a stage. Both can use filters and summaries, but they solve different boundaries.
How do you test an agent handoff boundary?
Test the contract as an interface, including the successful path and workflow rejections. The guide to testing an AI agent's trajectory covers tools, arguments, order, state, and stopping decisions. A handoff adds a smaller unit: the packet that must reach the next agent.
An initial matrix can include these cases:
- a valid packet with every reference available;
- a missing required field;
- a
taskIdfrom another run; - a state version older than the current one;
- a requested tool outside the receiver's authority;
- evidence pointing to a removed or replaced artifact;
- a repeated transfer after confirmation;
- a rejection that produces repair, escalation, or stop according to policy.
For each case, check more than the final answer. Confirm which agent received control, which tools were exposed, which state was persisted, and whether rejection prevented external effects. If the test only inspects the final sentence, it cannot tell whether the receiver received a permission it should not have.
A trace also helps debug the boundary, but it does not replace the contract. Record the task identifier, packet version, source agent, target agent, validation result, and rejection reason. Avoid recording the full history by default.
What are the limits of this pattern?
A handoff does not make a group of agents reliable by itself. It makes a change of responsibility explicit. You still need persisted state, tool policy, cost limits, timeouts, observability, and a way to repair old data.
There is also a coordination cost. A tool-using agent may solve a short task with fewer contracts, failure points, and duplicated context. The OpenAI Agents SDK orchestration guide, retrieved September 25, 2026, presents handoffs and agents as tools as complementary choices, not an automatic maturity ladder.
Names and formats vary across SDKs. inputType and inputFilter are OpenAI Agents SDK mechanisms. The durable idea is the boundary: a versioned packet that can be validated, has limited authority, and names its recovery path. If the provider changes, you can replace the adapter without hiding responsibility in the prompt.
Conclusion
A reliable handoff lets the second agent start without repeating the investigation or inheriting permissions by accident. Treat the transfer as an interface: write the next objective, confirmed state, evidence, authority, acceptance, and recovery.
Validate the packet at the receiver. Filter history with respect for the runtime protocol. Persist the transfer when it crosses processes. Test stale state, forbidden tools, missing evidence, and repetition. The model may choose when to request a handoff, but code must decide whether the transfer is valid.
How this analysis was done
Samuel Fajreldines is the accountable author of this article. The research compared current OpenAI Agents SDK and LangChain documentation, the existing orchestration, context, durable-execution, and trajectory owners, and recent public discussions. The six-part contract, schema, and rejection matrix are original editorial synthesis. The code is illustrative and was not run against a provider or multi-agent runtime. AI assistance supported discovery, drafting, image generation, localization, and consistency review. It did not provide production experience or replace source verification. The author also maintains RemoteCode as a work tool.