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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -8
  3. data/benchmark/bench_agent_invoke.rb +1 -0
  4. data/lib/phronomy/agent/base.rb +192 -78
  5. data/lib/phronomy/agent/concerns/error_translation.rb +1 -1
  6. data/lib/phronomy/agent/concerns/retryable.rb +6 -2
  7. data/lib/phronomy/agent/invocation_context.rb +171 -0
  8. data/lib/phronomy/agent/invocation_session.rb +337 -0
  9. data/lib/phronomy/agent/phase_machine_builder.rb +189 -0
  10. data/lib/phronomy/agent/suspended_session_registry.rb +54 -0
  11. data/lib/phronomy/agent/tool_call_intercepted.rb +26 -0
  12. data/lib/phronomy/configuration.rb +0 -6
  13. data/lib/phronomy/{concurrency → engine/concurrency}/async_queue.rb +68 -5
  14. data/lib/phronomy/{concurrency → engine/concurrency}/blocking_adapter_pool.rb +9 -3
  15. data/lib/phronomy/{event_loop.rb → engine/event_loop.rb} +71 -37
  16. data/lib/phronomy/engine/fsm_session.rb +248 -0
  17. data/lib/phronomy/{runtime → engine/runtime}/deterministic_scheduler.rb +28 -1
  18. data/lib/phronomy/{runtime → engine/runtime}/fake_scheduler.rb +1 -1
  19. data/lib/phronomy/{runtime.rb → engine/runtime.rb} +2 -4
  20. data/lib/phronomy/{task → engine/task}/backend.rb +2 -2
  21. data/lib/phronomy/engine/task/deferred_backend.rb +73 -0
  22. data/lib/phronomy/{task → engine/task}/fiber_backend.rb +1 -1
  23. data/lib/phronomy/{task → engine/task}/immediate_backend.rb +1 -1
  24. data/lib/phronomy/{task → engine/task}/mapped_backend.rb +15 -3
  25. data/lib/phronomy/{task → engine/task}/thread_backend.rb +1 -1
  26. data/lib/phronomy/{task.rb → engine/task.rb} +19 -7
  27. data/lib/phronomy/eval/scorer/llm_judge.rb +1 -1
  28. data/lib/phronomy/generator_verifier.rb +20 -11
  29. data/lib/phronomy/multi_agent/orchestrator.rb +4 -4
  30. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +3 -3
  31. data/lib/phronomy/tools/mcp.rb +1 -1
  32. data/lib/phronomy/version.rb +1 -1
  33. data/lib/phronomy/workflow/phase_machine_builder.rb +24 -13
  34. data/lib/phronomy/workflow.rb +6 -3
  35. data/lib/phronomy/workflow_context.rb +7 -5
  36. data/lib/phronomy/workflow_runner.rb +77 -27
  37. data/lib/phronomy.rb +17 -9
  38. metadata +35 -35
  39. data/lib/phronomy/agent/checkpoint.rb +0 -208
  40. data/lib/phronomy/agent/checkpoint_store.rb +0 -97
  41. data/lib/phronomy/agent/concerns/suspendable.rb +0 -190
  42. data/lib/phronomy/agent/suspend_signal.rb +0 -37
  43. data/lib/phronomy/context.rb +0 -12
  44. data/lib/phronomy/tool.rb +0 -9
  45. data/lib/phronomy/workflow/fsm_session.rb +0 -249
  46. /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_scope.rb +0 -0
  47. /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_token.rb +0 -0
  48. /data/lib/phronomy/{concurrency → engine/concurrency}/concurrency_gate.rb +0 -0
  49. /data/lib/phronomy/{concurrency → engine/concurrency}/deadline.rb +0 -0
  50. /data/lib/phronomy/{concurrency → engine/concurrency}/gate_registry.rb +0 -0
  51. /data/lib/phronomy/{concurrency → engine/concurrency}/pool_registry.rb +0 -0
  52. /data/lib/phronomy/{runtime → engine/runtime}/runtime_metrics.rb +0 -0
  53. /data/lib/phronomy/{runtime → engine/runtime}/scheduler.rb +0 -0
  54. /data/lib/phronomy/{runtime → engine/runtime}/scheduler_timer_adapter.rb +0 -0
  55. /data/lib/phronomy/{runtime → engine/runtime}/task_registry.rb +0 -0
  56. /data/lib/phronomy/{runtime → engine/runtime}/thread_scheduler.rb +0 -0
  57. /data/lib/phronomy/{runtime → engine/runtime}/timer_queue.rb +0 -0
  58. /data/lib/phronomy/{runtime → engine/runtime}/timer_service.rb +0 -0
  59. /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