Class: Phronomy::Agent::Base
- Inherits:
-
Object
- Object
- Phronomy::Agent::Base
- Includes:
- Concerns::BeforeCompletion, Concerns::ErrorTranslation, Concerns::Guardrailable, Concerns::Retryable, Concerns::Suspendable, 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
Attributes included from Concerns::BeforeCompletion
Class Method Summary collapse
- ._on_compact_callback ⇒ Proc? private
- ._on_compaction_trigger_callback ⇒ Proc? private
- ._on_trim_callback ⇒ Proc? private
-
.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.
-
.on_compact {|ctx| ... } ⇒ Object
Registers a callback that performs the actual compaction when the +on_compaction_trigger+ callback fires.
-
.on_compaction_trigger {|ctx| ... } ⇒ Boolean
Registers a callback that decides whether compaction should run.
-
.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.
-
.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 +invoke+ call re-fetches content from all registered static knowledge sources.
-
.static_knowledge_sources ⇒ Array<Phronomy::KnowledgeSource::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.
-
#context_version_cache ⇒ Object
private
Returns the Context::ContextVersionCache built during the most recent #invoke call on this agent instance.
-
#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.
-
#run_as_child(input, ctx:, messages: [], config: {}) ⇒ nil
Registers this agent as a child AgentFSM inside the given Workflow context.
-
#stream(input, messages: [], thread_id: nil, config: {}) {|Phronomy::Agent::StreamEvent| ... } ⇒ Hash
Streaming version of #invoke.
Methods included from Concerns::Suspendable
#on_approval_required, #resume, #scope_policy=
Methods included from Concerns::BeforeCompletion
Methods included from Concerns::Guardrailable
#add_input_guardrail, #add_output_guardrail
Methods included from Concerns::Retryable
Methods included from Runnable
Class Method Details
._on_compact_callback ⇒ Proc?
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.
375 376 377 |
# File 'lib/phronomy/agent/base.rb', line 375 def _on_compact_callback @on_compact_callback end |
._on_compaction_trigger_callback ⇒ Proc?
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.
351 352 353 |
# File 'lib/phronomy/agent/base.rb', line 351 def _on_compaction_trigger_callback @on_compaction_trigger_callback end |
._on_trim_callback ⇒ Proc?
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.
326 327 328 |
# File 'lib/phronomy/agent/base.rb', line 326 def _on_trim_callback @on_trim_callback 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.
395 396 397 398 399 400 401 |
# File 'lib/phronomy/agent/base.rb', line 395 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.
445 446 447 448 449 450 451 |
# File 'lib/phronomy/agent/base.rb', line 445 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.
429 430 431 432 433 434 435 |
# File 'lib/phronomy/agent/base.rb', line 429 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.
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 |
.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 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.
240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/phronomy/agent/base.rb', line 240 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.
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.
411 412 413 414 415 416 417 |
# File 'lib/phronomy/agent/base.rb', line 411 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.
207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/phronomy/agent/base.rb', line 207 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.
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 |
.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.
369 370 371 |
# File 'lib/phronomy/agent/base.rb', line 369 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.
345 346 347 |
# File 'lib/phronomy/agent/base.rb', line 345 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.
320 321 322 |
# File 'lib/phronomy/agent/base.rb', line 320 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).
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.
264 265 266 267 268 269 |
# File 'lib/phronomy/agent/base.rb', line 264 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.
283 284 285 286 287 |
# File 'lib/phronomy/agent/base.rb', line 283 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.
301 302 303 |
# File 'lib/phronomy/agent/base.rb', line 301 def static_knowledge_refresh! @static_knowledge_chunks = nil end |
.static_knowledge_sources ⇒ Array<Phronomy::KnowledgeSource::Base>
Returns the registered static knowledge sources.
274 275 276 |
# File 'lib/phronomy/agent/base.rb', line 274 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.
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_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.
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).
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
#_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.
459 460 461 462 463 |
# File 'lib/phronomy/agent/base.rb', line 459 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.
468 469 470 |
# File 'lib/phronomy/agent/base.rb', line 468 def _handoff_tools @_handoff_tools || [] 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.
Returns the Context::ContextVersionCache built during the most recent #invoke call on this agent instance. The thread-local cache entry is cleaned up in the +ensure+ block of #invoke, but a reference is kept in +@last_context_version_cache+ so callers can inspect it after invoke returns.
NOTE: Not thread-safe. When the same Agent instance is used concurrently, +@last_context_version_cache+ reflects the most recent +invoke+ on any thread. For per-invocation isolation, use a separate Agent instance per thread.
701 702 703 |
# File 'lib/phronomy/agent/base.rb', line 701 def context_version_cache @last_context_version_cache 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. GuardrailError is never retried.
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 |
# File 'lib/phronomy/agent/base.rb', line 517 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 if Phronomy.configuration.event_loop # Protect against blocking the EventLoop thread itself. if Phronomy::EventLoop.current? raise Phronomy::Error, "Cannot call Agent#invoke (EventLoop mode) from within an EventLoop " \ "entry action. Use agent.run_as_child(input, ctx: ctx) instead." end # Build an effective config that includes the invoke_timeout scope's # CancellationToken before constructing the FSM. This ensures that # every LLM, tool, and RAG call made inside _invoke_impl observes # cancellation when the deadline fires. timeout_sec = self.class.invoke_timeout effective_config, scope = if timeout_sec s = Phronomy::CancellationScope.new(parent_token: config[:cancellation_token]) s.deadline_in(timeout_sec) [config.merge(cancellation_token: s.token), s] else [config, nil] end fsm = Agent::FSM.new( agent: self, input: input, messages: , thread_id: thread_id || SecureRandom.uuid, config: effective_config ) completion_queue = Phronomy::EventLoop.instance.register(fsm) result = if scope scope.pop_queue(completion_queue) do raise Phronomy::TimeoutError, "Agent #{self.class.name} invoke timed out after #{timeout_sec}s" end else completion_queue.pop end raise result if result.is_a?(Exception) result else # Guard: calling invoke from inside a scheduler task would block the task # against itself when using a cooperative backend. Use invoke_async # instead to compose agents without introducing a blocking wait. if Phronomy::Task.current msg = "#{self.class.name}#invoke called from inside a scheduler task. " \ "This blocks the scheduler until the inner invocation completes, preventing " \ "other tasks from making progress. Use invoke_async + await instead." if Phronomy.configuration.strict_runtime_guards raise Phronomy::SchedulerReentrancyError, msg elsif Phronomy.configuration.logger Phronomy.configuration.logger.warn(msg) else Kernel.warn("[phronomy] WARNING: #{msg}") end end invoke_async(input, messages: , thread_id: thread_id, config: config).await end 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.
602 603 604 605 606 607 608 609 610 611 612 613 614 615 |
# File 'lib/phronomy/agent/base.rb', line 602 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 gate = Phronomy::Runtime.instance.gate(:agent) Phronomy::Runtime.instance.spawn(name: "agent-#{(self.class.name || "anonymous").downcase}-async") do gate.acquire(on_full: on_full, timeout: bp_timeout) do _invoke_impl(input, messages: , thread_id: thread_id, config: config) end end end |
#run_as_child(input, ctx:, messages: [], config: {}) ⇒ nil
Registers this agent as a child AgentFSM inside the given Workflow context.
Use this method from a Workflow entry action (running on the EventLoop thread) instead of #invoke, which would raise a deadlock error because +invoke+ blocks on a +Thread::Queue+ when EventLoop mode is active.
The agent runs asynchronously in a background IO thread. When it finishes, the parent FSMSession receives a +:child_completed+ event whose payload is the result hash +{ output:, messages:, usage: }+. Declare an +on: :child_completed+ transition in your Workflow to advance to the next state.
The result is delivered exclusively as the +:child_completed+ event payload. The parent Workflow task is the sole owner of the parent +WorkflowContext+ and applies the result after receiving the event — no background thread writes to the parent context directly.
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 |
# File 'lib/phronomy/agent/base.rb', line 645 def run_as_child(input, ctx:, messages: [], config: {}) unless Phronomy.configuration.event_loop raise Phronomy::Error, "run_as_child requires EventLoop mode. " \ "Enable with: Phronomy.configure { |c| c.event_loop = true }" end fsm = Agent::FSM.new( agent: self, input: input, messages: , thread_id: "#{ctx.thread_id}_agent_#{SecureRandom.uuid}", config: config, parent_id: ctx.thread_id ) Phronomy::EventLoop.instance.enqueue_child(fsm) nil 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 (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
681 682 683 684 685 686 687 688 |
# File 'lib/phronomy/agent/base.rb', line 681 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 |