phronomy 0.7.0 → 0.7.1

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -7
  3. data/CHANGELOG.md +151 -1
  4. data/README.md +155 -32
  5. data/Rakefile +33 -0
  6. data/benchmark/baseline.json +1 -1
  7. data/benchmark/bench_regression.rb +1 -0
  8. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +24 -0
  9. data/docs/decisions/006-no-built-in-guardrails.md +20 -2
  10. data/docs/decisions/010-cooperative-first-concurrency.md +248 -0
  11. data/lib/phronomy/agent/base.rb +250 -65
  12. data/lib/phronomy/agent/concerns/suspendable.rb +15 -0
  13. data/lib/phronomy/agent/fsm.rb +41 -64
  14. data/lib/phronomy/agent/orchestrator.rb +146 -121
  15. data/lib/phronomy/agent/parallel_tool_chat.rb +79 -22
  16. data/lib/phronomy/agent/react_agent.rb +8 -0
  17. data/lib/phronomy/async_queue.rb +155 -0
  18. data/lib/phronomy/blocking_adapter_pool.rb +435 -0
  19. data/lib/phronomy/cancellation_scope.rb +123 -0
  20. data/lib/phronomy/cancellation_token.rb +43 -2
  21. data/lib/phronomy/concurrency_gate.rb +155 -0
  22. data/lib/phronomy/configuration.rb +142 -0
  23. data/lib/phronomy/deadline.rb +63 -0
  24. data/lib/phronomy/diagnostics.rb +62 -0
  25. data/lib/phronomy/embeddings/base.rb +17 -0
  26. data/lib/phronomy/eval/runner.rb +9 -9
  27. data/lib/phronomy/event_loop.rb +181 -43
  28. data/lib/phronomy/fsm_session.rb +50 -4
  29. data/lib/phronomy/guardrail/prompt_injection_guardrail.rb +58 -0
  30. data/lib/phronomy/invocation_context.rb +152 -0
  31. data/lib/phronomy/knowledge_source/base.rb +18 -0
  32. data/lib/phronomy/llm_adapter/base.rb +104 -0
  33. data/lib/phronomy/llm_adapter/ruby_llm.rb +41 -0
  34. data/lib/phronomy/llm_adapter.rb +20 -0
  35. data/lib/phronomy/metrics.rb +38 -0
  36. data/lib/phronomy/runtime/deterministic_scheduler.rb +412 -0
  37. data/lib/phronomy/runtime/fake_scheduler.rb +165 -0
  38. data/lib/phronomy/runtime/gate_registry.rb +52 -0
  39. data/lib/phronomy/runtime/pool_registry.rb +57 -0
  40. data/lib/phronomy/runtime/runtime_metrics.rb +117 -0
  41. data/lib/phronomy/runtime/scheduler.rb +98 -0
  42. data/lib/phronomy/runtime/scheduler_timer_adapter.rb +79 -0
  43. data/lib/phronomy/runtime/task_registry.rb +48 -0
  44. data/lib/phronomy/runtime/thread_scheduler.rb +30 -0
  45. data/lib/phronomy/runtime/timer_queue.rb +106 -0
  46. data/lib/phronomy/runtime/timer_service.rb +42 -0
  47. data/lib/phronomy/runtime.rb +374 -0
  48. data/lib/phronomy/task/backend.rb +80 -0
  49. data/lib/phronomy/task/fiber_backend.rb +157 -0
  50. data/lib/phronomy/task/immediate_backend.rb +89 -0
  51. data/lib/phronomy/task/thread_backend.rb +84 -0
  52. data/lib/phronomy/task.rb +275 -0
  53. data/lib/phronomy/task_group.rb +265 -0
  54. data/lib/phronomy/testing/fake_clock.rb +109 -0
  55. data/lib/phronomy/testing/fake_scheduler.rb +104 -0
  56. data/lib/phronomy/testing/scheduler_helpers.rb +59 -0
  57. data/lib/phronomy/testing.rb +12 -0
  58. data/lib/phronomy/tool/base.rb +110 -2
  59. data/lib/phronomy/tool/mcp_tool.rb +47 -16
  60. data/lib/phronomy/tool/scope_policy.rb +50 -0
  61. data/lib/phronomy/tool_executor.rb +106 -0
  62. data/lib/phronomy/tracing/open_telemetry_tracer.rb +34 -0
  63. data/lib/phronomy/vector_store/async_backend.rb +110 -0
  64. data/lib/phronomy/vector_store/base.rb +7 -0
  65. data/lib/phronomy/version.rb +1 -1
  66. data/lib/phronomy/workflow.rb +52 -5
  67. data/lib/phronomy/workflow_context.rb +29 -2
  68. data/lib/phronomy/workflow_runner.rb +74 -3
  69. data/lib/phronomy.rb +42 -0
  70. metadata +40 -2
