Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions lib/agents/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
# )
module Agents
class Agent
attr_reader :name, :instructions, :model, :tools, :handoff_agents, :temperature, :response_schema, :headers, :params
attr_reader :name, :instructions, :model, :tools, :handoff_agents, :temperature, :response_schema, :headers,
:params, :handoff_description

# Initialize a new Agent instance
#
Expand All @@ -63,8 +64,9 @@ class Agent
# @param response_schema [Hash, nil] JSON schema for structured output responses
# @param headers [Hash, nil] Default HTTP headers applied to LLM requests
# @param params [Hash, nil] Default provider-specific parameters applied to LLM requests (e.g., service_tier)
# @param handoff_description [String, nil] Short description exposed when this agent is offered as a handoff target
def initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], handoff_agents: [], temperature: 0.7,
response_schema: nil, headers: nil, params: nil)
response_schema: nil, headers: nil, params: nil, handoff_description: nil)
@name = name
@instructions = instructions
@model = model
Expand All @@ -74,6 +76,7 @@ def initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], hando
@response_schema = response_schema
@headers = Helpers::HashNormalizer.normalize(headers, label: "headers", freeze_result: true)
@params = Helpers::HashNormalizer.normalize(params, label: "params", freeze_result: true)
@handoff_description = handoff_description

# Mutex for thread-safe handoff registration
# While agents are typically configured at startup, we want to ensure
Expand Down Expand Up @@ -170,7 +173,8 @@ def clone(**changes)
temperature: changes.fetch(:temperature, @temperature),
response_schema: changes.fetch(:response_schema, @response_schema),
headers: changes.fetch(:headers, @headers),
params: changes.fetch(:params, @params)
params: changes.fetch(:params, @params),
handoff_description: changes.fetch(:handoff_description, @handoff_description)
)
end

Expand Down
3 changes: 2 additions & 1 deletion lib/agents/handoff.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def initialize(target_agent)

# Set up the tool with a standardized name and description
@tool_name = "handoff_to_#{Helpers::NameNormalizer.to_tool_name(target_agent.name)}"
@tool_description = "Transfer conversation to #{target_agent.name}"
desc = target_agent.handoff_description
@tool_description = desc.is_a?(String) && !desc.empty? ? desc : "Transfer conversation to #{target_agent.name}"

super()
end
Expand Down
33 changes: 30 additions & 3 deletions spec/agents/agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

RSpec.describe Agents::Agent do
let(:test_tool) { instance_double(Agents::Tool, "TestTool") }
let(:other_agent) { instance_double(described_class, name: "Other Agent") }
let(:other_agent) { instance_double(described_class, name: "Other Agent", handoff_description: nil) }
let(:context) { {} }

describe "#initialize" do
Expand Down Expand Up @@ -95,6 +95,18 @@
expect(agent.response_schema).to be_nil
end

it "stores handoff_description when provided" do
agent = described_class.new(name: "Billing", handoff_description: "Handles payment and billing issues")

expect(agent.handoff_description).to eq("Handles payment and billing issues")
end

it "defaults handoff_description to nil" do
agent = described_class.new(name: "Test")

expect(agent.handoff_description).to be_nil
end

it "normalizes nil headers to empty hash" do
agent = described_class.new(name: "Test", headers: nil)

Expand Down Expand Up @@ -159,7 +171,7 @@

describe "#all_tools" do
let(:agent) { described_class.new(name: "Test Agent", tools: [test_tool]) }
let(:handoff_agent) { instance_double(described_class, name: "Handoff Agent") }
let(:handoff_agent) { instance_double(described_class, name: "Handoff Agent", handoff_description: nil) }

it "returns regular tools when no handoffs registered" do
expect(agent.all_tools).to eq([test_tool])
Expand All @@ -178,7 +190,7 @@
threads = []
5.times do |i|
threads << Thread.new do
agent.register_handoffs(instance_double(described_class, name: "Agent#{i}"))
agent.register_handoffs(instance_double(described_class, name: "Agent#{i}", handoff_description: nil))
agent.all_tools
end
end
Expand Down Expand Up @@ -283,6 +295,21 @@
expect(cloned.params).to eq(service_tier: "flex")
expect(agent_with_params.params).to eq(service_tier: "default")
end

it "preserves handoff_description when cloning" do
agent = described_class.new(name: "Billing", handoff_description: "Handles billing")
cloned = agent.clone(name: "Billing Clone")

expect(cloned.handoff_description).to eq("Handles billing")
end

it "allows overriding handoff_description when cloning" do
agent = described_class.new(name: "Billing", handoff_description: "Handles billing")
cloned = agent.clone(handoff_description: "Updated billing description")

expect(cloned.handoff_description).to eq("Updated billing description")
expect(agent.handoff_description).to eq("Handles billing")
end
end

describe "#get_system_prompt" do
Expand Down
28 changes: 26 additions & 2 deletions spec/agents/handoff_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require_relative "../../lib/agents"

RSpec.describe Agents::HandoffTool do
let(:target_agent) { instance_double(Agents::Agent, name: "Support Agent") }
let(:target_agent) { instance_double(Agents::Agent, name: "Support Agent", handoff_description: nil) }
let(:handoff_tool) { described_class.new(target_agent) }
let(:context) { {} }

Expand All @@ -18,7 +18,7 @@

context "with special characters in agent name" do
it "strips special characters from tool name" do
agent = instance_double(Agents::Agent, name: "Billing-Agent!")
agent = instance_double(Agents::Agent, name: "Billing-Agent!", handoff_description: nil)
tool = described_class.new(agent)

expect(tool.name).to eq("handoff_to_billingagent")
Expand All @@ -29,6 +29,30 @@
expected_description = "Transfer conversation to Support Agent"
expect(handoff_tool.description).to eq(expected_description)
end

it "uses handoff_description from target agent when present" do
agent_with_desc = instance_double(
Agents::Agent, name: "Billing Agent",
handoff_description: "Handles payment issues and refund requests"
)
tool = described_class.new(agent_with_desc)

expect(tool.description).to eq("Handles payment issues and refund requests")
end

it "falls back to default description when handoff_description is nil" do
agent_without_desc = instance_double(Agents::Agent, name: "Billing Agent", handoff_description: nil)
tool = described_class.new(agent_without_desc)

expect(tool.description).to eq("Transfer conversation to Billing Agent")
end

it "falls back to default description when handoff_description is blank" do
agent_blank_desc = instance_double(Agents::Agent, name: "Billing Agent", handoff_description: "")
tool = described_class.new(agent_blank_desc)

expect(tool.description).to eq("Transfer conversation to Billing Agent")
end
end

describe "#perform" do
Expand Down
3 changes: 2 additions & 1 deletion spec/agents/runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
response_schema: nil,
get_system_prompt: "You are a specialist",
headers: {},
params: {})
params: {},
handoff_description: nil)
end

let(:test_tool) do
Expand Down
Loading