phronomy 0.5.3 → 0.6.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.
@@ -8,18 +8,21 @@ module Phronomy
8
8
  #
9
9
  # Defines agent workflows in terms of *states* and *events* backed by
10
10
  # Phronomy::WorkflowRunner. This is the primary high-level API
11
- # for graph-based execution in phronomy.
11
+ # for workflow-based execution in phronomy.
12
12
  #
13
13
  # == Basic usage
14
14
  #
15
15
  # app = Phronomy::Workflow.define(MyContext) do
16
16
  # initial :fetch
17
17
  #
18
- # state :fetch, action: FETCH_NODE
19
- # state :process, action: PROCESS_NODE
18
+ # state :fetch
19
+ # state :process
20
20
  #
21
- # after :fetch, to: :process
22
- # after :process, to: :__finish__
21
+ # entry :fetch, FETCH_NODE
22
+ # entry :process, PROCESS_NODE
23
+ #
24
+ # transition from: :fetch, to: :process
25
+ # transition from: :process, to: :__finish__
23
26
  # end
24
27
  #
25
28
  # result = app.invoke({ url: "https://example.com" })
@@ -29,15 +32,18 @@ module Phronomy
29
32
  # app = Phronomy::Workflow.define(MyContext) do
30
33
  # initial :propose
31
34
  #
32
- # state :propose, action: PROPOSE_NODE
35
+ # state :propose
33
36
  # wait_state :awaiting_approval
34
- # state :execute, action: EXECUTE_NODE
37
+ # state :execute
38
+ #
39
+ # entry :propose, PROPOSE_NODE
40
+ # entry :execute, EXECUTE_NODE
35
41
  #
36
- # after :propose, to: :awaiting_approval
37
- # after :execute, to: :__finish__
42
+ # transition from: :propose, to: :awaiting_approval
43
+ # transition from: :execute, to: :__finish__
38
44
  #
39
- # event :approve, from: :awaiting_approval, to: :execute
40
- # event :reject, from: :awaiting_approval, to: :propose
45
+ # transition from: :awaiting_approval, on: :approve, to: :execute
46
+ # transition from: :awaiting_approval, on: :reject, to: :propose
41
47
  # end
42
48
  #
43
49
  # halted = app.invoke({ ... })
@@ -45,8 +51,8 @@ module Phronomy
45
51
  #
46
52
  # == Conditional transitions
47
53
  #
48
- # event :route, from: :decide, guard: ->(s) { s.score > 5 }, to: :high
49
- # event :route, from: :decide, to: :low # fallback (no guard)
54
+ # transition from: :decide, guard: ->(s) { s.score > 5 }, to: :high
55
+ # transition from: :decide, to: :low # fallback (no guard)
50
56
  #
51
57
  class Workflow
52
58
  include Phronomy::Runnable
@@ -91,7 +97,7 @@ module Phronomy
91
97
  @runner.send_event(state: state, event: event, input: input)
92
98
  end
93
99
 
94
- # Streaming execution. Yields { node: Symbol, state: Object } after each node.
100
+ # Streaming execution. Yields { state: Symbol, context: Object } after each state action.
95
101
  # @param input [Hash]
96
102
  # @param config [Hash]
97
103
  # @yield [Hash]
@@ -112,12 +118,14 @@ module Phronomy
112
118
  def initialize(context_class)
113
119
  @context_class = context_class
114
120
  @initial = nil
115
- # { node_name => callable }
116
- @states = {}
117
- # Array of { from:, to: } — auto-transitions after a state action
118
- @after_transitions = []
119
- # Array of { name:, from:, to:, guard: } — event-driven transitions
120
- @event_transitions = []
121
+ # Ordered list of declared state names (action states only, not wait states).
122
+ @declared_states = []
123
+ # { state_name => [callable, ...] } — entry actions registered via entry()
124
+ @entry_actions = {}
125
+ # { state_name => [callable, ...] } — exit actions registered via exit()
126
+ @exit_actions = {}
127
+ # Array of { from:, to:, guard:, on: } — all transitions in declaration order
128
+ @transitions = []
121
129
  # Set of wait state names
122
130
  @wait_state_names = []
123
131
  end
@@ -131,83 +139,87 @@ module Phronomy
131
139
  # rubocop:enable Style/TrivialAccessors
132
140
 
133
141
  # Declares an action state.
134
- # @param name [Symbol] state name
135
- # @param action [#call, nil] callable invoked when entering the state.
136
- # If nil, the state is treated as a no-op pass-through.
142
+ # @param name [Symbol] state name
143
+ # @param action [#call, nil] optional entry action shorthand.
144
+ # +state :generate, action: MY_PROC+ is equivalent to
145
+ # +state :generate; entry :generate, MY_PROC+.
137
146
  def state(name, action: nil)
138
- @states[name] = action || ->(s) { s }
147
+ @declared_states << name
148
+ entry(name, action) if action
149
+ end
150
+
151
+ # Declares an entry action for a state.
152
+ # The callable is invoked when the workflow enters +name+.
153
+ # It receives the current context and should mutate it in place.
154
+ # Return value is ignored.
155
+ # Multiple calls for the same state are allowed; callables fire in declaration order.
156
+ # @param name [Symbol] state name
157
+ # @param callable [#call] receives context, mutates it in place
158
+ def entry(name, callable)
159
+ (@entry_actions[name] ||= []) << callable
160
+ end
161
+
162
+ # Declares an exit action for a state.
163
+ # The callable is invoked when the workflow leaves +name+.
164
+ # It receives the current context and should mutate it in place.
165
+ # Return value is ignored.
166
+ # Multiple calls for the same state are allowed; callables fire in declaration order.
167
+ # @param name [Symbol] state name
168
+ # @param callable [#call] receives context, mutates it in place
169
+ def exit(name, callable)
170
+ (@exit_actions[name] ||= []) << callable
139
171
  end
