Class: RubyPi::Agent::Core

Inherits:
Object
  • Object
show all
Includes:
EventEmitter
Defined in:
lib/ruby_pi/agent/core.rb

Overview

The main agent class. Wraps State, Loop, and EventEmitter into a cohesive interface for running agentic LLM interactions with tool use, streaming, and lifecycle hooks.

Examples:

Full lifecycle

agent = RubyPi::Agent::Core.new(
  system_prompt: "You are Olli, an AI assistant.",
  model: RubyPi::LLM.model(:gemini, "gemini-2.0-flash"),
  tools: registry,
  max_iterations: 10,
  before_tool_call: ->(tc) { puts "Calling #{tc.name}" },
  after_tool_call: ->(tc, r) { puts "Done: #{r.success?}" }
)

agent.on(:text_delta) { |d| stream.write(d[:content]) }
agent.on(:agent_end) { |_| stream.close }

result = agent.run("Create a LinkedIn post")
result = agent.continue("Make it shorter")

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from EventEmitter

#emit, #off, #on

Constructor Details

#initialize(system_prompt:, model:, tools: nil, messages: [], max_iterations: 10, transform_context: nil, before_tool_call: nil, after_tool_call: nil, compaction: nil, user_data: {}, config: nil, execution_mode: :parallel, tool_timeout: 30) ⇒ Core

Creates a new Agent instance.

Parameters:

  • system_prompt (String)

    the system-level instruction prompt

  • model (RubyPi::LLM::BaseProvider)

    the LLM provider instance

  • tools (RubyPi::Tools::Registry, nil) (defaults to: nil)

    tool registry

  • messages (Array<Hash>) (defaults to: [])

    initial conversation history

  • max_iterations (Integer) (defaults to: 10)

    max think-act-observe cycles (default: 10)

  • transform_context (Proc, nil) (defaults to: nil)

    context transform hook

  • before_tool_call (Proc, nil) (defaults to: nil)

    pre-tool-execution hook

  • after_tool_call (Proc, nil) (defaults to: nil)

    post-tool-execution hook

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

    compaction strategy

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

    arbitrary data bag for transforms/extensions

  • config (RubyPi::Configuration, nil) (defaults to: nil)

    informational handle to the per-agent configuration. Stored for inspection by transforms and extensions but does NOT override the model’s provider config —the model is already constructed by the time it reaches the agent. To use a per-agent config for actual API calls, pass it to the model factory:

    RubyPi::LLM.model(:openai, "gpt-4o", config: custom_config)
    
  • execution_mode (Symbol) (defaults to: :parallel)

    tool execution mode (:parallel or :sequential, default: :parallel)

  • tool_timeout (Numeric) (defaults to: 30)

    per-tool execution timeout in seconds (default: 30)



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
# File 'lib/ruby_pi/agent/core.rb', line 89

def initialize(
  system_prompt:,
  model:,
  tools: nil,
  messages: [],
  max_iterations: 10,
  transform_context: nil,
  before_tool_call: nil,
  after_tool_call: nil,
  compaction: nil,
  user_data: {},
  config: nil,
  execution_mode: :parallel,
  tool_timeout: 30
)
  @state = State.new(
    system_prompt: system_prompt,
    model: model,
    tools: tools,
    messages: messages,
    max_iterations: max_iterations,
    transform_context: transform_context,
    before_tool_call: before_tool_call,
    after_tool_call: after_tool_call,
    user_data: user_data
  )
  @compaction = compaction
  @extensions = []
  @config = config
  @execution_mode = execution_mode
  @tool_timeout = tool_timeout
end

Instance Attribute Details

#configRubyPi::Configuration? (readonly)

Returns per-agent configuration handle exposed for inspection only — see #config= caveats below. The actual provider config is resolved at model construction time:

custom = RubyPi::Configuration.new
custom.openai_api_key = "sk-..."
model = RubyPi::LLM.model(:openai, "gpt-4o", config: custom)
agent = RubyPi::Agent.new(model: model, config: custom, ...)

