Agent SDK

Handoffs

Transfer control between specialized agents

Handoffs

A handoff transfers control from one agent to another. Unlike using an agent as a tool (where the parent waits for a result), a handoff completely replaces the current agent — the new agent takes over the conversation.

Basic Handoff

import { Agent } from 'assistme-agent-sdk'

const techSupport = new Agent({
  name: 'tech_support',
  model: claude('claude-sonnet-4-6'),
  instructions: 'You are a technical support specialist. Help users solve technical problems.',
  tools: [searchDocs, createTicket],
})

const billing = new Agent({
  name: 'billing',
  model: claude('claude-sonnet-4-6'),
  instructions: 'You are a billing specialist. Help users with payment and subscription issues.',
  tools: [lookupAccount, processRefund],
})

const triageAgent = new Agent({
  name: 'triage',
  model: claude('claude-haiku-4-5'),
  instructions: `You are a triage agent. Determine what the user needs and hand off to the right specialist:
- Technical questions → tech_support
- Billing questions → billing
- General questions → answer directly`,
  handoffs: [techSupport, billing],
})

When triageAgent decides to hand off, it uses a special tool call that the Runner interprets as a control transfer:

const result = await Runner.run(triageAgent, {
  messages: [{ role: 'user', content: 'I need to update my credit card' }],
})

// result.status === 'handoff'
// result.handoffTarget === billing
// The billing agent now has the conversation

How Handoffs Work

User: "I need to update my credit card"


┌────────────────┐
│  Triage Agent  │  Classifies request → billing
│  (Haiku)       │
└───────┬────────┘
        │ handoff

┌────────────────┐
│ Billing Agent  │  Takes over the conversation
│ (Sonnet)       │  Has full conversation history
└────────────────┘

Key behaviors:

  1. The new agent receives the full conversation history
  2. The old agent stops executing
  3. The stream seamlessly transitions to the new agent
  4. The handoff reason is logged for observability

Handoff with Context

Pass additional context during a handoff:

const triageAgent = new Agent({
  name: 'triage',
  model: claude('claude-haiku-4-5'),
  instructions: `Classify the user's issue and hand off with context.`,
  handoffs: [
    {
      agent: techSupport,
      description: 'Hand off technical issues to tech support',
      contextProvider: async (messages) => {
        // Enrich the handoff with additional context
        const userAccount = await lookupUser(messages)
        return {
          additionalInstructions: `User account: ${userAccount.id}, Plan: ${userAccount.plan}`,
        }
      },
    },
  ],
})

Circular Handoffs

Agents can hand off back and forth. This is useful for workflows where the user's needs evolve:

const salesAgent = new Agent({
  name: 'sales',
  instructions: 'Help users choose the right plan. Hand off to billing for payment.',
  handoffs: [billingAgent],
})

const billingAgent = new Agent({
  name: 'billing',
  instructions: 'Process payments. Hand off to sales if user wants to change plans.',
  handoffs: [salesAgent],
})

Caution: Set maxTurns to prevent infinite handoff loops.

Handoffs vs. Agent-as-Tool

HandoffAgent-as-Tool
ControlTransfers completelyParent retains control
ContextNew agent gets full conversationSub-agent gets isolated context
ReturnNew agent responds to userResult returns to parent agent
Token efficiencyGood (no supervisor overhead)Better (sub-agent context is smaller)
Use caseRouting to specialistsDelegating subtasks
// Handoff: billing agent takes over
const triage = new Agent({
  handoffs: [billingAgent], // Control transfers to billing
})

// Agent-as-Tool: supervisor uses billing as a tool
const supervisor = new Agent({
  tools: [
    billingAgent.asTool({ name: 'check_billing' }), // Supervisor stays in control
  ],
})

Streaming Through Handoffs

Streams seamlessly continue across handoffs:

const stream = Runner.stream(triageAgent, { messages })

for await (const event of stream) {
  if (event.type === 'handoff') {
    console.log(`Transferred from ${event.from} to ${event.to}`)
    // Stream continues with the new agent
  }
  if (event.type === 'text_delta') {
    // Works across agent boundaries
    process.stdout.write(event.delta)
  }
}

Multi-Level Handoffs

Chain handoffs for complex routing:

const frontDesk = new Agent({
  name: 'front_desk',
  handoffs: [salesTeam, supportTeam],
})

const salesTeam = new Agent({
  name: 'sales_team',
  handoffs: [enterpriseSales, smbSales],
})

const enterpriseSales = new Agent({
  name: 'enterprise_sales',
  // No handoffs — this is a leaf agent
  tools: [crm, scheduler, contractGenerator],
})

Best Practices

  1. Use cheap models for triage — Routing decisions don't need powerful models. Haiku-class models are fast and accurate for classification.

  2. Provide clear handoff descriptions — The triage agent decides based on the description field. Make them specific and unambiguous.

  3. Pass context on handoff — Don't make the specialist re-discover information the triage agent already found.

  4. Avoid deep handoff chains — Each handoff adds latency. If you need 4+ handoffs, consider a supervisor pattern instead.

  5. Log handoff decisions — Track which agents get handed off to and why. This reveals routing accuracy and optimization opportunities.