AgentRunner
The AgentRunner is the thread-safe execution manager that provides the main API for multi-agent conversations. It separates agent registry management from execution, enabling safe concurrent use across multiple threads while maintaining conversation continuity.
Two-Tier Architecture
The library uses a two-tier design separating long-lived configuration from short-lived execution:
AgentRunner (Thread-Safe Manager)
- Created once at application startup
- Maintains immutable agent registry
- Determines conversation continuity from history
- Thread-safe for concurrent conversations
Runner (Internal Execution Engine)
- Created per conversation turn
- Handles LLM communication and tool execution
- Manages context state during execution
- Stateless and garbage-collected after use
Conversation Flow
Each conversation follows this flow:
- Agent Selection: AgentRunner determines current agent from conversation history
- Context Isolation: Creates deep copy of context for thread safety
- LLM Communication: Sends message with context to language model
- Tool Execution: Executes any requested tools through RubyLLM
- Handoff Detection: Checks for agent handoffs and switches if needed
- State Persistence: Updates context with conversation state
Thread Safety
The AgentRunner ensures thread safety through several key mechanisms:
- Immutable Registry: Agent registry is frozen after initialization, preventing runtime modifications
- Context Isolation: Each execution receives a deep copy of context to prevent cross-contamination
- Stateless Execution: Internal Runner instances store no execution-specific state
- Tool Wrapping: ToolWrapper injects context through parameters, keeping tools stateless
Usage Pattern
Create an AgentRunner once and reuse it for multiple conversations:
# Create agents
triage_agent = Agents::Agent.new(
name: "Triage",
instructions: "Route users to appropriate specialists"
)
billing_agent = Agents::Agent.new(
name: "Billing",
instructions: "Handle billing inquiries"
)
# Register handoffs
triage_agent.register_handoffs(billing_agent)
# Create runner once (thread-safe)
runner = Agents::Runner.with_agents(triage_agent, billing_agent)
# Use from multiple threads safely
result1 = runner.run("I have a billing question")
result2 = runner.run("Follow up", context: result1.context)
Conversation Continuity
The AgentRunner automatically maintains conversation continuity by analyzing message history to determine which agent should handle each turn:
# First message -> Uses triage agent (default entry point)
result1 = runner.run("I need help with my bill")
# Triage hands off to billing agent
# Next message -> AgentRunner detects billing agent should continue
result2 = runner.run("What payment methods do you accept?", context: result1.context)
The run
method returns a RunResult
with output, conversation history, usage metrics, and updated context.