Passing ‘config:` to `Agent.new` does NOT retroactively change the model’s behavior — it is informational, intended for inspection by transforms and extensions. To override provider config you must pass ‘config:` to the model factory as shown above.

Returns:

  • (RubyPi::Configuration, nil)

    per-agent configuration handle exposed for inspection only — see #config= caveats below. The actual provider config is resolved at model construction time:

    custom = RubyPi::Configuration.new
    custom.openai_api_key = "sk-..."
    model = RubyPi::LLM.model(:openai, "gpt-4o", config: custom)
    agent = RubyPi::Agent.new(model: model, config: custom, ...)
    

    Passing ‘config:` to `Agent.new` does NOT retroactively change the model’s behavior — it is informational, intended for inspection by transforms and extensions. To override provider config you must pass ‘config:` to the model factory as shown above.



64
65
66
# File 'lib/ruby_pi/agent/core.rb', line 64

def config
  @config
end

#extensionsArray<Class> (readonly)

Returns registered extension classes for introspection.

Returns:

  • (Array<Class>)

    registered extension classes for introspection



49
50
51
# File 'lib/ruby_pi/agent/core.rb', line 49

def extensions
  @extensions
end

#stateRubyPi::Agent::State (readonly)

Returns the agent’s mutable state.

Returns:



46
47
48
# File 'lib/ruby_pi/agent/core.rb', line 46

def state
  @state
end

Instance Method Details

#continue(prompt) ⇒ RubyPi::Agent::Result

Continues the conversation with a follow-up user message. Preserves the existing conversation history and appends the new prompt before resuming the loop.

Issue #16: Uses the encapsulated reset_iteration! method instead of the old approach that bypassed encapsulation and was fragile.

Parameters:

  • prompt (String)

    the follow-up user message

Returns:



149
150
151
152
153
# File 'lib/ruby_pi/agent/core.rb', line 149

def continue(prompt)
  @state.reset_iteration!
  @state.add_message(role: :user, content: prompt)
  execute_loop
end

#effective_configRubyPi::Configuration

Returns the effective configuration for this agent. If a per-agent config was provided, returns that; otherwise falls back to the global RubyPi.configuration.

Returns:



184
185
186
# File 'lib/ruby_pi/agent/core.rb', line 184

def effective_config
  @config || RubyPi.configuration
end

#run(prompt) ⇒ RubyPi::Agent::Result

Runs the agent with an initial user prompt. Adds the prompt to the conversation history, executes the think-act-observe loop, emits :agent_end when done, and returns the result.

Issue #16: Resets the iteration counter at the start of each run() call using the encapsulated reset_iteration! method. Previously, the counter was never reset on run(), so a second call to run() on the same agent instance could immediately trip max_iterations_reached?.

Parameters:

  • prompt (String)

    the user’s initial message

Returns:



133
134
135
136
137
# File 'lib/ruby_pi/agent/core.rb', line 133

def run(prompt)
  @state.reset_iteration!
  @state.add_message(role: :user, content: prompt)
  execute_loop
end

#use(extension_class) ⇒ void

This method returns an undefined value.

Registers an extension with this agent. The extension’s hooks are automatically subscribed to the agent’s events.

Parameters:

  • extension_class (Class)

    a subclass of RubyPi::Extensions::Base

Raises:

  • (ArgumentError)

    if the argument is not a valid extension class



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/ruby_pi/agent/core.rb', line 161

def use(extension_class)
  unless extension_class.respond_to?(:hooks)
    raise ArgumentError,
          "Expected an extension class with a .hooks method, got #{extension_class.inspect}"
  end

  # Subscribe each hook to the corresponding event
  extension_class.hooks.each do |event, handlers|
    handlers.each do |handler|
      on(event) do |data|
        handler.call(data, self)
      end
    end
  end

  @extensions << extension_class
end