Welcome to another lesson about agentic patterns! In the previous lesson, you mastered orchestrating agents as tools, where a central planner agent could dynamically delegate tasks to specialist agents and receive their results back. Today, we're exploring a fundamentally different approach called the handoff pattern, where agents can completely transfer control to other specialized agents rather than just calling them as tools.
In this lesson, you'll work with the Ruby Agent class in src/agent.rb, which already supports the handoffs: parameter in its initialize method. You'll learn how the handoff tool schema enables control transfers, understand the core handoff logic that cleanly passes conversation context between agents, and see how the Anthropic::Client handles these interactions. We'll build a practical example with a general assistant that can hand off mathematical problems to a specialized calculator assistant, demonstrating how agents make intelligent decisions about when to transfer control versus when to handle tasks themselves.
The handoff pattern represents a different philosophy of agent collaboration compared to the tool delegation approach you learned previously. When an agent uses another agent as a tool, it's essentially asking for help while maintaining responsibility for the final response. When an agent performs a handoff, it's saying, "this other agent is better equipped to handle this entire conversation from here on."
Consider the difference in conversation flow. In tool delegation, the user interacts with the orchestrator throughout: the user asks a question, the orchestrator calls a specialist tool, receives the result, and then provides its own response incorporating that information. The user never directly interacts with the specialist agent.
In the handoff pattern, the conversation flow changes completely. The user starts by talking to one agent, but that agent recognizes when another agent should take over. The first agent transfers not just the task, but the entire conversation context to the specialist. From that point forward, the specialist agent responds directly to the user, and the original agent is no longer involved. This pattern is particularly powerful when you have agents with very different capabilities or when the nature of a request clearly falls into one agent's domain of expertise.
The Ruby Agent class in src/agent.rb has already been extended to support handoffs through its initialize method. Let's examine the key parameters that enable this functionality:
The handoffs: parameter accepts an array of other Agent instances to which this agent can transfer control. Notice the defensive copying pattern used throughout: handoffs ? handoffs.dup : []. This is a Ruby idiom that prevents external mutation of the agent's internal state. If handoffs is nil, we use an empty array []; otherwise, we create a shallow copy using dup. This ensures that modifications to the original array passed in won't affect the agent's internal handoff list.
We store handoffs as an array rather than a hash because agents are identified by their attribute, and we want to maintain the flexibility to search through available agents dynamically. With the constructor already supporting handoffs, the next step is understanding how the handoff tool schema enables control transfers.
The Agent class automatically creates a handoff tool schema when handoff targets are provided. This schema is stored in the @handoff_schema instance variable and enables agents to request control transfers:
The handoff schema uses Ruby hash syntax with string keys, following the format expected by the Anthropic API. It includes two required parameters: the name of the target agent and a reason for the handoff. The reason parameter serves as both documentation for debugging and a way to help the agent think through whether a handoff is truly necessary.
Notice how we dynamically include the list of available agents using string interpolation: #{available_handoff_names.join(", ")}. The available_handoff_names method returns an array of agent names, which we join into a comma-separated string. This helps Claude understand which handoff options are available at runtime. Now let's see how this schema is integrated into the agent's tool list.
To make handoffs work seamlessly, the build_request_args method combines regular tool schemas with the handoff schema when building API requests:
The method builds a hash using Ruby's symbol key syntax (model:, system:, messages:, max_tokens:). It creates an array called all_schemas and conditionally adds schemas based on what's available: regular tool schemas from @tool_schemas are concatenated if present, and the @handoff_schema is appended if any handoff targets exist.
This approach ensures that the handoff tool is automatically available to any agent that has handoff targets configured, without requiring manual schema management. The final hash only includes the :tools key if there are actually tools to provide. With the handoff tool now available to agents, let's examine the logic that actually performs the control transfer when this tool is called.
The core of the handoff pattern lies in the call_handoff method, which handles the actual transfer of control from one agent to another. This method performs several critical operations:
The method executes the following steps in sequence:
-
Input normalization: Transforms the tool input keys to strings using
transform_keys(&:to_s), ensuring consistent access regardless of how the Anthropic API returns the data. Extracts the target agent'snameand handoffreasonfor logging and agent lookup. -
Agent lookup: Uses
findwith a block to search the@handoffsarray for an agent matching the requested name. This returnsnilif no matching agent exists, which we check with theunlessguard clause.
The main execution loop in the run method needs to detect handoff tool calls and handle them differently from regular tools. When a handoff succeeds, it should immediately return the target agent's response rather than continuing the current agent's execution:
The key insight here is in how we handle the return value from call_handoff. We use Ruby's multiple assignment (success, handoff_result = call_handoff(...)) to unpack the two-element array. If the first element (success) is true, we immediately return handoff_result, which contains the target agent's complete response from target_agent.run(clean_messages).
This immediate return is what makes handoffs different from tool calls: instead of collecting the result in tool_results and continuing the conversation, a successful handoff ends the current agent's involvement and returns the target agent's complete response tuple .
Let's create a complete example that demonstrates how agents make intelligent handoff decisions. We'll set up a general assistant that can hand off mathematical problems to a specialized calculator assistant:
We load the tool schemas from JSON using JSON.parse(File.read("schemas.json")), which reads the file and parses it into Ruby hashes. The math_tools dictionary maps string names to Ruby method objects using the method(:function_name) syntax. This creates callable objects that the agent can invoke with keyword arguments.
Notice how we create the calculator_assistant first without any handoffs, then create the helpful_assistant with the calculator in its handoffs: array. This creates a clear hierarchy where the general assistant can transfer control to the specialist, but not vice versa. Now let's test the system with different types of questions to see how it makes handoff decisions.
Let's test the system with a general knowledge question to see how the agent decides whether to handle the task itself or perform a handoff:
When we run this test, the general assistant recognizes that this is a straightforward factual question that doesn't require mathematical expertise:
The run method returns a two-element array: the message history and the final response text. We use Ruby's underscore convention (_history) to indicate we're not using that value. The agent handled this question directly without any handoffs or tool calls, demonstrating that it can distinguish between tasks it should handle itself and those requiring specialist expertise. Now let's test with a mathematical problem that should trigger a handoff to see the complete control transfer process in action.
Now let's test with a mathematical problem that should trigger a handoff to demonstrate the complete control transfer process:
This test demonstrates the complete handoff process in action:
The execution trace shows the complete handoff process: the general assistant recognized that this was a mathematical problem requiring specialist expertise, initiated a handoff to the calculator_assistant with a clear reason, and then the calculator assistant took complete control of the conversation. The calculator assistant used its mathematical tools to solve the equation step by step and provided the final response directly to the user.
Notice that the tool call logs show Ruby hash syntax with symbol keys ({:a=>5, :b=>2}). The actual tool calls made by Claude may vary based on the model's reasoning, but the Ruby Agent class will log all tool invocations via puts statements in both and .
Understanding when to apply each pattern is crucial for building effective agent systems.
Use agents as tools when you need an orchestrating agent to maintain control and synthesize multiple specialist inputs into a unified response. This works well for complex tasks requiring coordination across different domains, like planning a trip that involves flights, hotels, and restaurants.
Use handoffs when a specialist is clearly better equipped to handle the entire conversation from a certain point forward. This is ideal when the task falls entirely within one domain of expertise and the specialist can provide more value through direct interaction than if it were filtered through an orchestrator.
The key question: Does the task require orchestration and synthesis, or does it need deep specialization with direct user interaction? Choose accordingly.
When implementing handoffs in Ruby, success depends heavily on designing clear decision boundaries and robust error handling. The most effective handoff systems define explicit criteria in agent prompts, helping agents make confident transfer decisions rather than hesitating between options. For example, your general assistant should know precisely when mathematical problems warrant a calculator handoff versus when they can provide basic arithmetic directly.
Key practices for reliable handoffs include:
- Define clear handoff criteria in agent prompts so agents know exactly when to transfer control
- Clean conversation context using the
messages[0...-1]slicing approach to remove handoff tool calls before transferring - Implement robust error handling with
unless target_agentandrescue => eblocks to gracefully handle failed handoffs - Use descriptive handoff reasons for debugging and system transparency
- Design handoff chains with clear direction to avoid circular transfers
- Respect the
max_turnslimit — even with handoffs, each agent has a finite number of turns before raising an error
The biggest pitfall to avoid is creating circular handoffs where agents pass control back and forth indefinitely. Design your handoff chains with clear directionality and avoid giving agents too many transfer options, which can lead to decision paralysis.
Remember that when a handoff fails, it becomes a regular tool result in the tool_results array, allowing the current agent to see the error message and respond appropriately. This fallback mechanism ensures your system can handle edge cases like requesting nonexistent agents or encountering runtime errors during transfer.
The Ruby implementation's defensive copying () and careful message cleaning () are essential for preventing bugs related to shared state and context pollution. Always maintain these safeguards when extending the handoff functionality.
You've now mastered the handoff pattern in Ruby, a powerful approach for building agent systems where specialists can take complete control of conversations when their expertise is needed. This pattern differs fundamentally from agent-as-tool delegation because it transfers not only the task but also the entire conversation ownership to the most appropriate agent.
You've learned how the Ruby Agent class uses the handoffs: parameter, defensive copying with dup, and the two-element return array pattern [success, result] to enable clean control transfers. You've seen how the call_handoff method finds target agents, cleans message context, and handles errors gracefully, and how the main run loop distinguishes between successful handoffs that immediately return versus failed handoffs that continue as tool results.
In your upcoming practice exercises, you'll build multi-agent systems with complex handoff chains, where agents can intelligently route conversations through multiple specialists based on the evolving needs of each interaction. This foundation will enable you to create sophisticated agent ecosystems that can handle diverse, complex tasks while maintaining clear specialization and efficient resource utilization.
