Class: Agents::AgentRunner

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

Overview

Thread-safe agent execution manager that provides a clean API for multi-agent conversations. This class is designed to be created once and reused across multiple threads safely.

The key insight here is separating agent registry/configuration (this class) from execution state (Runner instances). This allows the same AgentRunner to be used concurrently without thread safety issues.

## Usage Pattern

# Create once (typically at application startup)
runner = Agents::Runner.with_agents(triage_agent, billing_agent, support_agent)
  .on_tool_start { |tool_name, args| broadcast_event('tool_start', tool_name, args) }
  .on_tool_complete { |tool_name, result| broadcast_event('tool_complete', tool_name, result) }

# Use safely from multiple threads
result = runner.run("I need billing help")           # New conversation
result = runner.run("More help", context: context)   # Continue conversation

## Thread Safety Design

  • All instance variables are frozen after initialization (immutable state)

  • Agent registry is built once and never modified

  • Each run() call creates independent execution context

  • No shared mutable state between concurrent executions

## Callback Thread Safety Callback registration is thread-safe using internal synchronization. Multiple threads can safely register callbacks concurrently without data races.

Instance Method Summary collapse

Constructor Details

#initialize(agents) ⇒ AgentRunner

Initialize with a list of agents. The first agent becomes the default entry point.

Parameters:

  • agents (Array<Agents::Agent>)

    List of agents, first one is the default entry point

Raises:

  • (ArgumentError)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/agents/agent_runner.rb', line 35

def initialize(agents)
  raise ArgumentError, "At least one agent must be provided" if agents.empty?

  @agents = agents.dup.freeze
  @callbacks_mutex = Mutex.new
  @default_agent = agents.first

  # Build simple registry from provided agents - developer controls what's available
  @registry = build_registry(agents).freeze

  # Initialize callback storage - use thread-safe arrays
  @callbacks = {
    tool_start: [],
    tool_complete: [],
    agent_thinking: [],
    agent_handoff: []
  }
end

Instance Method Details

#on_agent_handoff(&block) ⇒ self

Register a callback for agent handoff events. Called when control is transferred from one agent to another.

Parameters:

  • block (Proc)

    Callback block that receives (from_agent, to_agent, reason)

Returns:

  • (self)

    For method chaining



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

def on_agent_handoff(&block)
  return self unless block

  @callbacks_mutex.synchronize { @callbacks[:agent_handoff] << block }
  self
end

#on_agent_thinking(&block) ⇒ self

Register a callback for agent thinking events. Called when an agent is about to make an LLM call.

Parameters:

  • block (Proc)

    Callback block that receives (agent_name, input)

Returns:

  • (self)

    For method chaining



108
109
110
111
112
113
# File 'lib/agents/agent_runner.rb', line 108

def on_agent_thinking(&block)
  return self unless block

  @callbacks_mutex.synchronize { @callbacks[:agent_thinking] << block }
  self
end

#on_tool_complete(&block) ⇒ self

Register a callback for tool completion events. Called when an agent has finished executing a tool.

Parameters:

  • block (Proc)

    Callback block that receives (tool_name, result)

Returns:

  • (self)

    For method chaining



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

def on_tool_complete(&block)
  return self unless block

  @callbacks_mutex.synchronize { @callbacks[:tool_complete] << block }
  self
end

#on_tool_start(&block) ⇒ self

Register a callback for tool start events. Called when an agent is about to execute a tool.

Parameters:

  • block (Proc)

    Callback block that receives (tool_name, args)

Returns:

  • (self)

    For method chaining



84
85
86
87
88
89
# File 'lib/agents/agent_runner.rb', line 84

def on_tool_start(&block)
  return self unless block

  @callbacks_mutex.synchronize { @callbacks[:tool_start] << block }
  self
end

#run(input, context: {}, max_turns: Runner::DEFAULT_MAX_TURNS) ⇒ RunResult

Execute a conversation turn with automatic agent selection. For new conversations, uses the default agent (first in the list). For continuing conversations, determines the appropriate agent from conversation history.

Parameters:

  • input (String)

    User’s message

  • context (Hash) (defaults to: {})

    Conversation context (will be restored if continuing conversation)

  • max_turns (Integer) (defaults to: Runner::DEFAULT_MAX_TURNS)

    Maximum turns before stopping (default: 10)

Returns:

  • (RunResult)

    Execution result with output, messages, and updated context



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/agents/agent_runner.rb', line 62

def run(input, context: {}, max_turns: Runner::DEFAULT_MAX_TURNS)
  # Determine which agent should handle this conversation
  # Uses conversation history to maintain continuity across handoffs
  current_agent = determine_conversation_agent(context)

  # Execute using stateless Runner - each execution is independent and thread-safe
  # Pass callbacks to enable real-time event notifications
  Runner.new.run(
    current_agent,
    input,
    context: context,
    registry: @registry,
    max_turns: max_turns,
    callbacks: @callbacks
  )
end