phronomy 0.23.0 → 0.24.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +2 -2
  3. data/CHANGELOG.md +18 -0
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +1 -1
  6. data/docs/architecture/multi-agent-handoff.md +35 -40
  7. data/docs/architecture/persistence.md +19 -8
  8. data/docs/architecture.md +8 -1
  9. data/docs/decisions/016-semantic-multi-agent-handoff.md +3 -1
  10. data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
  11. data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
  12. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
  13. data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
  14. data/docs/decisions/README.md +5 -1
  15. data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
  16. data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
  17. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
  18. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
  19. data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
  20. data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
  21. data/docs/features.md +35 -1
  22. data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
  23. data/docs/persistence-backends.md +42 -3
  24. data/lib/phronomy/agent/agent_execution.rb +2 -2
  25. data/lib/phronomy/agent/agent_invocation.rb +1 -1
  26. data/lib/phronomy/agent/async_event_api.rb +18 -1
  27. data/lib/phronomy/agent/base.rb +28 -0
  28. data/lib/phronomy/agent/context_assembler.rb +1 -1
  29. data/lib/phronomy/agent/exact_execution.rb +153 -0
  30. data/lib/phronomy/agent/execution_cancellation.rb +25 -0
  31. data/lib/phronomy/agent/execution_coordinator.rb +484 -27
  32. data/lib/phronomy/{multi_agent → agent}/handoff.rb +4 -4
  33. data/lib/phronomy/{multi_agent → agent}/handoff_capability_factory.rb +3 -45
  34. data/lib/phronomy/{multi_agent → agent}/handoff_context.rb +26 -1
  35. data/lib/phronomy/{multi_agent/execution_coordinator.rb → agent/handoff_execution_coordinator.rb} +32 -5
  36. data/lib/phronomy/{multi_agent → agent}/handoff_policy.rb +7 -1
  37. data/lib/phronomy/{multi_agent → agent}/handoff_projection.rb +18 -2
  38. data/lib/phronomy/{multi_agent → agent}/handoff_request.rb +2 -2
  39. data/lib/phronomy/agent/handoff_runner.rb +178 -0
  40. data/lib/phronomy/agent/handoff_state.rb +43 -0
  41. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +114 -211
  42. data/lib/phronomy/agent/recovery_coordinator/installation.rb +84 -130
  43. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +76 -200
  44. data/lib/phronomy/agent/recovery_coordinator.rb +11 -5
  45. data/lib/phronomy/agent/recovery_support.rb +15 -23
  46. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -8
  47. data/lib/phronomy/agent/tool_invocation.rb +4 -2
  48. data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
  49. data/lib/phronomy/engine/runtime.rb +16 -1
  50. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
  51. data/lib/phronomy/multi_agent/orchestrator.rb +59 -11
  52. data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
  53. data/lib/phronomy/multi_agent/team_execution.rb +44 -0
  54. data/lib/phronomy/multi_agent/team_root.rb +41 -0
  55. data/lib/phronomy/persistence/durable_codec.rb +60 -0
  56. data/lib/phronomy/persistence/in_memory.rb +264 -2
  57. data/lib/phronomy/persistence/repository_facades.rb +221 -2
  58. data/lib/phronomy/persistence.rb +95 -1
  59. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -0
  60. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
  61. data/lib/phronomy/testing/persistence_contract.rb +5 -0
  62. data/lib/phronomy/tools/agent.rb +1 -1
  63. data/lib/phronomy/version.rb +1 -1
  64. data/scripts/api_snapshot.rb +3 -3
  65. data/sig/phronomy/handoff.rbs +41 -0
  66. data/sig/phronomy/multi_agent.rbs +28 -32
  67. data/sig/phronomy/persistence.rbs +64 -3
  68. metadata +44 -12
  69. data/lib/phronomy/multi_agent/coordination_state.rb +0 -18
  70. data/lib/phronomy/multi_agent/coordinator.rb +0 -154
  71. data/lib/phronomy/multi_agent/runner.rb +0 -98
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.23.0
4
+ version: 0.24.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-08-28 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -78,6 +78,20 @@ dependencies:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
80
  version: '1.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: json
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "<"
86
+ - !ruby/object:Gem::Version
87
+ version: '3'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "<"
93
+ - !ruby/object:Gem::Version
94
+ version: '3'
81
95
  description: Phronomy is a Ruby AI agent framework that provides composable building
82
96
  blocks — Agents, Workflows, Tools, Filters, and Tracing — for building AI agents
83
97
  in Ruby. Powered by RubyLLM for LLM abstraction.
@@ -150,7 +164,17 @@ files:
150
164
  - docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md
151
165
  - docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md
152
166
  - docs/decisions/027-llm-adapter-provider-boundary.md
