phronomy 0.23.0 → 0.24.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +2 -2
  3. data/CHANGELOG.md +15 -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 +30 -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
@@ -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