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.
- checksums.yaml +4 -4
- data/.mutant.yml +8 -7
- data/CHANGELOG.md +151 -1
- data/README.md +155 -32
- data/Rakefile +33 -0
- data/benchmark/baseline.json +1 -1
- data/benchmark/bench_regression.rb +1 -0
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +24 -0
- data/docs/decisions/006-no-built-in-guardrails.md +20 -2
- data/docs/decisions/010-cooperative-first-concurrency.md +248 -0
- data/lib/phronomy/agent/base.rb +250 -65
- data/lib/phronomy/agent/concerns/suspendable.rb +15 -0
- data/lib/phronomy/agent/fsm.rb +41 -64
- data/lib/phronomy/agent/orchestrator.rb +146 -121
- data/lib/phronomy/agent/parallel_tool_chat.rb +79 -22
- data/lib/phronomy/agent/react_agent.rb +8 -0
- data/lib/phronomy/async_queue.rb +155 -0
- data/lib/phronomy/blocking_adapter_pool.rb +435 -0
- data/lib/phronomy/cancellation_scope.rb +123 -0
- data/lib/phronomy/cancellation_token.rb +43 -2
- data/lib/phronomy/concurrency_gate.rb +155 -0
- data/lib/phronomy/configuration.rb +142 -0
- data/lib/phronomy/deadline.rb +63 -0
- data/lib/phronomy/diagnostics.rb +62 -0
- data/lib/phronomy/embeddings/base.rb +17 -0
- data/lib/phronomy/eval/runner.rb +9 -9
- data/lib/phronomy/event_loop.rb +181 -43
- data/lib/phronomy/fsm_session.rb +50 -4
- data/lib/phronomy/guardrail/prompt_injection_guardrail.rb +58 -0
- data/lib/phronomy/invocation_context.rb +152 -0
- data/lib/phronomy/knowledge_source/base.rb +18 -0
- data/lib/phronomy/llm_adapter/base.rb +104 -0
- data/lib/phronomy/llm_adapter/ruby_llm.rb +41 -0
- data/lib/phronomy/llm_adapter.rb +20 -0
- data/lib/phronomy/metrics.rb +38 -0
- data/lib/phronomy/runtime/deterministic_scheduler.rb +412 -0
- data/lib/phronomy/runtime/fake_scheduler.rb +165 -0
- data/lib/phronomy/runtime/gate_registry.rb +52 -0
- data/lib/phronomy/runtime/pool_registry.rb +57 -0
- data/lib/phronomy/runtime/runtime_metrics.rb +117 -0
- data/lib/phronomy/runtime/scheduler.rb +98 -0
- data/lib/phronomy/runtime/scheduler_timer_adapter.rb +79 -0
- data/lib/phronomy/runtime/task_registry.rb +48 -0
- data/lib/phronomy/runtime/thread_scheduler.rb +30 -0
- data/lib/phronomy/runtime/timer_queue.rb +106 -0
- data/lib/phronomy/runtime/timer_service.rb +42 -0
- data/lib/phronomy/runtime.rb +374 -0
- data/lib/phronomy/task/backend.rb +80 -0
- data/lib/phronomy/task/fiber_backend.rb +157 -0
- data/lib/phronomy/task/immediate_backend.rb +89 -0
- data/lib/phronomy/task/thread_backend.rb +84 -0
- data/lib/phronomy/task.rb +275 -0
- data/lib/phronomy/task_group.rb +265 -0
- data/lib/phronomy/testing/fake_clock.rb +109 -0
- data/lib/phronomy/testing/fake_scheduler.rb +104 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +59 -0
- data/lib/phronomy/testing.rb +12 -0
- data/lib/phronomy/tool/base.rb +110 -2
- data/lib/phronomy/tool/mcp_tool.rb +47 -16
- data/lib/phronomy/tool/scope_policy.rb +50 -0
- data/lib/phronomy/tool_executor.rb +106 -0
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +34 -0
- data/lib/phronomy/vector_store/async_backend.rb +110 -0
- data/lib/phronomy/vector_store/base.rb +7 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +52 -5
- data/lib/phronomy/workflow_context.rb +29 -2
- data/lib/phronomy/workflow_runner.rb +74 -3
- data/lib/phronomy.rb +42 -0
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|