140
172
 
141
173
  # Declares a wait state that automatically halts execution when reached.
142
- # No action is registered; the workflow pauses here until an event resumes it.
174
+ # No entry action is registered; the workflow pauses here until an event resumes it.
143
175
  # @param name [Symbol] wait state name (conventionally :awaiting_something)
144
176
  def wait_state(name)
145
177
  @wait_state_names << name
146
178
  end
147
179
 
148
- # Declares an automatic transition that fires after a state's action completes.
149
- # @param from [Symbol] source state name
150
- # @param to [Symbol] destination state name or :__finish__
151
- def after(from, to:)
152
- dest = (to == :__finish__) ? FINISH : to
153
- @after_transitions << {from: from, to: dest}
154
- end
155
-
156
- # Declares an event-driven transition.
157
- # When +guard:+ is provided, the transition is taken only if the guard
158
- # returns truthy for the current context. Multiple events with the same
159
- # name and source are evaluated in declaration order; the first passing
160
- # guard wins.
161
- # @param name [Symbol] event name
162
- # @param from [Symbol] source state where this event can be fired
180
+ # Declares a transition between states.
181
+ # Auto-fire transitions (no +on:+) fire automatically when an action state's
182
+ # action completes. External transitions (+on: :event_name+) are triggered
183
+ # manually via +send_event+.
184
+ # When +guard:+ is provided the transition is taken only if the guard returns
185
+ # truthy for the current context. Multiple transitions from the same source are
186
+ # evaluated in declaration order; the first passing guard wins.
187
+ # @param from [Symbol] source state
163
188
  # @param to [Symbol] destination state or :__finish__
164
189
  # @param guard [Proc, nil] optional guard — receives context, returns truthy/falsy
165
- def event(name, from:, to:, guard: nil)
190
+ # @param on [Symbol, nil] named event for manual triggers (e.g. :approve)
191
+ def transition(from:, to:, guard: nil, on: nil)
166
192
  dest = (to == :__finish__) ? FINISH : to
167
- @event_transitions << {name: name, from: from, to: dest, guard: guard}
193
+ @transitions << {from: from, to: dest, guard: guard, on: on}
168
194
  end
169
195
 
170
196
  # Builds and returns a Phronomy::Workflow backed by a WorkflowRunner.
171
197
  def build
172
- nodes = @states.dup
173
-
174
- # After-transitions: { from => to }
175
- # Unconditional transitions that fire automatically after an action state completes.
176
- after_transitions = @after_transitions.each_with_object({}) do |t, h|
177
- h[t[:from]] = t[:to]
178
- end
179
-
180
- # Route transitions: { from => {event_name:, entries: [{guard:, to:}, ...]} }
181
- # Events declared from action states (not wait states) fire automatically
182
- # after the action completes. The event name is used to register the
183
- # state_machines event and may be any symbol (e.g. :route, :route_review).
184
- # Declaration order is preserved so guarded entries appear before fallbacks.
185
- route_transitions = {}
198
+ entry_actions = @entry_actions.dup
199
+ exit_actions = @exit_actions.dup
186
200
 
187
- # External events: { event_name => [{from:, to:, guard:}, ...] }
188
- # Events declared from wait states, triggered by human input (e.g. :approve).
201
+ # Auto-fire transitions (no :on): fire automatically when action completes.
202
+ # External events (with :on): triggered manually via send_event.
203
+ auto_transitions = []
189
204
  external_events = {}
190
205
 
191
- @event_transitions.each do |t|
192
- if @wait_state_names.include?(t[:from])
193
- # Source is a wait state → external event
194
- external_events[t[:name]] ||= []
195
- external_events[t[:name]] << {from: t[:from], to: t[:to], guard: t[:guard]}
206
+ @transitions.each do |t|
207
+ if t[:on]
208
+ external_events[t[:on]] ||= []
209
+ external_events[t[:on]] << {from: t[:from], to: t[:to], guard: t[:guard]}
196
210
  else
197
- # Source is an action state routing event (auto-fires after action)
198
- # The event name is taken from the first declaration for each from-state.
199
- route_transitions[t[:from]] ||= {event_name: t[:name], entries: []}
200
- route_transitions[t[:from]][:entries] << {guard: t[:guard], to: t[:to]}
211
+ auto_transitions << {from: t[:from], to: t[:to], guard: t[:guard]}
201
212
  end
202
213
  end
203
214
 
204
215
  runner = Phronomy::WorkflowRunner.new(
205
216
  state_class: @context_class,
206
- nodes: nodes,
207
- after_transitions: after_transitions,
208
- route_transitions: route_transitions,
217
+ entry_actions: entry_actions,
218
+ exit_actions: exit_actions,
219
+ declared_states: @declared_states.dup,
220
+ auto_transitions: auto_transitions,
209
221
  external_events: external_events,
210
- entry_point: @initial || nodes.keys.first,
222
+ entry_point: @initial || @declared_states.first,
211
223
  wait_state_names: @wait_state_names
212
224
  )
213
225
 
@@ -49,7 +49,7 @@ module Phronomy
49
49
  # Encoding:
50
50
  # :__end__ — workflow completed (or not yet started)
51
51
  # :awaiting_<name> — halted at a wait_state(:awaiting_<name>) declaration
52
- # :<node> — resuming at <node> (workflow paused before its execution)
52
+ # :<state> — resuming at <state> (workflow paused before its execution)
53
53
  # @return [Symbol]
54
54
  def phase
55
55
  @phase || :__end__