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, memory, 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, CalculatorTool
  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, persistence: nil, metadata: {}, load_existing: false) ⇒ Base

Returns a new instance of Base.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/phronomy/agent/base.rb', line 373

def initialize(
  agent_id: SecureRandom.uuid,
  context: nil,
  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, metadata: )
  end
end

Instance Attribute Details

#agent_idObject (readonly)

Returns the value of attribute agent_id.



371
372
373
# File 'lib/phronomy/agent/base.rb', line 371

def agent_id
  @agent_id
end

#persistenceObject (readonly)

Returns the value of attribute persistence.



371
372
373
# File 'lib/phronomy/agent/base.rb', line 371

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.



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/phronomy/agent/base.rb', line 322

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

  # Walk ancestors to support anonymous runtime subclasses and abstract bases.
  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



350
351
352
353
354
355
356
357
358
# File 'lib/phronomy/agent/base.rb', line 350

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



360
361
362
363
364
365
366
367
368
# File 'lib/phronomy/agent/base.rb', line 360

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.

Only has an effect when the agent also declares provider :anthropic. The cache_control field is provider-specific (the format differs between Anthropic direct, Bedrock, etc.), so the agent must explicitly declare its provider via the DSL rather than having it inferred from the model name.

Examples:

class MyAgent < Phronomy::Agent::Base
  provider :anthropic
  cache_instructions true
end


261
262
263
264
265
266
267
# File 'lib/phronomy/agent/base.rb', line 261

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

.context_overhead(val = nil) ⇒ Object

Tokens reserved in the legacy build_context path only. Manifest-first assembly ignores this value because ContextAssembler estimates actual mandatory content for each LLM Call.

Examples:

class MyAgent < Phronomy::Agent::Base
  context_overhead 500
end


312
313
314
315
316
317
318
# File 'lib/phronomy/agent/base.rb', line 312

def context_overhead(val = nil)
  if val.nil?
    @context_overhead || 0
  else
    @context_overhead = val.to_i
  end
end

.context_window(val = nil) ⇒ Object

Overrides the context window size used for token budget calculations. When set, this value takes precedence over the RubyLLM model registry, which is useful for locally-hosted models (e.g. LM Studio) where the actually-loaded context length may differ from the catalogue value.

Examples:

class MyAgent < Phronomy::Agent::Base
  context_window 4096
end


295
296
297
298
299
300
301
# File 'lib/phronomy/agent/base.rb', line 295

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, persistence: nil, metadata: {}) ⇒ Object



342
343
344
# File 'lib/phronomy/agent/base.rb', line 342

def create(agent_id: SecureRandom.uuid, context: nil, persistence: nil, metadata: {})
  new(agent_id: agent_id, context: context, 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



346
347
348
# File 'lib/phronomy/agent/base.rb', line 346

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)


185
186
187
188
189
190
191
# File 'lib/phronomy/agent/base.rb', line 185

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.

Examples:

class MyAgent < Phronomy::Agent::Base
  max_output_tokens 4096
end


277
278
279
280
281
282
283
# File 'lib/phronomy/agent/base.rb', line 277

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)


148
149
150
151
152
153
154
155
# File 'lib/phronomy/agent/base.rb', line 148

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

.static_knowledge(*sources) ⇒ Object

Registers one or more static knowledge sources on the agent class. Static source content is fetched and memoized at the class level the first time invoke is called. The cache persists for the lifetime of the process; call static_knowledge_refresh! to force a reload.

Examples:

class PolicyAgent < Phronomy::Agent::Base
  static_knowledge Phronomy::Agent::Context::Knowledge::StaticKnowledge.new(POLICY_TEXT)
end

Parameters:



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

def static_knowledge(*sources)
  @static_knowledge_sources = sources.flatten
  # Invalidate the cached chunks so the new sources are fetched on
  # the next call to static_knowledge_chunks.
  @static_knowledge_chunks = nil
end

.static_knowledge_chunksArray<Hash>

Returns the fetched content from all static knowledge sources. Results are cached at the class level so that each source is fetched only once regardless of how many times the agent is invoked.

Returns:

  • (Array<Hash>)


223
224
225
226
227
# File 'lib/phronomy/agent/base.rb', line 223

def static_knowledge_chunks
  @static_knowledge_chunks ||= static_knowledge_sources.flat_map { |ks|
    ks.fetch(query: nil)
  }
end

.static_knowledge_refresh!nil

Clears the class-level knowledge cache so that the next invoke call re-fetches content from all registered static knowledge sources.