@@ -45,7 +45,7 @@ module Phronomy
45
45
  # Sentinel value for the terminal state of a workflow.
46
46
  FINISH = :__end__
47
47
 
48
- def initialize(state_class:, entry_actions:, declared_states:, auto_transitions:, external_events:, entry_point:, exit_actions: {}, wait_state_names: [], state_store: nil)
48
+ def initialize(state_class:, entry_actions:, declared_states:, auto_transitions:, external_events:, entry_point:, exit_actions: {}, wait_state_names: [], state_store: nil, action_timeouts: {})
49
49
  @state_class = state_class
50
50
  @entry_actions = entry_actions # { state_name => [callable, ...] }
51
51
  @declared_states = declared_states
@@ -55,6 +55,7 @@ module Phronomy
55
55
  @entry_point = entry_point
56
56
  @wait_state_names = wait_state_names
57
57
  @state_store = state_store
58
+ @action_timeouts = action_timeouts # { state_name => seconds }
58
59
  @phase_machine_class = build_phase_machine_class(auto_transitions, exit_actions)
59
60
  end
60
61
 
@@ -170,6 +171,7 @@ module Phronomy
170
171
  external_events: @external_events,
171
172
  phase_machine_class: @phase_machine_class,
172
173
  recursion_limit: recursion_limit,
174
+ action_timeouts: @action_timeouts,
173
175
  resume_event: resume_event,
174
176
  resume_phase: resume_phase
175
177
  )
@@ -211,7 +213,20 @@ module Phronomy
211
213
  # The entry point has no prior transition, so we invoke its entry actions directly.
212
214
  @entry_actions[current_state]&.each do |c|
213
215
  result = c.call(ctx)
214
- ctx = result if result.is_a?(Phronomy::WorkflowContext)
216
+ if result.is_a?(Phronomy::Task)
217
+ timeout_secs = @action_timeouts[current_state]
218
+ if timeout_secs
219
+ if result.join(timeout_secs).nil?
220
+ result.cancel!
221
+ raise Phronomy::ActionTimeoutError,
222
+ "Action in state #{current_state.inspect} timed out after #{timeout_secs}s"
223
+ end
224
+ end
225
+ task_result = result.await
226
+ ctx = task_result if task_result.is_a?(Phronomy::WorkflowContext)
227
+ elsif result.is_a?(Phronomy::WorkflowContext)
228
+ ctx = result
229
+ end
215
230
  end
216
231
  tracker.context = ctx
217
232
  end
@@ -302,11 +317,17 @@ module Phronomy
302
317
  ext_events = @external_events
303
318
  entry_acts = @entry_actions
304
319
  exit_acts = exit_actions
320
+ act_timeouts = @action_timeouts # { state_name => seconds }
305
321
 
306
322
  Class.new do
307
323
  # Holds the current WorkflowContext so guards and callbacks can read it.
308
324
  attr_accessor :context
309
325
 
326
+ # Set to true by an entry action that returned an awaitable Task.
327
+ # When true, FSMSession skips the automatic advance_or_halt step and
328
+ # waits for the async worker thread to post a state_completed event back.
329
+ attr_accessor :async_pending
330
+
310
331
  state_machine :phase, initial: entry do
311
332
  all_states.each { |s| state s }
312
333
 
@@ -345,9 +366,59 @@ module Phronomy
345
366
  # the returned context replaces the current one on the tracker.
