Class: Phronomy::Agent::Base

Inherits:
Object
  • Object
show all
Includes:
AsyncEventApi, Concerns::BeforeLLMInput, Concerns::ErrorTranslation, Concerns::Filterable, Runnable
Defined in:
lib/phronomy/agent/base.rb

Overview

Base class for all Phronomy agents.

Subclass this to create a conversational agent powered by an LLM. DSL class methods configure the model, instructions, tools, and execution hooks. Instance methods handle invocation.

Examples:

Minimal agent

class GreetingAgent < Phronomy::Agent::Base
  agent_definition id: "greeting-agent", version: 1
  model "gpt-4o-mini"
  instructions "You are a friendly greeter."
end
result = GreetingAgent.new.invoke("Hello!")
puts result[:output]

Agent with tools

class ResearchAgent < Phronomy::Agent::Base
  agent_definition id: "research-agent", version: 1
  model "gpt-4o"
  instructions "You are a research assistant."
  tools(WebSearchTool => nil, CalculatorTool => nil)
  max_iterations 15
end

Direct Known Subclasses

MultiAgent::Orchestrator

Instance Attribute Summary collapse

Attributes included from Concerns::BeforeLLMInput

#before_llm_input

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Concerns::BeforeLLMInput

included

Methods included from Concerns::Filterable

#add_input_filter, #add_output_filter, #add_tool_result_filter, included

Methods included from Runnable

#batch, #invoke, #stream, #trace

Methods included from AsyncEventApi

#approve, #approve_async, #invoke, #invoke_async, #stream, #stream_async

Constructor Details

#initialize(agent_id: SecureRandom.uuid, context: nil, knowledge: [], persistence: nil, metadata: {}, load_existing: false) ⇒ Base

Returns a new instance of Base.



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/phronomy/agent/base.rb', line 278

def initialize(
  agent_id: SecureRandom.uuid,
  context: nil,
  knowledge: [],
  persistence: nil,
  metadata: {},
  load_existing: false
)
  @persistence = persistence || Phronomy::Persistence::InMemory.new
  @agent_id = agent_id.to_s.freeze
  @root = if load_existing
    loaded = @persistence.agents.load(@agent_id)
    definition = self.class.agent_definition
    unless loaded.agent_definition_id == definition.fetch(:id) &&
        loaded.definition_version == definition.fetch(:version)
      raise Phronomy::ConfigurationError,
        "Agent definition mismatch for #{@agent_id}: stored " \
        "#{loaded.agent_definition_id}@#{loaded.definition_version}, runtime " \
        "#{definition.fetch(:id)}@#{definition.fetch(:version)}"
    end
    loaded
  else
    create_agent_root!(context: context, knowledge: knowledge, metadata: )
  end
end

Instance Attribute Details

#agent_idObject (readonly)

Returns the value of attribute agent_id.



276
277
278
# File 'lib/phronomy/agent/base.rb', line 276

def agent_id
  @agent_id
end

#persistenceObject (readonly)

Returns the value of attribute persistence.



276
277
278
# File 'lib/phronomy/agent/base.rb', line 276

def persistence
  @persistence
end

Class Method Details

.agent_definition(id: nil, version: nil) ⇒ Object

Defines or reads the stable Agent definition identity. Subclass with no explicit declaration inherits the parent's definition.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/phronomy/agent/base.rb', line 222

def agent_definition(id: nil, version: nil)
  if id || version
    raise ArgumentError, "agent_definition requires id: and version:" unless id && version
    @agent_definition = {id: id.to_s.freeze, version: Integer(version)}.freeze
  end
  return @agent_definition if @agent_definition

  klass = superclass
  while klass.respond_to?(:agent_definition, true) &&
      klass < Phronomy::Agent::Base
    defn = klass.instance_variable_get(:@agent_definition)
    return defn if defn
    klass = klass.superclass
  end

  raise Phronomy::ConfigurationError,
    "#{name || self} must declare agent_definition id: ..., version: ..."
end

.approve(execution_id, approval_request_id:, persistence:, approved: true, config: {}) ⇒ Object



