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
@@ -45,6 +45,89 @@ module Phronomy
45
45
  # @api public
46
46
  attr_reader :workflow_states
47
47
 
48
+ # Purpose-specific durable Handoff and Team repositories in atomic_all.
49
+ # @api public
50
+ attr_reader :handoff_states, :teams, :team_executions
51
+
52
+ # Read-only exact Agent execution result; never creates a live Agent owner.
53
+ # Result is nil until a result_ref exists; errors remain canonical data.
54
+ # @api public
55
+ def execution_result(execution_id)
56
+ assert_observation_thread!
57
+ execution = executions.load(execution_id)
58
+ {
59
+ execution_id: execution.execution_id, agent_id: execution.agent_id,
60
+ status: execution.status, phase: execution.phase,
61
+ result_ref: execution.result_ref, error_ref: execution.error_ref,
62
+ result: execution.result_ref && contents.fetch_text(execution.result_ref),
63
+ error: execution.error_ref && contents.fetch_json(execution.error_ref)
64
+ }.then { |value| Phronomy::Agent::Immutable.copy(value) }
65
+ end
66
+
67
+ # Follows one exact Handoff turn without loading any Agent or graph.
68
+ # A committed absent Target reservation remains active; failed reads raise.
69
+ # @api public
70
+ def handoff_result(execution_id, main_agent_id: nil)
71
+ assert_observation_thread!
72
+ anchor = main_agent_id&.to_s
73
+ seen = {}
74
+ current = execution_id.to_s
75
+ reserved_agent_id = nil
76
+ loop do
77
+ raise Phronomy::Persistence::SerializationError, "Cyclic durable Handoff chain" if seen[current]
78
+ seen[current] = true
79
+ begin
80
+ execution = executions.load(current)
81
+ rescue Phronomy::Persistence::NotFoundError
82
+ routing = handoff_states.load(anchor)
83
+ if routing && Array(routing.metadata["cancelled_execution_ids"]).include?(current)
84
+ return {execution_id: current, agent_id: reserved_agent_id, status: :cancelled, result: nil, error: nil}.freeze
85
+ end
86
+ if reserved_agent_id && routing && routing.phase != "stable" && routing.pending_target_execution_id == current
87
+ return {execution_id: current, agent_id: reserved_agent_id, status: :active,
88
+ phase: :target_pending, reserved: true, result: nil, error: nil}.freeze
89
+ end
90
+ raise
91
+ end
92
+ anchor ||= execution.metadata.dig("coordination", "main_agent_id")
93
+ unless anchor && execution.metadata.dig("coordination", "main_agent_id") == anchor
94
+ raise Phronomy::Persistence::ConflictError, "Execution does not belong to this Handoff anchor"
95
+ end
96
+ return execution_result(current) unless execution.status == :handed_off
97
+ reserved_agent_id = execution.metadata.fetch("handoff_target_agent_id")
98
+ current = execution.metadata.fetch("handoff_target_execution_id")
99
+ end
100
+ end
101
+
102
+ # Lists retained active and terminal executions in lexical ID order.
103
+ # after is an exclusive ID cursor; discovery does not imply request dedup.
104
+ # @api public
105
+ def list_executions(agent_id, after: nil, limit: 100)
106
+ assert_observation_thread!
107
+ executions.list(agent_id, after: after, limit: limit)
108
+ end
109
+
110
+ # Read-only exact Team result, independent of current Team class/Proc wiring.
111
+ # @api public
112
+ def team_execution_result(team_execution_id)
113
+ assert_observation_thread!
114
+ execution = team_executions.load(team_execution_id)
115
+ {
116
+ team_execution_id: execution.team_execution_id, team_id: execution.team_id,
117
+ status: execution.status, phase: execution.phase,
118
+ result_ref: execution.result_ref, error_ref: execution.error_ref,
119
+ result: execution.result_ref && contents.fetch_json(execution.result_ref),
120
+ error: execution.error_ref && contents.fetch_json(execution.error_ref)
121
+ }.then { |value| Phronomy::Agent::Immutable.copy(value) }
122
+ end
123
+
124
+ # Lists retained Team runs; no continuation or callback delivery occurs.
125
+ # @api public
126
+ def list_team_executions(team_id, after: nil, limit: 100)
127
+ assert_observation_thread!
128
+ team_executions.list(team_id, after: after, limit: limit)
129
+ end
130
+
48
131
  # Initializes a Persistence backend from record-oriented storage
49
132
  # repositories.
50
133
  #
@@ -74,12 +157,15 @@ module Phronomy
74
157
  # advertised.
