Class: RubyPi::Agent::Loop
- Inherits:
-
Object
- Object
- RubyPi::Agent::Loop
- 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.
Constant Summary collapse
- PROGRAMMING_ERRORS =
Issue #18: Programming errors that should NOT be rescued by the loop. These indicate bugs in the calling code, not LLM/provider/tool failures. Rescuing them would silently swallow real bugs like typos or type mismatches.
[ NoMethodError, NameError, ArgumentError, TypeError ].freeze
Instance Method Summary collapse
-
#initialize(state:, emitter:, compaction: nil, execution_mode: :parallel, tool_timeout: 30) ⇒ Loop
constructor
Creates a new Loop bound to the given state and event emitter.
-
#run ⇒ RubyPi::Agent::Result
Runs the think-act-observe cycle until completion or max iterations.
Constructor Details
#initialize(state:, emitter:, compaction: nil, execution_mode: :parallel, tool_timeout: 30) ⇒ Loop
Creates a new Loop bound to the given state and event emitter.
54 55 56 57 58 59 60 61 62 |
# File 'lib/ruby_pi/agent/loop.rb', line 54 def initialize(state:, emitter:, compaction: nil, execution_mode: :parallel, tool_timeout: 30) @state = state @emitter = emitter @compaction = compaction @execution_mode = execution_mode @tool_timeout = tool_timeout @tool_calls_made = [] @total_usage = { input_tokens: 0, output_tokens: 0 } end |
Instance Method Details
#run ⇒ RubyPi::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.
Issue #18: Re-raises programming errors (NoMethodError, NameError, ArgumentError, TypeError) instead of swallowing them. Only LLM/provider/ tool errors are caught and wrapped in a failed Result.
Issue #19: When max_iterations is reached, the Result now includes stop_reason: :max_iterations and success? returns false. Previously, the Result had no error set, so success? returned true even though the agent was forcibly stopped.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/ruby_pi/agent/loop.rb', line 78 def run loop do # Check iteration limit before starting a new turn if @state.max_iterations_reached? return build_result( content: last_assistant_content, stop_reason: :max_iterations ) 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, stop_reason: :complete) end end rescue *PROGRAMMING_ERRORS # Issue #18: Re-raise programming errors immediately. These are bugs # in the calling code (typos, wrong argument counts, type mismatches) # that should never be silently swallowed. raise rescue StandardError => e @emitter.emit(:error, error: e, source: :agent_loop) Result.new( content: nil, messages: @state., tool_calls_made: @tool_calls_made, usage: @total_usage, turns: @state.iteration, error: e, stop_reason: :error ) end |