255
256
257
258
259
260
261
262
263
# File 'lib/phronomy/agent/base.rb', line 255

def approve(execution_id, approval_request_id:, persistence:, approved: true, config: {})
  approve_async(
    execution_id,
    approval_request_id: approval_request_id,
    approved: approved,
    config: config,
    persistence: persistence
  ).wait_result
end

.approve_async(execution_id, approval_request_id:, persistence:, approved: true, config: {}) ⇒ Object



265
266
267
268
269
270
271
272
273
# File 'lib/phronomy/agent/base.rb', line 265

def approve_async(execution_id, approval_request_id:, persistence:, approved: true, config: {})
  execution = persistence.executions.load(execution_id)
  load(execution.agent_id, persistence: persistence).approve_async(
    execution_id,
    approval_request_id: approval_request_id,
    approved: approved,
    config: config
  )
end

.cache_instructions(enabled = nil) ⇒ Object

When enabled, attaches Anthropic prompt-cache markers to the system message so that the fixed instructions are served from cache on subsequent turns, reducing input-token costs.



191
192
193
194
195
196
197
# File 'lib/phronomy/agent/base.rb', line 191

def cache_instructions(enabled = nil)
  if enabled.nil?
    @cache_instructions
  else
    @cache_instructions = enabled
  end
end

.context_window(val = nil) ⇒ Object

Overrides the context window size used for token budget calculations.



212
213
214
215
216
217
218
# File 'lib/phronomy/agent/base.rb', line 212

def context_window(val = nil)
  if val.nil?
    @context_window
  else
    @context_window = val.to_i
  end
end

.create(agent_id: SecureRandom.uuid, context: nil, knowledge: [], persistence: nil, metadata: {}) ⇒ Object



241
242
243
244
245
246
247
248
249
# File 'lib/phronomy/agent/base.rb', line 241

def create(agent_id: SecureRandom.uuid, context: nil, knowledge: [], persistence: nil, metadata: {})
  new(
    agent_id: agent_id,
    context: context,
    knowledge: knowledge,
    persistence: persistence,
    metadata: 
  )
end

.instructions(text = nil) { ... } ⇒ String, ...

Sets or reads the system instructions for this agent. Accepts a String, a Context::Instruction::PromptTemplate, or a block (Proc). When used as a reader (no argument, no block), returns the stored value.

Examples:

String instructions

class MyAgent < Phronomy::Agent::Base
  instructions "You are a helpful assistant."
end

Block instructions

class MyAgent < Phronomy::Agent::Base
  instructions { |input| "Answer in #{input[:lang]}." }
end

Parameters:

Yields:

  • optionally provide instructions as a block

Returns:



78
79
80
81
82
83
84
85
# File 'lib/phronomy/agent/base.rb', line 78

def instructions(text = nil, &block)
  if text || block_given?
    @instructions = text || block
  else
    return @instructions if instance_variable_defined?(:@instructions)
    superclass.respond_to?(:instructions) ? superclass.instructions : nil
  end
end

.load(agent_id, persistence:) ⇒ Object



251
252
253
# File 'lib/phronomy/agent/base.rb', line 251

def load(agent_id, persistence:)
  new(agent_id: agent_id, persistence: persistence, load_existing: true)
end

.max_iterations(val = nil) ⇒ Integer

Sets or reads the maximum number of LLM call cycles for ReAct agents. Each tool call and follow-up counts as one iteration. Defaults to 10.

Examples:

class MyAgent < Phronomy::Agent::Base
  max_iterations 5
end

Parameters:

  • val (Integer, nil) (defaults to: nil)

Returns:

  • (Integer)


179
180
181
182
183
184
185
# File 'lib/phronomy/agent/base.rb', line 179

def max_iterations(val = nil)
  if val
    @max_iterations = val
  else
    @max_iterations || 10
  end
end

.max_output_tokens(val = nil) ⇒ Object

Tokens to reserve for the model's output. When nil, the model's max_output_tokens from the registry is used.



202
203
204
205
206
207
208
# File 'lib/phronomy/agent/base.rb', line 202

def max_output_tokens(val = nil)
  if val.nil?
    @max_output_tokens
  else
    @max_output_tokens = val.to_i
  end
