phronomy 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +4 -4
- data/README.md +10 -7
- data/benchmark/bench_regression.rb +1 -1
- data/docs/architecture/agent-context.md +1 -1
- data/docs/architecture/persistence.md +2 -2
- data/docs/architecture/tracing.md +3 -3
- data/docs/async-composition.md +205 -0
- data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
- data/docs/features.md +12 -9
- data/docs/getting-started.md +6 -6
- data/docs/migrations/durable-semantic-coordination-v2.md +1 -1
- data/docs/persistence-backends.md +1 -1
- data/docs/runtime-and-concurrency.md +89 -29
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +1 -1
- data/lib/phronomy/agent/base.rb +4 -10
- data/lib/phronomy/agent/exact_execution.rb +3 -3
- data/lib/phronomy/agent/execution_coordinator.rb +20 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +2 -2
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +1 -1
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +4 -4
- data/lib/phronomy/agent/recovery_coordinator.rb +3 -3
- data/lib/phronomy/agent/tool_executor.rb +4 -4
- data/lib/phronomy/agent/tool_invocation.rb +1 -1
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +2 -2
- data/lib/phronomy/blocking.rb +15 -8
- data/lib/phronomy/engine/concurrency/offload_pool.rb +25 -16
- data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
- data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
- data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
- data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
- data/lib/phronomy/engine/event_loop.rb +7 -7
- data/lib/phronomy/engine/fsm_session.rb +3 -3
- data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
- data/lib/phronomy/engine/{task.rb → task_result.rb} +97 -42
- data/lib/phronomy/execution.rb +236 -0
- data/lib/phronomy/execution_cancellation_error.rb +12 -0
- data/lib/phronomy/execution_timeout_error.rb +12 -0
- data/lib/phronomy/invalid_async_entry_action_error.rb +1 -1
- data/lib/phronomy/invalid_async_transition_action_error.rb +1 -1
- data/lib/phronomy/invalid_async_workflow_action_error.rb +1 -1
- data/lib/phronomy/invocation_context.rb +13 -1
- data/lib/phronomy/llm_adapter/base.rb +2 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +3 -3
- data/lib/phronomy/multi_agent/orchestrator.rb +35 -67
- data/lib/phronomy/multi_agent/team_coordinator.rb +3 -3
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
- data/lib/phronomy/testing/fake_clock.rb +13 -9
- data/lib/phronomy/tools/agent.rb +5 -5
- data/lib/phronomy/vector_store/async_backend.rb +5 -5
- data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +6 -6
- data/lib/phronomy/workflow.rb +2 -2
- data/lib/phronomy/workflow_runner.rb +4 -4
- data/scripts/api_snapshot.rb +3 -1
- data/sig/phronomy/agent.rbs +4 -4
- data/sig/phronomy/extensions.rbs +5 -5
- data/sig/phronomy/multi_agent.rbs +1 -3
- data/sig/phronomy/runtime.rbs +34 -7
- data/sig/phronomy/tool.rbs +1 -1
- data/sig/phronomy/workflow.rbs +1 -1
- metadata +11 -5
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
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.
|
|
4
|
+
version: 0.26.0
|
|
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-09-
|
|
11
|
+
date: 2026-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -135,6 +135,7 @@ files:
|
|
|
135
135
|
- docs/archive/design/historical/00_design_philosophy.md
|
|
136
136
|
- docs/archive/design/historical/01_rubyllm_evaluation.md
|
|
137
137
|
- docs/archive/design/historical/06_design_decisions.md
|
|
138
|
+
- docs/async-composition.md
|
|
138
139
|
- docs/changelog/0.14-and-earlier.md
|
|
139
140
|
- docs/decisions/001-rubyllm-as-provider-layer.md
|
|
140
141
|
- docs/decisions/002-workflow-context-immutability.md
|
|
@@ -263,8 +264,12 @@ files:
|
|
|
263
264
|
- lib/phronomy/engine/concurrency/cancellation_token.rb
|
|
264
265
|
- lib/phronomy/engine/concurrency/deadline.rb
|
|
265
266
|
- lib/phronomy/engine/concurrency/offload_pool.rb
|
|
267
|
+
- lib/phronomy/engine/concurrency/operation_binding.rb
|
|
266
268
|
- lib/phronomy/engine/concurrency/physical_completion_task.rb
|
|
267
269
|
- lib/phronomy/engine/concurrency/pool_registry.rb
|
|
270
|
+
- lib/phronomy/engine/concurrency/result_collector.rb
|
|
271
|
+
- lib/phronomy/engine/concurrency/result_composition.rb
|
|
272
|
+
- lib/phronomy/engine/concurrency/subscriptions.rb
|
|
268
273
|
- lib/phronomy/engine/event_loop.rb
|
|
269
274
|
- lib/phronomy/engine/fsm_session.rb
|
|
270
275
|
- lib/phronomy/engine/runtime.rb
|
|
@@ -273,9 +278,12 @@ files:
|
|
|
273
278
|
- lib/phronomy/engine/runtime/team_ownership_registry.rb
|
|
274
279
|
- lib/phronomy/engine/runtime/timer_queue.rb
|
|
275
280
|
- lib/phronomy/engine/runtime/timer_service.rb
|
|
276
|
-
- lib/phronomy/engine/
|
|
281
|
+
- lib/phronomy/engine/task_result.rb
|
|
277
282
|
- lib/phronomy/event.rb
|
|
283
|
+
- lib/phronomy/execution.rb
|
|
284
|
+
- lib/phronomy/execution_cancellation_error.rb
|
|
278
285
|
- lib/phronomy/execution_rehydration_required_error.rb
|
|
286
|
+
- lib/phronomy/execution_timeout_error.rb
|
|
279
287
|
- lib/phronomy/filter.rb
|
|
280
288
|
- lib/phronomy/filter/base.rb
|
|
281
289
|
- lib/phronomy/filter/prompt_injection_filter.rb
|
|
@@ -293,8 +301,6 @@ files:
|
|
|
293
301
|
- lib/phronomy/metrics.rb
|
|
294
302
|
- lib/phronomy/multi_agent/admission_registry.rb
|
|
295
303
|
- lib/phronomy/multi_agent/durable_subagent_coordinator.rb
|
|
296
|
-
- lib/phronomy/multi_agent/fan_out_invocation.rb
|
|
297
|
-
- lib/phronomy/multi_agent/fan_out_session_builder.rb
|
|
298
304
|
- lib/phronomy/multi_agent/orchestrator.rb
|
|
299
305
|
- lib/phronomy/multi_agent/parallel_tool_chat.rb
|
|
300
306
|
- lib/phronomy/multi_agent/team_coordinator.rb
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module MultiAgent
|
|
5
|
-
# Mutable FSM context for one fan-out operation.
|
|
6
|
-
class FanOutInvocation
|
|
7
|
-
Child = Data.define(:index, :agent, :input, :config)
|
|
8
|
-
|
|
9
|
-
attr_reader :phase, :results
|
|
10
|
-
|
|
11
|
-
def initialize(children:, max_concurrency:, on_error:)
|
|
12
|
-
@phase = nil
|
|
13
|
-
@children = children
|
|
14
|
-
@max_concurrency = max_concurrency || children.length
|
|
15
|
-
@on_error = on_error
|
|
16
|
-
@pending = children.dup
|
|
17
|
-
@active = {}
|
|
18
|
-
@results = Array.new(children.length)
|
|
19
|
-
@child_errors = Array.new(children.length)
|
|
20
|
-
@fatal_error = nil
|
|
21
|
-
@cancelled = false
|
|
22
|
-
@timed_out = false
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def set_graph_metadata(phase: nil)
|
|
26
|
-
@phase = phase
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def handle_fsm_event(event)
|
|
30
|
-
case event.type
|
|
31
|
-
when :child_completed
|
|
32
|
-
payload = event.payload
|
|
33
|
-
index = payload.fetch(:index)
|
|
34
|
-
@active.delete(index)
|
|
35
|
-
child_error = payload[:error]
|
|
36
|
-
if child_error
|
|
37
|
-
if @on_error == :raise
|
|
38
|
-
@child_errors[index] = child_error
|
|
39
|
-
cancel_active_children! unless @child_errors.any?(&:itself)
|
|
40
|
-
end
|
|
41
|
-
else
|
|
42
|
-
@results[index] = payload[:result]
|
|
43
|
-
end
|
|
44
|
-
true
|
|
45
|
-
when :driver_failed
|
|
46
|
-
@fatal_error ||= event.payload.fetch(:error)
|
|
47
|
-
cancel_active_children!
|
|
48
|
-
true
|
|
49
|
-
when :timeout
|
|
50
|
-
@timed_out = true
|
|
51
|
-
@fatal_error ||= Phronomy::TimeoutError.new(event.payload.fetch(:message))
|
|
52
|
-
cancel_active_children!
|
|
53
|
-
true
|
|
54
|
-
when :cancel
|
|
55
|
-
@cancelled = true
|
|
56
|
-
@fatal_error ||= Phronomy::CancellationError.new("fan-out cancelled")
|
|
57
|
-
cancel_active_children!
|
|
58
|
-
true
|
|
59
|
-
else
|
|
60
|
-
false
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def start_available!(runtime, event_sink:)
|
|
65
|
-
return self if @fatal_error
|
|
66
|
-
|
|
67
|
-
while @active.length < @max_concurrency && (child = @pending.shift)
|
|
68
|
-
child_config = build_child_config(child.config)
|
|
69
|
-
handle = child.agent.invoke_async(
|
|
70
|
-
child.input,
|
|
71
|
-
config: child_config
|
|
72
|
-
)
|
|
73
|
-
@active[child.index] = {
|
|
74
|
-
handle: handle,
|
|
75
|
-
token: child_config[:cancellation_token]
|
|
76
|
-
}
|
|
77
|
-
[child.index].each do |captured_index|
|
|
78
|
-
handle.on_complete do |result, error|
|
|
79
|
-
event_sink.post(
|
|
80
|
-
:child_completed,
|
|
81
|
-
{
|
|
82
|
-
index: captured_index,
|
|
83
|
-
result: result,
|
|
84
|
-
error: error
|
|
85
|
-
}
|
|
86
|
-
)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
self
|
|
91
|
-
rescue => caught
|
|
92
|
-
@fatal_error ||= caught
|
|
93
|
-
cancel_active_children!
|
|
94
|
-
event_sink.post(:driver_failed, {error: caught})
|
|
95
|
-
self
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def completed?
|
|
99
|
-
!failed? && @pending.empty? && @active.empty?
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def failed?
|
|
103
|
-
return true if @fatal_error
|
|
104
|
-
|
|
105
|
-
@on_error == :raise &&
|
|
106
|
-
@pending.empty? &&
|
|
107
|
-
@active.empty? &&
|
|
108
|
-
@child_errors.any?(&:itself)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def error
|
|
112
|
-
@fatal_error || @child_errors.find(&:itself)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def timed_out?
|
|
116
|
-
@timed_out
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def cancelled?
|
|
120
|
-
@cancelled
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
def build_child_config(config)
|
|
126
|
-
config.dup
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def cancel_active_children!
|
|
130
|
-
@pending.clear
|
|
131
|
-
@active.each_value do |entry|
|
|
132
|
-
entry[:token]&.cancel!
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "state_machines"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
module MultiAgent
|
|
7
|
-
# Builds and registers a fan-out coordination FSMSession.
|
|
8
|
-
class FanOutSessionBuilder
|
|
9
|
-
AUTO_STATE_SET = {idle: true}.freeze
|
|
10
|
-
DECLARED_STATES = %i[idle running completed failed timed_out cancelled].freeze
|
|
11
|
-
WAIT_STATES = [].freeze
|
|
12
|
-
|
|
13
|
-
def self.start(
|
|
14
|
-
invocation:,
|
|
15
|
-
timeout: nil,
|
|
16
|
-
cancellation_token: nil,
|
|
17
|
-
runtime: Phronomy::Runtime.instance
|
|
18
|
-
)
|
|
19
|
-
result = Phronomy::Task.deferred(name: "fan-out")
|
|
20
|
-
|
|
21
|
-
if cancellation_token&.cancelled?
|
|
22
|
-
result.fail(Phronomy::CancellationError.new("fan-out cancelled"))
|
|
23
|
-
return result
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
session = build(invocation: invocation, runtime: runtime)
|
|
27
|
-
source = Phronomy::Task.deferred(name: "fan-out-source:#{session.id}")
|
|
28
|
-
source.on_complete do |context, error|
|
|
29
|
-
if error
|
|
30
|
-
result.fail(error)
|
|
31
|
-
elsif context.error
|
|
32
|
-
result.fail(context.error)
|
|
33
|
-
else
|
|
34
|
-
result.complete(context.results)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
runtime.event_loop.register(session, completion: source)
|
|
39
|
-
if timeout
|
|
40
|
-
runtime.timer_queue.schedule(seconds: timeout) do
|
|
41
|
-
session.event_sink.post(
|
|
42
|
-
:timeout,
|
|
43
|
-
{message: "dispatch_parallel timed out after #{timeout}s"}
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
cancellation_token&.on_cancel do
|
|
48
|
-
session.event_sink.post(:cancel, nil)
|
|
49
|
-
end
|
|
50
|
-
result
|
|
51
|
-
rescue => error
|
|
52
|
-
result ||= Phronomy::Task.deferred(name: "fan-out:registration")
|
|
53
|
-
result.fail(error)
|
|
54
|
-
result
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.build(invocation:, runtime:)
|
|
58
|
-
event_sink = Phronomy::FSMSession::EventSink.new(event_loop: runtime.event_loop)
|
|
59
|
-
phase_machine = build_phase_machine(runtime, event_sink)
|
|
60
|
-
external_events = {
|
|
61
|
-
child_completed: [{from: :running, to: :running, guard: nil}],
|
|
62
|
-
driver_failed: [{from: :running, to: :failed, guard: nil}],
|
|
63
|
-
timeout: [{from: :running, to: :timed_out, guard: nil}],
|
|
64
|
-
cancel: [{from: :running, to: :cancelled, guard: nil}]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
Phronomy::FSMSession.new(
|
|
68
|
-
context: invocation,
|
|
69
|
-
event_sink: event_sink,
|
|
70
|
-
entry_point: :idle,
|
|
71
|
-
entry_actions: {},
|
|
72
|
-
auto_state_set: AUTO_STATE_SET,
|
|
73
|
-
declared_states: DECLARED_STATES,
|
|
74
|
-
wait_state_names: WAIT_STATES,
|
|
75
|
-
external_events: external_events,
|
|
76
|
-
phase_machine_class: phase_machine,
|
|
77
|
-
recursion_limit: 10_000,
|
|
78
|
-
event_loop: runtime.event_loop
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
private_class_method :build
|
|
82
|
-
|
|
83
|
-
def self.build_phase_machine(runtime, event_sink)
|
|
84
|
-
Class.new do
|
|
85
|
-
attr_accessor :context, :current_event
|
|
86
|
-
|
|
87
|
-
state_machine :phase, initial: :idle do
|
|
88
|
-
state :idle
|
|
89
|
-
state :running
|
|
90
|
-
state :completed
|
|
91
|
-
state :failed
|
|
92
|
-
state :timed_out
|
|
93
|
-
state :cancelled
|
|
94
|
-
|
|
95
|
-
event :state_completed do
|
|
96
|
-
transition idle: :running
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
event :child_completed do
|
|
100
|
-
transition running: :failed, if: ->(m) { m.context&.failed? }
|
|
101
|
-
transition running: :completed, if: ->(m) { m.context&.completed? }
|
|
102
|
-
transition running: :running
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
event(:driver_failed) { transition running: :failed }
|
|
106
|
-
event(:timeout) { transition running: :timed_out }
|
|
107
|
-
event(:cancel) { transition running: :cancelled }
|
|
108
|
-
|
|
109
|
-
after_transition to: :running do |machine|
|
|
110
|
-
machine.context.start_available!(runtime, event_sink: event_sink)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
private_class_method :build_phase_machine
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|