> ## Documentation Index
> Fetch the complete documentation index at: https://lousho.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handoffs

> A handoff passes the whole conversation to another agent. A triage agent reads the user's request and hands it to a specialist; the specialist answers the user directly, and in a session it keeps the conversation in later turns.

## Handoffs or sub-agents?

| Use | What happens |
| - | - |
| **Handoffs** | The target takes over. It sees the conversation (or what an `inputFilter` keeps), answers the user itself, and stays the active agent of the session until it hands on or back. The agent that handed off does not see the answer. |
| **[Sub-agents](/sub-agents)** | The lead stays in charge. A sub-agent sees only the task prompt the lead wrote, does the work, and returns a result to the lead, which then answers the user. |

## Quick start

Give each target a `name` and a `description` (the model reads the
description to decide when to hand off), then pass them to the triage agent as
`handoffs`:

```ts theme={null}
import { createAgent, defineTool } from '@lousho/build-ai-agent';
import { z } from 'zod';

const lookupCharges = defineTool({
  name: 'lookup_charges',
  description: "Lists the user's recent charges",
  input: z.object({}),
  execute: async () => [{ date: '2026-09-01', amountUsd: 9.99 }],
});

const billing = createAgent({
  name: 'billing',
  description: 'Handles charges, refunds and subscriptions',
  instructions: 'You are the billing desk. Answer in one or two sentences.',
  model: 'openai/gpt-4o-mini',
  tools: [lookupCharges],
});

const techSupport = createAgent({
  name: 'tech-support',
  description: 'Handles logins, passwords and technical problems',
  instructions: 'You are tech support. Answer in one or two sentences.',
  model: 'openai/gpt-4o-mini',
});

const triage = createAgent({
  name: 'triage',
  instructions: 'Route the user: billing questions to billing, technical questions to tech-support.',
  model: 'openai/gpt-4o-mini',
  handoffs: [billing, techSupport],
});

const result = await triage.send('I was charged twice for my subscription.');
console.log(result.agentName, result.text); // 'billing', the billing desk's answer
```

Each target is offered to the model as one tool, `transfer_to_<name>`
(`transfer_to_billing`, `transfer_to_tech-support`). When the model calls it,
the run goes on as the target in the same `send()` / `stream()` call.
`result.agentName` names the agent that produced the final reply.

A target must come from `createAgent()` and have a `name` (unique among the
handoffs) and a `description` (or a `description` on `handoff()`); otherwise
`createAgent()` throws `LOUSHO_CONFIG_INVALID`. A handoff tool may not share a
name with one of the agent's tools.

## `handoff()` options

Wrap a target in `handoff()` to configure it:

```ts theme={null}
import { createAgent, handoff, handoffFilters } from '@lousho/build-ai-agent';
import { z } from 'zod';

const refunds = createAgent({ name: 'refunds', description: 'Issues refunds', model: 'openai/gpt-4o-mini' });

const triage = createAgent({
  name: 'triage',
  model: 'openai/gpt-4o-mini',
  handoffs: [
    handoff(refunds, {
      toolName: 'escalate_refund',
      description: 'Hand off when the user asks for money back',
      input: z.object({ orderId: z.string() }),
      inputFilter: handoffFilters.removeToolCalls,
      onHandoff: ({ from, to, args }) => console.log(`${from} -> ${to}`, args),
      isEnabled: ({ metadata }) => metadata?.plan === 'pro',
    }),
  ],
});
```

