Class: Phronomy::Agent::FSM

Inherits:
Object
  • Object
show all
Defined in:
lib/phronomy/agent/fsm.rb

Overview

EventLoop-registered execution unit for a single agent invocation.

+AgentFSM+ implements the minimal interface expected by EventLoop (+#id+, +#start+, +#handle+) so it can be managed alongside FSMSession instances. It is not a traditional finite-state machine; the name reflects its role in the EventLoop rather than internal state transitions.

== Execution model

#start is called by the EventLoop on the +:start+ event. It immediately returns after spawning a background IO thread that runs the agent's full invocation pipeline (via +_invoke_impl+). The EventLoop thread is never blocked by agent execution.

Inside the IO thread, the +:phronomy_agent_parallel_tools+ thread-local flag is set to +true+ so that Base#build_chat returns a ParallelToolChat instance, enabling concurrent tool dispatch when the LLM returns multiple tool calls in one response.

== Completion events

On success:

  • Posts +:finished+ to this FSM's own +#id+ so the EventLoop cleans up its registry entry and unblocks any +completion_queue.pop+ caller.
  • When +parent_id+ is set (child-FSM pattern), additionally posts +:child_completed+ to +parent_id+, carrying the result hash as the event payload. The parent FSMSession must declare an +on:+ transition for +:child_completed+ to advance correctly.

On error:

  • Posts +:error+ to this FSM's own +#id+. The EventLoop propagates the exception through the +completion_queue+ so that the original caller of +Agent::Base#invoke+ (in EventLoop mode) receives and re-raises it.

== Standalone usage (blocking caller)

Phronomy.configure { |c| c.event_loop = true } result = MyAgent.new.invoke("Hello!") # => { output:, messages:, usage: }

Base#invoke detects EventLoop mode, creates an +AgentFSM+, registers it via EventLoop#register, and blocks the calling thread on the returned +completion_queue+ until the agent finishes.

== Child-FSM usage (non-blocking, inside a Workflow)

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

Base#run_as_child creates an +AgentFSM+ with +parent_id+ set to +ctx.thread_id+, registers it with the EventLoop, and returns immediately. The parent FSMSession waits for the +:child_completed+ event.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent:, input:, messages: [], thread_id: nil, config: {}, parent_id: nil, result_writer: nil) ⇒ FSM

Returns a new instance of FSM.

Examples:

Writing result into context

entry :run_agent, ->(ctx) {
  MyAgent.new.run_as_child(ctx.query, ctx: ctx) { |r| ctx.answer = r[:output] }
}

Parameters:

  • agent (Phronomy::Agent::Base)

    agent instance to run

  • input (String, Hash)

    user input passed to +invoke_once+

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

    prior conversation history

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

    conversation thread id; auto-generated when nil

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

    invocation config forwarded to +_invoke_impl+

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

    EventLoop id of the parent FSMSession; when set, a +:child_completed+ event is posted on completion

  • result_writer (Proc, nil) (defaults to: nil)

    optional callable invoked with the result hash before +:child_completed+ is posted. Use this to write the agent output back into the parent WorkflowContext. Thread::Queue provides the happens-before guarantee.



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

def initialize(agent:, input:, messages: [], thread_id: nil, config: {}, parent_id: nil, result_writer: nil)
  @agent = agent
  @input = input
  @messages = Array(messages).dup
  @thread_id = thread_id || SecureRandom.uuid
  @config = config
  @parent_id = parent_id
  @result_writer = result_writer
  @id = @thread_id
  @current_phase = :idle
end

Instance Attribute Details

#current_phaseSymbol (readonly)

Returns current internal phase (:idle, :running).

Returns:

  • (Symbol)

    current internal phase (:idle, :running)



65
66
67
# File 'lib/phronomy/agent/fsm.rb', line 65

def current_phase
  @current_phase
end

#idString (readonly)

Returns unique identifier used as the EventLoop target_id.

Returns:

  • (String)

    unique identifier used as the EventLoop target_id



62
63
64
# File 'lib/phronomy/agent/fsm.rb', line 62

def id
  @id
end

Instance Method Details

#handle(_event) ⇒ Object

Called by EventLoop for external events dispatched to this id. +AgentFSM+ is fully driven by its own IO thread and does not respond to external events after #start.



112
113
114
# File 'lib/phronomy/agent/fsm.rb', line 112

def handle(_event)
  # No-op: AgentFSM is driven entirely by its IO thread.
end

#startObject

Called by EventLoop on the +:start+ event. Transitions to +:running+ and spawns the agent IO thread.



104
105
106
107
# File 'lib/phronomy/agent/fsm.rb', line 104

def start
  @current_phase = :running
  spawn_agent_thread
end