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.
- checksums.yaml +4 -4
- data/.mutant.yml +2 -2
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +2 -2
- data/README.md +1 -1
- data/docs/architecture/multi-agent-handoff.md +35 -40
- data/docs/architecture/persistence.md +19 -8
- data/docs/architecture.md +8 -1
- data/docs/decisions/016-semantic-multi-agent-handoff.md +3 -1
- data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
- data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
- data/docs/decisions/README.md +5 -1
- data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
- data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
- data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
- data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
- data/docs/features.md +35 -1
- data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +42 -3
- data/lib/phronomy/agent/agent_execution.rb +2 -2
- data/lib/phronomy/agent/agent_invocation.rb +1 -1
- data/lib/phronomy/agent/async_event_api.rb +18 -1
- data/lib/phronomy/agent/base.rb +28 -0
- data/lib/phronomy/agent/context_assembler.rb +1 -1
- data/lib/phronomy/agent/exact_execution.rb +153 -0
- data/lib/phronomy/agent/execution_cancellation.rb +25 -0
- data/lib/phronomy/agent/execution_coordinator.rb +484 -27
- data/lib/phronomy/{multi_agent → agent}/handoff.rb +4 -4
- data/lib/phronomy/{multi_agent → agent}/handoff_capability_factory.rb +3 -45
- data/lib/phronomy/{multi_agent → agent}/handoff_context.rb +26 -1
- data/lib/phronomy/{multi_agent/execution_coordinator.rb → agent/handoff_execution_coordinator.rb} +32 -5
- data/lib/phronomy/{multi_agent → agent}/handoff_policy.rb +7 -1
- data/lib/phronomy/{multi_agent → agent}/handoff_projection.rb +18 -2
- data/lib/phronomy/{multi_agent → agent}/handoff_request.rb +2 -2
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +114 -211
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +84 -130
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +76 -200
- data/lib/phronomy/agent/recovery_coordinator.rb +11 -5
- data/lib/phronomy/agent/recovery_support.rb +15 -23
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -8
- data/lib/phronomy/agent/tool_invocation.rb +4 -2
- data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +16 -1
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +59 -11
- data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
- data/lib/phronomy/multi_agent/team_execution.rb +44 -0
- data/lib/phronomy/multi_agent/team_root.rb +41 -0
- data/lib/phronomy/persistence/durable_codec.rb +60 -0
- data/lib/phronomy/persistence/in_memory.rb +264 -2
- data/lib/phronomy/persistence/repository_facades.rb +221 -2
- data/lib/phronomy/persistence.rb +95 -1
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -0
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
- data/lib/phronomy/testing/persistence_contract.rb +5 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/scripts/api_snapshot.rb +3 -3
- data/sig/phronomy/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +28 -32
- data/sig/phronomy/persistence.rbs +64 -3
- metadata +30 -12
- data/lib/phronomy/multi_agent/coordination_state.rb +0 -18
- data/lib/phronomy/multi_agent/coordinator.rb +0 -154
- 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
|