346
367
  entry_acts.each do |state_name, callables|
347
368
  callables.each do |callable|
369
+ timeout_secs = act_timeouts[state_name]
348
370
  after_transition to: state_name do |machine|
349
371
  result = callable.call(machine.context)
350
- machine.context = result if result.is_a?(Phronomy::WorkflowContext)
372
+ if result.is_a?(Phronomy::Task)
373
+ if Phronomy.configuration.event_loop
374
+ # EventLoop mode: await in a background task so the EventLoop
375
+ # thread is not blocked. Signal async_pending so FSMSession
376
+ # skips the automatic advance_or_halt step.
377
+ machine.async_pending = true
378
+ ctx_ref = machine.context
379
+ thread_id = ctx_ref.thread_id
380
+ Phronomy::Runtime.instance.spawn(name: "wf-await-#{thread_id}") do
381
+ if timeout_secs
382
+ if result.join(timeout_secs).nil?
383
+ result.cancel!
384
+ raise Phronomy::ActionTimeoutError,
385
+ "Action in state #{state_name.inspect} timed out after #{timeout_secs}s"
386
+ end
387
+ end
388
+ task_result = result.await
389
+ if task_result.is_a?(Phronomy::WorkflowContext)
390
+ Phronomy::EventLoop.instance.post(
391
+ Phronomy::Event.new(
392
+ type: :action_completed,
393
+ target_id: thread_id,
394
+ payload: task_result
395
+ )
396
+ )
397
+ else
398
+ Phronomy::EventLoop.instance.post(
399
+ Phronomy::Event.new(type: :state_completed, target_id: thread_id, payload: nil)
400
+ )
401
+ end
402
+ rescue => e
403
+ Phronomy::EventLoop.instance.post(
404
+ Phronomy::Event.new(type: :error, target_id: thread_id, payload: e)
405
+ )
406
+ end
407
+ else
408
+ # Non-EventLoop mode: block synchronously on the task result.
409
+ if timeout_secs
410
+ if result.join(timeout_secs).nil?
411
+ result.cancel!
412
+ raise Phronomy::ActionTimeoutError,
413
+ "Action in state #{state_name.inspect} timed out after #{timeout_secs}s"
414
+ end
415
+ end
416
+ task_result = result.await
417
+ machine.context = task_result if task_result.is_a?(Phronomy::WorkflowContext)
418
+ end
419
+ elsif result.is_a?(Phronomy::WorkflowContext)
420
+ machine.context = result
421
+ end
351
422
  end
352
423
  end
353
424
  end
data/lib/phronomy.rb CHANGED
@@ -12,6 +12,10 @@ loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
12
12
  loader.inflector.inflect("fsm_session" => "FSMSession")
13
13
  # AgentFSM: Zeitwerk would infer "Fsm" — override to "FSM".
14
14
  loader.inflector.inflect("fsm" => "FSM")
15
+ # LLMAdapter: Zeitwerk would infer "LlmAdapter" — override to "LLMAdapter".
16
+ loader.inflector.inflect("llm_adapter" => "LLMAdapter")
17
+ # LLMAdapter::RubyLLM: "ruby_llm" maps to "RubyLLM" (not "RubyLlm").
18
+ loader.inflector.inflect("ruby_llm" => "RubyLLM")
15
19
  loader.setup
16
20
 
17
21
  require_relative "phronomy/version"
@@ -50,6 +54,22 @@ module Phronomy
50
54
  # Separate from TimeoutError (deadline exceeded) — this is an intentional stop.
51
55
  class CancellationError < Error; end
52
56
 
57
+ # Raised when {Agent#invoke} (a synchronous, blocking call) is attempted from
58
+ # inside an active scheduler task and +strict_runtime_guards+ is enabled.
59
+ #
60
+ # Calling a blocking invocation from within a scheduler task stalls the
61
+ # scheduler until the inner invocation completes, preventing other tasks from
62
+ # making progress (hidden deadlock risk). Use {Agent#invoke_async} followed by
63
+ # +#await+ inside scheduler tasks instead.
64
+ #
65
+ # This error is only raised when:
66
+ # Phronomy.configure { |c| c.strict_runtime_guards = true }
67
+ #
68
+ # By default a warning is logged and execution continues.
69
+ #
70
+ # @see Phronomy::Runtime.in_scheduler_context?
71
+ class SchedulerReentrancyError < Error; end
72
+
53
73
  # Raised by {Phronomy::GeneratorVerifier#invoke} when +raise_if_untrusted: true+
