Class: Agents::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/agents/agent.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], handoff_agents: [], temperature: 0.7, response_schema: nil, headers: nil, params: nil) ⇒ Agent

Initialize a new Agent instance

Parameters:

  • name (String)

    The name of the agent

  • instructions (String, Proc, nil) (defaults to: nil)

    Static string or dynamic Proc that returns instructions

  • model (String) (defaults to: "gpt-4.1-mini")

    The LLM model to use (default: “gpt-4.1-mini”)

  • tools (Array<Agents::Tool>) (defaults to: [])

    Array of tool instances the agent can use

  • handoff_agents (Array<Agents::Agent>) (defaults to: [])

    Array of agents this agent can hand off to

  • temperature (Float) (defaults to: 0.7)

    Controls randomness in responses (0.0 = deterministic, 1.0 = very random, default: 0.7)

  • response_schema (Hash, nil) (defaults to: nil)

    JSON schema for structured output responses

  • headers (Hash, nil) (defaults to: nil)

    Default HTTP headers applied to LLM requests

  • params (Hash, nil) (defaults to: nil)

    Default provider-specific parameters applied to LLM requests (e.g., service_tier)



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/agents/agent.rb', line 66

def initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], handoff_agents: [], temperature: 0.7,
               response_schema: nil, headers: nil, params: nil)
  @name = name
  @instructions = instructions
  @model = model
  @tools = tools.dup
  @handoff_agents = []
  @temperature = temperature
  @response_schema = response_schema
  @headers = Helpers::HashNormalizer.normalize(headers, label: "headers", freeze_result: true)
  @params = Helpers::HashNormalizer.normalize(params, label: "params", freeze_result: true)

  # Mutex for thread-safe handoff registration
  # While agents are typically configured at startup, we want to ensure
  # that concurrent handoff registrations don't result in lost data.
  # For example, in a web server with multiple threads initializing
  # different parts of the system, we might have:
  #   Thread 1: triage.register_handoffs(billing)
  #   Thread 2: triage.register_handoffs(support)
  # Without synchronization, one registration could overwrite the other.
  @mutex = Mutex.new

  # Register initial handoff agents if provided
  register_handoffs(*handoff_agents) unless handoff_agents.empty?
end

Instance Attribute Details

#handoff_agentsObject (readonly)

Returns the value of attribute handoff_agents.



53
54
55
# File 'lib/agents/agent.rb', line 53

def handoff_agents
  @handoff_agents
end

#headersObject (readonly)

Returns the value of attribute headers.



53
54
55
# File 'lib/agents/agent.rb', line 53

def headers
  @headers
end

#instructionsObject (readonly)

Returns the value of attribute instructions.



53
54
55
# File 'lib/agents/agent.rb', line 53

def instructions
  @instructions
end

#modelObject (readonly)

Returns the value of attribute model.



53
54
55
# File 'lib/agents/agent.rb', line 53

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name.



53
54
55
# File 'lib/agents/agent.rb', line 53

def name
  @name
end

#paramsObject (readonly)

Returns the value of attribute params.



53
54
55
# File 'lib/agents/agent.rb', line 53

def params
  @params
end

#response_schemaObject (readonly)

Returns the value of attribute response_schema.



53
54
55
# File 'lib/agents/agent.rb', line 53

def response_schema
  @response_schema
end

#temperatureObject (readonly)

Returns the value of attribute temperature.



53
54
55
# File 'lib/agents/agent.rb', line 53

def temperature
  @temperature
end

#toolsObject (readonly)

Returns the value of attribute tools.



53
54
55
# File 'lib/agents/agent.rb', line 53

def tools
  @tools
end

Instance Method Details

#all_toolsArray<Agents::Tool>

Get all tools available to this agent, including any auto-generated handoff tools

Returns:

  • (Array<Agents::Tool>)

    All tools available to the agent



95
96
97
98
99
100
101
# File 'lib/agents/agent.rb', line 95

def all_tools
  @mutex.synchronize do
    # Compute handoff tools dynamically
    handoff_tools = @handoff_agents.map { |agent| HandoffTool.new(agent) }
    @tools + handoff_tools
  end
end

#as_tool(name: nil, description: nil, output_extractor: nil) ⇒ Agents::AgentTool

Transform this agent into a tool, callable by other agents. This enables agent-to-agent collaboration without conversation handoffs.

Agent-as-tool is different from handoffs in two key ways:

  1. The wrapped agent receives generated input, not conversation history

  2. The wrapped agent returns a result to the calling agent, rather than taking over

Examples:

Basic agent-as-tool

research_agent = Agent.new(name: "Researcher", instructions: "Research topics")
research_tool = research_agent.as_tool(
  name: "research_topic",
  description: "Research a topic using company knowledge base"
)

Custom output extraction

analyzer_tool = analyzer_agent.as_tool(
  output_extractor: ->(result) { result.context[:extracted_data]&.to_json || result.output }
)

