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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. 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
- ctx.cancelled?
32
- }},
33
- {from: :authorizing, to: :failed, guard: ->(ctx) {
34
- ctx.failed?
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
- ctx.cancelled?
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
- method(:authorizing_action).curry.call(runtime)
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
- method(:running_action).curry.call(runtime)
140
- ],
141
- completed: [
142
- method(:completed_action).curry.call(runtime)
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: :failed,
181
- if: ->(machine) { machine.context&.failed? }
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
- if: ->(machine) { machine.context&.cancelled? }
197
- transition authorizing: :failed,
198
- if: ->(machine) { machine.context&.failed? }
199
- transition authorizing: :rejected,
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
- if: ->(machine) { machine.context&.cancelled? }
212
- transition running: :failed,
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
- task = invocation.authorization_task(runtime: runtime)
271
- observe_task(
272
- runtime,
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
- # execution_task checks dispatchable? which requires :queued status;
302
- # mark_running! is deferred until after the task is started.
303
- task = invocation.execution_task(runtime: runtime)
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(
@@ -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(context: context, persistence: persistence)
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
- # Number of OS worker threads in the default {BlockingAdapterPool}.
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
- @blocking_io_pool_size = 10
216
- @blocking_io_queue_size = 100
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
@@ -1,62 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- # Developer-facing diagnostics for blocking operation detection (Issue #279).
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 " BlockingAdapterPool:"
28
- out.puts " pool_size : #{snap[:blocking_pool_size]}"
29
- out.puts " active_count : #{snap[:blocking_pool_active]}"
30
- out.puts " queue_depth : #{snap[:blocking_pool_queue_length]}"
31
- out.puts " abandoned_total : #{snap[:blocking_pool_abandoned_total]}"
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 : #{snap[:event_loop_lag_last_ms]}"
34
- out.puts " max_lag_ms : #{snap[:event_loop_lag_max_ms]}"
35
- out.puts " average_lag_ms : #{snap[:event_loop_lag_average_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::SchedulerReentrancyError,
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