54
74
  # and the pipeline's combined confidence score falls below the configured threshold.
55
75
  #
@@ -76,6 +96,28 @@ module Phronomy
76
96
  end
77
97
  end
78
98
 
99
+ # Raised when an operation is submitted to a {BlockingAdapterPool} that has
100
+ # already been shut down via {BlockingAdapterPool#shutdown}.
101
+ class PoolShutdownError < Error; end
102
+
103
+ # Raised when a concurrency limit is exceeded and the configured backpressure
104
+ # strategy is +:raise+. The caller should back off and retry.
105
+ class BackpressureError < Error; end
106
+
107
+ # Raised by {CancellationScope#pop_queue} when the deadline expires before a
108
+ # result is available. Extends {TimeoutError} for backwards compatibility.
109
+ class ScopeTimeoutError < TimeoutError; end
110
+
111
+ # Raised when a Workflow entry/exit action task exceeds the +action_timeout:+
112
+ # configured for its state. Extends {TimeoutError}.
113
+ class ActionTimeoutError < TimeoutError; end
114
+
115
+ # Raised when a {Phronomy::WorkflowContext} field is mutated from a thread
116
+ # that does not own the context (i.e. not the EventLoop dispatch thread).
117
+ # Only raised in EventLoop mode. Use +context.merge(...)+ to produce a new
118
+ # context, or deliver updates as +:child_completed+ event payloads.
119
+ class WorkflowContextOwnershipError < Error; end
120
+
79
121
  class << self
80
122
  def configuration
81
123
  @configuration ||= Configuration.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-23 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -98,6 +98,7 @@ files:
98
98
  - docs/decisions/007-mcp-is-beta-stability.md
99
99
  - docs/decisions/008-orchestrator-uses-os-threads.md
100
100
  - docs/decisions/009-state-store-abstraction.md
101
+ - docs/decisions/010-cooperative-first-concurrency.md
101
102
  - lib/phronomy.rb
102
103
  - lib/phronomy/agent.rb
103
104
  - lib/phronomy/agent/base.rb
@@ -117,7 +118,11 @@ files:
117
118
  - lib/phronomy/agent/shared_state.rb
118
119
  - lib/phronomy/agent/suspend_signal.rb
119
120
  - lib/phronomy/agent/team_coordinator.rb
121
+ - lib/phronomy/async_queue.rb
122
+ - lib/phronomy/blocking_adapter_pool.rb
123
+ - lib/phronomy/cancellation_scope.rb
120
124
  - lib/phronomy/cancellation_token.rb
125
+ - lib/phronomy/concurrency_gate.rb
121
126
  - lib/phronomy/configuration.rb
122
127
  - lib/phronomy/context.rb
123
128
  - lib/phronomy/context/assembler.rb
@@ -127,6 +132,8 @@ files:
127
132
  - lib/phronomy/context/token_estimator.rb
128
133
  - lib/phronomy/context/trigger_context.rb
129
134
  - lib/phronomy/context/trim_context.rb
135
+ - lib/phronomy/deadline.rb
136
+ - lib/phronomy/diagnostics.rb
130
137
  - lib/phronomy/embeddings.rb
131
138
  - lib/phronomy/embeddings/base.rb
132
139
  - lib/phronomy/embeddings/ruby_llm_embeddings.rb
@@ -150,16 +157,22 @@ files:
150
157
  - lib/phronomy/guardrail/base.rb
151
158
  - lib/phronomy/guardrail/input_guardrail.rb
152
159
  - lib/phronomy/guardrail/output_guardrail.rb
160
+ - lib/phronomy/guardrail/prompt_injection_guardrail.rb
161
+ - lib/phronomy/invocation_context.rb
153
162
  - lib/phronomy/knowledge_source.rb
