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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +10 -7
  5. data/benchmark/bench_regression.rb +1 -1
  6. data/docs/architecture/agent-context.md +1 -1
  7. data/docs/architecture/persistence.md +2 -2
  8. data/docs/architecture/tracing.md +3 -3
  9. data/docs/async-composition.md +205 -0
  10. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  11. data/docs/features.md +12 -9
  12. data/docs/getting-started.md +6 -6
  13. data/docs/migrations/durable-semantic-coordination-v2.md +1 -1
  14. data/docs/persistence-backends.md +1 -1
  15. data/docs/runtime-and-concurrency.md +89 -29
  16. data/lib/phronomy/agent/agent_invocation_session_builder.rb +1 -1
  17. data/lib/phronomy/agent/base.rb +4 -10
  18. data/lib/phronomy/agent/exact_execution.rb +3 -3
  19. data/lib/phronomy/agent/execution_coordinator.rb +20 -7
  20. data/lib/phronomy/agent/phase_machine_builder.rb +2 -2
  21. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +1 -1
  22. data/lib/phronomy/agent/recovery_coordinator/installation.rb +4 -4
  23. data/lib/phronomy/agent/recovery_coordinator.rb +3 -3
  24. data/lib/phronomy/agent/tool_executor.rb +4 -4
  25. data/lib/phronomy/agent/tool_invocation.rb +1 -1
  26. data/lib/phronomy/agent/tool_invocation_session_builder.rb +2 -2
  27. data/lib/phronomy/blocking.rb +15 -8
  28. data/lib/phronomy/engine/concurrency/offload_pool.rb +25 -16
  29. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  30. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  31. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  32. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  33. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  34. data/lib/phronomy/engine/event_loop.rb +7 -7
  35. data/lib/phronomy/engine/fsm_session.rb +3 -3
  36. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  37. data/lib/phronomy/engine/{task.rb → task_result.rb} +97 -42
  38. data/lib/phronomy/execution.rb +236 -0
  39. data/lib/phronomy/execution_cancellation_error.rb +12 -0
  40. data/lib/phronomy/execution_timeout_error.rb +12 -0
  41. data/lib/phronomy/invalid_async_entry_action_error.rb +1 -1
  42. data/lib/phronomy/invalid_async_transition_action_error.rb +1 -1
  43. data/lib/phronomy/invalid_async_workflow_action_error.rb +1 -1
  44. data/lib/phronomy/invocation_context.rb +13 -1
  45. data/lib/phronomy/llm_adapter/base.rb +2 -2
  46. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +3 -3
  47. data/lib/phronomy/multi_agent/orchestrator.rb +35 -67
  48. data/lib/phronomy/multi_agent/team_coordinator.rb +3 -3
  49. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  50. data/lib/phronomy/testing/fake_clock.rb +13 -9
  51. data/lib/phronomy/tools/agent.rb +5 -5
  52. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  53. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  54. data/lib/phronomy/version.rb +1 -1
  55. data/lib/phronomy/workflow/phase_machine_builder.rb +6 -6
  56. data/lib/phronomy/workflow.rb +2 -2
  57. data/lib/phronomy/workflow_runner.rb +4 -4
  58. data/scripts/api_snapshot.rb +3 -1
  59. data/sig/phronomy/agent.rbs +4 -4
  60. data/sig/phronomy/extensions.rbs +5 -5
  61. data/sig/phronomy/multi_agent.rbs +1 -3
  62. data/sig/phronomy/runtime.rbs +34 -7
  63. data/sig/phronomy/tool.rbs +1 -1
  64. data/sig/phronomy/workflow.rbs +1 -1
  65. metadata +11 -5
  66. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  67. 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.25.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-12 00:00:00.000000000 Z
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/task.rb
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