Class: Phronomy::Agent::Base

Inherits:
Object
  • Object
show all
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.

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

Orchestrator, ReactAgent

Instance Attribute Summary

Attributes included from Concerns::BeforeCompletion

#before_completion

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Concerns::Suspendable

#on_approval_required, #resume, #scope_policy=

Methods included from Concerns::BeforeCompletion

included

Methods included from Concerns::Guardrailable

#add_input_guardrail, #add_output_guardrail

Methods included from Concerns::Retryable

included

Methods included from Runnable

#batch, #trace

Class Method Details

._on_compact_callbackProc?

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:

  • (Proc, nil)


375
376
377
# File 'lib/phronomy/agent/base.rb', line 375

def _on_compact_callback
  @on_compact_callback
end

._on_compaction_trigger_callbackProc?

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:

  • (Proc, nil)


351
352
353
# File 'lib/phronomy/agent/base.rb', line 351

def _on_compaction_trigger_callback
  @on_compaction_trigger_callback
end

._on_trim_callbackProc?

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:

  • (Proc, nil)


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.

Examples:

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


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.

Examples:

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


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.

Examples:

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


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.

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

.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.

Examples:

class MyAgent < Phronomy::Agent::Base
  invoke_timeout 30
end

Parameters:

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

Returns:

  • (Numeric, nil)


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.

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


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.

Examples:

class MyAgent < Phronomy::Agent::Base
  max_parallel_tools 4
end

Parameters:

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

Returns:

  • (Integer)


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.

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

.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



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.

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



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.

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



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).

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::KnowledgeSource::StaticKnowledge.new(POLICY_TEXT)
end

Parameters:



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_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>)


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.

Examples:

Refresh after updating a knowledge file

MyAgent.static_knowledge_refresh!

Returns:

  • (nil)


301
302
303
# File 'lib/phronomy/agent/base.rb', line 301

def static_knowledge_refresh!
  @static_knowledge_chunks = nil
end

.static_knowledge_sourcesArray<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.

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

#_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)


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_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>)


468
469
470
# File 'lib/phronomy/agent/base.rb', line 468

def _handoff_tools
  @_handoff_tools || []
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 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.

Examples:

Normal invocation

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

Multi-turn conversation

result1 = agent.invoke("Hi, I'm Alice.")
result2 = agent.invoke("What's my name?", messages: result1[:messages])

Suspend / resume flow

result = agent.invoke("Perform task X")
if result[:suspended]
  result = agent.resume(result[:checkpoint], approved: true)
end
puts result[:output]

With InvocationContext (deadline-based timeout)

ctx = Phronomy::InvocationContext.new(
  thread_id: "conv-123",
  deadline: Phronomy::Deadline.in(30),
  task_id: SecureRandom.uuid
)
result = MyAgent.new.invoke("Hello", invocation_context: ctx)

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.

  • messages (Array<RubyLLM::Message>) (defaults to: [])

    conversation history from a previous invocation. The application owns and persists this array; pass it on every turn to maintain multi-turn context.

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

    conversation thread identifier, forwarded to the compaction context when on_compact is configured.

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

    additional runtime options: +:knowledge_sources+ (Array) — dynamic knowledge sources for this turn +:user_id+ (+String+, optional) — caller identity forwarded to the tracer +:session_id+ (+String+, optional) — session identity forwarded to the tracer

  • invocation_context (Phronomy::InvocationContext, nil) (defaults to: nil)

    optional first-class context object. When present, +thread_id+, +cancellation_token+, and +deadline+ are derived from it (existing +config:+ keys take precedence as backward-compat aliases). The object is also stored in +config[:invocation_context]+ so that +task_id+ / +parent_task_id+ appear in trace spans automatically.

Returns:

  • (Hash)

    +{ output: String, messages: Array, usage: Phronomy::TokenUsage }+, or +{ output: nil, suspended: true, checkpoint: Phronomy::Agent::Checkpoint, messages: Array }+ when the invocation was suspended awaiting tool approval.

Raises:



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: 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: 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.

Examples:

task = agent.invoke_async("Hello!")
result = task.await   # => { output: "...", messages: [...], usage: ... }

Parameters:

  • input (String, Hash)
  • messages (Array) (defaults to: [])
  • thread_id (String, nil) (defaults to: nil)
  • config (Hash) (defaults to: {})
  • invocation_context (Phronomy::InvocationContext, nil) (defaults to: nil)

Returns:



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: 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.

Examples:

entry :run_agent, ->(ctx) { MyAgent.new.run_as_child(ctx.query, ctx: ctx) }
transition from: :run_agent, on: :child_completed, to: :process_result

Parameters:

  • input (String, Hash)

    user input passed to the agent

  • ctx (Object)

    a WorkflowContext that responds to +#thread_id+

  • messages (Array) (defaults to: [])

    prior conversation history

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

    invocation config (forwarded to +_invoke_impl+)

Returns:

  • (nil)

    the caller must not wait on any return value; the result arrives as a +:child_completed+ event

Raises:



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: 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

Parameters:

  • input (String, Hash)

    same as #invoke

  • messages (Array<RubyLLM::Message>) (defaults to: [])

    same as #invoke

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

    same as #invoke

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

    same as #invoke

Yields:

Returns:

  • (Hash)

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



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: messages, thread_id: thread_id, config: config) unless block

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