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
importorg.llm4s.agent.{Agent,Handoff}importorg.llm4s.config.Llm4sConfigimportorg.llm4s.llmconnect.LLMConnectimportorg.llm4s.llmconnect.model.SystemMessagevalresult=for{providerConfig<-Llm4sConfig.provider()client<-LLMConnect.getClient(providerConfig)// Create specialist agentmathAgent=newAgent(client=client,systemMessage=Some(SystemMessage("You are a math expert. Solve problems step-by-step.")))// Create main agent with handoffsmainAgent=newAgent(client)// Run with handoff capabilitystate<-mainAgent.run(query="What is the integral of x^2?",tools=ToolRegistry.empty,handoffs=Seq(Handoff.to(mathAgent,"Math expertise required")))}yieldstate
// Preserve full conversation historyvalhandoff=Handoff(targetAgent=specialistAgent,transferReason=Some("Specialist needed"),preserveContext=true,// Keep conversation historytransferSystemMessage=false// Use target's system message)
Fresh Start (No Context)
1
2
3
4
5
6
7
// Start fresh with specialistvalhandoff=Handoff(targetAgent=specialistAgent,transferReason=Some("Fresh analysis needed"),preserveContext=false,// Don't transfer historytransferSystemMessage=false// Use target's system message)
Transfer System Message
1
2
3
4
5
6
7
// Transfer original instructions to specialistvalhandoff=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
valresult=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 handoffToolFunction(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:
Agent status changes to HandoffRequested
Context is prepared based on settings
Target agent receives the query
Target agent processes and returns response
4. Response Returns
The target agent’s response becomes part of the conversation.
// Agent with specialized toolsvaldataAgent=newAgent(client=client,systemMessage=Some(SystemMessage("You are a data analysis specialist.")))valdataTools=newToolRegistry(Seq(queryDatabaseTool,generateChartTool,exportCSVTool))// Main agent can hand off data queriesmainAgent.run(query="Analyze our Q4 sales data",tools=basicTools,handoffs=Seq(Handoff(targetAgent=dataAgent,transferReason=Some("Data analysis with database access"))))
// Specialist agentsvalbillingAgent=newAgent(client=client,systemMessage=Some(SystemMessage("You handle billing and payment issues.")))valtechnicalAgent=newAgent(client=client,systemMessage=Some(SystemMessage("You solve technical problems.")))valsalesAgent=newAgent(client=client,systemMessage=Some(SystemMessage("You help with purchases and upgrades.")))// Triage agentvaltriageAgent=newAgent(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
valresult=mainAgent.run(query,tools,handoffs)resultmatch{caseRight(state)ifstate.status==AgentStatus.Complete=>println(s"Completed: ${state.lastAssistantMessage}")caseRight(state)ifstate.status==AgentStatus.HandoffRequested=>// Handoff was requested but you're handling manuallyvalhandoffInfo=state.requestedHandoffprintln(s"Handoff to: ${handoffInfo.targetAgentName}")caseLeft(error)=>println(s"Error: $error")}
With Streaming Events
1
2
3
4
5
6
7
8
9
10
11
12
13
14
importorg.llm4s.agent.streaming._mainAgent.runWithEvents(query,tools,handoffs){caseHandoffStarted(targetName,reason,preserveContext,_)=>println(s"Handing off to $targetName: $reason")caseHandoffCompleted(targetName,success,_)=>println(s"Handoff to $targetName: ${if (success) "success" else "failed"}")caseAgentCompleted(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 conversationvalhandoff=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 freshvalhandoff=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 instructionsvalhandoff=Handoff(targetAgent=specialist,preserveContext=true,transferSystemMessage=true)// Original agent's system message is prepended to specialist's
// Good - specific and actionableHandoff.to(agent,"Database queries and SQL optimization")// Bad - vagueHandoff.to(agent,"Technical stuff")
2. Focused Specialists
1
2
3
4
5
6
7
8
9
10
11
12
13
// Good - focused specialistvalsqlAgent=newAgent(systemMessage=Some(SystemMessage("You are a SQL expert. Optimize queries and explain execution plans.")))// Bad - too broadvaleverythingAgent=newAgent(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 mattersHandoff(targetAgent=followUpAgent,preserveContext=true)// Fresh start for independent analysisHandoff(targetAgent=reviewAgent,preserveContext=false)
4. Don’t Overuse Handoffs
1
2
3
4
5
6
7
8
9
10
11
12
13
// Good - meaningful specializationhandoffs=Seq(Handoff.to(mathAgent,"Complex mathematical calculations"),Handoff.to(legalAgent,"Legal analysis and compliance"))// Bad - too granularhandoffs=Seq(Handoff.to(additionAgent,"Adding numbers"),Handoff.to(subtractionAgent,"Subtracting numbers"),Handoff.to(multiplicationAgent,"Multiplying numbers"),// ...)