Agent Handoffs

Delegate queries to specialist agents for domain expertise.

Table of contents

  1. Overview
  2. Quick Start
  3. Handoff Configuration
    1. Basic Handoff
    2. With Context Preservation
    3. Fresh Start (No Context)
    4. Transfer System Message
  4. Handoff Options
  5. Multiple Handoffs
  6. How Handoffs Work
    1. 1. Tools are Generated
    2. 2. LLM Decides
    3. 3. Handoff Executes
    4. 4. Response Returns
  7. Specialist Agent Patterns
    1. Domain Expert
    2. Tool Specialist
    3. Customer Support Triage
  8. Handling Handoff Results
    1. Check for Handoff Status
    2. With Streaming Events
  9. Context Preservation Examples
    1. Full Context (Default)
    2. Fresh Context
    3. With System Message
  10. Handoffs vs Orchestration
  11. Best Practices
    1. 1. Clear Transfer Reasons
    2. 2. Focused Specialists
    3. 3. Appropriate Context Decisions
    4. 4. Don’t Overuse Handoffs
  12. Examples
  13. Next Steps

Overview

Handoffs enable LLM-driven agent-to-agent delegation. When a primary agent determines that a query requires specialist expertise, it can hand off the conversation to another agent.

Key Benefits:

  • Specialization - Route queries to domain experts
  • Modularity - Build focused, maintainable agents
  • Scalability - Add specialists without modifying the main agent
  • Context Control - Choose what context to preserve

Quick Start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.llm4s.agent.{Agent, Handoff}
import org.llm4s.config.Llm4sConfig
import org.llm4s.llmconnect.LLMConnect
import org.llm4s.llmconnect.model.SystemMessage

val result = for {
  providerConfig <- Llm4sConfig.provider()
  client <- LLMConnect.getClient(providerConfig)

  // Create specialist agent
  mathAgent = new Agent(
    client = client,
    systemMessage = Some(SystemMessage(
      "You are a math expert. Solve problems step-by-step."
    ))
  )

  // Create main agent with handoffs
  mainAgent = new Agent(client)

  // Run with handoff capability
  state <- mainAgent.run(
    query = "What is the integral of x^2?",
    tools = ToolRegistry.empty,
    handoffs = Seq(
      Handoff.to(mathAgent, "Math expertise required")
    )
  )
} yield state

Handoff Configuration

Basic Handoff

1
2
3
4
5
6
import org.llm4s.agent.Handoff

val handoff = Handoff.to(
  targetAgent = specialistAgent,
  transferReason = "Domain expertise required"
)

With Context Preservation

1
2
3
4
5
6
7
// Preserve full conversation history
val handoff = Handoff(
  targetAgent = specialistAgent,
  transferReason = Some("Specialist needed"),
  preserveContext = true,        // Keep conversation history
  transferSystemMessage = false  // Use target's system message
)

Fresh Start (No Context)

1
2
3
4
5
6
7
// Start fresh with specialist
val handoff = Handoff(
  targetAgent = specialistAgent,
  transferReason = Some("Fresh analysis needed"),
  preserveContext = false,        // Don't transfer history
  transferSystemMessage = false   // Use target's system message
)

Transfer System Message

1
2
3
4
5
6
7
// Transfer original instructions to specialist
val handoff = Handoff(
  targetAgent = specialistAgent,
  transferReason = Some("Continue with same instructions"),
  preserveContext = true,
  transferSystemMessage = true  // Keep original system message
)

Handoff Options

Option Default Description
targetAgent Required The agent to hand off to
transferReason None Description shown to LLM for routing
preserveContext true Transfer conversation history
transferSystemMessage false Transfer original system message

Multiple Handoffs

Provide multiple specialists for intelligent routing:

1
2
3
4
5
6
7
8
9
10
val result = mainAgent.run(
  query = userQuery,
  tools = tools,
  handoffs = Seq(
    Handoff.to(mathAgent, "Mathematical calculations and proofs"),
    Handoff.to(codeAgent, "Programming and code review"),
    Handoff.to(legalAgent, "Legal questions and contracts"),
    Handoff.to(medicalAgent, "Health and medical information")
  )
)

The LLM sees descriptions and chooses the appropriate specialist.


How Handoffs Work

1. Tools are Generated

When handoffs are provided, the agent generates handoff tools:

1
2
3
4
5
6
// Internal tool generated for each handoff
ToolFunction(
  name = "handoff_to_math_expert",
  description = "Transfer to specialist: Mathematical calculations and proofs",
  function = () => RequestHandoff(targetAgent, transferReason, ...)
)

2. LLM Decides

The LLM can choose to:

  • Answer directly (no handoff)
  • Call a regular tool
  • Request a handoff by calling the handoff tool

3. Handoff Executes

When handoff is requested:

  1. Agent status changes to HandoffRequested
  2. Context is prepared based on settings
  3. Target agent receives the query
  4. Target agent processes and returns response

4. Response Returns

The target agent’s response becomes part of the conversation.


Specialist Agent Patterns

Domain Expert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val physicsAgent = new Agent(
  client = client,
  systemMessage = Some(SystemMessage(
    """You are a physics expert with deep knowledge of:
      |- Classical mechanics
      |- Quantum mechanics
      |- Thermodynamics
      |- Electromagnetism
      |
      |Provide detailed explanations with equations when helpful.""".stripMargin
  ))
)

mainAgent.run(
  query = "Explain quantum entanglement",
  tools = tools,
  handoffs = Seq(
    Handoff.to(physicsAgent, "Physics questions requiring expert explanation")
  )
)

Tool Specialist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Agent with specialized tools
val dataAgent = new Agent(
  client = client,
  systemMessage = Some(SystemMessage("You are a data analysis specialist."))
)

