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) ⇒ 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



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

def initialize(name:, instructions: nil, model: "gpt-4.1-mini", tools: [], handoff_agents: [], temperature: 0.7,
               response_schema: nil)
  @name = name
  @instructions = instructions
  @model = model
  @tools = tools.dup
  @handoff_agents = []
  @temperature = temperature
  @response_schema = response_schema

  # 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

#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

#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



91
92
93
94
95
96
97
# File 'lib/agents/agent.rb', line 91

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:



233
234
235
236
237
238
239
240
# File 'lib/agents/agent.rb', line 233

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



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/agents/agent.rb', line 159

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)
  )
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



196
197
198
199
200
201
202
203
204
205
206
# File 'lib/agents/agent.rb', line 196

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



115
116
117
118
119
120
121
# File 'lib/agents/agent.rb', line 115

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