end

.model(name = nil) ⇒ String?

Sets or reads the LLM model identifier for this agent. When called without an argument, returns the stored model or the global default from Phronomy.configuration.

Examples:

class MyAgent < Phronomy::Agent::Base
  model "gpt-4o"
end

Parameters:

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

    model identifier (e.g. "gpt-4o", "claude-3-5-sonnet")

Returns:

  • (String, nil)

    the model name when used as a reader



54
55
56
57
58
59
60
# File 'lib/phronomy/agent/base.rb', line 54

def model(name = nil)
  if name
    @model = name
  else
    @model || Phronomy.configuration.default_model
  end
end

.provider(name = nil) ⇒ Symbol?

Sets or reads the LLM provider for this agent. Required when using a model not registered in RubyLLM's model registry (e.g. locally-hosted models via LM Studio or Ollama).

Examples:

class MyAgent < Phronomy::Agent::Base
  model "openai/gpt-oss-20b"
  provider :openai
end

Parameters:

  • name (Symbol, nil) (defaults to: nil)

    e.g. :openai, :anthropic, :ollama

Returns:

  • (Symbol, nil)


142
143
144
145
146
147
148
149
# File 'lib/phronomy/agent/base.rb', line 142

def provider(name = nil)
  if name
    @provider = name
  else
    return @provider if instance_variable_defined?(:@provider)
    superclass.respond_to?(:provider) ? superclass.provider : nil
  end
end

.temperature(val = nil) ⇒ Float?

Sets or reads the sampling temperature sent to the LLM. When nil, the provider's default is used.

Examples:

class MyAgent < Phronomy::Agent::Base
  temperature 0.2
end

Parameters:

  • val (Float, nil) (defaults to: nil)

    temperature (0.0 to 2.0 depending on provider)

Returns:

  • (Float, nil)


161
162
163
164
165
166
167
# File 'lib/phronomy/agent/base.rb', line 161

def temperature(val = nil)
  if val
    @temperature = val
  else
    @temperature
  end
end

.tool_aliasesHash{Class => String}

Returns the alias map registered via .tools. Merges parent class aliases so subclasses inherit their parent's mappings. Subclass-specific aliases take precedence over parent aliases.

Returns:

  • (Hash{Class => String})


121
122
123
124
125
126
127
128
# File 'lib/phronomy/agent/base.rb', line 121

def tool_aliases
  own = @tool_aliases || {}
  if superclass.respond_to?(:tool_aliases)
    superclass.tool_aliases.merge(own)
  else
    own
  end
end

.tools(definitions = nil) ⇒ Object

Registers tool classes for this agent.

