Class: Phronomy::Agent::Base
- Inherits:
-
Object
- Object
- Phronomy::Agent::Base
- Includes:
- Concerns::BeforeCompletion, Concerns::ErrorTranslation, Concerns::Filterable, Concerns::Retryable, 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.
Direct Known Subclasses
Instance Attribute Summary collapse
-
#scope_policy ⇒ void
writeonly
Registers a scope policy callable for this agent instance.
Attributes included from Concerns::BeforeCompletion
Class Method Summary collapse
-
.approve(session_id, approved: true, config: {}) ⇒ Hash
Continues a suspended invocation identified by
session_id. -
.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.
-
.context_overhead(val = nil) ⇒ Object
Tokens reserved for the system prompt + tool definitions overhead.
-
.context_window(val = nil) ⇒ Object
Overrides the context window size used for token budget calculations.
-
.instructions(text = nil) { ... } ⇒ String, ...
Sets or reads the system instructions for this agent.
-
.invoke_timeout(val = nil) ⇒ Numeric?
Sets or reads the per-invocation timeout (in seconds) for EventLoop-mode agent calls.
-
.max_iterations(val = nil) ⇒ Integer
Sets or reads the maximum number of LLM call cycles for ReAct agents.
-
.max_output_tokens(val = nil) ⇒ Object
Tokens to reserve for the model's output.
-
.max_parallel_tools(val = nil) ⇒ Integer
Sets or reads the maximum number of tool calls executed concurrently when the LLM returns multiple tool calls in a single response (ParallelToolChat mode, active inside an AgentFSM IO thread).
-
.model(name = nil) ⇒ String?
Sets or reads the LLM model identifier for this agent.
-
.provider(name = nil) ⇒ Symbol?
Sets or reads the LLM provider for this agent.
-
.static_knowledge(*sources) ⇒ Object
Registers one or more static knowledge sources on the agent class.
-
.static_knowledge_chunks ⇒ Array<Hash>
Returns the fetched content from all static knowledge sources.
-
.static_knowledge_refresh! ⇒ nil
Clears the class-level knowledge cache so that the next
invokecall re-fetches content from all registered static knowledge sources. -
.static_knowledge_sources ⇒ Array<Phronomy::Agent::Context::Knowledge::Base>
Returns the registered static knowledge sources.
-
.temperature(val = nil) ⇒ Float?
Sets or reads the sampling temperature sent to the LLM.
-
.tool_aliases ⇒ Hash{Class => String}
Returns the alias map registered via the hash form of .tools.
-
.tools(*args) ⇒ Object
Registers tool classes for this agent.
Instance Method Summary collapse
-
#_add_handoff_tool(tool_class) ⇒ self
private
Registers an anonymous handoff tool class on this agent instance.
-
#_handoff_tools ⇒ Array<Class>
private
Returns handoff tool classes registered on this instance by Runner.
-
#approve(session_id, approved: true, config: {}) ⇒ Hash
Continues a suspended invocation identified by
session_id. -
#context_version_cache ⇒ Object
deprecated
private
Deprecated.
The context version cache has been removed. Returns nil. Retained for backward compatibility with callers using safe navigation (+&.reset+).
-
#invoke(input, messages: [], thread_id: nil, config: {}, invocation_context: nil) ⇒ Hash
Invokes the agent with the given input and returns a result Hash.
-
#invoke_async(input, messages: [], thread_id: nil, config: {}, invocation_context: nil) ⇒ Phronomy::Task
Invokes this agent asynchronously and returns a Task.
-
#on_approval_required(&block) ⇒ self
Registers a synchronous approval callback that is invoked before executing any tool that has
requires_approval trueset. -
#stream(input, messages: [], thread_id: nil, config: {}) {|Phronomy::Agent::StreamEvent| ... } ⇒ Hash
Streaming version of #invoke.
Methods included from Concerns::BeforeCompletion
Methods included from Concerns::Filterable
#add_input_filter, #add_output_filter, #add_tool_result_filter, included
Methods included from Concerns::Retryable
Methods included from Runnable
Instance Attribute Details
#scope_policy=(value) ⇒ void (writeonly)
This method returns an undefined value.
Registers a scope policy callable for this agent instance.
The callable receives (tool_class, scope, agent) and must return
:allow, :reject, or :approve.
439 440 441 |
# File 'lib/phronomy/agent/base.rb', line 439 def scope_policy=(value) @scope_policy = value end |
Class Method Details
.approve(session_id, approved: true, config: {}) ⇒ Hash
Continues a suspended invocation identified by session_id.
Instantiates a fresh agent and delegates to the instance-level #approve.
When approved: false, the agent rejects the pending tool call and ends
the invocation.
389 390 391 |
# File 'lib/phronomy/agent/base.rb', line 389 def approve(session_id, approved: true, config: {}) new.approve(session_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.
318 319 320 321 322 323 324 |
# File 'lib/phronomy/agent/base.rb', line 318 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.
368 369 370 371 372 373 374 |
# File 'lib/phronomy/agent/base.rb', line 368 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.
352 353 354 355 356 357 358 |
# File 'lib/phronomy/agent/base.rb', line 352 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 Context::Instruction::PromptTemplate, or a block (Proc). When used as a reader (no argument, no block), returns the stored value.
75 76 77 78 79 80 81 82 |
# File 'lib/phronomy/agent/base.rb', line 75 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 |
.invoke_timeout(val = nil) ⇒ Numeric?
Sets or reads the per-invocation timeout (in seconds) for EventLoop-mode
agent calls. When set, invoke raises TimeoutError if the
agent does not finish within the given number of seconds.
Has no effect when EventLoop mode is disabled (direct invoke path).
Defaults to nil (no timeout).
Inherited by subclasses; the most-specific definition wins.
When the timeout fires, a Concurrency::CancellationScope is cancelled
and its token is propagated to the FSM config so that in-flight LLM,
tool, and RAG calls observe cancellation via their cancellation_token:
keyword argument. Phronomy::TimeoutError is raised to the caller.
237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/phronomy/agent/base.rb', line 237 def invoke_timeout(val = nil) if val.nil? return @invoke_timeout if defined?(@invoke_timeout) superclass.respond_to?(:invoke_timeout) ? superclass.invoke_timeout : nil else unless val.is_a?(Numeric) && val > 0 raise ArgumentError, "invoke_timeout must be a positive number, got #{val.inspect}" end @invoke_timeout = val 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.
182 183 184 185 186 187 188 |
# File 'lib/phronomy/agent/base.rb', line 182 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.
334 335 336 337 338 339 340 |
# File 'lib/phronomy/agent/base.rb', line 334 def max_output_tokens(val = nil) if val.nil? @max_output_tokens else @max_output_tokens = val.to_i end end |
.max_parallel_tools(val = nil) ⇒ Integer
Sets or reads the maximum number of tool calls executed concurrently when the LLM returns multiple tool calls in a single response (ParallelToolChat mode, active inside an AgentFSM IO thread).
Defaults to 10. Set to 1 to force sequential execution. Inherited by subclasses; the most-specific definition wins.
204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/phronomy/agent/base.rb', line 204 def max_parallel_tools(val = nil) if val.nil? @max_parallel_tools || (superclass.respond_to?(:max_parallel_tools) ? superclass.max_parallel_tools : 10) else unless val.is_a?(Integer) && val >= 1 raise ArgumentError, "max_parallel_tools must be a positive Integer (>= 1), got #{val.inspect}" end @max_parallel_tools = val 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.
51 52 53 54 55 56 57 |
# File 'lib/phronomy/agent/base.rb', line 51 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).
145 146 147 148 149 150 151 152 |
# File 'lib/phronomy/agent/base.rb', line 145 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.
261 262 263 264 265 266 |
# File 'lib/phronomy/agent/base.rb', line 261 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_chunks ⇒ Array<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.
280 281 282 283 284 |
# File 'lib/phronomy/agent/base.rb', line 280 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.
298 299 300 |
# File 'lib/phronomy/agent/base.rb', line 298 def static_knowledge_refresh! @static_knowledge_chunks = nil end |
.static_knowledge_sources ⇒ Array<Phronomy::Agent::Context::Knowledge::Base>
Returns the registered static knowledge sources.
271 272 273 |
# File 'lib/phronomy/agent/base.rb', line 271 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.
164 165 166 167 168 169 170 |
# File 'lib/phronomy/agent/base.rb', line 164 def temperature(val = nil) if val @temperature = val else @temperature end end |
.tool_aliases ⇒ Hash{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.
124 125 126 127 128 129 130 131 |
# File 'lib/phronomy/agent/base.rb', line 124 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).
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/phronomy/agent/base.rb', line 101 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
#_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.
399 400 401 402 403 |
# File 'lib/phronomy/agent/base.rb', line 399 def _add_handoff_tool(tool_class) @_handoff_tools ||= [] @_handoff_tools << tool_class self end |
#_handoff_tools ⇒ Array<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.
408 409 410 |
# File 'lib/phronomy/agent/base.rb', line 408 def _handoff_tools @_handoff_tools || [] end |
#approve(session_id, approved: true, config: {}) ⇒ Hash
Continues a suspended invocation identified by session_id.
When approved: true, executes the pending tool and continues.
When approved: false, rejects the tool call and ends the invocation.
938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 |
# File 'lib/phronomy/agent/base.rb', line 938 def approve(session_id, approved: true, config: {}) ctx = Agent::SuspendedSessionRegistry.fetch(session_id) raise ArgumentError, "No suspended session found: #{session_id}" unless ctx # Reset approval_required so executing_tool_action proceeds instead of # re-suspending when called after the :approve FSM transition. ctx.approval_required = false ctx.approved = true if approved # signals executing_tool to run the tool ctx.rejected = !approved # signals _extract_invoke_result for rejection if approved _resume_fsm(ctx, :approve) else _resume_fsm(ctx, :reject) end end |
#context_version_cache ⇒ Object
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.
The context version cache has been removed. Returns nil. Retained for backward compatibility with callers using safe navigation (+&.reset+).
589 590 591 |
# File 'lib/phronomy/agent/base.rb', line 589 def context_version_cache nil end |
#invoke(input, messages: [], thread_id: nil, config: {}, invocation_context: nil) ⇒ 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. FilterBlockError is never retried.
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 |
# File 'lib/phronomy/agent/base.rb', line 485 def invoke(input, messages: [], thread_id: nil, config: {}, invocation_context: nil) if invocation_context thread_id, config = _apply_invocation_context(thread_id, config, invocation_context) end _check_scheduler_reentrancy timeout_sec = self.class.invoke_timeout unless timeout_sec return trace("agent.invoke", input: input, **(config)) do |_span| result = invoke_async(input, messages: , thread_id: thread_id, config: config).wait_result [result, result[:usage]] end end # invoke_timeout: create a CancellationScope with deadline, pass its token # to the async invocation, and use scope.pop_queue so the calling thread # unblocks as soon as either the result arrives or the deadline fires. scope = Phronomy::Concurrency::CancellationScope.new(parent_token: config[:cancellation_token]) scope.deadline_in(timeout_sec) effective_config = config.merge(cancellation_token: scope.token) task = invoke_async(input, messages: , thread_id: thread_id, config: effective_config) # Bridge the task result to an AsyncQueue so scope.pop_queue can observe the deadline. completion_queue = Phronomy::Concurrency::AsyncQueue.new Phronomy::Runtime.instance.spawn(name: "invoke-timeout-bridge:#{(self.class.name || "agent").downcase}") do completion_queue.push(task.wait_result) rescue => e completion_queue.push(e) end result = scope.pop_queue(completion_queue) do raise Phronomy::TimeoutError, "Agent #{self.class.name} invoke timed out after #{timeout_sec}s" end raise result if result.is_a?(Exception) result end |
#invoke_async(input, messages: [], thread_id: nil, config: {}, invocation_context: nil) ⇒ Phronomy::Task
Invokes this agent asynchronously and returns a Task.
This is the primary async entry point. #invoke is a synchronous wrapper
that calls this method and blocks the caller until the task completes.
Calling #invoke from inside an active scheduler task raises
SchedulerReentrancyError; use invoke_async directly in that
context.
The task is registered with the Runtime task registry so Runtime#shutdown drains in-flight invocations before process exit.
545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/phronomy/agent/base.rb', line 545 def invoke_async(input, messages: [], thread_id: nil, config: {}, invocation_context: nil) if invocation_context thread_id, config = _apply_invocation_context(thread_id, config, invocation_context) end bp = Phronomy.configuration.backpressure on_full = (bp == :raise) ? :reject : (bp || :wait) bp_timeout = Phronomy.configuration.backpressure_timeout result_task = Phronomy::Task.deferred(name: "agent-#{(self.class.name || "anonymous").downcase}-async") gate = Phronomy::Runtime.instance.gate(:agent) gate.acquire(on_full: on_full, timeout: bp_timeout) do _start_invoke_attempt(result_task, input, messages: , thread_id: thread_id, config: config, attempt: 0) return result_task end end |
#on_approval_required(&block) ⇒ self
Registers a synchronous approval 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.
When no handler is registered and a tool with requires_approval is
called, #invoke returns a suspended result hash containing a
session_id. Call #approve to continue execution.
426 427 428 429 |
# File 'lib/phronomy/agent/base.rb', line 426 def on_approval_required(&block) @approval_handler = block self end |
#stream(input, messages: [], thread_id: nil, 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
:tool_result — after a tool completes
:done — final event carrying output, messages, and usage
:error — if an unrecoverable error occurs
577 578 579 580 581 582 583 584 |
# File 'lib/phronomy/agent/base.rb', line 577 def stream(input, messages: [], thread_id: nil, config: {}, &block) return invoke(input, messages: , thread_id: thread_id, config: config) unless block _stream_impl(input, messages: , thread_id: thread_id, config: config, &block) rescue => e block&.call(StreamEvent.new(type: :error, payload: {error: e})) raise end |