diff --git a/lib/agents/agent.rb b/lib/agents/agent.rb index 371c15d..83bdcea 100644 --- a/lib/agents/agent.rb +++ b/lib/agents/agent.rb @@ -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 # @@ -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 @@ -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 @@ -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 diff --git a/lib/agents/handoff.rb b/lib/agents/handoff.rb index 7c4c15c..d360e75 100644 --- a/lib/agents/handoff.rb +++ b/lib/agents/handoff.rb @@ -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 diff --git a/spec/agents/agent_spec.rb b/spec/agents/agent_spec.rb index 98b9173..49b5afc 100644 --- a/spec/agents/agent_spec.rb +++ b/spec/agents/agent_spec.rb @@ -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 @@ -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) @@ -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]) @@ -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 @@ -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 diff --git a/spec/agents/handoff_spec.rb b/spec/agents/handoff_spec.rb index 782da99..85b7639 100644 --- a/spec/agents/handoff_spec.rb +++ b/spec/agents/handoff_spec.rb @@ -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) { {} } @@ -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") @@ -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 diff --git a/spec/agents/runner_spec.rb b/spec/agents/runner_spec.rb index 74d1140..b80b4ab 100644 --- a/spec/agents/runner_spec.rb +++ b/spec/agents/runner_spec.rb @@ -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