The setter accepts one Hash mapping each Tool class to an explicit alias name (String) or nil (use the Tool's own name). Calling without an argument returns the registered Tool classes.

Examples:

tools(
  Weather::SearchTool => "weather_search",
  Places::SearchTool  => "places_search",
  CurrentTimeTool     => nil
)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/phronomy/agent/base.rb', line 100

def tools(definitions = nil)
  if definitions.nil?
    return @tools if instance_variable_defined?(:@tools)
    return superclass.respond_to?(:tools) ? superclass.tools : []
  end

  unless definitions.is_a?(Hash)
    raise ArgumentError,
      "tools expects a Hash of ToolClass => alias_or_nil"
  end

  @tools = definitions.keys
  @tool_aliases = definitions.transform_values { |value| value&.to_s }
    .reject { |_, value| value.nil? }
end

Instance Method Details

#__replace_root(root) ⇒ Object

Internal hook used after a successful Persistence transaction.



401
402
403
# File 'lib/phronomy/agent/base.rb', line 401

def __replace_root(root)
  @root = root
end

#_add_handoff_tool(tool_class) ⇒ Object



516
517
518
519
520
# File 'lib/phronomy/agent/base.rb', line 516

def _add_handoff_tool(tool_class)
  @_handoff_tools ||= []
  @_handoff_tools << tool_class
  self
end

#_handoff_toolsObject



522
523
524
# File 'lib/phronomy/agent/base.rb', line 522

def _handoff_tools
  @_handoff_tools || []
end

#add_knowledge(content, metadata: {}) ⇒ Object

Appends persistent Knowledge to the Agent Journal. Knowledge is an optional Context candidate; it is not part of #transcript.



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/phronomy/agent/base.rb', line 339

def add_knowledge(content, metadata: {})
  next_root = nil
  persistence.transaction do |tx|
    tx.executions.assert_idle!(agent_id)
    current = tx.agents.load(agent_id)
    record = build_knowledge_record(
      tx: tx,
      root: current,
      content: content,
      metadata: 
    )
    appended = tx.journals.append(
      agent_id,
      expected_position: current.journal_position,
      records: [record]
    )
    next_root = current.with(
      agent_revision: current.agent_revision + 1,
      context_revision: current.context_revision + 1,
      journal_position: current.journal_position + appended.length
    )
    tx.agents.save(
      agent_id,
      expected_revision: current.agent_revision,
      root: next_root
    )
  end
  @root = next_root
  self
end

#agent_rootObject



304
305
306
# File 'lib/phronomy/agent/base.rb', line 304

def agent_root
  @root
end

#clear_knowledge!Object

Logically clears all persistent Knowledge registered before this point. Raw Journal records remain append-only and are not deleted.



328
329
330
331
332
333
334
335
# File 'lib/phronomy/agent/base.rb', line 328

def clear_knowledge!
  mutate_context!(:knowledge_cleared) do |root|
    root.with(
      agent_revision: root.agent_revision + 1,
      context_revision: root.context_revision + 1
    )
  end
end

#clear_transcript!Object



316
317
318
319
320
321
322
323
324
# File 'lib/phronomy/agent/base.rb', line 316

def clear_transcript!
  mutate_context!(:transcript_cleared) do |root|
    root.with(
      agent_revision: root.agent_revision + 1,
      context_revision: root.context_revision + 1,
      transcript_generation: root.transcript_generation + 1
    )
  end
end

#close!Object



380
381
382
383
384
385
386
387
# File 'lib/phronomy/agent/base.rb', line 380

def close!
  mutate_context!(:agent_closed, context_affecting: false) do |root|
    root.with(
      agent_revision: root.agent_revision + 1,
      lifecycle_status: :closed
    )
  end
end

#journal_projectionObject



308
309
310
# File 'lib/phronomy/agent/base.rb', line 308

def journal_projection
  Agent::JournalProjection.new(persistence: persistence, agent_root: @root)
end

#on_tool_approval_required(&block) ⇒ Object

Raises:

  • (ArgumentError)


533
534
535
536
537
538
# File 'lib/phronomy/agent/base.rb', line 533

def on_tool_approval_required(&block)
  raise ArgumentError, "on_tool_approval_required requires a block" unless block

  _approval_configuration_mutex.synchronize { @tool_approval_listener = block }
  self
end

#purge!Object



389
390
391
392
393
394
395
396
397
398
# File 'lib/phronomy/agent/base.rb', line 389

def purge!
  persistence.transaction do |tx|
    tx.executions.assert_idle!(agent_id)
    tx.journals.delete(agent_id)
    tx.executions.delete_for_agent(agent_id)
    tx.agents.delete(agent_id)
  end
  @root = nil
  true
end

#reset_context!Object



370
371
372
373
374
375
376
377
378
# File 'lib/phronomy/agent/base.rb', line 370

def reset_context!
  mutate_context!(:context_reset) do |root|
    root.with(
      agent_revision: root.agent_revision + 1,
      context_revision: root.context_revision + 1,
      transcript_generation: root.transcript_generation + 1
    )
  end
end

#tool_approval_policy(&block) ⇒ Object

Raises:

  • (ArgumentError)


526
527
528
529
530
531
# File 'lib/phronomy/agent/base.rb', line 526

def tool_approval_policy(&block)
  raise ArgumentError, "tool_approval_policy requires a block" unless block

  _approval_configuration_mutex.synchronize { @tool_approval_policy = block }
  self
end

#transcriptObject



312
313
314
# File 'lib/phronomy/agent/base.rb', line 312

def transcript
  journal_projection.transcript_records
end