val dataTools = new ToolRegistry(Seq(
  queryDatabaseTool,
  generateChartTool,
  exportCSVTool
))

// Main agent can hand off data queries
mainAgent.run(
  query = "Analyze our Q4 sales data",
  tools = basicTools,
  handoffs = Seq(
    Handoff(
      targetAgent = dataAgent,
      transferReason = Some("Data analysis with database access")
    )
  )
)

Customer Support Triage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Specialist agents
val billingAgent = new Agent(
  client = client,
  systemMessage = Some(SystemMessage("You handle billing and payment issues."))
)

val technicalAgent = new Agent(
  client = client,
  systemMessage = Some(SystemMessage("You solve technical problems."))
)

val salesAgent = new Agent(
  client = client,
  systemMessage = Some(SystemMessage("You help with purchases and upgrades."))
)

// Triage agent
val triageAgent = new Agent(
  client = client,
  systemMessage = Some(SystemMessage(
    "You are a customer support triage agent. Route queries to the appropriate specialist."
  ))
)

triageAgent.run(
  query = "I can't log into my account and my payment failed",
  tools = ToolRegistry.empty,
  handoffs = Seq(
    Handoff.to(billingAgent, "Billing, payments, and subscription issues"),
    Handoff.to(technicalAgent, "Technical problems and account access"),
    Handoff.to(salesAgent, "Purchases, upgrades, and pricing questions")
  )
)

Handling Handoff Results

Check for Handoff Status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val result = mainAgent.run(query, tools, handoffs)

result match {
  case Right(state) if state.status == AgentStatus.Complete =>
    println(s"Completed: ${state.lastAssistantMessage}")

  case Right(state) if state.status == AgentStatus.HandoffRequested =>
    // Handoff was requested but you're handling manually
    val handoffInfo = state.requestedHandoff
    println(s"Handoff to: ${handoffInfo.targetAgentName}")

  case Left(error) =>
    println(s"Error: $error")
}

With Streaming Events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.llm4s.agent.streaming._

mainAgent.runWithEvents(query, tools, handoffs) {
  case HandoffStarted(targetName, reason, preserveContext, _) =>
    println(s"Handing off to $targetName: $reason")

  case HandoffCompleted(targetName, success, _) =>
    println(s"Handoff to $targetName: ${if (success) "success" else "failed"}")

  case AgentCompleted(state, _, _, _) =>
    println(s"Final: ${state.lastAssistantMessage}")

  case _ => ()
}

Context Preservation Examples

Full Context (Default)

1
2
3
4
5
6
7
8
9
10
11
12
// Specialist sees entire conversation
val handoff = Handoff(
  targetAgent = specialist,
  preserveContext = true,
  transferSystemMessage = false
)

// User: "I'm building a Scala app"
// Assistant: "Great! What kind of app?"
// User: "A REST API with database access"
// <handoff to database specialist>
// Specialist sees all messages above

Fresh Context

1
2
3
4
5
6
7
8
9
10
11
12
// Specialist starts fresh
val handoff = Handoff(
  targetAgent = specialist,
  preserveContext = false,
  transferSystemMessage = false
)

// User: "I'm building a Scala app"
// Assistant: "Great! What kind of app?"
// User: "A REST API with database access"
// <handoff to database specialist>
// Specialist only sees: "A REST API with database access"

With System Message

1
2
3
4
5
6
7
8
// Specialist inherits original instructions
val handoff = Handoff(
  targetAgent = specialist,
  preserveContext = true,
  transferSystemMessage = true
)

// Original agent's system message is prepended to specialist's

Handoffs vs Orchestration

Use Case Approach
2-3 specialists, LLM-driven routing Handoffs
Complex multi-agent workflows Orchestration (DAGs)
Dynamic specialist selection Handoffs
Parallel agent execution Orchestration
Simple delegation Handoffs
Type-safe data flow Orchestration

For complex workflows, see Orchestration documentation.


Best Practices

1. Clear Transfer Reasons

1
2
3
4
5
// Good - specific and actionable
Handoff.to(agent, "Database queries and SQL optimization")

// Bad - vague
Handoff.to(agent, "Technical stuff")

2. Focused Specialists

1
2
3
4
5
6
7
8
9
10
11
12
13
// Good - focused specialist
val sqlAgent = new Agent(
  systemMessage = Some(SystemMessage(
    "You are a SQL expert. Optimize queries and explain execution plans."
  ))
)

// Bad - too broad
val everythingAgent = new Agent(
  systemMessage = Some(SystemMessage(
    "You know everything about databases, APIs, UI, and infrastructure."
  ))
)

3. Appropriate Context Decisions

1
2
3
4
5
// Preserve context when history matters
Handoff(targetAgent = followUpAgent, preserveContext = true)

// Fresh start for independent analysis
Handoff(targetAgent = reviewAgent, preserveContext = false)

4. Don’t Overuse Handoffs

1
2
3
4
5
6
7
8
9
10
11
12
13
// Good - meaningful specialization
handoffs = Seq(
  Handoff.to(mathAgent, "Complex mathematical calculations"),
  Handoff.to(legalAgent, "Legal analysis and compliance")
)

// Bad - too granular
handoffs = Seq(
  Handoff.to(additionAgent, "Adding numbers"),
  Handoff.to(subtractionAgent, "Subtracting numbers"),
  Handoff.to(multiplicationAgent, "Multiplying numbers"),
  // ...
)

Examples

Example Description
SimpleTriageHandoffExample Basic query routing
MathSpecialistHandoffExample Math specialist delegation
ContextPreservationExample Context preservation patterns

Browse all examples →


Next Steps