phronomy 0.18.0 → 0.19.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 +25 -0
- data/README.md +2 -0
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +7 -1
- data/docs/getting-started.md +37 -1
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +93 -2
- data/lib/phronomy/agent/base.rb +81 -36
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +420 -249
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/configuration.rb +2 -1
- data/lib/phronomy/engine/event_loop.rb +86 -8
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime.rb +7 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +361 -95
- data/lib/phronomy.rb +3 -0
- metadata +4 -4
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
|
@@ -5,11 +5,13 @@ require "securerandom"
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
# Execution boundary for compiled Workflows.
|
|
7
7
|
#
|
|
8
|
-
# WorkflowRunner
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# WorkflowRunner separates three identities:
|
|
9
|
+
# - application session_id remains caller/tracing metadata;
|
|
10
|
+
# - thread_id identifies durable Workflow state;
|
|
11
|
+
# - fsm_session_id identifies one Runtime FSMSession execution.
|
|
12
12
|
#
|
|
13
|
+
# Workflow persistence is synchronous at the repository contract but is always
|
|
14
|
+
# invoked through Runtime's OffloadPool from EventLoop-driven lifecycle paths.
|
|
13
15
|
# @api private
|
|
14
16
|
class WorkflowRunner
|
|
15
17
|
include Phronomy::Runnable
|
|
@@ -19,9 +21,20 @@ module Phronomy
|
|
|
19
21
|
Execution = Data.define(
|
|
20
22
|
:context,
|
|
21
23
|
:thread_id,
|
|
24
|
+
:fsm_session_id,
|
|
22
25
|
:recursion_limit,
|
|
23
|
-
:
|
|
24
|
-
:persist
|
|
26
|
+
:repository,
|
|
27
|
+
:persist,
|
|
28
|
+
:expected_revision
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
WorkflowPersistenceCommand = Struct.new(
|
|
32
|
+
:runner,
|
|
33
|
+
:result_task,
|
|
34
|
+
:result,
|
|
35
|
+
:error,
|
|
36
|
+
:thread_id,
|
|
37
|
+
:fsm_session_id
|
|
25
38
|
)
|
|
26
39
|
|
|
27
40
|
def initialize(
|
|
@@ -33,7 +46,7 @@ module Phronomy
|
|
|
33
46
|
entry_point:,
|
|
34
47
|
exit_actions: {},
|
|
35
48
|
wait_state_names: [],
|
|
36
|
-
|
|
49
|
+
persistence: nil
|
|
37
50
|
)
|
|
38
51
|
@state_class = state_class
|
|
39
52
|
@entry_actions = entry_actions
|
|
@@ -44,7 +57,7 @@ module Phronomy
|
|
|
44
57
|
@external_events = external_events
|
|
45
58
|
@entry_point = entry_point
|
|
46
59
|
@wait_state_names = wait_state_names
|
|
47
|
-
@
|
|
60
|
+
@persistence = persistence
|
|
48
61
|
@phase_machine_class = Workflow::PhaseMachineBuilder.new(
|
|
49
62
|
entry_point: @entry_point,
|
|
50
63
|
declared_states: @declared_states,
|
|
@@ -63,28 +76,20 @@ module Phronomy
|
|
|
63
76
|
caller_meta[:session_id] = config[:session_id] if config[:session_id]
|
|
64
77
|
|
|
65
78
|
trace("workflow.invoke", input: input.inspect, **caller_meta) do |_span|
|
|
66
|
-
|
|
67
|
-
result = start_execution(execution).wait_result
|
|
79
|
+
result = start_new_execution(input, config).wait_result
|
|
68
80
|
[result, nil]
|
|
69
81
|
end
|
|
70
82
|
end
|
|
71
83
|
|
|
72
84
|
def invoke_deferred(input, config: {})
|
|
73
|
-
|
|
74
|
-
start_execution(execution)
|
|
75
|
-
rescue => error
|
|
76
|
-
failed_task("workflow-async:preparation", error)
|
|
85
|
+
start_new_execution(input, config)
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
def stream(input, config: {}, &observer)
|
|
80
89
|
ensure_blocking_call_allowed!(:stream, :invoke_async)
|
|
81
90
|
raise ArgumentError, "stream requires a block" unless observer
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
start_execution(
|
|
85
|
-
execution,
|
|
86
|
-
stable_observer: observer
|
|
87
|
-
).wait_result
|
|
92
|
+
start_new_execution(input, config, stable_observer: observer).wait_result
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
def resume(state:, input: nil)
|
|
@@ -93,38 +98,26 @@ module Phronomy
|
|
|
93
98
|
|
|
94
99
|
def send_event(state:, event:, input: nil)
|
|
95
100
|
ensure_blocking_call_allowed!(:send_event, :signal)
|
|
96
|
-
|
|
97
|
-
current_phase = context.phase.to_sym
|
|
101
|
+
current_phase = state.phase.to_sym
|
|
98
102
|
event_name = resolve_resume_event(current_phase, event)
|
|
99
|
-
thread_id =
|
|
103
|
+
thread_id = state.thread_id
|
|
100
104
|
unless thread_id
|
|
101
105
|
raise ArgumentError, "Halted WorkflowContext has no thread_id"
|
|
102
106
|
end
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
persist: true
|
|
110
|
-
)
|
|
111
|
-
start_execution(
|
|
112
|
-
execution,
|
|
113
|
-
resume_event: event_name,
|
|
114
|
-
resume_phase: current_phase
|
|
108
|
+
start_resume_execution(
|
|
109
|
+
state,
|
|
110
|
+
input: input,
|
|
111
|
+
event_name: event_name,
|
|
112
|
+
current_phase: current_phase
|
|
115
113
|
).wait_result
|
|
116
114
|
end
|
|
117
115
|
|
|
118
|
-
# Posts an application-defined event to
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
# event for an admitted session; it does not mean that a transition matched.
|
|
122
|
-
# A false result means that the Runtime is stopping or the session is no
|
|
123
|
-
# longer admitted.
|
|
116
|
+
# Posts an application-defined event to the currently live Workflow owner.
|
|
117
|
+
# thread_id is resolved to its Runtime-only fsm_session_id by EventLoop; the
|
|
118
|
+
# application session_id is deliberately unrelated to this routing.
|
|
124
119
|
def signal(thread_id:, event:, payload: nil)
|
|
125
|
-
if thread_id.nil?
|
|
126
|
-
raise ArgumentError, "thread_id is required"
|
|
127
|
-
end
|
|
120
|
+
raise ArgumentError, "thread_id is required" if thread_id.nil?
|
|
128
121
|
|
|
129
122
|
event_name = event.to_sym
|
|
130
123
|
unless @external_events.key?(event_name)
|
|
@@ -133,15 +126,27 @@ module Phronomy
|
|
|
133
126
|
"Valid events: #{@external_events.keys.inspect}"
|
|
134
127
|
end
|
|
135
128
|
|
|
136
|
-
Phronomy::Runtime.instance.event_loop.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
payload: payload
|
|
141
|
-
)
|
|
129
|
+
Phronomy::Runtime.instance.event_loop.post_to_workflow(
|
|
130
|
+
thread_id: thread_id,
|
|
131
|
+
event: event_name,
|
|
132
|
+
payload: payload
|
|
142
133
|
)
|
|
143
134
|
end
|
|
144
135
|
|
|
136
|
+
# Called only by EventLoop for terminal Workflow persistence completion.
|
|
137
|
+
def deliver_persistence_on_event_loop(command)
|
|
138
|
+
event_loop = Phronomy::Runtime.instance.event_loop
|
|
139
|
+
event_loop.release_workflow(
|
|
140
|
+
command.thread_id,
|
|
141
|
+
owner_fsm_session_id: command.fsm_session_id
|
|
142
|
+
)
|
|
143
|
+
if command.error
|
|
144
|
+
fail_task(command.result_task, command.error)
|
|
145
|
+
else
|
|
146
|
+
complete_task(command.result_task, command.result)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
145
150
|
private
|
|
146
151
|
|
|
147
152
|
def ensure_blocking_call_allowed!(method_name, async_alternative)
|
|
@@ -152,16 +157,186 @@ module Phronomy
|
|
|
152
157
|
"Use #{async_alternative} instead."
|
|
153
158
|
end
|
|
154
159
|
|
|
155
|
-
def
|
|
160
|
+
def start_new_execution(input, config, stable_observer: nil)
|
|
161
|
+
runtime = Phronomy::Runtime.instance
|
|
162
|
+
event_loop = runtime.event_loop
|
|
163
|
+
result_task = Phronomy::Task.deferred(name: "workflow:preparing")
|
|
164
|
+
explicit_thread_id = !config[:thread_id].nil?
|
|
156
165
|
thread_id = (config[:thread_id] || SecureRandom.uuid).to_s
|
|
166
|
+
fsm_session_id = SecureRandom.uuid
|
|
157
167
|
recursion_limit = config.fetch(
|
|
158
168
|
:recursion_limit,
|
|
159
169
|
Phronomy.configuration.recursion_limit
|
|
160
170
|
)
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
repository = configured_repository
|
|
172
|
+
persist = explicit_thread_id && !repository.nil?
|
|
173
|
+
|
|
174
|
+
event_loop.admit_workflow(
|
|
175
|
+
thread_id,
|
|
176
|
+
owner_fsm_session_id: fsm_session_id
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if persist
|
|
180
|
+
load_operation = runtime.offload.submit(on_full: :raise) do
|
|
181
|
+
repository.load(thread_id)
|
|
182
|
+
end
|
|
183
|
+
load_operation.on_complete do |record, error|
|
|
184
|
+
if error
|
|
185
|
+
release_and_fail(
|
|
186
|
+
event_loop, result_task, thread_id, fsm_session_id, error
|
|
187
|
+
)
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
execution = build_new_execution(
|
|
193
|
+
input,
|
|
194
|
+
thread_id: thread_id,
|
|
195
|
+
fsm_session_id: fsm_session_id,
|
|
196
|
+
recursion_limit: recursion_limit,
|
|
197
|
+
repository: repository,
|
|
198
|
+
persist: true,
|
|
199
|
+
record: record
|
|
200
|
+
)
|
|
201
|
+
register_execution(
|
|
202
|
+
execution,
|
|
203
|
+
result_task,
|
|
204
|
+
stable_observer: stable_observer
|
|
205
|
+
)
|
|
206
|
+
rescue => preparation_error
|
|
207
|
+
release_and_fail(
|
|
208
|
+
event_loop,
|
|
209
|
+
result_task,
|
|
210
|
+
thread_id,
|
|
211
|
+
fsm_session_id,
|
|
212
|
+
preparation_error
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
else
|
|
217
|
+
execution = build_new_execution(
|
|
218
|
+
input,
|
|
219
|
+
thread_id: thread_id,
|
|
220
|
+
fsm_session_id: fsm_session_id,
|
|
221
|
+
recursion_limit: recursion_limit,
|
|
222
|
+
repository: repository,
|
|
223
|
+
persist: false,
|
|
224
|
+
record: nil
|
|
225
|
+
)
|
|
226
|
+
register_execution(
|
|
227
|
+
execution,
|
|
228
|
+
result_task,
|
|
229
|
+
stable_observer: stable_observer
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
result_task
|
|
233
|
+
rescue => error
|
|
234
|
+
if defined?(event_loop) && event_loop &&
|
|
235
|
+
defined?(thread_id) && defined?(fsm_session_id)
|
|
236
|
+
event_loop.release_workflow(
|
|
237
|
+
thread_id,
|
|
238
|
+
owner_fsm_session_id: fsm_session_id
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
fail_task(result_task, error) if defined?(result_task) && result_task
|
|
242
|
+
result_task || failed_task("workflow:preparation", error)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def start_resume_execution(state, input:, event_name:, current_phase:)
|
|
246
|
+
runtime = Phronomy::Runtime.instance
|
|
247
|
+
event_loop = runtime.event_loop
|
|
248
|
+
thread_id = state.thread_id.to_s
|
|
249
|
+
fsm_session_id = SecureRandom.uuid
|
|
250
|
+
repository = configured_repository
|
|
251
|
+
result_task = Phronomy::Task.deferred(name: "workflow-resume:#{thread_id}")
|
|
252
|
+
|
|
253
|
+
event_loop.admit_workflow(
|
|
254
|
+
thread_id,
|
|
255
|
+
owner_fsm_session_id: fsm_session_id
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if repository
|
|
259
|
+
load_operation = runtime.offload.submit(on_full: :raise) do
|
|
260
|
+
repository.load(thread_id)
|
|
261
|
+
end
|
|
262
|
+
load_operation.on_complete do |record, error|
|
|
263
|
+
if error
|
|
264
|
+
release_and_fail(
|
|
265
|
+
event_loop, result_task, thread_id, fsm_session_id, error
|
|
266
|
+
)
|
|
267
|
+
next
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
begin
|
|
271
|
+
expected_revision = validate_resume_snapshot!(state, record)
|
|
272
|
+
context = input ? state.merge(input) : state
|
|
273
|
+
execution = Execution.new(
|
|
274
|
+
context: context,
|
|
275
|
+
thread_id: thread_id,
|
|
276
|
+
fsm_session_id: fsm_session_id,
|
|
277
|
+
recursion_limit: Phronomy.configuration.recursion_limit,
|
|
278
|
+
repository: repository,
|
|
279
|
+
persist: true,
|
|
280
|
+
expected_revision: expected_revision
|
|
281
|
+
)
|
|
282
|
+
register_execution(
|
|
283
|
+
execution,
|
|
284
|
+
result_task,
|
|
285
|
+
resume_event: event_name,
|
|
286
|
+
resume_phase: current_phase
|
|
287
|
+
)
|
|
288
|
+
rescue => preparation_error
|
|
289
|
+
release_and_fail(
|
|
290
|
+
event_loop,
|
|
291
|
+
result_task,
|
|
292
|
+
thread_id,
|
|
293
|
+
fsm_session_id,
|
|
294
|
+
preparation_error
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
context = input ? state.merge(input) : state
|
|
300
|
+
execution = Execution.new(
|
|
301
|
+
context: context,
|
|
302
|
+
thread_id: thread_id,
|
|
303
|
+
fsm_session_id: fsm_session_id,
|
|
304
|
+
recursion_limit: Phronomy.configuration.recursion_limit,
|
|
305
|
+
repository: nil,
|
|
306
|
+
persist: false,
|
|
307
|
+
expected_revision: nil
|
|
308
|
+
)
|
|
309
|
+
register_execution(
|
|
310
|
+
execution,
|
|
311
|
+
result_task,
|
|
312
|
+
resume_event: event_name,
|
|
313
|
+
resume_phase: current_phase
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
result_task
|
|
317
|
+
rescue => error
|
|
318
|
+
if defined?(event_loop) && event_loop &&
|
|
319
|
+
defined?(thread_id) && defined?(fsm_session_id)
|
|
320
|
+
event_loop.release_workflow(
|
|
321
|
+
thread_id,
|
|
322
|
+
owner_fsm_session_id: fsm_session_id
|
|
323
|
+
)
|
|
324
|
+
end
|
|
325
|
+
fail_task(result_task, error) if defined?(result_task) && result_task
|
|
326
|
+
result_task || failed_task("workflow:resume-preparation", error)
|
|
327
|
+
end
|
|
163
328
|
|
|
164
|
-
|
|
329
|
+
def build_new_execution(
|
|
330
|
+
input,
|
|
331
|
+
thread_id:,
|
|
332
|
+
fsm_session_id:,
|
|
333
|
+
recursion_limit:,
|
|
334
|
+
repository:,
|
|
335
|
+
persist:,
|
|
336
|
+
record:
|
|
337
|
+
)
|
|
338
|
+
snapshot = record_value(record, :snapshot)
|
|
339
|
+
stored_fields = snapshot && (snapshot[:fields] || snapshot["fields"])
|
|
165
340
|
initial_fields = if stored_fields
|
|
166
341
|
stored_fields
|
|
167
342
|
.transform_keys(&:to_sym)
|
|
@@ -176,45 +351,84 @@ module Phronomy
|
|
|
176
351
|
Execution.new(
|
|
177
352
|
context: context,
|
|
178
353
|
thread_id: thread_id,
|
|
354
|
+
fsm_session_id: fsm_session_id,
|
|
179
355
|
recursion_limit: recursion_limit,
|
|
180
|
-
|
|
181
|
-
persist:
|
|
356
|
+
repository: repository,
|
|
357
|
+
persist: persist,
|
|
358
|
+
expected_revision: record_value(record, :revision)
|
|
182
359
|
)
|
|
183
360
|
end
|
|
184
361
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
362
|
+
def validate_resume_snapshot!(state, record)
|
|
363
|
+
return nil unless record
|
|
364
|
+
|
|
365
|
+
durable_snapshot = normalize_snapshot(record_value(record, :snapshot))
|
|
366
|
+
local_snapshot = normalize_snapshot(snapshot_for(state))
|
|
367
|
+
return record_value(record, :revision) if durable_snapshot == local_snapshot
|
|
368
|
+
|
|
369
|
+
raise Phronomy::Persistence::ConflictError,
|
|
370
|
+
"Workflow state changed since the supplied halted context for " \
|
|
371
|
+
"thread_id #{state.thread_id.inspect}; explicit reload/reconciliation is required"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def normalize_snapshot(snapshot)
|
|
375
|
+
snapshot ||= {}
|
|
376
|
+
fields = snapshot[:fields] || snapshot["fields"] || {}
|
|
377
|
+
phase = snapshot[:phase] || snapshot["phase"]
|
|
378
|
+
{
|
|
379
|
+
fields: normalize_workflow_value(fields),
|
|
380
|
+
phase: phase&.to_s
|
|
381
|
+
}
|
|
188
382
|
end
|
|
189
383
|
|
|
190
|
-
def
|
|
384
|
+
def normalize_workflow_value(value)
|
|
385
|
+
case value
|
|
386
|
+
when Hash
|
|
387
|
+
value.each_with_object({}) do |(key, child), result|
|
|
388
|
+
result[key.to_s] = normalize_workflow_value(child)
|
|
389
|
+
end
|
|
390
|
+
when Array
|
|
391
|
+
value.map { |child| normalize_workflow_value(child) }
|
|
392
|
+
when Symbol
|
|
393
|
+
value.to_s
|
|
394
|
+
else
|
|
395
|
+
value
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def record_value(record, key)
|
|
400
|
+
return nil unless record
|
|
401
|
+
record.key?(key) ? record[key] : record[key.to_s]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def configured_repository
|
|
405
|
+
persistence = @persistence || Phronomy.configuration.persistence
|
|
406
|
+
persistence&.workflow_states
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def register_execution(
|
|
191
410
|
execution,
|
|
411
|
+
result_task,
|
|
192
412
|
resume_event: nil,
|
|
193
413
|
resume_phase: nil,
|
|
194
414
|
stable_observer: nil
|
|
195
415
|
)
|
|
196
416
|
runtime = Phronomy::Runtime.instance
|
|
197
|
-
result_task = Phronomy::Task.deferred(
|
|
198
|
-
name: "workflow:#{execution.thread_id}"
|
|
199
|
-
)
|
|
200
417
|
source_task = Phronomy::Task.deferred(
|
|
201
|
-
name: "workflow-source:#{execution.
|
|
418
|
+
name: "workflow-source:#{execution.fsm_session_id}"
|
|
202
419
|
)
|
|
203
420
|
|
|
204
421
|
source_task.on_complete do |result, error|
|
|
205
422
|
finalize_execution(
|
|
423
|
+
execution: execution,
|
|
206
424
|
result_task: result_task,
|
|
207
425
|
result: result,
|
|
208
|
-
error: error
|
|
209
|
-
store: execution.store,
|
|
210
|
-
thread_id: execution.thread_id,
|
|
211
|
-
persist: execution.persist
|
|
426
|
+
error: error
|
|
212
427
|
)
|
|
213
428
|
end
|
|
214
429
|
|
|
215
430
|
session = build_session_for(
|
|
216
|
-
|
|
217
|
-
recursion_limit: execution.recursion_limit,
|
|
431
|
+
execution: execution,
|
|
218
432
|
runtime: runtime,
|
|
219
433
|
resume_event: resume_event,
|
|
220
434
|
resume_phase: resume_phase,
|
|
@@ -223,56 +437,108 @@ module Phronomy
|
|
|
223
437
|
runtime.event_loop.register(session, completion: source_task)
|
|
224
438
|
result_task
|
|
225
439
|
rescue => error
|
|
226
|
-
|
|
227
|
-
|
|
440
|
+
Phronomy::Runtime.instance.event_loop.release_workflow(
|
|
441
|
+
execution.thread_id,
|
|
442
|
+
owner_fsm_session_id: execution.fsm_session_id
|
|
443
|
+
)
|
|
444
|
+
fail_task(result_task, error)
|
|
445
|
+
result_task
|
|
228
446
|
end
|
|
229
447
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
error:,
|
|
234
|
-
store:,
|
|
235
|
-
thread_id:,
|
|
236
|
-
persist:
|
|
237
|
-
)
|
|
448
|
+
# Called from source_task completion on EventLoop.
|
|
449
|
+
def finalize_execution(execution:, result_task:, result:, error:)
|
|
450
|
+
event_loop = Phronomy::Runtime.instance.event_loop
|
|
238
451
|
if error
|
|
452
|
+
event_loop.release_workflow(
|
|
453
|
+
execution.thread_id,
|
|
454
|
+
owner_fsm_session_id: execution.fsm_session_id
|
|
455
|
+
)
|
|
239
456
|
fail_task(result_task, error)
|
|
240
457
|
return
|
|
241
458
|
end
|
|
242
459
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
460
|
+
unless execution.repository && execution.persist
|
|
461
|
+
event_loop.release_workflow(
|
|
462
|
+
execution.thread_id,
|
|
463
|
+
owner_fsm_session_id: execution.fsm_session_id
|
|
464
|
+
)
|
|
465
|
+
complete_task(result_task, result)
|
|
247
466
|
return
|
|
248
467
|
end
|
|
249
468
|
|
|
250
|
-
|
|
469
|
+
snapshot = snapshot_for(result)
|
|
470
|
+
runtime = Phronomy::Runtime.instance
|
|
471
|
+
operation = runtime.offload.submit(on_full: :raise) do
|
|
472
|
+
execution.repository.save(
|
|
473
|
+
execution.thread_id,
|
|
474
|
+
expected_revision: execution.expected_revision,
|
|
475
|
+
snapshot: snapshot
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
operation.on_complete do |_revision, persistence_error|
|
|
479
|
+
command = WorkflowPersistenceCommand.new(
|
|
480
|
+
self,
|
|
481
|
+
result_task,
|
|
482
|
+
result,
|
|
483
|
+
persistence_error,
|
|
484
|
+
execution.thread_id,
|
|
485
|
+
execution.fsm_session_id
|
|
486
|
+
)
|
|
487
|
+
posted = event_loop.post(
|
|
488
|
+
Phronomy::Event.new(
|
|
489
|
+
type: :workflow_persistence_ready,
|
|
490
|
+
target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
|
|
491
|
+
payload: {command: command}
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
settle_persistence_without_event_loop(command, event_loop) unless posted
|
|
495
|
+
end
|
|
496
|
+
rescue => persistence_start_error
|
|
497
|
+
event_loop.release_workflow(
|
|
498
|
+
execution.thread_id,
|
|
499
|
+
owner_fsm_session_id: execution.fsm_session_id
|
|
500
|
+
)
|
|
501
|
+
fail_task(result_task, persistence_start_error)
|
|
251
502
|
end
|
|
252
503
|
|
|
253
|
-
def
|
|
254
|
-
|
|
504
|
+
def settle_persistence_without_event_loop(command, event_loop)
|
|
505
|
+
event_loop.release_workflow(
|
|
506
|
+
command.thread_id,
|
|
507
|
+
owner_fsm_session_id: command.fsm_session_id
|
|
508
|
+
)
|
|
509
|
+
command.error ?
|
|
510
|
+
fail_task(command.result_task, command.error) :
|
|
511
|
+
complete_task(command.result_task, command.result)
|
|
512
|
+
rescue => error
|
|
513
|
+
fail_task(command.result_task, error)
|
|
514
|
+
end
|
|
255
515
|
|
|
256
|
-
|
|
516
|
+
def release_and_fail(event_loop, result_task, thread_id, fsm_session_id, error)
|
|
517
|
+
event_loop.release_workflow(
|
|
257
518
|
thread_id,
|
|
258
|
-
|
|
259
|
-
fields: context.to_h,
|
|
260
|
-
phase: context.phase.to_s
|
|
261
|
-
}
|
|
519
|
+
owner_fsm_session_id: fsm_session_id
|
|
262
520
|
)
|
|
521
|
+
fail_task(result_task, error)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def snapshot_for(context)
|
|
525
|
+
{
|
|
526
|
+
fields: context.to_h,
|
|
527
|
+
phase: context.phase.to_s
|
|
528
|
+
}
|
|
263
529
|
end
|
|
264
530
|
|
|
265
531
|
def build_session_for(
|
|
266
|
-
|
|
267
|
-
recursion_limit:,
|
|
532
|
+
execution:,
|
|
268
533
|
runtime:,
|
|
269
534
|
resume_event: nil,
|
|
270
535
|
resume_phase: nil,
|
|
271
536
|
stable_observer: nil
|
|
272
537
|
)
|
|
273
538
|
Phronomy::FSMSession.new(
|
|
274
|
-
id:
|
|
275
|
-
|
|
539
|
+
id: execution.fsm_session_id,
|
|
540
|
+
graph_thread_id: execution.thread_id,
|
|
541
|
+
context: execution.context,
|
|
276
542
|
entry_point: @entry_point,
|
|
277
543
|
entry_actions: @entry_actions,
|
|
278
544
|
auto_state_set: @auto_state_set,
|
|
@@ -280,7 +546,7 @@ module Phronomy
|
|
|
280
546
|
wait_state_names: @wait_state_names,
|
|
281
547
|
external_events: @external_events,
|
|
282
548
|
phase_machine_class: @phase_machine_class,
|
|
283
|
-
recursion_limit: recursion_limit,
|
|
549
|
+
recursion_limit: execution.recursion_limit,
|
|
284
550
|
event_loop: runtime.event_loop,
|
|
285
551
|
resume_event: resume_event,
|
|
286
552
|
resume_phase: resume_phase,
|
data/lib/phronomy.rb
CHANGED
|
@@ -17,7 +17,10 @@ loader.inflector.inflect("llm_call_record" => "LLMCallRecord")
|
|
|
17
17
|
loader.inflector.inflect("llm_input_manifest" => "LLMInputManifest")
|
|
18
18
|
loader.inflector.inflect("llm_input_build_context" => "LLMInputBuildContext")
|
|
19
19
|
loader.inflector.inflect("llm_input_patch" => "LLMInputPatch")
|
|
20
|
+
loader.inflector.inflect("before_llm_input" => "BeforeLLMInput")
|
|
20
21
|
loader.collapse("#{__dir__}/phronomy/engine")
|
|
22
|
+
# Loaded via require_relative before loader.setup; ignore to avoid Zeitwerk constant-name mismatch.
|
|
23
|
+
loader.ignore("#{__dir__}/phronomy/ruby_llm_patches.rb")
|
|
21
24
|
loader.setup
|
|
22
25
|
|
|
23
26
|
require_relative "phronomy/version"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phronomy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raizo T.C.S
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -119,11 +119,13 @@ files:
|
|
|
119
119
|
- docs/decisions/011-delegate-transport-policy-to-adapters.md
|
|
120
120
|
- docs/decisions/012-canonical-execution-log-and-context-policy.md
|
|
121
121
|
- docs/decisions/013-journal-backed-knowledge-as-context-candidates.md
|
|
122
|
+
- docs/decisions/014-unified-persistence-durable-state.md
|
|
122
123
|
- docs/features.md
|
|
123
124
|
- docs/getting-started.md
|
|
124
125
|
- docs/mcp-client.md
|
|
125
126
|
- docs/migrations/0.15.md
|
|
126
127
|
- docs/migrations/0.16.md
|
|
128
|
+
- docs/migrations/0.19.md
|
|
127
129
|
- docs/runtime-and-concurrency.md
|
|
128
130
|
- examples/workflows/agent_event_mapping.rb
|
|
129
131
|
- examples/workflows/generic_task_event_mapping.rb
|
|
@@ -233,8 +235,6 @@ files:
|
|
|
233
235
|
- lib/phronomy/persistence/in_memory.rb
|
|
234
236
|
- lib/phronomy/ruby_llm_patches.rb
|
|
235
237
|
- lib/phronomy/runnable.rb
|
|
236
|
-
- lib/phronomy/state_store/base.rb
|
|
237
|
-
- lib/phronomy/state_store/in_memory.rb
|
|
238
238
|
- lib/phronomy/stream_callback_error.rb
|
|
239
239
|
- lib/phronomy/testing.rb
|
|
240
240
|
- lib/phronomy/testing/eval.rb
|