Parameters:

  • name (String, nil) (defaults to: nil)

    Override the tool name (defaults to snake_case agent name)

  • description (String, nil) (defaults to: nil)

    Override the tool description

  • output_extractor (Proc, nil) (defaults to: nil)

    Custom proc to extract/transform the agent’s output

  • params (Hash)

    Additional parameter definitions for the tool

Returns:



239
240
241
242
243
244
245
246
# File 'lib/agents/agent.rb', line 239

def as_tool(name: nil, description: nil, output_extractor: nil)
  AgentTool.new(
    agent: self,
    name: name,
    description: description,
    output_extractor: output_extractor
  )
end

#clone(**changes) ⇒ Agents::Agent

Creates a new agent instance with modified attributes while preserving immutability. The clone method is used when you need to create variations of agents without mutating the original. This can be used for runtime agent modifications, say in a multi-tenant environment we can do something like the following:

The key insight to note here is that clone ensures immutability - you never accidentally modify a shared agent instance that other requests might be using. This is critical for thread safety in concurrent environments.

This also ensures we also get to leverage the syntax sugar defining a class provides us with.

Examples:

Multi-tenant agent customization

def agent_for_tenant(tenant)
  @base_agent.clone(
    instructions: "You work for #{tenant.company_name}",
    tools: @base_agent.tools + tenant.custom_tools
  )
end

Creating specialized variants

finance_writer = @writer_agent.clone(
  tools: @writer_agent.tools + [financial_research_tool]
)

marketing_writer = @writer_agent.clone(
  tools: @writer_agent.tools + [marketing_research_tool]
)

Parameters:

  • changes (Hash)

    Keyword arguments for attributes to change

Options Hash (**changes):

  • :name (String)

    New agent name

  • :instructions (String, Proc)

    New instructions

  • :model (String)

    New model identifier

  • :tools (Array<Agents::Tool>)

    New tools array (replaces all tools)

  • :handoff_agents (Array<Agents::Agent>)

    New handoff agents

  • :temperature (Float)

    Temperature for LLM responses (0.0-1.0)

  • :response_schema (Hash, nil)

    JSON schema for structured output

Returns:

  • (Agents::Agent)

    A new frozen agent instance with the specified changes



163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/agents/agent.rb', line 163

def clone(**changes)
  self.class.new(
    name: changes.fetch(:name, @name),
    instructions: changes.fetch(:instructions, @instructions),
    model: changes.fetch(:model, @model),
    tools: changes.fetch(:tools, @tools.dup),
    handoff_agents: changes.fetch(:handoff_agents, @handoff_agents),
    temperature: changes.fetch(:temperature, @temperature),
    response_schema: changes.fetch(:response_schema, @response_schema),
    headers: changes.fetch(:headers, @headers),
    params: changes.fetch(:params, @params)
  )
end

#get_system_prompt(context) ⇒ String?

Get the system prompt for the agent, potentially customized based on runtime context. We will allow setting up a Proc for instructions. This will allow us the inject context in runtime.

Examples:

Static instructions (most common)

agent = Agent.new(
  name: "Support",
  instructions: "You are a helpful support agent"
)

Dynamic instructions with state awareness

agent = Agent.new(
  name: "Sales Agent",
  instructions: ->(context) {
    state = context.context[:state] || {}
    base = "You are a sales agent."
    if state[:customer_name] && state[:current_plan]
      base += " Customer: #{state[:customer_name]} on #{state[:current_plan]} plan."
    end
    base
  }
)

Parameters:

Returns:

  • (String, nil)

    The system prompt string or nil if no instructions are set



202
203
204
205
206
207
208
209
210
211
212
# File 'lib/agents/agent.rb', line 202

def get_system_prompt(context)
  # TODO: Add string interpolation support for instructions
  # Allow instructions like "You are helping %{customer_name}" that automatically
  # get state values injected from context[:state] using Ruby's % formatting
  case instructions
  when String
    instructions
  when Proc
    instructions.call(context)
  end
end

#register_handoffs(*agents) ⇒ self

Register agents that this agent can hand off to. This method can be called after agent creation to set up handoff relationships. Thread-safe: Multiple threads can safely call this method concurrently.

Examples:

Setting up hub-and-spoke pattern

# Create agents
triage = Agent.new(name: "Triage", instructions: "Route to specialists")
billing = Agent.new(name: "Billing", instructions: "Handle payments")
support = Agent.new(name: "Support", instructions: "Fix technical issues")

# Wire up handoffs after creation - much cleaner than complex factories!
triage.register_handoffs(billing, support)
billing.register_handoffs(triage)  # Specialists only handoff back to triage
support.register_handoffs(triage)

Parameters:

  • agents (Array<Agents::Agent>)

    Agents to register as handoff targets

Returns:

  • (self)

    Returns self for method chaining



119
120
121
122
123
124
125
# File 'lib/agents/agent.rb', line 119

def register_handoffs(*agents)
  @mutex.synchronize do
    @handoff_agents.concat(agents)
    @handoff_agents.uniq! # Prevent duplicates
  end
  self
end