Call this method when the underlying knowledge source has been updated at runtime (e.g. a file was rewritten, a DB record changed) and you want the agent to pick up the new content without restarting the process.

Examples:

Refresh after updating a knowledge file

MyAgent.static_knowledge_refresh!

Returns:

  • (nil)


241
242
243
# File 'lib/phronomy/agent/base.rb', line 241

def static_knowledge_refresh!
  @static_knowledge_chunks = nil
end

.static_knowledge_sourcesArray<Phronomy::Agent::Context::Knowledge::Base>

Returns the registered static knowledge sources.



214
215
216
# File 'lib/phronomy/agent/base.rb', line 214

def static_knowledge_sources
  @static_knowledge_sources || []
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)


167
168
169
170
171
172
173
# File 'lib/phronomy/agent/base.rb', line 167

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

.tool_aliasesHash{Class => String}

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

Returns:

  • (Hash{Class => String})


127
128
129
130
131
132
133
134
# File 'lib/phronomy/agent/base.rb', line 127

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

.tools(*args) ⇒ Object

Registers tool classes for this agent.

Accepts either a splat of classes (backward-compatible) or a Hash mapping each class to an explicit alias name (String) or nil (use tool's own name). The alias form is useful when two tools share the same auto-generated name (e.g. two SearchTool classes from different modules).

Examples:

Splat form (no alias)

tools WeatherTool, TimeTool

Hash form (with optional per-tool alias)

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


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/phronomy/agent/base.rb', line 104

def tools(*args)
  if args.empty?
    if instance_variable_defined?(:@tools)
      return @tools
    end
    return superclass.respond_to?(:tools) ? superclass.tools : []
  end

  if args.length == 1 && args.first.is_a?(Hash)
    hash = args.first
    @tools = hash.keys
    @tool_aliases = hash.transform_values { |v| v&.to_s }.reject { |_, v| v.nil? }
  else
    @tools = args
    @tool_aliases = {}
  end
end

Instance Method Details

#__replace_root(root) ⇒ Object

Internal hook used after a successful Persistence transaction.



462
463
464
# File 'lib/phronomy/agent/base.rb', line 462

def __replace_root(root)
  @root = root
end

#_add_handoff_tool(tool_class) ⇒ self

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Registers an anonymous handoff tool class on this agent instance. Called by Runner during construction when routes are configured.

Parameters:

Returns:

  • (self)


556
557
558
559
560
# File 'lib/phronomy/agent/base.rb', line 556

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

#_handoff_toolsArray<Class>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns handoff tool classes registered on this instance by Runner.

Returns:

  • (Array<Class>)


565
566
567
# File 'lib/phronomy/agent/base.rb', line 565

def _handoff_tools
  @_handoff_tools || []
end

#agent_rootObject



398
399
400
# File 'lib/phronomy/agent/base.rb', line 398

def agent_root
  @root
end

#clear_memory!Object



420
421
422
423
424
425
426
427
428
# File 'lib/phronomy/agent/base.rb', line 420

def clear_memory!
  mutate_context!(:memory_cleared) do |root|
    root.with(
      agent_revision: root.agent_revision + 1,
      context_revision: root.context_revision + 1,
      memory_generation: root.memory_generation + 1
    )
  end
end

#clear_transcript!Object



410
411
412
413
414
415
416
417
418
# File 'lib/phronomy/agent/base.rb', line 410

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



441
442
443
444
445
446
447
448
# File 'lib/phronomy/agent/base.rb', line 441

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



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

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

#on_tool_approval_required(&block) ⇒ self

Registers a non-blocking Application notification listener.

Returns:

  • (self)

Raises:

  • (ArgumentError)


584
585
586
587
588
589
# File 'lib/phronomy/agent/base.rb', line 584

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



450
451
452
453
454
455
456
457
458
459
# File 'lib/phronomy/agent/base.rb', line 450

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



430
431
432
433
434
435
436
437
438
439
# File 'lib/phronomy/agent/base.rb', line 430

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,
      memory_generation: root.memory_generation + 1
    )
  end
end

#tool_approval_policy(&block) ⇒ self

Registers the final Agent/Application authorization policy. The block runs on the Runtime authorization pool and must return :allow, :require_approval, or :reject.

Returns:

  • (self)

Raises:

  • (ArgumentError)


574
575
576
577
578
579
# File 'lib/phronomy/agent/base.rb', line 574

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



406
407
408
# File 'lib/phronomy/agent/base.rb', line 406

def transcript
  journal_projection.transcript_records
end