| Option | Default | What it does |
| - | - | - |
| `toolName` | `transfer_to_<name>` | The tool the model calls. |
| `description` | the target's `description` | The tool's description. |
| `input` | `{ reason?: string }` | The arguments the model passes (a zod schema or any Standard Schema). Arguments that do not match give the model a tool error, and no handoff happens. |
| `inputFilter` | everything | What the target sees (see [Input filters](#input-filters)). |
| `onHandoff` | none | Called with `{ messages, from, to, args, sessionId }` once the handoff is decided, before the target's first model call. |
| `isEnabled` | `true` | Whether the handoff is offered; a function gets the run's `{ input, metadata, principal, sessionId }`. A disabled handoff's tool is not sent to the model. |

## Input filters

The target sees the conversation as `inputFilter` returns it, under its own
system prompt (the system prompt of the agent that handed off is replaced,
never kept). The filter gets `{ messages, from, to, args }`; `messages` is the
transcript so far without the system prompt, ending with the handoff call and
its result. Two filters are built in:

* `handoffFilters.removeToolCalls`: keeps user and assistant text, drops tool
  calls and tool results.
* `handoffFilters.lastUserMessage`: keeps only the last user message.

```ts theme={null}
import { handoffFilters, type HandoffInputData } from '@lousho/build-ai-agent';

// The last four messages, without tool traffic.
const recent = (data: HandoffInputData) => handoffFilters.removeToolCalls(data).slice(-4);
```

What the filter returns becomes the run's transcript: `result.messages` and a
session's history continue from it. A filter must keep at least one message.
Approvals remembered with `once()` in the earlier part of the transcript are
dropped at a handoff, so a target's tools ask again.

## What switches and what stays

| At a handoff, the target's own | The run's, for its whole length |
| - | - |
| instructions (system prompt), model and provider | `hooks`, `limits` and the spend so far (one budget) |
| tools, hosted tools, skills and sub-agents | `maxSteps`, counted across agents |
| `reasoning` | the abort signal |
| its own `handoffs` | the approval, checkpoint and token stores |
| guardrails and permission rules: the lead's first, then the target's own | `onEvent` and `exporter` |
| | `output`: the result is typed by the lead's schema (a target's own `output` is not used) |
| | the permission mode and the principal (who the run acts for) |

The agent the run started with is the lead. A target gets none of the lead's
tools, sub-agents or [memory](/memory) slots; its own memory slots are not
used either (as for a sub-agent). The lead's `output` instruction is added to
every target's system prompt.

## Sessions

In a session the active agent is the one the transcript last handed off to
(the `to` of the last `metadata.handoff` marker), so the next
`session.send()` / `session.stream()` runs that agent:

```ts theme={null}
import { createAgent } from '@lousho/build-ai-agent';

const billing = createAgent({ name: 'billing', description: 'Handles charges and refunds', model: 'openai/gpt-4o-mini' });
const triage = createAgent({ name: 'triage', model: 'openai/gpt-4o-mini', handoffs: [billing] });

const session = triage.session();
await session.send('I was charged twice.'); // triage hands off; billing answers
const next = await session.send('Can you refund one?'); // billing answers again
console.log(next.agentName); // 'billing'
```

`agent.send()` without a session always starts at the lead.

A target hands back with its own `handoffs`. Since the lead is created after
its targets, give the target an array and add the lead to it afterwards (the
array is read at every run):

```ts theme={null}
import { createAgent, handoff, type Handoff } from '@lousho/build-ai-agent';

const billingHandoffs: Handoff[] = [];
const billing = createAgent({
  name: 'billing',
  description: 'Handles charges and refunds',
  instructions: 'For anything that is not about billing, hand back to triage.',
  model: 'openai/gpt-4o-mini',
  handoffs: billingHandoffs,
});
const triage = createAgent({
  name: 'triage',
  description: 'Routes the user to the right desk',
  model: 'openai/gpt-4o-mini',
  handoffs: [billing],
});
billingHandoffs.push(handoff(triage));
```

Names identify agents: every agent reachable through `handoffs` must have its
own name. If the agent a transcript names is no longer reachable, the lead runs.

## Approvals and resuming after a handoff

A target's tool that needs approval pauses the run in the lead's approval
store; `agent.approvals.resolve()` on the lead continues it as the target. A
checkpointed run (`send(message, { sessionId })` or a session with a store)
that crashed after a handoff resumes as the target with `agent.resume(id)` or
`session.resume()`. The drift check on resume (`onAgentDrift`, see
[Durable execution](/durable-execution#resuming-with-a-changed-agent))
compares with the target's saved fingerprint.

When a step has a handoff call next to a call that pauses for approval, the
handoff waits: the run pauses as the agent that handed off, and the handoff
happens once the approval is decided and the step's other calls ran.

## Events

A handoff is reported between the handoff call's `tool.start` / `tool.done`
and the target's first `step.start`:

```ts theme={null}
import { createAgent } from '@lousho/build-ai-agent';

const billing = createAgent({ name: 'billing', description: 'Handles charges and refunds', model: 'openai/gpt-4o-mini' });
const triage = createAgent({ name: 'triage', model: 'openai/gpt-4o-mini', handoffs: [billing] });

for await (const event of triage.stream('I was charged twice.')) {
  if (event.type === 'handoff') console.log(`${event.from} -> ${event.to}`); // triage -> billing
}
```

`handoff` carries `from`, `to` and `toolCallId`. A run has one `run.start`
and one `run.done` however many handoffs it makes. See the
[event schema](/stream-events).

## Limits

* Only one handoff per model turn: when a turn calls several handoff tools,
  the first is honored and the others get a tool error.
* `maxHandoffs` (a `createAgent()` option, default 5) caps the handoffs of one
  run, to stop agents handing a conversation back and forth. A handoff call
  over it gets a tool error and the agent answers itself. A run resumed after
  an approval or a crash counts again from zero.
* Remote agents (`remoteAgent()`) cannot be handoff targets; use them as
  sub-agents.
* A target does not hand back on its own at the end of its turn; give it a
  handoff to the lead (see [Sessions](#sessions)).

## The executor API

`AgentExecutor.execute()` takes `handoffs` (`ResolvedHandoff[]`: `name`,
`toolName`, `description`, `input`, `spec(input)` resolving the target's run
configuration and its own handoffs, `inputFilter`, `onHandoff`) and
`maxHandoffs`. `createAgent()` builds them; pass them yourself only when you
drive the executor directly.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.