167
+ - docs/decisions/028-preparing-recovery-replay-contract.md
168
+ - docs/decisions/029-semantic-completion-and-application-effect-boundary.md
169
+ - docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md
170
+ - docs/decisions/031-durable-multi-agent-coordination.md
153
171
  - docs/decisions/README.md
172
+ - docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md
173
+ - docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md
174
+ - docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md
175
+ - docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md
176
+ - docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md
177
+ - docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md
154
178
  - docs/features.md
155
179
  - docs/getting-started.md
156
180
  - docs/mcp-client.md
@@ -158,6 +182,7 @@ files:
158
182
  - docs/migrations/0.16.md
159
183
  - docs/migrations/0.19.md
160
184
  - docs/migrations/0.22.md
185
+ - docs/migrations/durable-semantic-coordination-v2.md
161
186
  - docs/persistence-backends.md
162
187
  - docs/runtime-and-concurrency.md
163
188
  - examples/README.md
@@ -186,7 +211,18 @@ files:
186
211
  - lib/phronomy/agent/context_policy.rb
187
212
  - lib/phronomy/agent/context_policy_input.rb
188
213
  - lib/phronomy/agent/context_policy_input_builder.rb
214
+ - lib/phronomy/agent/exact_execution.rb
215
+ - lib/phronomy/agent/execution_cancellation.rb
189
216
  - lib/phronomy/agent/execution_coordinator.rb
217
+ - lib/phronomy/agent/handoff.rb
218
+ - lib/phronomy/agent/handoff_capability_factory.rb
219
+ - lib/phronomy/agent/handoff_context.rb
220
+ - lib/phronomy/agent/handoff_execution_coordinator.rb
221
+ - lib/phronomy/agent/handoff_policy.rb
222
+ - lib/phronomy/agent/handoff_projection.rb
223
+ - lib/phronomy/agent/handoff_request.rb
224
+ - lib/phronomy/agent/handoff_runner.rb
225
+ - lib/phronomy/agent/handoff_state.rb
190
226
  - lib/phronomy/agent/immutable.rb
191
227
  - lib/phronomy/agent/journal_projection.rb
192
228
  - lib/phronomy/agent/journal_record.rb
@@ -233,6 +269,7 @@ files:
233
269
  - lib/phronomy/engine/runtime.rb
234
270
  - lib/phronomy/engine/runtime/agent_ownership_registry.rb
235
271
  - lib/phronomy/engine/runtime/shutdown_result.rb
272
+ - lib/phronomy/engine/runtime/team_ownership_registry.rb
236
273
  - lib/phronomy/engine/runtime/timer_queue.rb
237
274
  - lib/phronomy/engine/runtime/timer_service.rb
238
275
  - lib/phronomy/engine/task.rb
@@ -254,21 +291,14 @@ files:
254
291
  - lib/phronomy/llm_context_window/token_estimator.rb
255
292
  - lib/phronomy/metrics.rb
256
293
  - lib/phronomy/multi_agent/admission_registry.rb
257
- - lib/phronomy/multi_agent/coordination_state.rb
258
- - lib/phronomy/multi_agent/coordinator.rb
259
- - lib/phronomy/multi_agent/execution_coordinator.rb
294
+ - lib/phronomy/multi_agent/durable_subagent_coordinator.rb
260
295
  - lib/phronomy/multi_agent/fan_out_invocation.rb
261
296
  - lib/phronomy/multi_agent/fan_out_session_builder.rb
262
- - lib/phronomy/multi_agent/handoff.rb
263
- - lib/phronomy/multi_agent/handoff_capability_factory.rb
264
- - lib/phronomy/multi_agent/handoff_context.rb
265
- - lib/phronomy/multi_agent/handoff_policy.rb
266
- - lib/phronomy/multi_agent/handoff_projection.rb
267
- - lib/phronomy/multi_agent/handoff_request.rb
268
297
  - lib/phronomy/multi_agent/orchestrator.rb
269
298
  - lib/phronomy/multi_agent/parallel_tool_chat.rb
270
- - lib/phronomy/multi_agent/runner.rb
271
299
  - lib/phronomy/multi_agent/team_coordinator.rb
300
+ - lib/phronomy/multi_agent/team_execution.rb
301
+ - lib/phronomy/multi_agent/team_root.rb
272
302
  - lib/phronomy/output_parser.rb
273
303
  - lib/phronomy/output_parser/base.rb
274
304
  - lib/phronomy/output_parser/json_parser.rb
@@ -304,6 +334,7 @@ files:
304
334
  - lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb
305
335
  - lib/phronomy/testing/persistence_contract/an_agent_repository.rb
306
336
  - lib/phronomy/testing/persistence_contract/an_execution_repository.rb
