phronomy 0.13.0 → 0.15.0
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/CHANGELOG.md +155 -0
- data/README.md +266 -38
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/docs/mcp-client.md +75 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/gemfiles/mcp_1_0.gemfile +9 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +509 -420
- data/lib/phronomy/agent/context/capability/base.rb +57 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +58 -53
- data/lib/phronomy/diagnostics.rb +1 -1
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
- data/lib/phronomy/engine/event_loop.rb +507 -303
- data/lib/phronomy/engine/fsm_session.rb +181 -140
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
- data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
- data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
- data/lib/phronomy/engine/runtime.rb +247 -57
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +6 -3
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +410 -81
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +55 -104
- data/lib/phronomy/workflow_runner.rb +239 -291
- data/lib/phronomy.rb +30 -23
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +63 -11
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -346
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
data/lib/phronomy/workflow.rb
CHANGED
|
@@ -4,167 +4,75 @@ require_relative "workflow_runner"
|
|
|
4
4
|
require_relative "runnable"
|
|
5
5
|
|
|
6
6
|
module Phronomy
|
|
7
|
-
# StateChart-style
|
|
8
|
-
#
|
|
9
|
-
# Defines agent workflows in terms of *states* and *events* backed by
|
|
10
|
-
# Phronomy::WorkflowRunner. This is the primary high-level API
|
|
11
|
-
# for workflow-based execution in phronomy.
|
|
12
|
-
#
|
|
13
|
-
# == Basic usage
|
|
14
|
-
#
|
|
15
|
-
# app = Phronomy::Workflow.define(MyContext) do
|
|
16
|
-
# initial :fetch
|
|
17
|
-
#
|
|
18
|
-
# state :fetch
|
|
19
|
-
# state :process
|
|
20
|
-
#
|
|
21
|
-
# entry :fetch, FETCH_NODE
|
|
22
|
-
# entry :process, PROCESS_NODE
|
|
23
|
-
#
|
|
24
|
-
# transition from: :fetch, to: :process
|
|
25
|
-
# transition from: :process, to: :__finish__
|
|
26
|
-
# end
|
|
27
|
-
#
|
|
28
|
-
# result = app.invoke({ url: "https://example.com" })
|
|
29
|
-
#
|
|
30
|
-
# == Wait states
|
|
31
|
-
#
|
|
32
|
-
# app = Phronomy::Workflow.define(MyContext) do
|
|
33
|
-
# initial :propose
|
|
34
|
-
#
|
|
35
|
-
# state :propose
|
|
36
|
-
# wait_state :awaiting_approval
|
|
37
|
-
# state :execute
|
|
38
|
-
#
|
|
39
|
-
# entry :propose, PROPOSE_NODE
|
|
40
|
-
# entry :execute, EXECUTE_NODE
|
|
41
|
-
#
|
|
42
|
-
# transition from: :propose, to: :awaiting_approval
|
|
43
|
-
# transition from: :execute, to: :__finish__
|
|
44
|
-
#
|
|
45
|
-
# transition from: :awaiting_approval, on: :approve, to: :execute
|
|
46
|
-
# transition from: :awaiting_approval, on: :reject, to: :propose
|
|
47
|
-
# end
|
|
48
|
-
#
|
|
49
|
-
# halted = app.invoke({ ... })
|
|
50
|
-
# final = app.send_event(state: halted, event: :approve)
|
|
51
|
-
#
|
|
52
|
-
# == Conditional transitions
|
|
53
|
-
#
|
|
54
|
-
# transition from: :decide, guard: ->(s) { s.score > 5 }, to: :high
|
|
55
|
-
# transition from: :decide, to: :low # fallback (no guard)
|
|
56
|
-
#
|
|
7
|
+
# StateChart-style Workflow definition DSL.
|
|
57
8
|
class Workflow
|
|
58
9
|
include Phronomy::Runnable
|
|
59
10
|
|
|
60
|
-
# Defines a new Workflow.
|
|
61
|
-
# @param context_class [Class] class that includes Phronomy::WorkflowContext
|
|
62
|
-
# @param state_store [Phronomy::StateStore::Base, nil] optional per-workflow state store.
|
|
63
|
-
# Takes precedence over the global +Phronomy.configuration.state_store+.
|
|
64
|
-
# @yield block evaluated in DSL context
|
|
65
|
-
# @return [Phronomy::Workflow] compiled and ready-to-run workflow instance
|
|
66
|
-
# @raise [ArgumentError] if no states are declared (empty workflow)
|
|
67
|
-
# @raise [ArgumentError] if any transition references an undeclared +to:+ or +from:+ state
|
|
68
|
-
# @api public
|
|
69
11
|
def self.define(context_class, state_store: nil, &block)
|
|
70
12
|
builder = Builder.new(context_class, state_store: state_store)
|
|
71
13
|
builder.instance_eval(&block)
|
|
72
14
|
builder.build
|
|
73
15
|
end
|
|
74
16
|
|
|
75
|
-
# @param runner [Phronomy::WorkflowRunner]
|
|
76
|
-
# @api public
|
|
77
17
|
def initialize(runner)
|
|
78
18
|
@runner = runner
|
|
79
19
|
end
|
|
80
20
|
|
|
81
|
-
# Executes the workflow from the initial state.
|
|
82
|
-
# @param input [Hash] initial context field values
|
|
83
|
-
# @param config [Hash] { thread_id:, recursion_limit:, user_id:, session_id: }
|
|
84
|
-
# @param invocation_context [Phronomy::InvocationContext, nil] optional first-class context
|
|
85
|
-
# object. When present, +thread_id+, +cancellation_token+, and +deadline+ are
|
|
86
|
-
# derived from it (existing +config:+ keys take precedence). The object is also
|
|
87
|
-
# stored in +config[:invocation_context]+ for downstream tracing.
|
|
88
|
-
# @return [Object] final context
|
|
89
|
-
# @api public
|
|
90
21
|
def invoke(input, config: {}, invocation_context: nil)
|
|
91
|
-
if invocation_context
|
|
92
|
-
config = _apply_invocation_context(config, invocation_context)
|
|
93
|
-
end
|
|
22
|
+
config = _apply_invocation_context(config, invocation_context) if invocation_context
|
|
94
23
|
@runner.invoke(input, config: config)
|
|
95
24
|
end
|
|
96
25
|
|
|
97
|
-
# Invokes this workflow asynchronously and returns a {Phronomy::Task}.
|
|
98
|
-
#
|
|
99
|
-
# Unlike {#invoke}, this method registers the workflow session with the
|
|
100
|
-
# EventLoop and returns immediately without spawning an extra OS thread.
|
|
101
|
-
# The returned Task resolves with the final context when the workflow
|
|
102
|
-
# finishes.
|
|
103
|
-
#
|
|
104
|
-
# @param input [Hash]
|
|
105
|
-
# @param config [Hash]
|
|
106
|
-
# @param invocation_context [Phronomy::InvocationContext, nil]
|
|
107
|
-
# @return [Phronomy::Task]
|
|
108
|
-
# @api public
|
|
109
26
|
def invoke_async(input, config: {}, invocation_context: nil)
|
|
110
|
-
if invocation_context
|
|
111
|
-
config = _apply_invocation_context(config, invocation_context)
|
|
112
|
-
end
|
|
27
|
+
config = _apply_invocation_context(config, invocation_context) if invocation_context
|
|
113
28
|
@runner.invoke_deferred(input, config: config)
|
|
114
29
|
end
|
|
115
30
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
31
|
+
def stream(input, config: {}, invocation_context: nil, &block)
|
|
32
|
+
config = _apply_invocation_context(config, invocation_context) if invocation_context
|
|
33
|
+
@runner.stream(input, config: config, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
121
36
|
def resume(state:, input: nil)
|
|
122
37
|
@runner.resume(state: state, input: input)
|
|
123
38
|
end
|
|
124
39
|
|
|
125
|
-
# Fires a named event to advance a halted workflow.
|
|
126
|
-
# @param state [Object] halted context
|
|
127
|
-
# @param event [Symbol] event name (e.g. :approve, :reject, :resume)
|
|
128
|
-
# @param input [Hash, nil] optional field updates to merge before resuming
|
|
129
|
-
# @return [Object] final context
|
|
130
|
-
# @api public
|
|
131
40
|
def send_event(state:, event:, input: nil)
|
|
132
41
|
@runner.send_event(state: state, event: event, input: input)
|
|
133
42
|
end
|
|
134
43
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
44
|
+
# Sends an event to an active Workflow session without blocking.
|
|
45
|
+
#
|
|
46
|
+
# This method is safe to call from an Agent/Tool listener running on the
|
|
47
|
+
# EventLoop thread because it only enqueues a later dispatch.
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean] true when admitted; false when the session is not live
|
|
50
|
+
# or Runtime shutdown has begun
|
|
140
51
|
# @api public
|
|
141
|
-
def
|
|
142
|
-
@runner.
|
|
52
|
+
def signal(thread_id:, event:, payload: nil)
|
|
53
|
+
@runner.signal(
|
|
54
|
+
thread_id: thread_id,
|
|
55
|
+
event: event,
|
|
56
|
+
payload: payload
|
|
57
|
+
)
|
|
143
58
|
end
|
|
144
59
|
|
|
145
60
|
private
|
|
146
61
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
62
|
+
def _apply_invocation_context(config, invocation_context)
|
|
63
|
+
effective = config.merge(invocation_context: invocation_context)
|
|
64
|
+
if effective[:thread_id].nil? && invocation_context.thread_id
|
|
65
|
+
effective = effective.merge(thread_id: invocation_context.thread_id)
|
|
66
|
+
end
|
|
152
67
|
if effective[:cancellation_token].nil?
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
68
|
+
token = invocation_context.effective_timeout_token
|
|
69
|
+
effective = effective.merge(cancellation_token: token) if token
|
|
156
70
|
end
|
|
157
71
|
effective
|
|
158
72
|
end
|
|
159
73
|
|
|
160
74
|
public
|
|
161
75
|
|
|
162
|
-
# ---------------------------------------------------------------------------
|
|
163
|
-
# Internal DSL builder
|
|
164
|
-
# ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
# DSL builder for Phronomy::Workflow.define.
|
|
167
|
-
# Collects state/event/transition declarations and produces a WorkflowRunner.
|
|
168
76
|
class Builder
|
|
169
77
|
FINISH = Phronomy::WorkflowRunner::FINISH
|
|
170
78
|
|
|
@@ -172,197 +80,150 @@ module Phronomy
|
|
|
172
80
|
@context_class = context_class
|
|
173
81
|
@state_store = state_store
|
|
174
82
|
@initial = nil
|
|
175
|
-
# Ordered list of declared state names (action states only, not wait states).
|
|
176
83
|
@declared_states = []
|
|
177
|
-
# { state_name => [callable, ...] } — entry actions registered via entry()
|
|
178
84
|
@entry_actions = {}
|
|
179
|
-
# { state_name => [callable, ...] } — exit actions registered via exit()
|
|
180
85
|
@exit_actions = {}
|
|
181
|
-
# Array of { from:, to:, guard:, on: } — all transitions in declaration order
|
|
182
86
|
@transitions = []
|
|
183
|
-
# Set of wait state names
|
|
184
87
|
@wait_state_names = []
|
|
185
|
-
# { state_name => Numeric } — per-state action timeout in seconds
|
|
186
|
-
@action_timeouts = {}
|
|
187
88
|
end
|
|
188
89
|
|
|
189
|
-
|
|
190
|
-
# @param state_name [Symbol]
|
|
191
|
-
# rubocop:disable Style/TrivialAccessors
|
|
192
|
-
# @api public
|
|
193
|
-
def initial(state_name)
|
|
90
|
+
def initial(state_name) # rubocop:disable Style/TrivialAccessors
|
|
194
91
|
@initial = state_name
|
|
195
92
|
end
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
# @param action_timeout [Numeric, nil] seconds before an async (Task-returning)
|
|
204
|
-
# entry action is cancelled and {Phronomy::ActionTimeoutError} is raised.
|
|
205
|
-
# Only applies when the action returns a {Task} or {PendingOperation}.
|
|
206
|
-
# @api public
|
|
207
|
-
def state(name, action: nil, action_timeout: nil)
|
|
93
|
+
|
|
94
|
+
# Declares a Workflow state.
|
|
95
|
+
#
|
|
96
|
+
# Entry actions are synchronous Run-to-Completion callbacks. To start
|
|
97
|
+
# asynchronous work, register its listener/callback inside the action and
|
|
98
|
+
# return the context or nil. Returning Phronomy::Task is an error.
|
|
99
|
+
def state(name, action: nil)
|
|
208
100
|
@declared_states << name
|
|
209
|
-
@action_timeouts[name] = action_timeout if action_timeout
|
|
210
101
|
entry(name, action) if action
|
|
211
102
|
end
|
|
212
103
|
|
|
213
|
-
# Declares an entry action for a state.
|
|
214
|
-
# The callable is invoked when the workflow enters +name+.
|
|
215
|
-
# It receives the current context. Two styles are supported:
|
|
216
|
-
# - Mutation-in-place: mutate context fields directly (+s.field = value+);
|
|
217
|
-
# the return value is ignored.
|
|
218
|
-
# - Immutable update: return a new context via +s.merge(field: value)+;
|
|
219
|
-
# the returned context replaces the current one.
|
|
220
|
-
# Multiple calls for the same state are allowed; callables fire in declaration order.
|
|
221
|
-
# @param name [Symbol] state name
|
|
222
|
-
# @param callable [#call] receives context; may return a new WorkflowContext
|
|
223
|
-
# @api public
|
|
224
104
|
def entry(name, callable)
|
|
225
105
|
(@entry_actions[name] ||= []) << callable
|
|
226
106
|
end
|
|
227
107
|
|
|
228
|
-
# Declares an exit action for a state.
|
|
229
|
-
# The callable is invoked when the workflow leaves +name+.
|
|
230
|
-
# It receives the current context and should mutate it in place.
|
|
231
|
-
# Return value is ignored.
|
|
232
|
-
# Multiple calls for the same state are allowed; callables fire in declaration order.
|
|
233
|
-
# @param name [Symbol] state name
|
|
234
|
-
# @param callable [#call] receives context, mutates it in place
|
|
235
|
-
# @api public
|
|
236
108
|
def exit(name, callable)
|
|
237
109
|
(@exit_actions[name] ||= []) << callable
|
|
238
110
|
end
|
|
239
111
|
|
|
240
|
-
# Declares a wait state that automatically halts execution when reached.
|
|
241
|
-
# No entry action is registered; the workflow pauses here until an event resumes it.
|
|
242
|
-
# @param name [Symbol] wait state name (conventionally :awaiting_something)
|
|
243
|
-
# @api public
|
|
244
112
|
def wait_state(name)
|
|
245
113
|
@wait_state_names << name
|
|
246
114
|
end
|
|
247
115
|
|
|
248
|
-
# Declares a transition
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
116
|
+
# Declares a transition.
|
|
117
|
+
#
|
|
118
|
+
# Guards and actions may accept either (context) or (context, event).
|
|
119
|
+
# Transition actions are synchronous Run-to-Completion callbacks executed
|
|
120
|
+
# after the source exit callbacks and before the target entry callbacks.
|
|
121
|
+
# They may start asynchronous work and register listeners, but returning
|
|
122
|
+
# Phronomy::Task is an error; completion must arrive as a later event.
|
|
123
|
+
def transition(from:, to:, guard: nil, on: nil, action: nil)
|
|
124
|
+
destination = (to == :__finish__) ? FINISH : to
|
|
125
|
+
@transitions << {
|
|
126
|
+
from: from,
|
|
127
|
+
to: destination,
|
|
128
|
+
guard: guard,
|
|
129
|
+
on: on,
|
|
130
|
+
action: action
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build
|
|
135
|
+
validate_graph!
|
|
136
|
+
|
|
137
|
+
auto_transitions = []
|
|
138
|
+
external_events = {}
|
|
139
|
+
@transitions.each do |transition|
|
|
140
|
+
if transition[:on]
|
|
141
|
+
event_name = transition[:on].to_sym
|
|
142
|
+
external_events[event_name] ||= []
|
|
143
|
+
external_events[event_name] << {
|
|
144
|
+
from: transition[:from],
|
|
145
|
+
to: transition[:to],
|
|
146
|
+
guard: transition[:guard],
|
|
147
|
+
action: transition[:action],
|
|
148
|
+
event: event_name
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
auto_transitions << {
|
|
152
|
+
from: transition[:from],
|
|
153
|
+
to: transition[:to],
|
|
154
|
+
guard: transition[:guard],
|
|
155
|
+
action: transition[:action],
|
|
156
|
+
event: :state_completed
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
runner = Phronomy::WorkflowRunner.new(
|
|
162
|
+
state_class: @context_class,
|
|
163
|
+
entry_actions: @entry_actions.dup,
|
|
164
|
+
exit_actions: @exit_actions.dup,
|
|
165
|
+
declared_states: @declared_states.dup,
|
|
166
|
+
auto_transitions: auto_transitions,
|
|
167
|
+
external_events: external_events,
|
|
168
|
+
entry_point: @initial || @declared_states.first,
|
|
169
|
+
wait_state_names: @wait_state_names.dup,
|
|
170
|
+
state_store: @state_store
|
|
171
|
+
)
|
|
172
|
+
Workflow.new(runner)
|
|
263
173
|
end
|
|
264
174
|
|
|
265
175
|
private
|
|
266
176
|
|
|
267
|
-
# Performs build-time structural validation of the workflow graph.
|
|
268
|
-
# Raises ArgumentError for hard errors; warns for unreachable states.
|
|
269
177
|
def validate_graph!
|
|
270
178
|
all_states = (@declared_states + @wait_state_names).uniq
|
|
271
179
|
entry_point = @initial || @declared_states.first
|
|
272
180
|
|
|
273
|
-
|
|
274
|
-
raise ArgumentError,
|
|
181
|
+
unless entry_point
|
|
182
|
+
raise ArgumentError,
|
|
183
|
+
"Workflow has no states declared — call state(...) or " \
|
|
184
|
+
"wait_state(...) at least once"
|
|
275
185
|
end
|
|
276
186
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
unless
|
|
187
|
+
undefined_targets = @transitions
|
|
188
|
+
.map { |transition| transition[:to] }
|
|
189
|
+
.reject { |target| target == FINISH } - all_states
|
|
190
|
+
unless undefined_targets.empty?
|
|
281
191
|
raise ArgumentError,
|
|
282
|
-
"Workflow transition(s) reference undefined state(s):
|
|
283
|
-
"
|
|
192
|
+
"Workflow transition(s) reference undefined state(s): " \
|
|
193
|
+
"#{undefined_targets.sort.inspect}"
|
|
284
194
|
end
|
|
285
195
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
undefined_sources = referenced_sources - all_states
|
|
196
|
+
undefined_sources = @transitions
|
|
197
|
+
.map { |transition| transition[:from] } - all_states
|
|
289
198
|
unless undefined_sources.empty?
|
|
290
199
|
raise ArgumentError,
|
|
291
|
-
"Workflow transition(s) originate from undefined state(s):
|
|
292
|
-
"
|
|
200
|
+
"Workflow transition(s) originate from undefined state(s): " \
|
|
201
|
+
"#{undefined_sources.sort.inspect}"
|
|
293
202
|
end
|
|
294
203
|
|
|
295
|
-
# Reachability check: warn about declared states that cannot be reached
|
|
296
|
-
# from the initial state (transition target not referenced by any transition).
|
|
297
204
|
reachable = Set.new([entry_point])
|
|
298
205
|
queue = [entry_point]
|
|
299
206
|
until queue.empty?
|
|
300
207
|
current = queue.shift
|
|
301
|
-
@transitions.each do |
|
|
302
|
-
next
|
|
303
|
-
next if
|
|
304
|
-
|
|
305
|
-
reachable.add(t[:to])
|
|
306
|
-
queue << t[:to]
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
end
|
|
208
|
+
@transitions.each do |transition|
|
|
209
|
+
next unless transition[:from] == current
|
|
210
|
+
next if transition[:to] == FINISH
|
|
211
|
+
next if reachable.include?(transition[:to])
|
|
310
212
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
msg = "[Phronomy] Workflow has unreachable state(s): #{unreachable.sort.inspect}. " \
|
|
314
|
-
"These states can never be entered from the initial state '#{entry_point}'."
|
|
315
|
-
if Phronomy.configuration.logger
|
|
316
|
-
Phronomy.configuration.logger.warn(msg)
|
|
317
|
-
else
|
|
318
|
-
warn msg
|
|
213
|
+
reachable.add(transition[:to])
|
|
214
|
+
queue << transition[:to]
|
|
319
215
|
end
|
|
320
216
|
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
public
|
|
324
|
-
|
|
325
|
-
# Builds and returns a Phronomy::Workflow backed by a WorkflowRunner.
|
|
326
|
-
# Performs build-time validation of the graph structure:
|
|
327
|
-
# - raises ArgumentError when no initial state is declared and no states have been defined
|
|
328
|
-
# - raises ArgumentError when a transition references an undeclared target state
|
|
329
|
-
# - warns when declared states are unreachable from the initial state
|
|
330
|
-
# @raise [ArgumentError] on structural errors
|
|
331
|
-
# @api public
|
|
332
|
-
def build
|
|
333
|
-
entry_actions = @entry_actions.dup
|
|
334
|
-
exit_actions = @exit_actions.dup
|
|
335
|
-
|
|
336
|
-
validate_graph!
|
|
337
217
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
else
|
|
348
|
-
auto_transitions << {from: t[:from], to: t[:to], guard: t[:guard]}
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
runner = Phronomy::WorkflowRunner.new(
|
|
353
|
-
state_class: @context_class,
|
|
354
|
-
entry_actions: entry_actions,
|
|
355
|
-
exit_actions: exit_actions,
|
|
356
|
-
declared_states: @declared_states.dup,
|
|
357
|
-
auto_transitions: auto_transitions,
|
|
358
|
-
external_events: external_events,
|
|
359
|
-
entry_point: @initial || @declared_states.first,
|
|
360
|
-
wait_state_names: @wait_state_names,
|
|
361
|
-
state_store: @state_store,
|
|
362
|
-
action_timeouts: @action_timeouts.dup
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
Workflow.new(runner)
|
|
218
|
+
unreachable = all_states - reachable.to_a
|
|
219
|
+
return if unreachable.empty?
|
|
220
|
+
|
|
221
|
+
message =
|
|
222
|
+
"[Phronomy] Workflow has unreachable state(s): " \
|
|
223
|
+
"#{unreachable.sort.inspect}. These states can never be entered " \
|
|
224
|
+
"from the initial state #{entry_point.inspect}."
|
|
225
|
+
logger = Phronomy.configuration.logger
|
|
226
|
+
logger ? logger.warn(message) : Kernel.warn(message)
|
|
366
227
|
end
|
|
367
228
|
end
|
|
368
229
|
end
|