75
158
  #
76
159
  # @api public
77
- def initialize(contents:, agents:, journals:, executions:, workflow_states:)
160
+ def initialize(contents:, agents:, journals:, executions:, workflow_states:, handoff_states:, teams:, team_executions:)
78
161
  @contents = contents
79
162
  @agents = RepositoryFacades::Agents.new(agents)
80
163
  @journals = RepositoryFacades::Journals.new(journals)
81
164
  @executions = RepositoryFacades::Executions.new(executions)
82
165
  @workflow_states = RepositoryFacades::WorkflowStates.new(workflow_states)
166
+ @handoff_states = RepositoryFacades::HandoffStates.new(handoff_states)
167
+ @teams = RepositoryFacades::Teams.new(teams)
168
+ @team_executions = RepositoryFacades::TeamExecutions.new(team_executions)
83
169
  validate_capabilities!
84
170
  end
85
171
 
@@ -141,6 +227,7 @@ module Phronomy
141
227
  journals:,
142
228
  executions:,
143
229
  workflow_states:,
230
+ handoff_states:, teams:, team_executions:,
144
231
  watermark:
145
232
  )
146
233
  RepositoryFacades::View.new(
@@ -149,6 +236,7 @@ module Phronomy
149
236
  journals: journals,
150
237
  executions: executions,
151
238
  workflow_states: workflow_states,
239
+ handoff_states: handoff_states, teams: teams, team_executions: team_executions,
152
240
  watermark: watermark
153
241
  )
154
242
  end
@@ -169,6 +257,12 @@ module Phronomy
169
257
 
170
258
  private
171
259
 
260
+ def assert_observation_thread!
261
+ if Phronomy::Runtime.in_event_loop_context?
262
+ raise Phronomy::EventLoopReentrancyError, "Persistence observation cannot block EventLoop"
263
+ end
264
+ end
265
+
172
266
  def validate_capabilities!
173
267
  missing = REQUIRED_CAPABILITIES.reject do |key, value|
174
268
  capabilities[key] == value
@@ -36,6 +36,7 @@ RSpec.shared_examples "a Persistence backend" do
36
36
  :journals,
37
37
  :executions,
38
38
  :workflow_states,
39
+ :handoff_states, :teams, :team_executions,
39
40
  :assert_agent_watermark!
40
41
  )
