Class: RubyPi::Agent::Loop

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

Overview

Executes the think-act-observe cycle against a given State, emitting events through the provided EventEmitter-compatible emitter. Returns an Agent::Result when the cycle terminates.

The cycle:

1. THINK — Call the LLM with current messages and tools. Apply
   transform_context if present. Emit :turn_start and stream
   :text_delta events.
2. ACT — If the LLM returned tool calls, execute them via
   Tools::Executor. Fire before_tool_call / after_tool_call hooks
   and emit :tool_execution_start / :tool_execution_end events.
3. OBSERVE — Append tool results to messages and loop back to THINK.
4. DONE — Return when finish_reason == "stop" (no more tool calls)
   or max_iterations is reached.

Examples:

Running the loop directly

loop = RubyPi::Agent::Loop.new(state: state, emitter: agent)
result = loop.run

Instance Method Summary collapse

Constructor Details

#initialize(state:, emitter:, compaction: nil) ⇒ Loop

Creates a new Loop bound to the given state and event emitter.

Parameters:

  • state (RubyPi::Agent::State)

    mutable agent state

  • emitter (#emit)

    object that responds to ‘emit(event, data)`

  • compaction (RubyPi::Context::Compaction, nil) (defaults to: nil)

    optional compaction strategy for managing context window size



40
41
42
43
44
45
46
# File 'lib/ruby_pi/agent/loop.rb', line 40

def initialize(state:, emitter:, compaction: nil)
  @state = state
  @emitter = emitter
  @compaction = compaction
  @tool_calls_made = []
  @total_usage = { input_tokens: 0, output_tokens: 0 }
end

Instance Method Details

#runRubyPi::Agent::Result

Runs the think-act-observe cycle until completion or max iterations. Returns an Agent::Result capturing the final content, messages, tool calls, usage, and turn count.

Returns:



53
54
55
56
57
58
59
60
61
62
63
64
65
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
91
92
93
94
# File 'lib/ruby_pi/agent/loop.rb', line 53

def run
  loop do
    # Check iteration limit before starting a new turn
    if @state.max_iterations_reached?
      return build_result(content: last_assistant_content)
    end

    # Apply context compaction if configured and needed
    compact_if_needed!

    # THINK: Call the LLM
    response = think

    # Track usage from this turn
    accumulate_usage(response.usage)

    # Increment iteration counter
    @state.increment_iteration!

    if response.tool_calls?
      # ACT: Execute tool calls
      act(response)

      # OBSERVE: Tool results have been added to messages; loop continues
      @emitter.emit(:turn_end, turn: @state.iteration, has_tool_calls: true)
    else
      # No tool calls — the LLM is done
      @emitter.emit(:turn_end, turn: @state.iteration, has_tool_calls: false)
      return build_result(content: response.content)
    end
  end
rescue StandardError => e
  @emitter.emit(:error, error: e, source: :agent_loop)
  Result.new(
    content: nil,
    messages: @state.messages,
    tool_calls_made: @tool_calls_made,
    usage: @total_usage,
    turns: @state.iteration,
    error: e
  )
end