Class: Phronomy::Agent::Base

Inherits:
Object
  • Object
show all
Includes:
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 retry behaviour. Instance methods handle invocation.

Examples:

Minimal agent

class GreetingAgent < Phronomy::Agent::Base
  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
  model "gpt-4o"
  instructions "You are a research assistant."
  tools WebSearchTool, CalculatorTool
  max_iterations 15
end

Direct Known Subclasses

ReactAgent

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Runnable

#batch, #trace

Class Attribute Details

._retry_policyHash? (readonly)

Returns the configured retry policy, or nil when none is set.

Returns:

  • (Hash, nil)


180
181
182
# File 'lib/phronomy/agent/base.rb', line 180

def _retry_policy
  @_retry_policy
end

._sleep_proc#call

Injectable sleep callable for testing (shared with Tool::Base pattern).

Returns:

  • (#call)


184
185
186
# File 'lib/phronomy/agent/base.rb', line 184

def _sleep_proc
  @_sleep_proc || method(:sleep)
end

Instance Attribute Details

#before_completion#call?

Instance-level before_completion hook. When set, takes precedence over the class-level hook for this specific agent instance only.

Returns:

  • (#call, nil)


379
380
381
# File 'lib/phronomy/agent/base.rb', line 379

def before_completion
  @before_completion
end

Class Method Details

._before_completion#call?

Returns:

  • (#call, nil)


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

def _before_completion
  @before_completion
end

._on_compact_callbackProc?

Returns:

  • (Proc, nil)


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

def _on_compact_callback
  @on_compact_callback
end

._on_compaction_trigger_callbackProc?

Returns:

  • (Proc, nil)


255
256
257
# File 'lib/phronomy/agent/base.rb', line 255

def _on_compaction_trigger_callback
  @on_compaction_trigger_callback
end

._on_trim_callbackProc?

Returns:

  • (Proc, nil)


232
233
234
# File 'lib/phronomy/agent/base.rb', line 232

def _on_trim_callback
  @on_trim_callback
end

.before_completion(callable = nil) ⇒ #call?

Sets or reads the class-level before_completion hook. The hook is called before every LLM request for instances of this class. Receives a Phronomy::Agent::BeforeCompletionContext; must return a Hash of params to merge into the LLM call, or nil to pass through unchanged.

Examples:

class MyAgent < Phronomy::Agent::Base
  before_completion ->(ctx) { { temperature: 0.2 } }
end

Parameters:

  • callable (#call, nil) (defaults to: nil)

    lambda/proc to register, or nil to clear

Returns:

  • (#call, nil)


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

def before_completion(callable = nil)
  if callable.nil? && !block_given?
    @before_completion
  else
    @before_completion = callable
  end
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


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

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

.context_overhead(val = nil) ⇒ Object

Tokens reserved for the system prompt + tool definitions overhead. Subtract this from the context window before computing the memory budget.

Examples:

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


343
344
345
346
347
348
349
# File 'lib/phronomy/agent/base.rb', line 343

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


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

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

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

Sets or reads the system instructions for this agent. Accepts a String, a 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:



65
66
67
68
69
70
71
# File 'lib/phronomy/agent/base.rb', line 65

def instructions(text = nil, &block)
  if text || block_given?
    @instructions = text || block
  else
    @instructions
  end
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)


155
156
157
158
159
160
161
# File 'lib/phronomy/agent/base.rb', line 155

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


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

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



42
43
44
45
46
47
48
# File 'lib/phronomy/agent/base.rb', line 42

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

.on_compact {|ctx| ... } ⇒ Object

Registers a callback that performs the actual compaction when the +on_compaction_trigger+ callback fires. The block receives a Context::CompactionContext and should call +ctx.compact+ to specify which messages to summarise.

Examples:

Replace the first 4 messages with a short summary

on_compact do |ctx|
  ctx.compact(0..3) do |elements|
    texts = elements.map { |e| e[:message].content }.join(" | ")
    "Earlier conversation summary: #{texts}"
  end
end

Yields:

  • (ctx)

    Phronomy::Context::CompactionContext



272
273
274
# File 'lib/phronomy/agent/base.rb', line 272

def on_compact(&block)
  @on_compact_callback = block
end

.on_compaction_trigger {|ctx| ... } ⇒ Boolean

Registers a callback that decides whether compaction should run. Evaluated before every LLM call (after on_trim). If the block returns truthy AND an +on_compact+ callback is also registered, the compact pipeline is executed.

The block receives a read-only Context::TriggerContext.

Examples:

Trigger when messages exceed 70% of token budget

on_compaction_trigger do |ctx|
  limit = ctx.budget&.available(used: 0) || Float::INFINITY
  ctx.total_tokens > limit * 0.7
end

Yields:

  • (ctx)

    Phronomy::Context::TriggerContext

Returns:

  • (Boolean)

    truthy → run on_compact; falsy → skip



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

def on_compaction_trigger(&block)
  @on_compaction_trigger_callback = block
end

.on_trim {|ctx| ... } ⇒ Object

Registers a callback that is invoked before every LLM call so the application can remove stale or irrelevant messages from the conversation history.

The block receives a Context::TrimContext and may call +ctx.remove(seqs)+ to drop messages by seq number. Changes affect only the current invocation; the underlying memory store is unchanged.

Examples:

Drop the oldest message when over 80% of budget is used

on_trim do |ctx|
  limit = ctx.budget&.available(used: 0) || Float::INFINITY
  ctx.remove(ctx.message_elements.first[:seq]) if ctx.total_tokens > limit * 0.8
end

Yields:

  • (ctx)

    Phronomy::Context::TrimContext



227
228
229
# File 'lib/phronomy/agent/base.rb', line 227

def on_trim(&block)
  @on_trim_callback = block
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)


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

def provider(name = nil)
  if name
    @provider = name
  else
    @provider
  end
end

.retry_policy(times: 0, wait: 0, base: 1.0) ⇒ Object

Configures a retry policy that wraps the full #invoke call. GuardrailError is never retried regardless of this setting.

Examples:

class MyAgent < Phronomy::Agent::Base
  retry_policy times: 2, wait: :exponential, base: 1.0
end

Parameters:

  • times (Integer) (defaults to: 0)

    maximum retry attempts (default: 0)

  • wait (Symbol, Numeric) (defaults to: 0)

    :exponential, :linear, or a fixed Float

  • base (Float) (defaults to: 1.0)

    base wait time in seconds (default: 1.0)



174
175
176
# File 'lib/phronomy/agent/base.rb', line 174

def retry_policy(times: 0, wait: 0, base: 1.0)
  @_retry_policy = {times: times, wait: wait, base: base}
end

.static_knowledge(*sources) ⇒ Object

Registers one or more static knowledge sources on the agent class. Static sources are fetched once per agent instance and their content is cached in ContextVersionCache keyed by a fingerprint of the instruction text + source content. The cache is invalidated automatically when the fingerprint changes (e.g. because a source was updated).

Examples:

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

Parameters:



203
204
205
# File 'lib/phronomy/agent/base.rb', line 203

def static_knowledge(*sources)
  @static_knowledge_sources = sources.flatten
end

.static_knowledge_sourcesArray<Phronomy::KnowledgeSource::Base>

Returns the registered static knowledge sources.



209
210
211
# File 'lib/phronomy/agent/base.rb', line 209

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)


138
139
140
141
142
143
144
# File 'lib/phronomy/agent/base.rb', line 138

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.

Returns:

  • (Hash{Class => String})


106
107
108
# File 'lib/phronomy/agent/base.rb', line 106

def tool_aliases
  @tool_aliases ||= {}
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
)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/phronomy/agent/base.rb', line 89

def tools(*args)
  if args.empty?
    return @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

#_add_handoff_tool(tool_class) ⇒ self

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

Parameters:

Returns:

  • (self)


385
386
387
388
389
# File 'lib/phronomy/agent/base.rb', line 385

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

#_handoff_toolsArray<Class>

Returns handoff tool classes registered on this instance by Runner.

Returns:

  • (Array<Class>)


393
394
395
# File 'lib/phronomy/agent/base.rb', line 393

def _handoff_tools
  @_handoff_tools || []
end

#add_input_guardrail(guardrail) ⇒ Object

Attach a guardrail that validates input before every #invoke call.

Parameters:



463
464
465
466
467
# File 'lib/phronomy/agent/base.rb', line 463

def add_input_guardrail(guardrail)
  @input_guardrails ||= []
  @input_guardrails << guardrail
  self
end

#add_output_guardrail(guardrail) ⇒ Object

Attach a guardrail that validates output before it is returned.

Parameters:



471
472
473
474
475
# File 'lib/phronomy/agent/base.rb', line 471

def add_output_guardrail(guardrail)
  @output_guardrails ||= []
  @output_guardrails << guardrail
  self
end

#context_version_cacheObject

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 the Context::ContextVersionCache for the current thread.



479
480
481
# File 'lib/phronomy/agent/base.rb', line 479

def context_version_cache
  (Thread.current[:phronomy_context_version_caches] ||= {})[object_id]
end

#invoke(input, config: {}) ⇒ Hash

Invokes the agent with the given input and returns a result Hash. Applies the retry policy configured via retry_policy when transient errors occur. GuardrailError is never retried.

Examples:

result = MyAgent.new.invoke("What is Ruby?")
puts result[:output]

Parameters:

  • input (String, Hash)

    the user message; a Hash may supply +:message+, +:query+, or +:user+ as the text key, plus any template variables consumed by the configured instructions template.

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

    runtime options: +:memory+ (Memory::ConversationManager) — memory backend +:thread_id+ (+String+) — conversation thread identifier +:user_id+ (+String+, optional) — caller identity forwarded to the tracer +:session_id+ (+String+, optional) — session identity forwarded to the tracer

Returns:

  • (Hash)

    +{ output: String, messages: Array, usage: Phronomy::TokenUsage }+

Raises:



414
415
416
417
# File 'lib/phronomy/agent/base.rb', line 414

def invoke(input, config: {})
  thread_id = config[:thread_id]
  _run_in_thread_actor(thread_id) { _invoke_impl(input, config: config) }
end

#on_approval_required(&block) ⇒ self

Registers a callback that is invoked before executing any tool that has +requires_approval true+ set. The block receives the tool name (String) and the arguments Hash, and must return a truthy value to allow execution. Returning a falsy value causes the tool to return a denial message instead of executing.

When no handler is registered, tools with +requires_approval+ execute without interruption (backward-compatible behaviour).

Examples:

agent = MyAgent.new
agent.on_approval_required { |tool_name, args| prompt_user(tool_name, args) }

Returns:

  • (self)


456
457
458
459
# File 'lib/phronomy/agent/base.rb', line 456

def on_approval_required(&block)
  @approval_handler = block
  self
end

#stream(input, config: {}) {|Phronomy::Agent::StreamEvent| ... } ⇒ Hash

Streaming version of #invoke. Yields StreamEvent objects as they are produced by the underlying LLM.

Events emitted (in order): :token — each content delta from the LLM :tool_call — when the LLM requests a tool (ReactAgent subclasses only) :tool_result — after a tool completes (ReactAgent subclasses only) :done — final event carrying output, messages, and usage :error — if an unrecoverable error occurs

Parameters:

  • input (String, Hash)

    same as #invoke

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

    same as #invoke

Yields:

Returns:

  • (Hash)

    { output:, messages:, usage: } — same as #invoke



433
434
435
436
437
438
439
440
441
# File 'lib/phronomy/agent/base.rb', line 433

def stream(input, config: {}, &block)
  return invoke(input, config: config) unless block

  thread_id = config[:thread_id]
  _run_in_thread_actor(thread_id) { _stream_impl(input, config: config, &block) }
rescue => e
  block&.call(StreamEvent.new(type: :error, payload: {error: e}))
  raise
end