Class: Phronomy::Agent::ReactAgent

Inherits:
Base
  • Object
show all
Defined in:
lib/phronomy/agent/react_agent.rb

Overview

ReAct pattern (Reasoning + Acting) agent. Repeats the LLM <-> Tool loop until no more tool calls are made.

Instance Attribute Summary

Attributes included from Concerns::BeforeCompletion

#before_completion

Instance Method Summary collapse

Methods inherited from Base

#_add_handoff_tool, #_handoff_tools, _on_compact_callback, _on_compaction_trigger_callback, _on_trim_callback, cache_instructions, context_overhead, #context_version_cache, context_window, instructions, #invoke, max_iterations, max_output_tokens, model, on_compact, on_compaction_trigger, on_trim, provider, #run_as_child, static_knowledge, static_knowledge_sources, temperature, tool_aliases, tools

Methods included from Concerns::Suspendable

#on_approval_required, #resume

Methods included from Concerns::BeforeCompletion

included

Methods included from Concerns::Guardrailable

#add_input_guardrail, #add_output_guardrail

Methods included from Concerns::Retryable

included

Methods included from Runnable

#batch, #invoke, #trace

Instance Method Details

#stream(input, messages: [], thread_id: nil, config: {}) {|Phronomy::Agent::StreamEvent| ... } ⇒ Hash

Streaming version of #invoke for the ReAct loop. Yields StreamEvent events while the LLM-tool loop runs.

Parameters:

  • input (String, Hash)
  • messages (Array<RubyLLM::Message>) (defaults to: [])

    same as #invoke

  • thread_id (String, nil) (defaults to: nil)

    same as #invoke

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

Yields:

Returns:

  • (Hash)

    { output:, messages:, usage: }



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
95
96
97
98
99
100
101
102
103
# File 'lib/phronomy/agent/react_agent.rb', line 63

def stream(input, messages: [], thread_id: nil, config: {}, &block)
  return invoke(input, messages: messages, thread_id: thread_id, config: config) unless block

  caller_meta = {}
  caller_meta[:user_id] = config[:user_id] if config[:user_id]
  caller_meta[:session_id] = config[:session_id] if config[:session_id]

  trace("agent.invoke", input: input, **caller_meta) do |_span|
    run_input_guardrails!(input)

    max_iter = self.class.max_iterations

    messages = Array(messages).dup
    user_asked = false
    total_usage = Phronomy::TokenUsage.zero
    iterations_exhausted = true

    max_iter.times do
      response = stream_step(messages, input, user_asked: user_asked, thread_id: thread_id, config: config, &block)
      user_asked = true
      messages = response[:messages]
      total_usage += response[:usage]
      if response[:done]
        iterations_exhausted = false
        break
      end
    end

    # Fall back to the last message that carries non-nil content (same as
    # the non-streaming path above).
    output = messages.reverse.find { |m| m.content && !m.content.empty? }&.content
    run_output_guardrails!(output)

    result = {output: output, messages: messages, usage: total_usage, iterations_exhausted: iterations_exhausted}
    block.call(StreamEvent.new(type: :done, payload: result))
    [result, total_usage]
  end
rescue => e
  block&.call(StreamEvent.new(type: :error, payload: {error: e}))
  raise
end