41
42
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ RSpec.shared_context "coordination repository values" do
7
+ let(:coordination_team) do
8
+ now = Time.now.utc.iso8601(6)
9
+ Phronomy::MultiAgent::TeamRoot.new(team_id: SecureRandom.uuid,
10
+ team_definition_id: "contract-team", team_definition_version: 1,
11
+ team_revision: 0, lifecycle_status: "idle", created_at: now, updated_at: now, metadata: {})
12
+ end
13
+ let(:coordination_run) do
14
+ now = Time.now.utc.iso8601(6)
15
+ Phronomy::MultiAgent::TeamExecution.new(team_execution_id: SecureRandom.uuid,
16
+ team_id: coordination_team.team_id, execution_revision: 0, status: "active", phase: "coordinator",
17
+ input_ref: nil, coordinator: {}, tasks: [], workers: [], assignments: [], result_ref: nil,
18
+ error_ref: nil, created_at: now, updated_at: now, metadata: {})
19
+ end
20
+ let(:coordination_handoff) do
21
+ now = Time.now.utc.iso8601(6)
22
+ Phronomy::Agent::HandoffState.new(main_agent_id: SecureRandom.uuid, handoff_revision: 1,
23
+ active_agent_id: "contract-target", active_handoff_context_ref: nil, phase: "target_pending",
24
+ pending_source_execution_id: "source-execution", pending_target_execution_id: "target-execution",
25
+ created_at: now, updated_at: now, metadata: {})
26
+ end
27
+ end
28
+
29
+ RSpec.shared_examples "a Handoff state repository" do
30
+ include_context "coordination repository values"
31
+
32
+ it "returns nil only for an authoritative absent Handoff anchor" do
33
+ expect(persistence.handoff_states.load(SecureRandom.uuid)).to be_nil
34
+ end
35
+
36
+ it "round-trips the immutable Handoff snapshot and rejects stale routing CAS" do
37
+ value = coordination_handoff
38
+ persistence.handoff_states.save(value.main_agent_id, expected_revision: nil, state: value)
39
+ loaded = persistence.handoff_states.load(value.main_agent_id)
40
+ expect(loaded.to_h).to eq(value.to_h)
41
+ expect(loaded).to be_frozen
42
+ updated = value.with(phase: "stable")
43
+ persistence.handoff_states.save(value.main_agent_id, expected_revision: 1, state: updated)
44
+ expect do
45
+ persistence.handoff_states.save(value.main_agent_id, expected_revision: 1, state: updated)
46
+ end.to raise_error(Phronomy::Persistence::ConflictError)
47
+ expect(persistence.handoff_states.load(value.main_agent_id).handoff_revision).to eq(2)
48
+ end
49
+
50
+ it "rejects a snapshot for a different routing anchor" do
51
+ expect do
52
+ persistence.handoff_states.save("other", expected_revision: nil, state: coordination_handoff)
53
+ end.to raise_error(Phronomy::Persistence::SerializationError)
54
+ end
55
+ end
56
+
57
+ RSpec.shared_examples "a Team repository" do
58
+ include_context "coordination repository values"
59
+
60
+ it "round-trips a Team lineage and rejects duplicate creation" do
61
+ persistence.teams.create(coordination_team)
62
+ expect(persistence.teams.load(coordination_team.team_id).to_h).to eq(coordination_team.to_h)
63
+ expect { persistence.teams.create(coordination_team) }.to raise_error(Phronomy::Persistence::ConflictError)
64
+ end
65
+
66
+ it "rejects a stale Team revision" do
67
+ persistence.teams.create(coordination_team)
68
+ updated = coordination_team.with(lifecycle_status: "active")
69
+ persistence.teams.save(coordination_team.team_id, expected_revision: 0, root: updated)
70
+ expect do
71
+ persistence.teams.save(coordination_team.team_id, expected_revision: 0, root: updated)
72
+ end.to raise_error(Phronomy::Persistence::ConflictError)
73
+ end
74
+
75
+ it "raises NotFoundError for an absent Team" do
76
+ expect { persistence.teams.load(SecureRandom.uuid) }.to raise_error(Phronomy::Persistence::NotFoundError)
77
+ end
78
+ end
79
+
80
+ RSpec.shared_examples "a Team execution repository" do
81
+ include_context "coordination repository values"
82
+ before { persistence.teams.create(coordination_team) }
83
+
84
+ it "atomically admits at most one active run per Team" do
85
+ persistence.team_executions.create_active(coordination_run)
86
+ other = coordination_run.with(team_execution_id: SecureRandom.uuid, execution_revision: 0)
87
+ expect { persistence.team_executions.create_active(other) }.to raise_error(Phronomy::AgentBusyError)
88
+ expect(persistence.team_executions.list_active(coordination_team.team_id).map(&:team_execution_id)).to eq([coordination_run.team_execution_id])
89
+ end
90
+
91
+ it "releases admission after terminal CAS while retaining discoverable outcomes" do
92
+ persistence.team_executions.create_active(coordination_run)
93
+ completed = coordination_run.with(status: "completed", phase: "completed")
94
+ persistence.team_executions.save(coordination_run.team_execution_id, expected_revision: 0, execution: completed)
95
+ other = coordination_run.with(team_execution_id: SecureRandom.uuid, execution_revision: 0)
96
+ persistence.team_executions.create_active(other)
97
+ ids = [completed.team_execution_id, other.team_execution_id].sort
98
+ expect(persistence.team_executions.list(coordination_team.team_id, limit: 1).map(&:team_execution_id)).to eq(ids.first(1))
99
+ expect(persistence.team_executions.list(coordination_team.team_id, after: ids.first).map(&:team_execution_id)).to eq(ids.last(1))
100
+ expect(persistence.team_executions.list("other-team")).to be_empty
101
+ expect do
102
+ persistence.team_executions.save(completed.team_execution_id, expected_revision: 0, execution: completed)
103
+ end.to raise_error(Phronomy::Persistence::ConflictError)
104
+ expect do
105
+ persistence.team_executions.save(completed.team_execution_id, expected_revision: 1,
106
+ execution: completed.with(status: "active", phase: "coordinator"))
107
+ end.to raise_error(Phronomy::Persistence::ConflictError)
108
+ end
109
+
110
+ it "rolls the new three repositories back with the existing transaction domain" do
111
+ handoff = coordination_handoff
112
+ root_id = "coordination-rollback-agent-#{SecureRandom.uuid}"
113
+ content_ref = nil
114
+ expect do
115
+ persistence.transaction do |tx|
116
+ tx.team_executions.create_active(coordination_run)
117
+ tx.teams.save(coordination_team.team_id, expected_revision: 0, root: coordination_team.with(lifecycle_status: "active"))
118
+ tx.handoff_states.save(handoff.main_agent_id, expected_revision: nil, state: handoff)
119
+ content_ref = tx.contents.put_text("coordination-rollback-#{SecureRandom.uuid}")
120
+ root = Phronomy::Agent::AgentRoot.create(agent_id: root_id, agent_definition_id: "rollback", agent_definition_version: 1)
121
+ tx.agents.create(root)
122
+ tx.journals.append(root_id, expected_position: 0, records: [])
123
+ record = Phronomy::Agent::JournalRecord.new(agent_id: root_id, kind: :input_received, channel: :external)
124
+ tx.executions.create_active(Phronomy::Agent::AgentExecution.start(agent_root: root, input_record: record))
125
+ tx.workflow_states.save(root_id, expected_revision: nil, snapshot: {fields: {value: "committed"}, phase: "pause"})
126
+ raise "rollback all eight"
127
+ end
128
+ end.to raise_error(RuntimeError, "rollback all eight")
129
+ expect(persistence.teams.load(coordination_team.team_id).team_revision).to eq(0)
130
+ expect(persistence.handoff_states.load(handoff.main_agent_id)).to be_nil
131
+ expect(persistence.team_executions.list(coordination_team.team_id)).to be_empty
132
+ expect { persistence.agents.load(root_id) }.to raise_error(Phronomy::Persistence::NotFoundError)
133
+ expect(persistence.executions.list(root_id)).to be_empty
134
+ expect(persistence.workflow_states.load(root_id)).to be_nil
135
+ expect(persistence.contents.exist?(content_ref)).to be(false)
136
+ end
137
+ end
@@ -27,6 +27,9 @@ module Phronomy
27
27
  "a Journal repository",