337
+ - lib/phronomy/testing/persistence_contract/coordination_repositories.rb
307
338
  - lib/phronomy/token_usage.rb
308
339
  - lib/phronomy/tool.rb
309
340
  - lib/phronomy/tool/base.rb
@@ -350,6 +381,7 @@ files:
350
381
  - sig/phronomy.rbs
351
382
  - sig/phronomy/agent.rbs
352
383
  - sig/phronomy/extensions.rbs
384
+ - sig/phronomy/handoff.rbs
353
385
  - sig/phronomy/llm_adapter.rbs
354
386
  - sig/phronomy/multi_agent.rbs
355
387
  - sig/phronomy/persistence.rbs
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module MultiAgent
5
- CoordinationState = Data.define(:active_agent, :active_handoff_context) do
6
- def initialize(active_agent:, active_handoff_context: nil)
7
- unless active_agent.is_a?(Phronomy::Agent::Base)
8
- raise ArgumentError, "active_agent must be a Phronomy::Agent::Base"
9
- end
10
- if active_handoff_context && !active_handoff_context.is_a?(HandoffContext)
11
- raise ArgumentError, "active_handoff_context must be a HandoffContext"
12
- end
13
- super
14
- freeze
15
- end
16
- end
17
- end
18
- end
@@ -1,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module MultiAgent
5
- class Coordinator
6
- ATTACH_MUTEX = Mutex.new
7
- private_constant :ATTACH_MUTEX
8
-
9
- ApplyHandoffCommand = Data.define(:coordinator, :request, :context, :completion)
10
-
11
- attr_reader :main_agent, :handoffs
12
-
13
- def self.attach(main_agent:, handoffs:)
14
- unless main_agent.is_a?(Phronomy::Agent::Base)
15
- raise ArgumentError, "main_agent must be a Phronomy::Agent::Base"
16
- end
17
- normalized = Array(handoffs).freeze
18
-
19
- ATTACH_MUTEX.synchronize do
20
- existing = main_agent.instance_variable_get(:@_phronomy_multi_agent_coordinator)
21
- if existing&.runtime_current?
22
- existing.assert_compatible!(normalized)
23
- return existing
24
- end
25
-
26
- created = new(main_agent: main_agent, handoffs: normalized)
27
- main_agent.instance_variable_set(:@_phronomy_multi_agent_coordinator, created)
28
- created
29
- end
30
- end
31
-
32
- def initialize(main_agent:, handoffs:)
33
- @main_agent = main_agent
34
- @handoffs = Array(handoffs).freeze
35
- @runtime = Phronomy::Runtime.instance
36
- validate_graph!
37
- @bindings_by_source = build_bindings
38
- @state_mutex = Mutex.new
39
- @state = CoordinationState.new(active_agent: main_agent)
40
- end
41
-
42
- def runtime_current?
43
- Phronomy::Runtime.instance.equal?(@runtime)
44
- end
45
-
46
- def snapshot
47
- unless runtime_current?
48
- raise Phronomy::RuntimeShutdownError,
49
- "Multi-Agent coordination state belongs to a previous Runtime"
50
- end
51
- @state_mutex.synchronize { @state }
52
- end
53
-
54
- def outgoing_bindings(agent)
55
- @bindings_by_source.fetch(agent.object_id, []).freeze
56
- end
57
-
58
- def transition!(request, context)
59
- completion = Phronomy::Task.deferred(name: "multi-agent-handoff")
60
- command = ApplyHandoffCommand.new(
61
- coordinator: self,
62
- request: request,
63
- context: context,
64
- completion: completion
65
- )
66
- posted = Phronomy::Runtime.instance.event_loop.post(
67
- Phronomy::Event.new(
68
- type: :agent_terminal_ready,
69
- target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
70
- payload: {command: command}
71
- )
72
- )
73
- unless posted
74
- completion.fail(
75
- Phronomy::RuntimeShutdownError.new(
76
- "EventLoop is not accepting Multi-Agent Handoff transitions"
77
- )
78
- )
79
- end
80
- completion.wait_result
81
- end
82
-
83
- # @api private
84
- def deliver_on_event_loop(command)
85
- runtime = Phronomy::Runtime.instance
86
- unless runtime.event_loop.current?
87
- raise Phronomy::Error,
88
- "Multi-Agent coordination state may only be mutated on EventLoop"
89
- end
90
-
91
- request = command.request
92
- context = command.context
93
- current = snapshot
94
- unless request.handoff.source_agent.equal?(current.active_agent)
95
- raise Phronomy::HandoffError,
96
- "Handoff source is no longer the active Agent"
97
- end
98
-
99
- next_state = CoordinationState.new(
100
- active_agent: request.handoff.target_agent,
101
- active_handoff_context: context
102
- )
103
- @state_mutex.synchronize { @state = next_state }
104
- command.completion.complete(next_state)
105
- rescue => error
106
- command.completion.fail(error)
107
- end
108
-
109
- def assert_compatible!(handoffs)
110
- incoming = graph_signature(handoffs)
111
- current = graph_signature(@handoffs)
112
- return true if incoming == current
113
-
114
- raise Phronomy::ConfigurationError,
115
- "a MultiAgent::Runner for this main Agent already exists with a different Handoff graph"
116
- end
117
-
118
- private
119
-
120
- def validate_graph!
121
- unless @handoffs.all? { |handoff| handoff.is_a?(Handoff) }
122
- raise ArgumentError, "handoffs must contain only MultiAgent::Handoff values"
123
- end
124
-
125
- duplicates = @handoffs.group_by do |handoff|
126
- [handoff.source_agent.object_id, handoff.target_agent.object_id]
127
- end.select { |_key, values| values.length > 1 }
128
- unless duplicates.empty?
129
- raise ArgumentError, "duplicate Source → Target Handoff edges are not allowed"
130
- end
131
- end
132
-
133
- def build_bindings
134
- @handoffs.group_by(&:source_agent).to_h do |source, edges|
135
- [
136
- source.object_id,
137
- edges.map { |handoff| HandoffCapabilityFactory.build(handoff) }.freeze
138
- ]
139
- end.freeze
140
- end
141
-
142
- def graph_signature(handoffs)
143
- Array(handoffs).map do |handoff|
144
- [
145
- handoff.source_agent.object_id,
146
- handoff.target_agent.object_id,
147
- handoff.policy.to_h,
148
- handoff.description
149
- ]
150
- end.sort_by { |row| [row[0], row[1], row[3]] }
151
- end
152
- end
153
- end
154
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module MultiAgent
5
- class Runner
6
- MAX_HANDOFFS = 20
7
-
8
- attr_reader :main_agent, :handoffs
9
-
10
- # @api public
11
- def initialize(main_agent:, handoffs: [])
12
- @main_agent = main_agent
13
- @handoffs = Array(handoffs).freeze
14
- @coordinator = Coordinator.attach(
15
- main_agent: main_agent,
16
- handoffs: @handoffs
17
- )
18
- end
19
-
20
- # @api public
21
- def invoke(input, config: {})
22
- trace_handle = Phronomy::Tracing::Automatic.start(
23
- "multi_agent.turn",
24
- input: input,
25
- main_agent_id: @main_agent.agent_id,
26
- **@main_agent.send(:_build_caller_meta, config)
27
- )
28
- result = nil
29
- operation_error = nil
30
-
31
- @coordinator = Coordinator.attach(
32
- main_agent: @main_agent,
33
- handoffs: @handoffs
34
- )
35
- runtime = Phronomy::Runtime.instance
36
- runtime.__admit_multi_agent(@coordinator)
37
- handoffs_taken = 0
38
- current_input = input
39
-
40
- result = loop do
41
- state = @coordinator.snapshot
42
- active_agent = state.active_agent
43
- bindings = @coordinator.outgoing_bindings(active_agent)
44
- agent_result = active_agent.invoke(
45
- current_input,
46
- config: config.merge(
47
- phronomy_handoff_bindings: bindings,
48
- phronomy_handoff_context: state.active_handoff_context
49
- )
50
- )
51
-
52
- request = agent_result[:handoff_request]
53
- break public_result(agent_result, active_agent) unless request
54
-
55
- if handoffs_taken >= MAX_HANDOFFS
56
- raise Phronomy::HandoffError,
57
- "Exceeded maximum Handoffs (#{MAX_HANDOFFS}) in one user turn"
58
- end
59
-
60
- manifest = agent_result.fetch(:_phronomy_handoff_manifest)
61
- context = HandoffProjection.new.build(
62
- request: request,
63
- manifest: manifest,
64
- persistence: active_agent.persistence,
65
- source_agent: active_agent
66
- )
67
- @coordinator.transition!(request, context)
68
- current_input = request.responsibility
69
- handoffs_taken += 1
70
- end
71
- result
72
- rescue => error
73
- operation_error = error
74
- raise
75
- ensure
76
- runtime&.__release_multi_agent(@coordinator) if defined?(@coordinator)
77
- trace_output = if result.is_a?(Hash)
78
- result[:output] || result["output"]
79
- else
80
- result
81
- end
82
- Phronomy::Tracing::Automatic.finish(
83
- trace_handle,
84
- output: trace_output,
85
- error: operation_error
86
- )
87
- end
88
-
89
- private
90
-
91
- def public_result(result, agent)
92
- result.reject { |key, _| key.to_s.start_with?("_phronomy_") }
93
- .except(:handoff_request)
94
- .merge(agent: agent)
95
- end
96
- end
97
- end
98
- end