phronomy 0.17.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 +122 -1134
- data/README.md +70 -1280
- data/benchmark/bench_regression.rb +25 -5
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +93 -0
- data/docs/getting-started.md +387 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +349 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +116 -55
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +423 -252
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/shared_state.rb +2 -0
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +97 -47
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/configuration.rb +7 -30
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +170 -193
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +52 -158
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +363 -99
- data/lib/phronomy.rb +10 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +25 -33
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -193
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -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,
|
|
@@ -311,13 +577,11 @@ module Phronomy
|
|
|
311
577
|
end
|
|
312
578
|
|
|
313
579
|
def complete_task(task, value)
|
|
314
|
-
task.
|
|
315
|
-
task.transition!(:completed, value: value)
|
|
580
|
+
task.complete(value)
|
|
316
581
|
end
|
|
317
582
|
|
|
318
583
|
def fail_task(task, error)
|
|
319
|
-
task.
|
|
320
|
-
task.transition!(:failed, error: error)
|
|
584
|
+
task.fail(error)
|
|
321
585
|
end
|
|
322
586
|
|
|
323
587
|
def failed_task(name, error)
|
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"
|
|
@@ -38,7 +41,13 @@ module Phronomy
|
|
|
38
41
|
class ContextLengthError < Error; end
|
|
39
42
|
class CancellationError < Error; end
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
# Raised when a synchronous API would block the EventLoop control thread.
|
|
45
|
+
class EventLoopReentrancyError < Error; end
|
|
46
|
+
|
|
47
|
+
# Backward-compatible error class name for callers that still rescue the old
|
|
48
|
+
# scheduler-oriented exception. New code should use EventLoopReentrancyError.
|
|
49
|
+
class SchedulerReentrancyError < EventLoopReentrancyError; end
|
|
50
|
+
|
|
42
51
|
class RuntimeShutdownError < Error; end
|
|
43
52
|
class RuntimeShutdownReentrancyError < RuntimeShutdownError; end
|
|
44
53
|
|
|
@@ -63,9 +72,6 @@ module Phronomy
|
|
|
63
72
|
class PoolShutdownError < Error; end
|
|
64
73
|
class BackpressureError < Error; end
|
|
65
74
|
|
|
66
|
-
# Raised when a WorkflowContext field is mutated from a thread that does not
|
|
67
|
-
# own the context. Deliver asynchronous updates back to the Workflow as later
|
|
68
|
-
# events via Workflow#signal instead of mutating context from worker callbacks.
|
|
69
75
|
class WorkflowContextOwnershipError < Error; end
|
|
70
76
|
|
|
71
77
|
class << self
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
# scripts/api_snapshot.rb
|
|
5
5
|
#
|
|
6
|
-
# Dumps the public instance methods of all Stable/Beta
|
|
7
|
-
# JSON.
|
|
8
|
-
#
|
|
6
|
+
# Dumps the public instance methods of all Stable/Beta product API classes to
|
|
7
|
+
# JSON. Testing helpers are intentionally excluded from this compatibility gate.
|
|
8
|
+
# The snapshot is stored in spec/fixtures/api_snapshot.json and is used by
|
|
9
|
+
# spec/phronomy/api_compatibility_spec.rb to detect unintended API removals.
|
|
9
10
|
#
|
|
10
11
|
# Usage:
|
|
11
12
|
# ruby scripts/api_snapshot.rb --write
|
|
@@ -33,7 +34,6 @@ PUBLIC_API_ENTRIES = [
|
|
|
33
34
|
Phronomy::VectorStore::Embeddings::Base,
|
|
34
35
|
Phronomy::Tracing::Base,
|
|
35
36
|
Phronomy::Tracing::NullTracer,
|
|
36
|
-
Phronomy::Eval::Runner,
|
|
37
37
|
Phronomy::Tools::Mcp,
|
|
38
38
|
Phronomy::Tools::Agent,
|
|
39
39
|
Phronomy::Tools::VectorSearch
|