phronomy 0.17.0 → 0.18.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 +97 -1134
- data/README.md +68 -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/010-cooperative-first-concurrency.md +155 -235
- data/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -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 +35 -19
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +6 -6
- 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 +5 -29
- 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 +89 -190
- 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 +45 -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/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_runner.rb +2 -4
- data/lib/phronomy.rb +7 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +23 -31
- 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/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -5,18 +5,9 @@ require "state_machines"
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
module Agent
|
|
7
7
|
# Builds an FSMSession for one ToolInvocation.
|
|
8
|
-
#
|
|
9
|
-
# Authorization and execution Tasks are observed by ToolInvocation-specific
|
|
10
|
-
# callbacks that post explicit FSM events. The generated phase machine does
|
|
11
|
-
# not await Tasks.
|
|
12
|
-
#
|
|
13
|
-
# @api private
|
|
8
|
+
# Async completion is represented only by explicit FSM events.
|
|
14
9
|
class ToolInvocationSessionBuilder
|
|
15
|
-
AUTO_STATE_SET = {
|
|
16
|
-
idle: true,
|
|
17
|
-
validating: true,
|
|
18
|
-
queued: true
|
|
19
|
-
}.freeze
|
|
10
|
+
AUTO_STATE_SET = {idle: true, validating: true, queued: true}.freeze
|
|
20
11
|
|
|
21
12
|
DECLARED_STATES = %i[
|
|
22
13
|
idle validating authorizing awaiting_approval authorized queued running
|
|
@@ -27,42 +18,20 @@ module Phronomy
|
|
|
27
18
|
|
|
28
19
|
EXTERNAL_EVENTS = {
|
|
29
20
|
authorization_completed: [
|
|
30
|
-
{from: :authorizing, to: :cancelled, guard: ->(ctx) {
|
|
31
|
-
|
|
32
|
-
}},
|
|
33
|
-
{from: :authorizing, to: :
|
|
34
|
-
|
|
35
|
-
}},
|
|
36
|
-
{from: :authorizing, to: :rejected, guard: ->(ctx) {
|
|
37
|
-
ctx.rejected?
|
|
38
|
-
}},
|
|
39
|
-
{from: :authorizing, to: :awaiting_approval, guard: ->(ctx) {
|
|
40
|
-
ctx.awaiting_approval?
|
|
41
|
-
}},
|
|
42
|
-
{from: :authorizing, to: :authorized, guard: ->(ctx) {
|
|
43
|
-
ctx.authorized?
|
|
44
|
-
}}
|
|
21
|
+
{from: :authorizing, to: :cancelled, guard: ->(ctx) { ctx.cancelled? }},
|
|
22
|
+
{from: :authorizing, to: :failed, guard: ->(ctx) { ctx.failed? }},
|
|
23
|
+
{from: :authorizing, to: :rejected, guard: ->(ctx) { ctx.rejected? }},
|
|
24
|
+
{from: :authorizing, to: :awaiting_approval, guard: ->(ctx) { ctx.awaiting_approval? }},
|
|
25
|
+
{from: :authorizing, to: :authorized, guard: ->(ctx) { ctx.authorized? }}
|
|
45
26
|
],
|
|
46
27
|
execution_completed: [
|
|
47
|
-
{from: :running, to: :cancelled, guard: ->(ctx) {
|
|
48
|
-
|
|
49
|
-
}}
|
|
50
|
-
{from: :running, to: :failed, guard: ->(ctx) {
|
|
51
|
-
ctx.failed?
|
|
52
|
-
}},
|
|
53
|
-
{from: :running, to: :completed, guard: ->(ctx) {
|
|
54
|
-
ctx.execution_completed?
|
|
55
|
-
}}
|
|
56
|
-
],
|
|
57
|
-
approve: [
|
|
58
|
-
{from: :awaiting_approval, to: :authorized, guard: nil}
|
|
59
|
-
],
|
|
60
|
-
reject: [
|
|
61
|
-
{from: :awaiting_approval, to: :rejected, guard: nil}
|
|
62
|
-
],
|
|
63
|
-
dispatch: [
|
|
64
|
-
{from: :authorized, to: :queued, guard: nil}
|
|
28
|
+
{from: :running, to: :cancelled, guard: ->(ctx) { ctx.cancelled? }},
|
|
29
|
+
{from: :running, to: :failed, guard: ->(ctx) { ctx.failed? }},
|
|
30
|
+
{from: :running, to: :completed, guard: ->(ctx) { ctx.execution_completed? }}
|
|
65
31
|
],
|
|
32
|
+
approve: [{from: :awaiting_approval, to: :authorized, guard: nil}],
|
|
33
|
+
reject: [{from: :awaiting_approval, to: :rejected, guard: nil}],
|
|
34
|
+
dispatch: [{from: :authorized, to: :queued, guard: nil}],
|
|
66
35
|
cancel: [
|
|
67
36
|
{from: :awaiting_approval, to: :cancelled, guard: nil},
|
|
68
37
|
{from: :authorized, to: :cancelled, guard: nil},
|
|
@@ -71,14 +40,8 @@ module Phronomy
|
|
|
71
40
|
]
|
|
72
41
|
}.freeze
|
|
73
42
|
|
|
74
|
-
def self.build(
|
|
75
|
-
tool_invocation
|
|
76
|
-
runtime: Phronomy::Runtime.instance
|
|
77
|
-
)
|
|
78
|
-
build_session(
|
|
79
|
-
tool_invocation: tool_invocation,
|
|
80
|
-
runtime: runtime
|
|
81
|
-
)
|
|
43
|
+
def self.build(tool_invocation:, runtime: Phronomy::Runtime.instance)
|
|
44
|
+
build_session(tool_invocation: tool_invocation, runtime: runtime)
|
|
82
45
|
end
|
|
83
46
|
|
|
84
47
|
def self.build_for_resume(
|
|
@@ -125,31 +88,15 @@ module Phronomy
|
|
|
125
88
|
def self.build_entry_actions(runtime)
|
|
126
89
|
{
|
|
127
90
|
validating: [method(:validating_action)],
|
|
128
|
-
authorizing: [
|
|
129
|
-
|
|
130
|
-
],
|
|
131
|
-
awaiting_approval: [
|
|
132
|
-
method(:awaiting_approval_action).curry.call(runtime)
|
|
133
|
-
],
|
|
134
|
-
authorized: [
|
|
135
|
-
method(:authorized_action).curry.call(runtime)
|
|
136
|
-
],
|
|
91
|
+
authorizing: [method(:authorizing_action).curry.call(runtime)],
|
|
92
|
+
awaiting_approval: [method(:awaiting_approval_action).curry.call(runtime)],
|
|
93
|
+
authorized: [method(:authorized_action).curry.call(runtime)],
|
|
137
94
|
queued: [method(:queued_action)],
|
|
138
|
-
running: [
|
|
139
|
-
|
|
140
|
-
],
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
],
|
|
144
|
-
failed: [
|
|
145
|
-
method(:failed_action).curry.call(runtime)
|
|
146
|
-
],
|
|
147
|
-
rejected: [
|
|
148
|
-
method(:rejected_action).curry.call(runtime)
|
|
149
|
-
],
|
|
150
|
-
cancelled: [
|
|
151
|
-
method(:cancelled_action).curry.call(runtime)
|
|
152
|
-
]
|
|
95
|
+
running: [method(:running_action).curry.call(runtime)],
|
|
96
|
+
completed: [method(:completed_action).curry.call(runtime)],
|
|
97
|
+
failed: [method(:failed_action).curry.call(runtime)],
|
|
98
|
+
rejected: [method(:rejected_action).curry.call(runtime)],
|
|
99
|
+
cancelled: [method(:cancelled_action).curry.call(runtime)]
|
|
153
100
|
}
|
|
154
101
|
end
|
|
155
102
|
private_class_method :build_entry_actions
|
|
@@ -176,59 +123,29 @@ module Phronomy
|
|
|
176
123
|
|
|
177
124
|
event :state_completed do
|
|
178
125
|
transition idle: :validating
|
|
179
|
-
|
|
180
|
-
transition validating: :
|
|
181
|
-
|
|
182
|
-
transition validating: :completed,
|
|
183
|
-
if: ->(machine) {
|
|
184
|
-
machine.context&.validation_completed?
|
|
185
|
-
}
|
|
186
|
-
transition validating: :authorizing,
|
|
187
|
-
if: ->(machine) {
|
|
188
|
-
machine.context&.validation_passed?
|
|
189
|
-
}
|
|
190
|
-
|
|
126
|
+
transition validating: :failed, if: ->(m) { m.context&.failed? }
|
|
127
|
+
transition validating: :completed, if: ->(m) { m.context&.validation_completed? }
|
|
128
|
+
transition validating: :authorizing, if: ->(m) { m.context&.validation_passed? }
|
|
191
129
|
transition queued: :running
|
|
192
130
|
end
|
|
193
131
|
|
|
194
132
|
event :authorization_completed do
|
|
195
|
-
transition authorizing: :cancelled,
|
|
196
|
-
|
|
197
|
-
transition authorizing: :
|
|
198
|
-
|
|
199
|
-
transition authorizing: :
|
|
200
|
-
if: ->(machine) { machine.context&.rejected? }
|
|
201
|
-
transition authorizing: :awaiting_approval,
|
|
202
|
-
if: ->(machine) {
|
|
203
|
-
machine.context&.awaiting_approval?
|
|
204
|
-
}
|
|
205
|
-
transition authorizing: :authorized,
|
|
206
|
-
if: ->(machine) { machine.context&.authorized? }
|
|
133
|
+
transition authorizing: :cancelled, if: ->(m) { m.context&.cancelled? }
|
|
134
|
+
transition authorizing: :failed, if: ->(m) { m.context&.failed? }
|
|
135
|
+
transition authorizing: :rejected, if: ->(m) { m.context&.rejected? }
|
|
136
|
+
transition authorizing: :awaiting_approval, if: ->(m) { m.context&.awaiting_approval? }
|
|
137
|
+
transition authorizing: :authorized, if: ->(m) { m.context&.authorized? }
|
|
207
138
|
end
|
|
208
139
|
|
|
209
140
|
event :execution_completed do
|
|
210
|
-
transition running: :cancelled,
|
|
211
|
-
|
|
212
|
-
transition running: :
|
|
213
|
-
if: ->(machine) { machine.context&.failed? }
|
|
214
|
-
transition running: :completed,
|
|
215
|
-
if: ->(machine) {
|
|
216
|
-
machine.context&.execution_completed?
|
|
217
|
-
}
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
event :approve do
|
|
221
|
-
transition awaiting_approval: :authorized
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
event :reject do
|
|
225
|
-
transition awaiting_approval: :rejected
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
event :dispatch do
|
|
229
|
-
transition authorized: :queued
|
|
141
|
+
transition running: :cancelled, if: ->(m) { m.context&.cancelled? }
|
|
142
|
+
transition running: :failed, if: ->(m) { m.context&.failed? }
|
|
143
|
+
transition running: :completed, if: ->(m) { m.context&.execution_completed? }
|
|
230
144
|
end
|
|
231
145
|
|
|
146
|
+
event(:approve) { transition awaiting_approval: :authorized }
|
|
147
|
+
event(:reject) { transition awaiting_approval: :rejected }
|
|
148
|
+
event(:dispatch) { transition authorized: :queued }
|
|
232
149
|
event :cancel do
|
|
233
150
|
transition awaiting_approval: :cancelled
|
|
234
151
|
transition authorized: :cancelled
|
|
@@ -267,13 +184,9 @@ module Phronomy
|
|
|
267
184
|
private_class_method :validating_action
|
|
268
185
|
|
|
269
186
|
def self.authorizing_action(runtime, invocation)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
invocation,
|
|
274
|
-
task,
|
|
275
|
-
event_type: :authorization_completed
|
|
276
|
-
)
|
|
187
|
+
invocation.start_authorization(runtime: runtime) do |outcome|
|
|
188
|
+
post_to_invocation(runtime, invocation.id, :authorization_completed, outcome)
|
|
189
|
+
end
|
|
277
190
|
invocation
|
|
278
191
|
end
|
|
279
192
|
private_class_method :authorizing_action
|
|
@@ -298,45 +211,14 @@ module Phronomy
|
|
|
298
211
|
private_class_method :queued_action
|
|
299
212
|
|
|
300
213
|
def self.running_action(runtime, invocation)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
214
|
+
invocation.start_execution(runtime: runtime) do |outcome|
|
|
215
|
+
post_to_invocation(runtime, invocation.id, :execution_completed, outcome)
|
|
216
|
+
end
|
|
304
217
|
invocation.mark_running!
|
|
305
|
-
observe_task(
|
|
306
|
-
runtime,
|
|
307
|
-
invocation,
|
|
308
|
-
task,
|
|
309
|
-
event_type: :execution_completed
|
|
310
|
-
)
|
|
311
218
|
invocation
|
|
312
219
|
end
|
|
313
220
|
private_class_method :running_action
|
|
314
221
|
|
|
315
|
-
def self.observe_task(
|
|
316
|
-
runtime,
|
|
317
|
-
invocation,
|
|
318
|
-
task,
|
|
319
|
-
event_type:
|
|
320
|
-
)
|
|
321
|
-
task.on_complete do |outcome, error|
|
|
322
|
-
payload = error || outcome
|
|
323
|
-
accepted = runtime.event_loop.post_to_session(
|
|
324
|
-
Phronomy::Event.new(
|
|
325
|
-
type: event_type,
|
|
326
|
-
target_id: invocation.id,
|
|
327
|
-
payload: payload
|
|
328
|
-
)
|
|
329
|
-
)
|
|
330
|
-
next if accepted
|
|
331
|
-
|
|
332
|
-
Phronomy.configuration.logger&.warn(
|
|
333
|
-
"[Phronomy] Dropped #{event_type.inspect} for " \
|
|
334
|
-
"ToolInvocation #{invocation.id}"
|
|
335
|
-
)
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
private_class_method :observe_task
|
|
339
|
-
|
|
340
222
|
def self.completed_action(runtime, invocation)
|
|
341
223
|
notify_parent(runtime, invocation, :tool_completed)
|
|
342
224
|
invocation
|
|
@@ -363,6 +245,18 @@ module Phronomy
|
|
|
363
245
|
end
|
|
364
246
|
private_class_method :cancelled_action
|
|
365
247
|
|
|
248
|
+
def self.post_to_invocation(runtime, id, event_type, payload)
|
|
249
|
+
accepted = runtime.event_loop.post_to_session(
|
|
250
|
+
Phronomy::Event.new(type: event_type, target_id: id, payload: payload)
|
|
251
|
+
)
|
|
252
|
+
return if accepted
|
|
253
|
+
|
|
254
|
+
Phronomy.configuration.logger&.warn(
|
|
255
|
+
"[Phronomy] Dropped #{event_type.inspect} for ToolInvocation #{id}"
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
private_class_method :post_to_invocation
|
|
259
|
+
|
|
366
260
|
def self.notify_parent(runtime, invocation, event_type)
|
|
367
261
|
runtime.event_loop.post_to_session(
|
|
368
262
|
Phronomy::Event.new(
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
# Holds global configuration for the entire framework.
|
|
5
|
-
# Configure via the Phronomy.configure block.
|
|
6
5
|
class Configuration
|
|
7
6
|
STREAM_CALLBACK_ERROR_POLICIES = %i[report fail_task].freeze
|
|
8
|
-
|
|
9
|
-
private_constant :STREAM_CALLBACK_ERROR_POLICIES, :RUNTIME_BACKENDS
|
|
7
|
+
private_constant :STREAM_CALLBACK_ERROR_POLICIES
|
|
10
8
|
|
|
11
9
|
attr_accessor :default_model
|
|
12
10
|
attr_accessor :default_embedding_model
|
|
@@ -23,17 +21,12 @@ module Phronomy
|
|
|
23
21
|
attr_accessor :llm_adapter
|
|
24
22
|
attr_accessor :event_loop_starvation_threshold_seconds
|
|
25
23
|
attr_accessor :event_loop_dispatch_threshold_seconds
|
|
26
|
-
attr_accessor :scheduler_debug
|
|
27
|
-
attr_accessor :blocking_detect_threshold_ms
|
|
28
24
|
attr_reader :stream_callback_error_policy
|
|
29
|
-
attr_accessor :
|
|
30
|
-
attr_accessor :
|
|
25
|
+
attr_accessor :offload_pool_size
|
|
26
|
+
attr_accessor :offload_queue_size
|
|
31
27
|
attr_accessor :authorization_pool_size
|
|
32
28
|
attr_accessor :authorization_queue_size
|
|
33
29
|
attr_accessor :authorization_timeout
|
|
34
|
-
attr_accessor :starvation_threshold_ms
|
|
35
|
-
attr_reader :runtime_backend
|
|
36
|
-
attr_accessor :strict_runtime_guards
|
|
37
30
|
|
|
38
31
|
def stream_callback_error_policy=(value)
|
|
39
32
|
unless STREAM_CALLBACK_ERROR_POLICIES.include?(value)
|
|
@@ -45,18 +38,6 @@ module Phronomy
|
|
|
45
38
|
@stream_callback_error_policy = value
|
|
46
39
|
end
|
|
47
40
|
|
|
48
|
-
# Scheduler backend used for newly-created Runtime instances.
|
|
49
|
-
# Supported values are :thread, :immediate, and :fiber.
|
|
50
|
-
def runtime_backend=(value)
|
|
51
|
-
value = value.to_sym if value.respond_to?(:to_sym)
|
|
52
|
-
unless RUNTIME_BACKENDS.include?(value)
|
|
53
|
-
allowed = RUNTIME_BACKENDS.map(&:inspect).join(", ")
|
|
54
|
-
raise Phronomy::ConfigurationError,
|
|
55
|
-
"runtime_backend must be one of: #{allowed}"
|
|
56
|
-
end
|
|
57
|
-
@runtime_backend = value
|
|
58
|
-
end
|
|
59
|
-
|
|
60
41
|
def initialize
|
|
61
42
|
@recursion_limit = 25
|
|
62
43
|
@tracer = Phronomy::Tracing::NullTracer.new
|
|
@@ -66,17 +47,12 @@ module Phronomy
|
|
|
66
47
|
@llm_adapter = Phronomy::LLMAdapter::RubyLLM.new
|
|
67
48
|
@event_loop_starvation_threshold_seconds = nil
|
|
68
49
|
@event_loop_dispatch_threshold_seconds = nil
|
|
69
|
-
@scheduler_debug = false
|
|
70
|
-
@blocking_detect_threshold_ms = nil
|
|
71
50
|
@stream_callback_error_policy = :report
|
|
72
|
-
@
|
|
73
|
-
@
|
|
51
|
+
@offload_pool_size = 10
|
|
52
|
+
@offload_queue_size = 100
|
|
74
53
|
@authorization_pool_size = 4
|
|
75
54
|
@authorization_queue_size = 100
|
|
76
55
|
@authorization_timeout = 5
|
|
77
|
-
@starvation_threshold_ms = 50
|
|
78
|
-
@runtime_backend = :thread
|
|
79
|
-
@strict_runtime_guards = false
|
|
80
56
|
end
|
|
81
57
|
end
|
|
82
58
|
end
|
data/lib/phronomy/diagnostics.rb
CHANGED
|
@@ -1,62 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
# Developer-facing diagnostics for
|
|
5
|
-
#
|
|
6
|
-
# Provides debug dump utilities that can be called from an IRB / Rails console
|
|
7
|
-
# or in test helpers to inspect the current state of the Runtime.
|
|
8
|
-
#
|
|
9
|
-
# @example Enable diagnostics and print a dump
|
|
10
|
-
# Phronomy.configure { |c| c.scheduler_debug = true }
|
|
11
|
-
# Phronomy::Diagnostics.dump
|
|
4
|
+
# Developer-facing diagnostics for EventLoop and offload execution boundaries.
|
|
12
5
|
module Diagnostics
|
|
13
|
-
# Prints a formatted summary of the current Runtime state to +$stderr+
|
|
14
|
-
# (or the supplied IO).
|
|
15
|
-
#
|
|
16
|
-
# Includes:
|
|
17
|
-
# - BlockingAdapterPool: active workers, queue depth, abandoned count
|
|
18
|
-
# - EventLoop: last / max / average lag in milliseconds
|
|
19
|
-
#
|
|
20
|
-
# @param out [IO] output destination (default: $stderr)
|
|
21
|
-
# @return [void]
|
|
22
|
-
# @api public
|
|
23
6
|
def self.dump(out: $stderr)
|
|
24
7
|
snap = Phronomy::Metrics.snapshot
|
|
25
8
|
|
|
26
9
|
out.puts "[Phronomy::Diagnostics] Runtime state dump"
|
|
27
|
-
out.puts "
|
|
28
|
-
out.puts " pool_size
|
|
29
|
-
out.puts " active_count
|
|
30
|
-
out.puts " queue_depth
|
|
31
|
-
out.puts "
|
|
10
|
+
out.puts " OffloadPool:"
|
|
11
|
+
out.puts " pool_size : #{snap[:offload_pool_size]}"
|
|
12
|
+
out.puts " active_count : #{snap[:offload_pool_active]}"
|
|
13
|
+
out.puts " queue_depth : #{snap[:offload_pool_queue_length]}"
|
|
14
|
+
out.puts " abandoned_active : #{snap[:offload_pool_abandoned_active]}"
|
|
15
|
+
out.puts " abandoned_total : #{snap[:offload_pool_abandoned_total]}"
|
|
32
16
|
out.puts " EventLoop:"
|
|
33
|
-
out.puts " last_lag_ms
|
|
34
|
-
out.puts " max_lag_ms
|
|
35
|
-
out.puts " average_lag_ms
|
|
17
|
+
out.puts " last_lag_ms : #{snap[:event_loop_lag_last_ms]}"
|
|
18
|
+
out.puts " max_lag_ms : #{snap[:event_loop_lag_max_ms]}"
|
|
19
|
+
out.puts " average_lag_ms : #{snap[:event_loop_lag_average_ms]}"
|
|
36
20
|
end
|
|
37
21
|
|
|
38
|
-
# Returns the diagnostics state as a plain Hash (useful for JSON export).
|
|
39
|
-
#
|
|
40
|
-
# @return [Hash]
|
|
41
|
-
# @api public
|
|
42
22
|
def self.snapshot
|
|
43
23
|
Phronomy::Metrics.snapshot
|
|
44
24
|
end
|
|
45
25
|
|
|
46
|
-
# Raises an error if +invoke+ (blocking) is called from inside an EventLoop
|
|
47
|
-
# action, preventing accidental scheduler stalls.
|
|
48
|
-
#
|
|
49
|
-
# Called by Agent::Base#invoke and Workflow#invoke before executing.
|
|
50
|
-
#
|
|
51
|
-
# @raise [Phronomy::SchedulerReentrancyError] when called from EventLoop thread
|
|
52
|
-
# @return [void]
|
|
53
|
-
# @api private
|
|
54
26
|
def self.assert_not_in_event_loop!
|
|
55
27
|
return unless Phronomy::Runtime.in_event_loop_context?
|
|
56
28
|
|
|
57
|
-
raise Phronomy::
|
|
58
|
-
"Blocking invoke called from inside an EventLoop action. "
|
|
59
|
-
"Use invoke_async instead."
|
|
29
|
+
raise Phronomy::EventLoopReentrancyError,
|
|
30
|
+
"Blocking invoke called from inside an EventLoop action. Use invoke_async instead."
|
|
60
31
|
end
|
|
61
32
|
end
|
|
62
33
|
end
|
|
@@ -2,219 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Concurrency
|
|
5
|
-
#
|
|
5
|
+
# Thread-safe FIFO queue used at explicit thread boundaries.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# backing primitive without changing call sites.
|
|
10
|
-
#
|
|
11
|
-
# @example Producer / consumer
|
|
12
|
-
# queue = Phronomy::Concurrency::AsyncQueue.new
|
|
13
|
-
# Runtime.instance.spawn { queue.push(expensive_io()) }
|
|
14
|
-
# value = queue.pop # blocks until the producer pushes
|
|
15
|
-
# @api private
|
|
7
|
+
# It intentionally contains no scheduler/Fiber semantics. Cooperative
|
|
8
|
+
# application execution is represented by FSMSession state and EventLoop events.
|
|
16
9
|
class AsyncQueue
|
|
17
|
-
# @param max_size [Integer, nil] optional upper bound on queue depth.
|
|
18
|
-
# When set, {#push} blocks the caller until a slot is available.
|
|
19
|
-
# @api private
|
|
20
10
|
def initialize(max_size: nil)
|
|
21
11
|
@queue = max_size ? SizedQueue.new(max_size) : Thread::Queue.new
|
|
22
|
-
@max_size = max_size
|
|
23
|
-
@waiter_mutex = Mutex.new
|
|
24
|
-
@cross_thread_waiter = nil # [fiber, scheduler] set by _pop_cooperative; consumed by push
|
|
25
|
-
@cross_thread_scheduler = nil # set by expect_cross_thread_push
|
|
26
12
|
end
|
|
27
13
|
|
|
28
|
-
# Enqueues +item+.
|
|
29
|
-
# In a cooperative scheduler context with a bounded queue (max_size:), suspends
|
|
30
|
-
# the current Fiber via a scheduler signal when the queue is full rather than
|
|
31
|
-
# blocking the OS thread. Without a scheduler, falls back to the standard
|
|
32
|
-
# SizedQueue blocking behaviour.
|
|
33
|
-
# @param item [Object] value to enqueue
|
|
34
|
-
# @return [self]
|
|
35
|
-
# @api private
|
|
36
14
|
def push(item)
|
|
37
|
-
|
|
38
|
-
if scheduler && @max_size
|
|
39
|
-
_push_cooperative(scheduler, item)
|
|
40
|
-
else
|
|
41
|
-
@queue.push(item)
|
|
42
|
-
scheduler.raise_signal(@coop_signal) if scheduler && @coop_signal
|
|
43
|
-
# Wake a cross-thread waiter if one is registered.
|
|
44
|
-
# Handles the case where a DeterministicScheduler Fiber is suspended
|
|
45
|
-
# in _pop_cooperative waiting for a push from a non-scheduler thread
|
|
46
|
-
# (e.g. EventLoop thread where Scheduler.current is nil).
|
|
47
|
-
# enqueue_fiber is thread-safe; complete_blocking_await decrements
|
|
48
|
-
# @pending_awaits so run_until_idle can eventually exit.
|
|
49
|
-
if @cross_thread_scheduler
|
|
50
|
-
waiter = @waiter_mutex.synchronize do
|
|
51
|
-
w = @cross_thread_waiter
|
|
52
|
-
@cross_thread_waiter = nil
|
|
53
|
-
w
|
|
54
|
-
end
|
|
55
|
-
if waiter
|
|
56
|
-
fiber, sched = waiter
|
|
57
|
-
sched.complete_blocking_await
|
|
58
|
-
sched.enqueue_fiber(-> { fiber.resume })
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
15
|
+
@queue.push(item)
|
|
62
16
|
self
|
|
63
17
|
end
|
|
64
18
|
|
|
65
|
-
# Dequeues and returns the next item.
|
|
66
|
-
# In a cooperative scheduler context, suspends the current Fiber (yielding
|
|
67
|
-
# control back to the scheduler) rather than blocking the OS thread.
|
|
68
|
-
#
|
|
69
|
-
# When +timeout+ is given the semantics depend on the active backend:
|
|
70
|
-
#
|
|
71
|
-
# * **Thread backend** (`:thread`) — uses real wall-clock time via
|
|
72
|
-
# +Thread::Queue#pop(timeout:)+. Requires Ruby 3.2+.
|
|
73
|
-
# Returns +nil+ if no item arrives within the specified number of real seconds.
|
|
74
|
-
# * **DeterministicScheduler / `:fiber` backend** — uses the scheduler's
|
|
75
|
-
# *virtual time* (+scheduler.virtual_time+). The timeout elapses only when
|
|
76
|
-
# the virtual clock is advanced (e.g. via {Phronomy::Testing::FakeClock#advance}).
|
|
77
|
-
# In tests this means the timeout is fully deterministic and does not depend on
|
|
78
|
-
# actual elapsed wall time. However, in production `:fiber` mode the timeout
|
|
79
|
-
# may never expire unless the scheduler explicitly advances virtual time.
|
|
80
|
-
#
|
|
81
|
-
# @note The `:fiber` backend is **EXPERIMENTAL**. Real-time timeout behaviour
|
|
82
|
-
# in production workloads is not guaranteed and may differ from wall-clock
|
|
83
|
-
# expectations.
|
|
84
|
-
# @note **Cooperative timeout limitation**: on the cooperative path, the
|
|
85
|
-
# deadline is re-checked *after* a wake-up signal arrives. If virtual time
|
|
86
|
-
# has already passed the deadline when the consumer is woken by a producer
|
|
87
|
-
# push, the consumer returns +nil+ rather than the pushed item. Without any
|
|
88
|
-
# wake-up signal the waiting Fiber remains suspended even after
|
|
89
|
-
# +scheduler.advance+ — the timeout does not self-fire.
|
|
90
|
-
# @param timeout [Numeric, nil] seconds to wait before returning +nil+.
|
|
91
|
-
# Semantics are wall-clock on `:thread` and virtual-time on `:fiber`.
|
|
92
|
-
# @return [Object, nil] the next item, or +nil+ when timeout expires
|
|
93
|
-
# @api private
|
|
94
19
|
def pop(timeout: nil)
|
|
95
|
-
|
|
96
|
-
if scheduler
|
|
97
|
-
_pop_cooperative(scheduler, timeout: timeout)
|
|
98
|
-
elsif timeout
|
|
99
|
-
@queue.pop(timeout: timeout)
|
|
100
|
-
else
|
|
101
|
-
@queue.pop
|
|
102
|
-
end
|
|
20
|
+
timeout ? @queue.pop(timeout: timeout) : @queue.pop
|
|
103
21
|
end
|
|
104
22
|
|
|
105
|
-
# Returns the current number of items in the queue.
|
|
106
|
-
# @return [Integer]
|
|
107
|
-
# @api private
|
|
108
23
|
def size
|
|
109
24
|
@queue.size
|
|
110
25
|
end
|
|
111
26
|
|
|
112
|
-
# Returns +true+ when the queue contains no items.
|
|
113
|
-
# @return [Boolean]
|
|
114
|
-
# @api private
|
|
115
27
|
def empty?
|
|
116
28
|
@queue.empty?
|
|
117
29
|
end
|
|
118
30
|
|
|
119
|
-
# Closes the queue. Subsequent {#pop} calls raise +ClosedQueueError+.
|
|
120
|
-
# @return [self]
|
|
121
|
-
# @api private
|
|
122
31
|
def close
|
|
123
32
|
@queue.close
|
|
124
33
|
self
|
|
125
34
|
end
|
|
126
|
-
|
|
127
|
-
# Marks this queue as expecting pushes from a non-scheduler OS thread.
|
|
128
|
-
# When set, {#pop} in cooperative mode uses +track_blocking_await+ so that
|
|
129
|
-
# {Runtime::DeterministicScheduler#run_until_idle} does not exit while
|
|
130
|
-
# waiting for the cross-thread push. Called by {EventLoop#register} when
|
|
131
|
-
# a cooperative scheduler is active on the calling thread.
|
|
132
|
-
# @param scheduler [Runtime::Scheduler]
|
|
133
|
-
# @return [self]
|
|
134
|
-
# @api private
|
|
135
|
-
def expect_cross_thread_push(scheduler)
|
|
136
|
-
@cross_thread_scheduler = scheduler
|
|
137
|
-
self
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
# Cooperative pop for DeterministicScheduler context.
|
|
143
|
-
# Suspends the current Fiber via the scheduler's signal mechanism rather than
|
|
144
|
-
# blocking the OS thread.
|
|
145
|
-
#
|
|
146
|
-
# Two suspension paths:
|
|
147
|
-
# * **Same-scheduler** (default): uses {CoopSignal} — the producer is another
|
|
148
|
-
# Fiber on the same DeterministicScheduler. run_until_idle is allowed to
|
|
149
|
-
# exit; the producer's push will enqueue the consumer Fiber.
|
|
150
|
-
# * **Cross-thread** ({#expect_cross_thread_push} was called): uses
|
|
151
|
-
# +track_blocking_await+ so that run_until_idle does not exit while waiting
|
|
152
|
-
# for a push from a non-scheduler OS thread (e.g. EventLoop). The push
|
|
153
|
-
# side calls +complete_blocking_await+ + +enqueue_fiber+ to resume.
|
|
154
|
-
#
|
|
155
|
-
# The empty?/register pair for the cross-thread path is wrapped in
|
|
156
|
-
# @waiter_mutex to eliminate the race between the empty check and the
|
|
157
|
-
# registration of the waker.
|
|
158
|
-
# @api private
|
|
159
|
-
# @param scheduler [Runtime::Scheduler]
|
|
160
|
-
# @param timeout [Numeric, nil]
|
|
161
|
-
# @return [Object, nil]
|
|
162
|
-
def _pop_cooperative(scheduler, timeout:)
|
|
163
|
-
@coop_signal ||= scheduler.new_signal
|
|
164
|
-
deadline = timeout ? (scheduler.virtual_time + timeout) : nil
|
|
165
|
-
|
|
166
|
-
loop do
|
|
167
|
-
unless @queue.empty?
|
|
168
|
-
item = @queue.pop(timeout: 0)
|
|
169
|
-
# Notify a push-waiter (bounded queue) that a slot opened up.
|
|
170
|
-
scheduler.raise_signal(@push_signal) if @push_signal
|
|
171
|
-
return item
|
|
172
|
-
end
|
|
173
|
-
return nil if deadline && scheduler.virtual_time >= deadline
|
|
174
|
-
|
|
175
|
-
if @cross_thread_scheduler
|
|
176
|
-
# Cross-thread path: atomically check the queue and register a waker
|
|
177
|
-
# so that a concurrent push cannot slip between the empty? check above
|
|
178
|
-
# and the registration below.
|
|
179
|
-
will_yield = false
|
|
180
|
-
@waiter_mutex.synchronize do
|
|
181
|
-
if @queue.empty?
|
|
182
|
-
@cross_thread_waiter = [Fiber.current, scheduler]
|
|
183
|
-
scheduler.track_blocking_await
|
|
184
|
-
will_yield = true
|
|
185
|
-
end
|
|
186
|
-
# else: push arrived between the loop's empty? check and here;
|
|
187
|
-
# will_yield stays false and the next loop iteration dequeues it.
|
|
188
|
-
end
|
|
189
|
-
Fiber.yield(:cooperative_suspend) if will_yield
|
|
190
|
-
else
|
|
191
|
-
scheduler.wait_for_signal(@coop_signal)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
return nil if deadline && scheduler.virtual_time >= deadline
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Cooperative push for DeterministicScheduler context with a bounded queue.
|
|
199
|
-
# Suspends the current Fiber via a scheduler signal when the queue is full,
|
|
200
|
-
# rather than blocking the OS thread.
|
|
201
|
-
# @api private
|
|
202
|
-
# @param scheduler [Runtime::Scheduler]
|
|
203
|
-
# @param item [Object]
|
|
204
|
-
# @return [void]
|
|
205
|
-
def _push_cooperative(scheduler, item)
|
|
206
|
-
@push_signal ||= scheduler.new_signal
|
|
207
|
-
|
|
208
|
-
loop do
|
|
209
|
-
unless @queue.size >= @max_size
|
|
210
|
-
@queue.push(item)
|
|
211
|
-
# Notify any pop-waiter that an item is now available.
|
|
212
|
-
scheduler.raise_signal(@coop_signal) if @coop_signal
|
|
213
|
-
return
|
|
214
|
-
end
|
|
215
|
-
scheduler.wait_for_signal(@push_signal)
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
35
|
end
|
|
219
36
|
end
|
|
220
37
|
end
|