phronomy 0.16.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/.mutant.yml +8 -9
- data/CHANGELOG.md +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- 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.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +6 -7
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- 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 +51 -79
- 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 +49 -377
- 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/llm_context_window/token_budget.rb +8 -79
- 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 +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- 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/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- 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 -265
- 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/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
- 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(
|
data/lib/phronomy/agent.rb
CHANGED
|
@@ -4,24 +4,20 @@ module Phronomy
|
|
|
4
4
|
module Agent
|
|
5
5
|
StreamEvent = Data.define(:type, :payload)
|
|
6
6
|
|
|
7
|
-
def self.run_once(definition:, input:, context: nil, **invoke_options)
|
|
7
|
+
def self.run_once(definition:, input:, context: nil, knowledge: [], **invoke_options)
|
|
8
8
|
persistence = Phronomy::Persistence::InMemory.new
|
|
9
|
-
agent = definition.create(
|
|
9
|
+
agent = definition.create(
|
|
10
|
+
context: context,
|
|
11
|
+
knowledge: knowledge,
|
|
12
|
+
persistence: persistence
|
|
13
|
+
)
|
|
10
14
|
agent.invoke(input, **invoke_options)
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
require_relative "agent/fsm_runtime_adapter"
|
|
16
19
|
require_relative "agent/async_event_api"
|
|
17
20
|
|
|
18
|
-
unless Phronomy::Agent::AgentInvocationSessionBuilder.singleton_class <
|
|
19
|
-
Phronomy::Agent::FsmRuntimeAdapter
|
|
20
|
-
Phronomy::Agent::AgentInvocationSessionBuilder.singleton_class.prepend(
|
|
21
|
-
Phronomy::Agent::FsmRuntimeAdapter
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
21
|
unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
|
|
26
22
|
Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
|
|
27
23
|
end
|
|
@@ -2,194 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
# Holds global configuration for the entire framework.
|
|
5
|
-
# Configure via the Phronomy.configure block.
|
|
6
|
-
#
|
|
7
|
-
# @example
|
|
8
|
-
# Phronomy.configure do |config|
|
|
9
|
-
# config.default_model = "claude-3-5-sonnet-20241022"
|
|
10
|
-
# config.recursion_limit = 50
|
|
11
|
-
# end
|
|
12
5
|
class Configuration
|
|
13
6
|
STREAM_CALLBACK_ERROR_POLICIES = %i[report fail_task].freeze
|
|
14
7
|
private_constant :STREAM_CALLBACK_ERROR_POLICIES
|
|
15
8
|
|
|
16
|
-
# Default LLM model name (nil delegates to RubyLLM default)
|
|
17
9
|
attr_accessor :default_model
|
|
18
|
-
|
|
19
|
-
# Default embedding model name
|
|
20
10
|
attr_accessor :default_embedding_model
|
|
21
|
-
|
|
22
|
-
# Tracer instance
|
|
23
11
|
attr_accessor :tracer
|
|
24
|
-
|
|
25
|
-
# Global before_llm_input hook callable (Proc / lambda).
|
|
26
|
-
# Called before every LLM request across all agents.
|
|
27
|
-
# Receives a {Phronomy::Agent::LLMInputBuildContext}; must return a
|
|
28
|
-
# {Phronomy::Agent::LLMInputPatch} or nil to pass through unchanged.
|
|
29
12
|
attr_accessor :before_llm_input
|
|
30
|
-
|
|
31
|
-
# Default output token reservation when an agent does not set max_output_tokens
|
|
32
|
-
# and the model registry value equals the context window (making it unusable
|
|
33
|
-
# as a per-request output reserve). Integer or nil.
|
|
34
13
|
attr_accessor :default_output_reserve
|
|
35
|
-
|
|
36
|
-
# Recursion limit for graph execution (default: 25)
|
|
37
14
|
attr_accessor :recursion_limit
|
|
38
|
-
|
|
39
|
-
# When true, agent LLM calls use {Phronomy::MultiAgent::ParallelToolChat}
|
|
40
|
-
# for concurrent tool dispatch within a single agent turn.
|
|
41
|
-
# Defaults to false.
|
|
42
|
-
#
|
|
43
|
-
# Previously, this was automatically enabled when +event_loop+ was true.
|
|
44
|
-
# As of Phase 3, +parallel_tool_execution+ is a separate setting that must
|
|
45
|
-
# be explicitly enabled.
|
|
46
|
-
# @example
|
|
47
|
-
# Phronomy.configure { |c| c.parallel_tool_execution = true }
|
|
48
|
-
# @return [Boolean]
|
|
49
15
|
attr_accessor :parallel_tool_execution
|
|
50
|
-
|
|
51
|
-
# When true, user input and LLM output are recorded in trace spans.
|
|
52
|
-
# Defaults to false; set to true only in environments where PII capture is acceptable.
|
|
53
|
-
# Set to false in privacy-sensitive environments to prevent PII from reaching
|
|
54
|
-
# the tracing backend (OTel, Langfuse, etc.).
|
|
55
16
|
attr_accessor :trace_pii
|
|
56
|
-
|
|
57
|
-
# Optional logger for framework diagnostic messages (e.g. unreachable-state warnings).
|
|
58
|
-
# Must respond to +#warn(message)+. When nil (default), messages are written to +$stderr+
|
|
59
|
-
# via +Kernel#warn+.
|
|
60
|
-
# @example
|
|
61
|
-
# Phronomy.configure { |c| c.logger = Rails.logger }
|
|
62
17
|
attr_accessor :logger
|
|
63
|
-
|
|
64
|
-
# Grace period (in seconds) before the EventLoop background thread is force-killed
|
|
65
|
-
# after a cooperative stop request. Applies both to the overall thread join
|
|
66
|
-
# and to the drain-and-cancel phase when +stop(drain: true)+ is used.
|
|
67
|
-
# Default: 5 seconds.
|
|
68
|
-
# @see Phronomy::EventLoop#stop
|
|
69
18
|
attr_accessor :event_loop_stop_grace_seconds
|
|
70
|
-
|
|
71
|
-
# Global state store for workflow persistence.
|
|
72
|
-
# When set, WorkflowRunner routes all state reads and writes through this store.
|
|
73
|
-
# Must be an instance of a class that inherits from Phronomy::StateStore::Base.
|
|
74
|
-
# Defaults to +nil+ (no persistence — state lives only for the duration of invoke).
|
|
75
|
-
# @example
|
|
76
|
-
# Phronomy.configure { |c| c.state_store = Phronomy::StateStore::InMemory.new }
|
|
77
19
|
attr_accessor :state_store
|
|
78
|
-
|
|
79
|
-
# Maximum byte length of a tool result returned to the LLM.
|
|
80
|
-
# When a tool returns a String longer than this limit, the string is truncated
|
|
81
|
-
# and a warning is logged. Set to +nil+ (default) to disable truncation.
|
|
82
|
-
# @example
|
|
83
|
-
# Phronomy.configure { |c| c.tool_result_max_size = 8192 }
|
|
84
20
|
attr_accessor :tool_result_max_size
|
|
85
|
-
|
|
86
|
-
# LLM adapter used by Agent::Base to perform LLM calls.
|
|
87
|
-
# Must be an instance of a class that inherits from
|
|
88
|
-
# {Phronomy::LLMAdapter::Base}. Defaults to
|
|
89
|
-
# {Phronomy::LLMAdapter::RubyLLM} which delegates to +chat.ask+ via
|
|
90
|
-
# {BlockingAdapterPool}.
|
|
91
|
-
# Set to a custom adapter to swap in an alternative LLM client without
|
|
92
|
-
# changing any agent code.
|
|
93
|
-
# @example
|
|
94
|
-
# Phronomy.configure { |c| c.llm_adapter = MyAsyncLLMAdapter.new }
|
|
95
21
|
attr_accessor :llm_adapter
|
|
96
|
-
|
|
97
|
-
# Set to +nil+ to disable the warning.
|
|
98
|
-
# @return [Numeric, nil]
|
|
99
22
|
attr_accessor :event_loop_starvation_threshold_seconds
|
|
100
|
-
|
|
101
|
-
# Warn when processing a single event on the EventLoop thread takes longer
|
|
102
|
-
# than this many seconds (long-running task / blocking-on-loop detection).
|
|
103
|
-
# Set to +nil+ to disable the warning.
|
|
104
|
-
# @return [Numeric, nil]
|
|
105
23
|
attr_accessor :event_loop_dispatch_threshold_seconds
|
|
106
|
-
|
|
107
|
-
# When true, enables all blocking operation diagnostics (Issue #279).
|
|
108
|
-
# Equivalent to setting all diagnostic thresholds to their defaults.
|
|
109
|
-
# @return [Boolean]
|
|
110
|
-
attr_accessor :scheduler_debug
|
|
111
|
-
|
|
112
|
-
# Wall-clock threshold (milliseconds) after which a task that has not
|
|
113
|
-
# yielded the scheduler emits a warning log. nil disables the check.
|
|
114
|
-
# @return [Float, nil]
|
|
115
|
-
attr_accessor :blocking_detect_threshold_ms
|
|
116
|
-
|
|
117
|
-
# Determines how an unhandled Application exception from a terminal stream
|
|
118
|
-
# callback affects the Task returned by Agent#stream_async or
|
|
119
|
-
# Agent#approve_async.
|
|
120
|
-
#
|
|
121
|
-
# +:report+ logs the callback failure and preserves the Agent result.
|
|
122
|
-
# +:fail_task+ logs the callback failure and fails the current Task with
|
|
123
|
-
# {Phronomy::StreamCallbackError}. Neither policy terminates EventLoop.
|
|
124
|
-
#
|
|
125
|
-
# Default: +:report+.
|
|
126
|
-
# @return [:report, :fail_task]
|
|
127
24
|
attr_reader :stream_callback_error_policy
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# All LLM calls, MCP tool calls, and other blocking I/O share this pool.
|
|
131
|
-
# Increase for higher LLM/tool throughput; decrease to limit
|
|
132
|
-
# concurrency (e.g. to stay within a provider's rate limit).
|
|
133
|
-
# Default: 10.
|
|
134
|
-
# @return [Integer]
|
|
135
|
-
attr_accessor :blocking_io_pool_size
|
|
136
|
-
|
|
137
|
-
# Maximum number of operations that may wait in the {BlockingAdapterPool}
|
|
138
|
-
# queue before {Phronomy::BackpressureError} is raised (on_full: :raise) or
|
|
139
|
-
# the caller blocks (on_full: :wait, the default). Default: 100.
|
|
140
|
-
# @return [Integer]
|
|
141
|
-
attr_accessor :blocking_io_queue_size
|
|
142
|
-
|
|
143
|
-
# Worker count for Tool authorization evaluation. The named pool is owned
|
|
144
|
-
# by Runtime#pool(:authorization) and shares PoolRegistry lifecycle.
|
|
145
|
-
# @return [Integer]
|
|
25
|
+
attr_accessor :offload_pool_size
|
|
26
|
+
attr_accessor :offload_queue_size
|
|
146
27
|
attr_accessor :authorization_pool_size
|
|
147
|
-
|
|
148
|
-
# Maximum queued Tool authorization evaluations.
|
|
149
|
-
# @return [Integer]
|
|
150
28
|
attr_accessor :authorization_queue_size
|
|
151
|
-
|
|
152
|
-
# Operation-wide deadline for approval_facts, requires_approval callables,
|
|
153
|
-
# and Agent#tool_approval_policy. Timeout fails closed to Human approval.
|
|
154
|
-
# @return [Numeric]
|
|
155
29
|
attr_accessor :authorization_timeout
|
|
156
30
|
|
|
157
|
-
# Scheduler starvation threshold (milliseconds).
|
|
158
|
-
# When a task waits more than this many milliseconds after calling
|
|
159
|
-
# +runtime.yield+ before being resumed, the wait is counted as a starvation
|
|
160
|
-
# event. Used by the fairness regression test and by the
|
|
161
|
-
# +tasks_waiting_over_threshold+ metric on {Phronomy::Runtime}.
|
|
162
|
-
# Default: 50ms.
|
|
163
|
-
# @return [Numeric]
|
|
164
|
-
attr_accessor :starvation_threshold_ms
|
|
165
|
-
|
|
166
|
-
# Scheduler backend to use for new {Phronomy::Runtime} instances.
|
|
167
|
-
#
|
|
168
|
-
# | Value | Scheduler | Typical use |
|
|
169
|
-
# |-------|-----------|-------------|
|
|
170
|
-
# | +:thread+ | {Runtime::ThreadScheduler} | **Default** — production-ready; one OS thread per task |
|
|
171
|
-
# | +:immediate+ | {Runtime::FakeScheduler} | Tests — tasks run synchronously, no extra threads |
|
|
172
|
-
# | +:fiber+ | {Runtime::DeterministicScheduler} (autorun) | **EXPERIMENTAL** — Fiber-based cooperative scheduler; do not use as production default |
|
|
173
|
-
# | +:cooperative+ | {Runtime::FakeScheduler} | **Deprecated** — alias for +:immediate+; do not use in new code |
|
|
174
|
-
#
|
|
175
|
-
# The default is +:thread+. The +:fiber+ backend remains experimental and opt-in;
|
|
176
|
-
# it will not become the default until integration test coverage is production grade
|
|
177
|
-
# and virtual-time/timeout semantics are fully resolved (see Issues #350, #347, #348).
|
|
178
|
-
#
|
|
179
|
-
# When this setting is changed, the change only takes effect on the NEXT
|
|
180
|
-
# call to {Runtime.instance} that auto-creates a new instance (i.e. after the
|
|
181
|
-
# previous instance has been replaced or reset). To replace the current
|
|
182
|
-
# instance immediately call +Phronomy::Runtime.instance = nil+ first.
|
|
183
|
-
#
|
|
184
|
-
# @return [:thread, :immediate, :fiber]
|
|
185
|
-
attr_accessor :runtime_backend
|
|
186
|
-
|
|
187
|
-
# When +true+, calling {Agent#invoke} from inside a scheduler task
|
|
188
|
-
# raises {SchedulerReentrancyError}. When +false+ (default), a warning
|
|
189
|
-
# is logged instead so that existing callers have time to migrate.
|
|
190
|
-
# @return [Boolean]
|
|
191
|
-
attr_accessor :strict_runtime_guards
|
|
192
|
-
|
|
193
31
|
def stream_callback_error_policy=(value)
|
|
194
32
|
unless STREAM_CALLBACK_ERROR_POLICIES.include?(value)
|
|
195
33
|
allowed = STREAM_CALLBACK_ERROR_POLICIES.map(&:inspect).join(", ")
|
|
@@ -209,17 +47,12 @@ module Phronomy
|
|
|
209
47
|
@llm_adapter = Phronomy::LLMAdapter::RubyLLM.new
|
|
210
48
|
@event_loop_starvation_threshold_seconds = nil
|
|
211
49
|
@event_loop_dispatch_threshold_seconds = nil
|
|
212
|
-
@scheduler_debug = false
|
|
213
|
-
@blocking_detect_threshold_ms = nil
|
|
214
50
|
@stream_callback_error_policy = :report
|
|
215
|
-
@
|
|
216
|
-
@
|
|
51
|
+
@offload_pool_size = 10
|
|
52
|
+
@offload_queue_size = 100
|
|
217
53
|
@authorization_pool_size = 4
|
|
218
54
|
@authorization_queue_size = 100
|
|
219
55
|
@authorization_timeout = 5
|
|
220
|
-
@starvation_threshold_ms = 50
|
|
221
|
-
@runtime_backend = :thread
|
|
222
|
-
@strict_runtime_guards = false
|
|
223
56
|
end
|
|
224
57
|
end
|
|
225
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
|