phronomy 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -8
- data/benchmark/bench_agent_invoke.rb +1 -0
- data/lib/phronomy/agent/base.rb +192 -78
- data/lib/phronomy/agent/concerns/error_translation.rb +1 -1
- data/lib/phronomy/agent/concerns/retryable.rb +6 -2
- data/lib/phronomy/agent/invocation_context.rb +171 -0
- data/lib/phronomy/agent/invocation_session.rb +337 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +189 -0
- data/lib/phronomy/agent/suspended_session_registry.rb +54 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +26 -0
- data/lib/phronomy/configuration.rb +0 -6
- data/lib/phronomy/{concurrency → engine/concurrency}/async_queue.rb +68 -5
- data/lib/phronomy/{concurrency → engine/concurrency}/blocking_adapter_pool.rb +9 -3
- data/lib/phronomy/{event_loop.rb → engine/event_loop.rb} +71 -37
- data/lib/phronomy/engine/fsm_session.rb +248 -0
- data/lib/phronomy/{runtime → engine/runtime}/deterministic_scheduler.rb +28 -1
- data/lib/phronomy/{runtime → engine/runtime}/fake_scheduler.rb +1 -1
- data/lib/phronomy/{runtime.rb → engine/runtime.rb} +2 -4
- data/lib/phronomy/{task → engine/task}/backend.rb +2 -2
- data/lib/phronomy/engine/task/deferred_backend.rb +73 -0
- data/lib/phronomy/{task → engine/task}/fiber_backend.rb +1 -1
- data/lib/phronomy/{task → engine/task}/immediate_backend.rb +1 -1
- data/lib/phronomy/{task → engine/task}/mapped_backend.rb +15 -3
- data/lib/phronomy/{task → engine/task}/thread_backend.rb +1 -1
- data/lib/phronomy/{task.rb → engine/task.rb} +19 -7
- data/lib/phronomy/eval/scorer/llm_judge.rb +1 -1
- data/lib/phronomy/generator_verifier.rb +20 -11
- data/lib/phronomy/multi_agent/orchestrator.rb +4 -4
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +3 -3
- data/lib/phronomy/tools/mcp.rb +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +24 -13
- data/lib/phronomy/workflow.rb +6 -3
- data/lib/phronomy/workflow_context.rb +7 -5
- data/lib/phronomy/workflow_runner.rb +77 -27
- data/lib/phronomy.rb +17 -9
- metadata +35 -35
- data/lib/phronomy/agent/checkpoint.rb +0 -208
- data/lib/phronomy/agent/checkpoint_store.rb +0 -97
- data/lib/phronomy/agent/concerns/suspendable.rb +0 -190
- data/lib/phronomy/agent/suspend_signal.rb +0 -37
- data/lib/phronomy/context.rb +0 -12
- data/lib/phronomy/tool.rb +0 -9
- data/lib/phronomy/workflow/fsm_session.rb +0 -249
- /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_scope.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_token.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/concurrency_gate.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/deadline.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/gate_registry.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/pool_registry.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/runtime_metrics.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/scheduler.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/scheduler_timer_adapter.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/task_registry.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/thread_scheduler.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/timer_queue.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/timer_service.rb +0 -0
- /data/lib/phronomy/{task_group.rb → engine/task_group.rb} +0 -0
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
class Workflow
|
|
5
|
-
# Event-driven execution wrapper for a single workflow run.
|
|
6
|
-
#
|
|
7
|
-
# Created by WorkflowRunner and registered with EventLoop. All public methods
|
|
8
|
-
# are called from the EventLoop thread — FSMSession is NOT thread-safe and must
|
|
9
|
-
# not be accessed concurrently from multiple threads.
|
|
10
|
-
#
|
|
11
|
-
# == Lifecycle
|
|
12
|
-
#
|
|
13
|
-
# register(session) → EventLoop posts :start → session.start
|
|
14
|
-
# ↓ (auto-transition present)
|
|
15
|
-
# EventLoop posts :state_completed → session.handle
|
|
16
|
-
# ↓ (repeat)
|
|
17
|
-
# session posts :finished or :halted
|
|
18
|
-
# ↓
|
|
19
|
-
# EventLoop pushes ctx to completion_queue → caller unblocks
|
|
20
|
-
#
|
|
21
|
-
# == Async IO pattern (EventLoop mode only)
|
|
22
|
-
#
|
|
23
|
-
# When a state has no auto-transition and is not a wait_state, but has an
|
|
24
|
-
# external event registered (e.g. +transition from: :fetching, on: :fetch_done+),
|
|
25
|
-
# the FSMSession stays registered in the EventLoop and waits for that event.
|
|
26
|
-
# The entry action is expected to spawn an IO thread that posts the event back:
|
|
27
|
-
#
|
|
28
|
-
# entry :fetching, ->(ctx) {
|
|
29
|
-
# Thread.new {
|
|
30
|
-
# ctx.result = http.get(ctx.url)
|
|
31
|
-
# Phronomy::EventLoop.instance.post(
|
|
32
|
-
# Phronomy::Event.new(type: :fetch_done, target_id: ctx.thread_id, payload: nil)
|
|
33
|
-
# )
|
|
34
|
-
# }
|
|
35
|
-
# }
|
|
36
|
-
# transition from: :fetching, on: :fetch_done, to: :process
|
|
37
|
-
class FSMSession
|
|
38
|
-
FINISH = WorkflowRunner::FINISH
|
|
39
|
-
|
|
40
|
-
# @return [String] workflow thread_id (matches WorkflowContext#thread_id)
|
|
41
|
-
attr_reader :id
|
|
42
|
-
|
|
43
|
-
# @param id [String]
|
|
44
|
-
# @param context [Object] includes Phronomy::WorkflowContext
|
|
45
|
-
# @param entry_point [Symbol] initial state name
|
|
46
|
-
# @param entry_actions [Hash] { state_name => [callable, ...] }
|
|
47
|
-
# @param auto_state_set [Hash] { state_name => true }
|
|
48
|
-
# @param declared_states [Array<Symbol>] all action state names
|
|
49
|
-
# @param wait_state_names [Array<Symbol>]
|
|
50
|
-
# @param external_events [Hash] { event_name => [{from:, to:, guard:}] }
|
|
51
|
-
# @param phase_machine_class [Class] state_machines-backed phase tracker class
|
|
52
|
-
# @param recursion_limit [Integer]
|
|
53
|
-
# @param action_timeouts [Hash] { state_name => seconds }
|
|
54
|
-
# @param resume_event [Symbol, nil] external event to fire when resuming
|
|
55
|
-
# @param resume_phase [Symbol, nil] wait state name to resume from
|
|
56
|
-
# @api private
|
|
57
|
-
def initialize(id:, context:, entry_point:, entry_actions:, auto_state_set:,
|
|
58
|
-
declared_states:, wait_state_names:, external_events:, phase_machine_class:,
|
|
59
|
-
recursion_limit:, action_timeouts: {}, resume_event: nil, resume_phase: nil)
|
|
60
|
-
@id = id
|
|
61
|
-
@ctx = context
|
|
62
|
-
@entry_point = entry_point
|
|
63
|
-
@entry_actions = entry_actions
|
|
64
|
-
@auto_state_set = auto_state_set
|
|
65
|
-
@declared_states = declared_states
|
|
66
|
-
@wait_state_names = wait_state_names
|
|
67
|
-
@external_events = external_events
|
|
68
|
-
@phase_machine_class = phase_machine_class
|
|
69
|
-
@recursion_limit = recursion_limit
|
|
70
|
-
@action_timeouts = action_timeouts
|
|
71
|
-
@resume_event = resume_event
|
|
72
|
-
@resume_phase = resume_phase
|
|
73
|
-
@step = 0
|
|
74
|
-
@done = false
|
|
75
|
-
@current_state = nil
|
|
76
|
-
@tracker = nil
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Begins workflow execution. Called by EventLoop on :start event.
|
|
80
|
-
def start
|
|
81
|
-
if @resume_event
|
|
82
|
-
# Resume from wait state: position tracker at the wait state, then fire the
|
|
83
|
-
# external event. state_machines fires before_transition (exit) and
|
|
84
|
-
# after_transition (entry) callbacks, so both actions execute here.
|
|
85
|
-
@current_state = @resume_phase
|
|
86
|
-
@tracker = build_tracker(@current_state)
|
|
87
|
-
@tracker.context = @ctx
|
|
88
|
-
fire_and_advance!(@resume_event)
|
|
89
|
-
else
|
|
90
|
-
# Fresh start: state_machines does not fire callbacks on initialization,
|
|
91
|
-
# so we invoke the entry action for the initial state manually.
|
|
92
|
-
@current_state = @entry_point
|
|
93
|
-
@tracker = build_tracker(@current_state)
|
|
94
|
-
@tracker.context = @ctx
|
|
95
|
-
(@entry_actions[@current_state] || []).each do |c|
|
|
96
|
-
result = c.call(@ctx)
|
|
97
|
-
if result.is_a?(Phronomy::Task)
|
|
98
|
-
# Awaitable action: spawn a task to await without blocking EventLoop.
|
|
99
|
-
@tracker.async_pending = true
|
|
100
|
-
session_id = @id
|
|
101
|
-
current_state_name = @current_state
|
|
102
|
-
timeout_secs = @action_timeouts[current_state_name]
|
|
103
|
-
Phronomy::Runtime.instance.spawn(name: "fsm-await-#{session_id}") do
|
|
104
|
-
if timeout_secs
|
|
105
|
-
if result.join(timeout_secs).nil?
|
|
106
|
-
result.cancel!
|
|
107
|
-
raise Phronomy::ActionTimeoutError,
|
|
108
|
-
"Action in state #{current_state_name.inspect} timed out after #{timeout_secs}s"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
task_result = result.await
|
|
112
|
-
if task_result.is_a?(Phronomy::WorkflowContext)
|
|
113
|
-
event_loop.post(Event.new(type: :action_completed, target_id: session_id, payload: task_result))
|
|
114
|
-
else
|
|
115
|
-
event_loop.post(Event.new(type: :state_completed, target_id: session_id, payload: nil))
|
|
116
|
-
end
|
|
117
|
-
rescue => e
|
|
118
|
-
event_loop.post(Event.new(type: :error, target_id: session_id, payload: e))
|
|
119
|
-
end
|
|
120
|
-
break # Only one async action at a time per state
|
|
121
|
-
elsif result.is_a?(Phronomy::WorkflowContext)
|
|
122
|
-
@ctx = result
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
@tracker.context = @ctx
|
|
126
|
-
advance_or_halt unless @tracker.async_pending
|
|
127
|
-
end
|
|
128
|
-
rescue => e
|
|
129
|
-
finish_with_error(e)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Processes an event dispatched from EventLoop.
|
|
133
|
-
# Called for :state_completed, :action_completed, and all user-defined external events.
|
|
134
|
-
#
|
|
135
|
-
# @param event [Phronomy::Event]
|
|
136
|
-
# @api private
|
|
137
|
-
def handle(event)
|
|
138
|
-
return if @done
|
|
139
|
-
|
|
140
|
-
if event.type == :action_completed
|
|
141
|
-
# An awaitable entry action completed: update context and advance.
|
|
142
|
-
@ctx = event.payload if event.payload.is_a?(Phronomy::WorkflowContext)
|
|
143
|
-
@tracker.context = @ctx
|
|
144
|
-
@tracker.async_pending = false # Reset flag set by start or fire_and_advance!
|
|
145
|
-
advance_or_halt
|
|
146
|
-
return
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
fire_and_advance!(event.type)
|
|
150
|
-
rescue => e
|
|
151
|
-
finish_with_error(e)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
# Fires event_name on the phase tracker, updates @current_state, then
|
|
157
|
-
# calls advance_or_halt to decide what to do next.
|
|
158
|
-
def fire_and_advance!(event_name)
|
|
159
|
-
if @step >= @recursion_limit
|
|
160
|
-
raise Phronomy::RecursionLimitError,
|
|
161
|
-
"Recursion limit (#{@recursion_limit}) exceeded"
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
fire_event!(@tracker, event_name, @current_state)
|
|
165
|
-
@ctx = @tracker.context
|
|
166
|
-
next_phase = @tracker.phase.to_sym
|
|
167
|
-
# When next_phase == @current_state, no transition matched → treat as terminal.
|
|
168
|
-
@current_state = (next_phase == @current_state) ? FINISH : next_phase
|
|
169
|
-
@step += 1
|
|
170
|
-
|
|
171
|
-
# If an entry action returned a Task, the after_transition callback set
|
|
172
|
-
# async_pending = true and spawned a thread. Skip advance_or_halt — the
|
|
173
|
-
# background thread will post :action_completed or :state_completed.
|
|
174
|
-
if @tracker.async_pending
|
|
175
|
-
@tracker.async_pending = false
|
|
176
|
-
return
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
advance_or_halt
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Determines the next action after the FSM has entered @current_state.
|
|
183
|
-
def advance_or_halt
|
|
184
|
-
return finish! if @current_state == FINISH
|
|
185
|
-
|
|
186
|
-
if @wait_state_names.include?(@current_state)
|
|
187
|
-
return halt!
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
if @auto_state_set.key?(@current_state)
|
|
191
|
-
event_loop.post(Event.new(type: :state_completed, target_id: @id, payload: nil))
|
|
192
|
-
return
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
if has_external_event_from?(@current_state)
|
|
196
|
-
# Async IO pattern: the entry action spawned an IO thread that will post
|
|
197
|
-
# an external event back. Stay registered; do nothing here.
|
|
198
|
-
return
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# No transition declared — validate the state is known, then treat as terminal.
|
|
202
|
-
unless @declared_states.include?(@current_state)
|
|
203
|
-
raise ArgumentError, "State #{@current_state.inspect} is not defined"
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
finish!
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def finish!
|
|
210
|
-
@done = true
|
|
211
|
-
@ctx.set_graph_metadata(thread_id: @id, phase: :__end__)
|
|
212
|
-
event_loop.post(Event.new(type: :finished, target_id: @id, payload: @ctx))
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def halt!
|
|
216
|
-
@done = true
|
|
217
|
-
@ctx.set_graph_metadata(thread_id: @id, phase: @current_state)
|
|
218
|
-
event_loop.post(Event.new(type: :halted, target_id: @id, payload: @ctx))
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def finish_with_error(err)
|
|
222
|
-
@done = true
|
|
223
|
-
event_loop.post(Event.new(type: :error, target_id: @id, payload: err))
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def fire_event!(tracker, event_name, from_state)
|
|
227
|
-
return if tracker.send(event_name)
|
|
228
|
-
|
|
229
|
-
raise ArgumentError,
|
|
230
|
-
"Transition from #{from_state.inspect} via event #{event_name.inspect} failed. " \
|
|
231
|
-
"Ensure at least one guard matches or add a fallback (no-guard) transition."
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def has_external_event_from?(state)
|
|
235
|
-
@external_events.any? { |_, transitions| transitions.any? { |t| t[:from] == state } }
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def build_tracker(from_state)
|
|
239
|
-
machine = @phase_machine_class.new
|
|
240
|
-
machine.instance_variable_set(:@phase, from_state.to_s)
|
|
241
|
-
machine
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def event_loop
|
|
245
|
-
Phronomy::EventLoop.instance
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|