28
28
  "an Execution repository",
29
29
  "a workflow state repository",
30
+ "a Handoff state repository",
31
+ "a Team repository",
32
+ "a Team execution repository",
30
33
  "a Persistence backend"
31
34
  ].freeze
32
35
  end
@@ -39,3 +42,5 @@ require_relative "persistence_contract/a_journal_repository"
39
42
  require_relative "persistence_contract/an_execution_repository"
40
43
  require_relative "persistence_contract/a_workflow_state_repository"
41
44
  require_relative "persistence_contract/a_persistence_backend"
45
+
46
+ require_relative "persistence_contract/coordination_repositories"
@@ -161,7 +161,7 @@ module Phronomy
161
161
  end
162
162
 
163
163
  def settle_async_error(task, error)
164
- if error.is_a?(Phronomy::ToolError) || error.is_a?(Phronomy::CancellationError)
164
+ if error.is_a?(Phronomy::ToolError) || error.is_a?(Phronomy::CancellationError) || error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
165
165
  task.fail(error)
166
166
  return task
167
167
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.23.0"
4
+ VERSION = "0.24.0"
5
5
  end
@@ -34,9 +34,9 @@ PUBLIC_API_ENTRIES = [
34
34
  Phronomy::Runnable,
35
35
  Phronomy::Agent::Context::Instruction::PromptTemplate,
36
36
  # Beta
37
- Phronomy::MultiAgent::Handoff,
38
- Phronomy::MultiAgent::HandoffPolicy,
39
- Phronomy::MultiAgent::Runner,
37
+ Phronomy::Agent::Handoff,
38
+ Phronomy::Agent::HandoffPolicy,
39
+ Phronomy::Agent::HandoffRunner,
40
40
  Phronomy::MultiAgent::Orchestrator,
41
41
  Phronomy::MultiAgent::TeamCoordinator,
42
42
  Phronomy::Filter::Base,
@@ -0,0 +1,41 @@
1
+ module Phronomy
2
+ module Agent
3
+ class Handoff
4
+ attr_reader source_agent: ::Phronomy::Agent::Base
5
+ attr_reader target_agent: ::Phronomy::Agent::Base
6
+ attr_reader policy: HandoffPolicy
7
+ attr_reader description: String
8
+
9
+ def initialize: (source_agent: ::Phronomy::Agent::Base, target_agent: ::Phronomy::Agent::Base, ?policy: HandoffPolicy, ?description: String?) -> void
10
+ end
11
+
12
+ class HandoffPolicy
13
+ CATEGORIES: Array[Symbol]
14
+ MODES: Array[Symbol]
15
+ DEFAULTS: Array[Symbol]
16
+
17
+ def self.define: () { () -> void } -> HandoffPolicy
18
+ def self.default: () -> HandoffPolicy
19
+
20
+ def rule: (Symbol | String) -> untyped
21
+ def required?: (Symbol | String) -> bool
22
+ def forbidden?: (Symbol | String) -> bool
23
+ def selectable?: (Symbol | String) -> bool
24
+ def default_include?: (Symbol | String) -> bool
25
+ def selectable_categories: () -> Array[Symbol]
26
+ def to_h: () -> Hash[String, Hash[String, String]]
27
+ end
28
+
29
+ class HandoffRunner
30
+ MAX_HANDOFFS: Integer
31
+
32
+ attr_reader main_agent: ::Phronomy::Agent::Base
33
+ attr_reader handoffs: Array[Handoff]
34
+
35
+ def initialize: (main_agent: ::Phronomy::Agent::Base, ?handoffs: Array[Handoff]) -> void
36
+ def invoke: (untyped input, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
37
+ def result: (String execution_id) -> Hash[Symbol, untyped]
38
+ def cancel: (String execution_id) -> Hash[Symbol, untyped]
39
+ end
40
+ end
41
+ end
@@ -1,39 +1,35 @@
1
1
  module Phronomy
2
2
  module MultiAgent
3
- class Handoff
4
- attr_reader source_agent: ::Phronomy::Agent::Base
5
- attr_reader target_agent: ::Phronomy::Agent::Base
6
- attr_reader policy: HandoffPolicy
7
- attr_reader description: String
8
-
9
- def initialize: (source_agent: ::Phronomy::Agent::Base, target_agent: ::Phronomy::Agent::Base, ?policy: HandoffPolicy, ?description: String?) -> void
3
+ class Orchestrator < ::Phronomy::Agent::Base
4
+ def self.subagent: (Symbol | String name, singleton(::Phronomy::Agent::Base) agent_class, ?on_error: Symbol, ?inherit_knowledge: bool) -> untyped
5
+ def self.registered_subagents: () -> Hash[Symbol | String, untyped]
6
+ def resume: (String execution_id, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
7
+ def dispatch_parallel: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> Array[untyped]
8
+ def dispatch_parallel_async: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> ::Phronomy::Task[untyped]
9
+ def fan_out: (agent: singleton(::Phronomy::Agent::Base), inputs: Array[untyped], ?config: Hash[Symbol, untyped], ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> Array[untyped]
10
+ def fan_out_async: (agent: singleton(::Phronomy::Agent::Base), inputs: Array[untyped], ?config: Hash[Symbol, untyped], ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> ::Phronomy::Task[untyped]
10
11
  end
11
12
 
12
- class HandoffPolicy
13
- CATEGORIES: Array[Symbol]
14
- MODES: Array[Symbol]
15
- DEFAULTS: Array[Symbol]
16
-
17
- def self.define: () { () -> void } -> HandoffPolicy
18
- def self.default: () -> HandoffPolicy
19
-
20
- def rule: (Symbol | String) -> untyped
21
- def required?: (Symbol | String) -> bool
22
- def forbidden?: (Symbol | String) -> bool
23
- def selectable?: (Symbol | String) -> bool
24
- def default_include?: (Symbol | String) -> bool
25
- def selectable_categories: () -> Array[Symbol]
26
- def to_h: () -> Hash[String, Hash[String, String]]
27
- end
28
-
29
- class Runner
30
- MAX_HANDOFFS: Integer
31
-
32
- attr_reader main_agent: ::Phronomy::Agent::Base
33
- attr_reader handoffs: Array[Handoff]
34
-
35
- def initialize: (main_agent: ::Phronomy::Agent::Base, ?handoffs: Array[Handoff]) -> void
36
- def invoke: (untyped input, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
13
+ class TeamCoordinator
14
+ attr_reader team_id: String
15
+ attr_reader persistence: ::Phronomy::Persistence
16
+ def self.team_definition: (?id: String?, ?version: Integer?) -> Hash[Symbol, String | Integer]
17
+ def self.coordinator_model: (?String? value) -> String?
18
+ def self.coordinator_instructions: (?String? value) -> String?
19
+ def self.coordinator_provider: (?Symbol? value) -> Symbol?
20
+ def self.pool: (size: Integer, agent: singleton(::Phronomy::Agent::Base), ?on_error: Symbol) -> untyped
21
+ def self.schedule: () { (Array[untyped]) -> untyped } -> untyped
22
+ def self.aggregate: () { (Array[Hash[Symbol, untyped]]) -> untyped } -> untyped
23
+ def self.new: (?team_id: String, ?persistence: ::Phronomy::Persistence?, ?metadata: Hash[String, untyped], ?on_event: ^(untyped) -> untyped) -> instance
24
+ def self.create: (**untyped options) -> instance
25
+ def self.load: (String team_id, persistence: ::Phronomy::Persistence, ?on_event: ^(untyped) -> untyped) -> instance
26
+ def self.get: (String team_id) -> instance?
27
+ def invoke: (untyped input, ?config: Hash[Symbol, untyped]) -> untyped
28
+ def stream: (untyped input, ?config: Hash[Symbol, untyped]) ?{ (Hash[Symbol, untyped]) -> untyped } -> untyped
29
+ def resume: (String team_execution_id, ?config: Hash[Symbol, untyped]) -> untyped
30
+ def result: (String team_execution_id) -> Hash[Symbol, untyped]
31
+ def executions: (?after: String?, ?limit: Integer) -> Array[untyped]
32
+ def cancel: (String team_execution_id) -> Hash[Symbol, untyped]
37
33
  end
38
34
  end
39
35
  end
@@ -25,6 +25,7 @@ module Phronomy
25
25
  def load: (String execution_id) -> untyped
26
26
  def save: (String execution_id, expected_revision: Integer, execution: untyped) -> untyped
27
27
  def list_active: (String agent_id) -> Array[untyped]
28
+ def list: (String agent_id, ?after: String?, ?limit: Integer) -> Array[untyped]
28
29
  def delete: (String execution_id) -> untyped
29
30
  def delete_for_agent: (String agent_id) -> untyped
30
31
  def assert_idle!: (String agent_id) -> untyped
@@ -58,6 +59,7 @@ module Phronomy
58
59
  def load: (String execution_id) -> Persistence::DurableRecord
59
60
  def save: (String execution_id, expected_revision: Integer, next_revision: Integer, agent_id: String, active: bool, record: Persistence::DurableRecord) -> Persistence::DurableRecord
60
61
  def list_active: (String agent_id) -> Array[Persistence::DurableRecord]
62
+ def list: (String agent_id, ?after: String?, ?limit: Integer) -> Array[Persistence::DurableRecord]
61
63
  def delete: (String execution_id) -> untyped
62
64
  def delete_for_agent: (String agent_id) -> untyped
63
65
  def assert_idle!: (String agent_id) -> untyped
@@ -69,6 +71,54 @@ module Phronomy
69
71
  def delete: (String workflow_instance_id, expected_revision: Integer) -> untyped
70
72
  end
71
73
 
74
+ interface _HandoffStateRepository
75
+ def load: (String main_agent_id) -> untyped
76
+ def save: (String main_agent_id, expected_revision: Integer?, state: untyped) -> untyped
77
+ def delete: (String main_agent_id, expected_revision: Integer) -> untyped
78
+ end
79
+
80
+ interface _TeamRepository
81
+ def create: (untyped root) -> untyped
82
+ def load: (String team_id) -> untyped
83
+ def save: (String team_id, expected_revision: Integer, root: untyped) -> untyped
84
+ def delete: (String team_id) -> untyped
85
+ end
86
+
87
+ interface _TeamExecutionRepository
88
+ def create_active: (untyped execution) -> untyped
89
+ def load: (String team_execution_id) -> untyped
90
+ def save: (String team_execution_id, expected_revision: Integer, execution: untyped) -> untyped
91
+ def list_active: (String team_id) -> Array[untyped]
92
+ def list: (String team_id, ?after: String?, ?limit: Integer) -> Array[untyped]
93
+ def delete: (String team_execution_id) -> untyped
94
+ def delete_for_team: (String team_id) -> untyped
95
+ def assert_idle!: (String team_id) -> untyped
96
+ end
97
+
98
+ interface _BackendHandoffStateRecordRepository
99
+ def load: (String main_agent_id) -> Persistence::DurableRecord?
100
+ def save: (String main_agent_id, expected_revision: Integer?, next_revision: Integer, active_agent_id: String, record: Persistence::DurableRecord) -> Persistence::DurableRecord
101
+ def delete: (String main_agent_id, expected_revision: Integer) -> untyped
102
+ end
103
+
104
+ interface _BackendTeamRecordRepository
105
+ def create: (team_id: String, team_revision: Integer, record: Persistence::DurableRecord) -> Persistence::DurableRecord
106
+ def load: (String team_id) -> Persistence::DurableRecord
107
+ def save: (String team_id, expected_revision: Integer, next_revision: Integer, record: Persistence::DurableRecord) -> Persistence::DurableRecord
108
+ def delete: (String team_id) -> untyped
109
+ end
110
+
111
+ interface _BackendTeamExecutionRecordRepository
112
+ def create_active: (team_execution_id: String, team_id: String, execution_revision: Integer, record: Persistence::DurableRecord) -> Persistence::DurableRecord
113
+ def load: (String team_execution_id) -> Persistence::DurableRecord
114
+ def save: (String team_execution_id, expected_revision: Integer, next_revision: Integer, team_id: String, active: bool, record: Persistence::DurableRecord) -> Persistence::DurableRecord
115
+ def list_active: (String team_id) -> Array[Persistence::DurableRecord]
116
+ def list: (String team_id, ?after: String?, ?limit: Integer) -> Array[Persistence::DurableRecord]
117
+ def delete: (String team_execution_id) -> untyped
118
+ def delete_for_team: (String team_id) -> untyped
119
+ def assert_idle!: (String team_id) -> untyped
120
+ end
121
+
72
122
  interface _BackendWatermark
73
123
  def assert_agent_watermark!: (agent_id: String, agent_revision: Integer, journal_position: Integer) -> true
74
124
  end
@@ -79,6 +129,9 @@ module Phronomy
79
129
  def journals: () -> _JournalRepository
80
130
  def executions: () -> _ExecutionRepository
81
131
  def workflow_states: () -> _WorkflowStateRepository
132
+ def handoff_states: () -> _HandoffStateRepository
133
+ def teams: () -> _TeamRepository
134
+ def team_executions: () -> _TeamExecutionRepository
82
135
  def assert_agent_watermark!: (agent_id: String, agent_revision: Integer, journal_position: Integer) -> true
83
136
  end
84
137
 
@@ -118,11 +171,19 @@ module Phronomy
118
171
  attr_reader journals: _JournalRepository
119
172
  attr_reader executions: _ExecutionRepository
120
173
  attr_reader workflow_states: _WorkflowStateRepository
121
-
122
- def initialize: (contents: _ContentRepository, agents: _BackendAgentRecordRepository, journals: _BackendJournalRecordRepository, executions: _BackendExecutionRecordRepository, workflow_states: _BackendWorkflowStateRecordRepository) -> void
174
+ attr_reader handoff_states: _HandoffStateRepository
175
+ attr_reader teams: _TeamRepository
176
+ attr_reader team_executions: _TeamExecutionRepository
177
+ def handoff_result: (String execution_id, ?main_agent_id: String?) -> Hash[Symbol, untyped]
178
+ def execution_result: (String execution_id) -> Hash[Symbol, untyped]
179
+ def list_executions: (String agent_id, ?after: String?, ?limit: Integer) -> Array[untyped]
180
+ def team_execution_result: (String team_execution_id) -> Hash[Symbol, untyped]
181
+ def list_team_executions: (String team_id, ?after: String?, ?limit: Integer) -> Array[untyped]
182
+
183
+ def initialize: (contents: _ContentRepository, agents: _BackendAgentRecordRepository, journals: _BackendJournalRecordRepository, executions: _BackendExecutionRecordRepository, workflow_states: _BackendWorkflowStateRecordRepository, handoff_states: _BackendHandoffStateRecordRepository, teams: _BackendTeamRecordRepository, team_executions: _BackendTeamExecutionRecordRepository) -> void
123
184
  def capabilities: () -> Hash[Symbol, bool]
124
185
  def transaction: () { (_PersistenceTransactionView) -> untyped } -> untyped
125
- def build_transaction_view: (contents: _ContentRepository, agents: _BackendAgentRecordRepository, journals: _BackendJournalRecordRepository, executions: _BackendExecutionRecordRepository, workflow_states: _BackendWorkflowStateRecordRepository, watermark: _BackendWatermark) -> _PersistenceTransactionView
186
+ def build_transaction_view: (contents: _ContentRepository, agents: _BackendAgentRecordRepository, journals: _BackendJournalRecordRepository, executions: _BackendExecutionRecordRepository, workflow_states: _BackendWorkflowStateRecordRepository, handoff_states: _BackendHandoffStateRecordRepository, teams: _BackendTeamRecordRepository, team_executions: _BackendTeamExecutionRecordRepository, watermark: _BackendWatermark) -> _PersistenceTransactionView
126
187
  def assert_agent_watermark!: (agent_id: String, agent_revision: Integer, journal_position: Integer) -> true
127
188
  end
128
189
  end
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.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-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
@@ -150,7 +150,17 @@ files:
150
150
  - docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md
151
151
  - docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md
152
152
  - docs/decisions/027-llm-adapter-provider-boundary.md
153
+ - docs/decisions/028-preparing-recovery-replay-contract.md
154
+ - docs/decisions/029-semantic-completion-and-application-effect-boundary.md
155
+ - docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md
156
+ - docs/decisions/031-durable-multi-agent-coordination.md
153
157
  - docs/decisions/README.md
158
+ - docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md
159
+ - docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md
160
+ - docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md
161
+ - docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md
162
+ - docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md
163
+ - docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md
154
164
  - docs/features.md
155
165
  - docs/getting-started.md
156
166
  - docs/mcp-client.md
@@ -158,6 +168,7 @@ files:
158
168
  - docs/migrations/0.16.md
159
169
  - docs/migrations/0.19.md
160
170
  - docs/migrations/0.22.md
171
+ - docs/migrations/durable-semantic-coordination-v2.md
161
172
  - docs/persistence-backends.md
162
173
  - docs/runtime-and-concurrency.md
163
174
  - examples/README.md
@@ -186,7 +197,18 @@ files:
186
197
  - lib/phronomy/agent/context_policy.rb
187
198
  - lib/phronomy/agent/context_policy_input.rb
188
199
  - lib/phronomy/agent/context_policy_input_builder.rb
200
+ - lib/phronomy/agent/exact_execution.rb
201
+ - lib/phronomy/agent/execution_cancellation.rb
189
202
  - lib/phronomy/agent/execution_coordinator.rb
203
+ - lib/phronomy/agent/handoff.rb
204
+ - lib/phronomy/agent/handoff_capability_factory.rb
205
+ - lib/phronomy/agent/handoff_context.rb
206
+ - lib/phronomy/agent/handoff_execution_coordinator.rb
207
+ - lib/phronomy/agent/handoff_policy.rb
208
+ - lib/phronomy/agent/handoff_projection.rb
209
+ - lib/phronomy/agent/handoff_request.rb
210
+ - lib/phronomy/agent/handoff_runner.rb
211
+ - lib/phronomy/agent/handoff_state.rb
190
212
  - lib/phronomy/agent/immutable.rb
191
213
  - lib/phronomy/agent/journal_projection.rb
192
214
  - lib/phronomy/agent/journal_record.rb
@@ -233,6 +255,7 @@ files:
233
255
  - lib/phronomy/engine/runtime.rb
234
256
  - lib/phronomy/engine/runtime/agent_ownership_registry.rb
235
257
  - lib/phronomy/engine/runtime/shutdown_result.rb
258
+ - lib/phronomy/engine/runtime/team_ownership_registry.rb
236
259
  - lib/phronomy/engine/runtime/timer_queue.rb
237
260
  - lib/phronomy/engine/runtime/timer_service.rb
238
261
  - lib/phronomy/engine/task.rb
@@ -254,21 +277,14 @@ files:
254
277
  - lib/phronomy/llm_context_window/token_estimator.rb
255
278
  - lib/phronomy/metrics.rb
256
279
  - 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
280
+ - lib/phronomy/multi_agent/durable_subagent_coordinator.rb
260
281
  - lib/phronomy/multi_agent/fan_out_invocation.rb
261
282
  - 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
283
  - lib/phronomy/multi_agent/orchestrator.rb
269
284
  - lib/phronomy/multi_agent/parallel_tool_chat.rb
270
- - lib/phronomy/multi_agent/runner.rb
271
285
  - lib/phronomy/multi_agent/team_coordinator.rb
286
+ - lib/phronomy/multi_agent/team_execution.rb
287
+ - lib/phronomy/multi_agent/team_root.rb
272
288
  - lib/phronomy/output_parser.rb
273
289
  - lib/phronomy/output_parser/base.rb
274
290
  - lib/phronomy/output_parser/json_parser.rb
@@ -304,6 +320,7 @@ files:
304
320
  - lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb
305
321
  - lib/phronomy/testing/persistence_contract/an_agent_repository.rb
306
322
  - lib/phronomy/testing/persistence_contract/an_execution_repository.rb
323
+ - lib/phronomy/testing/persistence_contract/coordination_repositories.rb
307
324
  - lib/phronomy/token_usage.rb
308
325
  - lib/phronomy/tool.rb
309
326
  - lib/phronomy/tool/base.rb
@@ -350,6 +367,7 @@ files:
350
367
  - sig/phronomy.rbs
351
368
  - sig/phronomy/agent.rbs
352
369
  - sig/phronomy/extensions.rbs
370
+ - sig/phronomy/handoff.rbs
353
371
  - sig/phronomy/llm_adapter.rbs
354
372
  - sig/phronomy/multi_agent.rbs
355
373
  - sig/phronomy/persistence.rbs