154
163
  - lib/phronomy/knowledge_source/base.rb
155
164
  - lib/phronomy/knowledge_source/entity_knowledge.rb
156
165
  - lib/phronomy/knowledge_source/rag_knowledge.rb
157
166
  - lib/phronomy/knowledge_source/static_knowledge.rb
167
+ - lib/phronomy/llm_adapter.rb
168
+ - lib/phronomy/llm_adapter/base.rb
169
+ - lib/phronomy/llm_adapter/ruby_llm.rb
158
170
  - lib/phronomy/loader.rb
159
171
  - lib/phronomy/loader/base.rb
160
172
  - lib/phronomy/loader/csv_loader.rb
161
173
  - lib/phronomy/loader/markdown_loader.rb
162
174
  - lib/phronomy/loader/plain_text_loader.rb
175
+ - lib/phronomy/metrics.rb
163
176
  - lib/phronomy/output_parser.rb
164
177
  - lib/phronomy/output_parser/base.rb
165
178
  - lib/phronomy/output_parser/json_parser.rb
@@ -167,23 +180,48 @@ files:
167
180
  - lib/phronomy/prompt_template.rb
168
181
  - lib/phronomy/ruby_llm_patches.rb
169
182
  - lib/phronomy/runnable.rb
183
+ - lib/phronomy/runtime.rb
184
+ - lib/phronomy/runtime/deterministic_scheduler.rb
185
+ - lib/phronomy/runtime/fake_scheduler.rb
186
+ - lib/phronomy/runtime/gate_registry.rb
187
+ - lib/phronomy/runtime/pool_registry.rb
188
+ - lib/phronomy/runtime/runtime_metrics.rb
189
+ - lib/phronomy/runtime/scheduler.rb
190
+ - lib/phronomy/runtime/scheduler_timer_adapter.rb
191
+ - lib/phronomy/runtime/task_registry.rb
192
+ - lib/phronomy/runtime/thread_scheduler.rb
193
+ - lib/phronomy/runtime/timer_queue.rb
194
+ - lib/phronomy/runtime/timer_service.rb
170
195
  - lib/phronomy/splitter.rb
171
196
  - lib/phronomy/splitter/base.rb
172
197
  - lib/phronomy/splitter/fixed_size_splitter.rb
173
198
  - lib/phronomy/splitter/recursive_splitter.rb
174
199
  - lib/phronomy/state_store/base.rb
175
200
  - lib/phronomy/state_store/in_memory.rb
201
+ - lib/phronomy/task.rb
202
+ - lib/phronomy/task/backend.rb
203
+ - lib/phronomy/task/fiber_backend.rb
204
+ - lib/phronomy/task/immediate_backend.rb
205
+ - lib/phronomy/task/thread_backend.rb
206
+ - lib/phronomy/task_group.rb
207
+ - lib/phronomy/testing.rb
208
+ - lib/phronomy/testing/fake_clock.rb
209
+ - lib/phronomy/testing/fake_scheduler.rb
210
+ - lib/phronomy/testing/scheduler_helpers.rb
176
211
  - lib/phronomy/token_usage.rb
177
212
  - lib/phronomy/tool.rb
178
213
  - lib/phronomy/tool/agent_tool.rb
179
214
  - lib/phronomy/tool/base.rb
180
215
  - lib/phronomy/tool/mcp_tool.rb
216
+ - lib/phronomy/tool/scope_policy.rb
217
+ - lib/phronomy/tool_executor.rb
181
218
  - lib/phronomy/tracing.rb
182
219
  - lib/phronomy/tracing/base.rb
183
220
  - lib/phronomy/tracing/langfuse_tracer.rb
184
221
  - lib/phronomy/tracing/null_tracer.rb
185
222
  - lib/phronomy/tracing/open_telemetry_tracer.rb
186
223
  - lib/phronomy/vector_store.rb
224
+ - lib/phronomy/vector_store/async_backend.rb
187
225
  - lib/phronomy/vector_store/base.rb
188
226
  - lib/phronomy/vector_store/in_memory.rb
189
227
  - lib/phronomy/vector_store/pgvector.rb