phronomy 0.22.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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +215 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +147 -0
  15. data/docs/architecture/persistence.md +186 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +89 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
  45. data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
  46. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
  47. data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
  48. data/docs/decisions/README.md +176 -0
  49. data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
  50. data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
  51. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
  52. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
  53. data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
  54. data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
  55. data/docs/features.md +65 -11
  56. data/docs/getting-started.md +77 -45
  57. data/docs/migrations/0.19.md +14 -7
  58. data/docs/migrations/0.22.md +390 -0
  59. data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
  60. data/docs/persistence-backends.md +129 -40
  61. data/docs/runtime-and-concurrency.md +227 -33
  62. data/examples/README.md +13 -0
  63. data/lib/phronomy/agent/agent_execution.rb +21 -17
  64. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  65. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  66. data/lib/phronomy/agent/agent_root.rb +3 -3
  67. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  68. data/lib/phronomy/agent/async_event_api.rb +162 -72
  69. data/lib/phronomy/agent/base.rb +416 -181
  70. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  71. data/lib/phronomy/agent/context_assembler.rb +437 -178
  72. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  73. data/lib/phronomy/agent/context_plan.rb +18 -13
  74. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  75. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  76. data/lib/phronomy/agent/context_policy.rb +109 -3
  77. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  78. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  79. data/lib/phronomy/agent/exact_execution.rb +153 -0
  80. data/lib/phronomy/agent/execution_cancellation.rb +25 -0
  81. data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
  82. data/lib/phronomy/agent/handoff.rb +43 -0
  83. data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
  84. data/lib/phronomy/agent/handoff_context.rb +120 -0
  85. data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
  86. data/lib/phronomy/agent/handoff_policy.rb +143 -0
  87. data/lib/phronomy/agent/handoff_projection.rb +207 -0
  88. data/lib/phronomy/agent/handoff_request.rb +45 -0
  89. data/lib/phronomy/agent/handoff_runner.rb +178 -0
  90. data/lib/phronomy/agent/handoff_state.rb +43 -0
  91. data/lib/phronomy/agent/journal_record.rb +17 -4
  92. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  93. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  94. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  95. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  96. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  97. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
  98. data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
  99. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
  100. data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
  101. data/lib/phronomy/agent/recovery_support.rb +504 -0
  102. data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
  103. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  104. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  105. data/lib/phronomy/agent/shared_state.rb +38 -1
  106. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  107. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  108. data/lib/phronomy/agent/tool_invocation.rb +339 -103
  109. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  110. data/lib/phronomy/agent.rb +20 -2
  111. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  112. data/lib/phronomy/agent_purged_error.rb +5 -0
  113. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  114. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  115. data/lib/phronomy/engine/event_loop.rb +622 -63
  116. data/lib/phronomy/engine/fsm_session.rb +194 -21
  117. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  118. data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
  119. data/lib/phronomy/engine/runtime.rb +92 -20
  120. data/lib/phronomy/generator_verifier.rb +12 -14
  121. data/lib/phronomy/invocation_context.rb +9 -29
  122. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  123. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
  124. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  125. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  126. data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
  127. data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
  128. data/lib/phronomy/multi_agent/team_execution.rb +44 -0
  129. data/lib/phronomy/multi_agent/team_root.rb +41 -0
  130. data/lib/phronomy/persistence/durable_codec.rb +706 -0
  131. data/lib/phronomy/persistence/durable_record.rb +117 -0
  132. data/lib/phronomy/persistence/in_memory.rb +471 -133
  133. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  134. data/lib/phronomy/persistence/repository_facades.rb +535 -0
  135. data/lib/phronomy/persistence.rb +174 -40
  136. data/lib/phronomy/recovery.rb +186 -0
  137. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  138. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
  139. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  140. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  141. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  142. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
  143. data/lib/phronomy/testing/persistence_contract.rb +5 -0
  144. data/lib/phronomy/tools/agent.rb +1 -1
  145. data/lib/phronomy/tracing/automatic.rb +176 -0
  146. data/lib/phronomy/tracing/base.rb +11 -2
  147. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  148. data/lib/phronomy/version.rb +1 -1
  149. data/lib/phronomy/workflow.rb +3 -6
  150. data/lib/phronomy/workflow_context.rb +14 -5
  151. data/lib/phronomy/workflow_recovery.rb +123 -0
  152. data/lib/phronomy/workflow_runner.rb +468 -256
  153. data/lib/phronomy.rb +6 -0
  154. data/scripts/api_snapshot.rb +12 -0
  155. data/sig/phronomy/agent.rbs +209 -7
  156. data/sig/phronomy/handoff.rbs +41 -0
  157. data/sig/phronomy/multi_agent.rbs +35 -0
  158. data/sig/phronomy/persistence.rbs +123 -4
  159. data/sig/phronomy/runtime.rbs +1 -4
  160. data/sig/phronomy/workflow.rbs +2 -2
  161. data/sig/phronomy.rbs +10 -0
  162. metadata +84 -18
  163. data/examples/workflows/agent_event_mapping.rb +0 -101
  164. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  165. data/lib/phronomy/agent/activation_registry.rb +0 -28
  166. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  167. data/lib/phronomy/agent/context_candidate.rb +0 -47
  168. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  169. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  170. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  171. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  172. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  173. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  174. data/lib/phronomy/agent/context_request.rb +0 -35
  175. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  176. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  177. data/lib/phronomy/agent/runner.rb +0 -97
  178. data/lib/phronomy/multi_agent/handoff.rb +0 -64
@@ -8,19 +8,19 @@ module Phronomy
8
8
  LIFECYCLE_STATUSES = %i[idle active suspended closed invalidated].freeze
9
9
 
10
10
  ATTRIBUTES = %i[
11
- agent_id agent_definition_id definition_version agent_revision
11
+ agent_id agent_definition_id agent_definition_version agent_revision
12
12
  context_revision journal_position lifecycle_status transcript_generation
13
13
  created_at updated_at metadata
14
14
  ].freeze
15
15
 
16
16
  attr_reader(*ATTRIBUTES)
17
17
 
18
- def self.create(agent_id:, agent_definition_id:, definition_version:, metadata: {})
18
+ def self.create(agent_id:, agent_definition_id:, agent_definition_version:, metadata: {})
19
19
  now = Time.now.utc.iso8601(6)
20
20
  new(
21
21
  agent_id: agent_id,
22
22
  agent_definition_id: agent_definition_id,
23
- definition_version: definition_version,
23
+ agent_definition_version: agent_definition_version,
24
24
  agent_revision: 0,
25
25
  context_revision: 0,
26
26
  journal_position: 0,
@@ -2,17 +2,25 @@
2
2
 
3
3
  module Phronomy
4
4
  module Agent
5
- # Immutable input passed to Tool approval policy callables.
5
+ # Value-oriented input passed to Tool approval policy callables.
6
6
  #
7
7
  # This object intentionally contains the internal, validated arguments used
8
- # for policy evaluation. It must not be exposed directly to Application UI.
8
+ # for policy evaluation. It carries Agent identity metadata and Tool
9
+ # description data only; it never exposes live Agent or Tool objects.
10
+ #
11
+ # Hash, Array, and String values are recursively copied/frozen. Other
12
+ # application-owned opaque values remain application-owned; complete
13
+ # value-type enforcement for those objects is outside ACS-11.
14
+ #
15
+ # This object must not be exposed directly to Application UI.
9
16
  # Use {ToolApprovalRequest} for notifications.
10
17
  #
11
18
  # @api public
12
19
  class ApprovalEvaluationRequest
13
- attr_reader :agent,
14
- :agent_invocation_id,
15
- :tool,
20
+ attr_reader :agent_id,
21
+ :agent_definition_id,
22
+ :agent_definition_version,
23
+ :execution_id,
16
24
  :tool_name,
17
25
  :tool_schema,
18
26
  :tool_invocation_id,
@@ -27,9 +35,10 @@ module Phronomy
27
35
  VALID_DECISIONS = %i[allow require_approval reject].freeze
28
36
 
29
37
  def initialize(
30
- agent:,
31
- agent_invocation_id:,
32
- tool:,
38
+ agent_id:,
39
+ agent_definition_id:,
40
+ agent_definition_version:,
41
+ execution_id:,
33
42
  tool_name:,
34
43
  tool_schema:,
35
44
  tool_invocation_id:,
@@ -41,9 +50,21 @@ module Phronomy
41
50
  metadata: {},
42
51
  default_decision: nil
43
52
  )
44
- @agent = agent
45
- @agent_invocation_id = agent_invocation_id
46
- @tool = tool
53
+ if agent_id.nil? || agent_id.to_s.empty?
54
+ raise ArgumentError, "ApprovalEvaluationRequest requires agent_id"
55
+ end
56
+ if agent_definition_id.nil? || agent_definition_id.to_s.empty?
57
+ raise ArgumentError,
58
+ "ApprovalEvaluationRequest requires agent_definition_id"
59
+ end
60
+ if execution_id.nil? || execution_id.to_s.empty?
61
+ raise ArgumentError, "ApprovalEvaluationRequest requires execution_id"
62
+ end
63
+
64
+ @agent_id = agent_id.to_s.freeze
65
+ @agent_definition_id = agent_definition_id.to_s.freeze
66
+ @agent_definition_version = Integer(agent_definition_version)
67
+ @execution_id = execution_id.to_s.freeze
47
68
  @tool_name = tool_name.to_s.freeze
48
69
  @tool_schema = immutable_copy(tool_schema)
49
70
  @tool_invocation_id = tool_invocation_id.to_s.freeze
@@ -61,9 +82,10 @@ module Phronomy
61
82
  # @api private
62
83
  def with(facts: @facts, default_decision: @default_decision)
63
84
  self.class.new(
64
- agent: @agent,
65
- agent_invocation_id: @agent_invocation_id,
66
- tool: @tool,
85
+ agent_id: @agent_id,
86
+ agent_definition_id: @agent_definition_id,
87
+ agent_definition_version: @agent_definition_version,
88
+ execution_id: @execution_id,
67
89
  tool_name: @tool_name,
68
90
  tool_schema: @tool_schema,
69
91
  tool_invocation_id: @tool_invocation_id,
@@ -90,11 +112,7 @@ module Phronomy
90
112
  when String
91
113
  value.dup.freeze
92
114
  else
93
- begin
94
- value.frozen? ? value : value.dup.freeze
95
- rescue TypeError
96
- value
97
- end
115
+ value
98
116
  end
99
117
  end
100
118
  end
@@ -2,108 +2,85 @@
2
2
 
3
3
  module Phronomy
4
4
  module Agent
5
- # Public Agent execution API. Agent instances are always stateful and all
6
- # operations are coordinated through Phronomy::Persistence.
5
+ # Public Agent execution facade.
6
+ #
7
+ # CG-09 clean break:
8
+ # - application event listeners are bound to the live Agent incarnation at
9
+ # new/create/load time;
10
+ # - invoke/invoke_async/stream/stream_async do not accept per-call event
11
+ # listeners or listener blocks;
12
+ # - approval notifications share the Agent listener as :approval_required;
13
+ # - Recovery resolution uses resolve/resolve_async.
7
14
  module AsyncEventApi
8
15
  def invoke(
9
16
  input,
10
- thread_id: nil,
11
17
  config: {},
12
18
  invocation_context: nil,
13
- on_event: nil,
14
- &block
19
+ **removed_options,
20
+ &removed_block
15
21
  )
16
- listener = resolve_event_listener(on_event, block)
17
- if invocation_context
18
- thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
19
- end
22
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
23
+ config = _prepare_invocation_config(config, invocation_context)
20
24
  _check_event_loop_reentrancy(:invoke, :invoke_async)
21
- trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
22
- result = invoke_async(
23
- input,
24
- thread_id: thread_id,
25
- config: config,
26
- on_event: listener
27
- ).wait_result
28
- [result, result[:usage]]
29
- end
25
+ _start_agent_operation(
26
+ input,
27
+ config: config,
28
+ mode: :invoke,
29
+ listener: _phronomy_event_listener
30
+ ).wait_result
30
31
  end
31
32
 
32
33
  def invoke_async(
33
34
  input,
34
- thread_id: nil,
35
35
  config: {},
36
36
  invocation_context: nil,
37
- on_tool_approval_required: nil,
38
- on_event: nil,
39
- &block
37
+ **removed_options,
38
+ &removed_block
40
39
  )
41
- listener = resolve_event_listener(on_event, block)
42
- if invocation_context
43
- thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
44
- end
45
- approval = _approval_configuration_snapshot(on_tool_approval_required)
46
- execution_coordinator.start(
40
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
41
+ config = _prepare_invocation_config(config, invocation_context)
42
+ _start_agent_operation(
47
43
  input,
48
- thread_id: thread_id,
49
44
  config: config,
50
45
  mode: :invoke,
51
- approval_policy: approval[:policy],
52
- approval_listener: approval[:listener],
53
- on_event: listener
46
+ listener: _phronomy_event_listener
54
47
  )
55
48
  end
56
49
 
57
50
  def stream(
58
51
  input,
59
- thread_id: nil,
60
52
  config: {},
61
53
  invocation_context: nil,
62
- on_tool_approval_required: nil,
63
- on_event: nil,
64
- &block
54
+ **removed_options,
55
+ &removed_block
65
56
  )
66
- listener = resolve_event_listener(on_event, block)
67
- raise ArgumentError, "stream requires on_event: or a block" unless listener
68
- if invocation_context
69
- thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
70
- end
57
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
58
+ listener = _required_stream_listener!(:stream)
59
+ config = _prepare_invocation_config(config, invocation_context)
71
60
  _check_event_loop_reentrancy(:stream, :stream_async)
72
- trace("agent.stream", input: input, **_build_caller_meta(config)) do |_span|
73
- result = stream_async(
74
- input,
75
- thread_id: thread_id,
76
- config: config,
77
- on_tool_approval_required: on_tool_approval_required,
78
- on_event: listener
79
- ).wait_result
80
- [result, result[:usage]]
81
- end
61
+ _start_agent_operation(
62
+ input,
63
+ config: config,
64
+ mode: :stream,
65
+ listener: listener
66
+ ).wait_result
82
67
  end
83
68
 
84
69
  def stream_async(
85
70
  input,
86
- thread_id: nil,
87
71
  config: {},
88
72
  invocation_context: nil,
89
- on_tool_approval_required: nil,
90
- on_event: nil,
91
- &block
73
+ **removed_options,
74
+ &removed_block
92
75
  )
93
- listener = resolve_event_listener(on_event, block)
94
- raise ArgumentError, "stream_async requires on_event: or a block" unless listener
95
- if invocation_context
96
- thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
97
- end
98
- approval = _approval_configuration_snapshot(on_tool_approval_required)
99
- execution_coordinator.start(
76
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
77
+ listener = _required_stream_listener!(:stream_async)
78
+ config = _prepare_invocation_config(config, invocation_context)
79
+ _start_agent_operation(
100
80
  input,
101
- thread_id: thread_id,
102
81
  config: config,
103
82
  mode: :stream,
104
- approval_policy: approval[:policy],
105
- approval_listener: approval[:listener],
106
- on_event: listener
83
+ listener: listener
107
84
  )
108
85
  end
109
86
 
@@ -118,7 +95,14 @@ module Phronomy
118
95
  end
119
96
 
120
97
  def approve_async(execution_id, approval_request_id:, approved: true, config: {})
121
- execution_coordinator.resume(
98
+ _reject_removed_generic_identity_keys!(config)
99
+ owner = Phronomy::Runtime.instance.__agent_execution_owner(execution_id)
100
+ coordinator = if owner&.agent&.equal?(self)
101
+ owner.coordinator
102
+ else
103
+ execution_coordinator_for(config)
104
+ end
105
+ coordinator.resume(
122
106
  execution_id,
123
107
  approval_request_id: approval_request_id,
124
108
  approved: approved,
@@ -126,17 +110,123 @@ module Phronomy
126
110
  )
127
111
  end
128
112
 
113
+ def resolve(
114
+ execution_id,
115
+ expected_execution_revision:,
116
+ subject:,
117
+ outcome:,
118
+ result: Phronomy::Recovery::MISSING,
119
+ error: Phronomy::Recovery::MISSING
120
+ )
121
+ _check_event_loop_reentrancy(:resolve, :resolve_async)
122
+ resolve_async(
123
+ execution_id,
124
+ expected_execution_revision: expected_execution_revision,
125
+ subject: subject,
126
+ outcome: outcome,
127
+ result: result,
128
+ error: error
129
+ ).wait_result
130
+ end
131
+
132
+ def resolve_async(
133
+ execution_id,
134
+ expected_execution_revision:,
135
+ subject:,
136
+ outcome:,
137
+ result: Phronomy::Recovery::MISSING,
138
+ error: Phronomy::Recovery::MISSING
139
+ )
140
+ Phronomy::Agent::RecoveryCoordinator.new(self).resolve(
141
+ execution_id,
142
+ expected_execution_revision: expected_execution_revision,
143
+ subject: subject,
144
+ outcome: outcome,
145
+ result: result,
146
+ error: error
147
+ )
148
+ end
149
+
129
150
  private
130
151
 
152
+ # Framework-private execution-local event routing. This is intentionally
153
+ # not a public compatibility path for invoke_async(..., on_event:).
154
+ def __invoke_async_with_event_sink(
155
+ input,
156
+ on_event:, config: {},
157
+ invocation_context: nil
158
+ )
159
+ raise ArgumentError, "on_event is required" unless on_event
160
+
161
+ config = _prepare_invocation_config(config, invocation_context)
162
+ _start_agent_operation(
163
+ input,
164
+ config: config,
165
+ mode: :invoke,
166
+ listener: on_event
167
+ )
168
+ end
169
+
170
+ def _start_agent_operation(input, config:, mode:, listener:)
171
+ config = _snapshot_durable_context(config)
172
+ approval = _approval_configuration_snapshot(nil)
173
+ execution_coordinator_for(config).start(
174
+ input,
175
+ config: config.merge(phronomy_recovery_mode: mode.to_sym),
176
+ mode: mode,
177
+ approval_policy: approval[:policy],
178
+ approval_listener: nil,
179
+ on_event: listener
180
+ )
181
+ end
182
+
183
+ def _snapshot_durable_context(config)
184
+ return config unless config.key?(:durable_context)
185
+
186
+ value = config[:durable_context]
187
+ unless value.is_a?(Hash)
188
+ raise ArgumentError,
189
+ "config[:durable_context] must be a canonical JSON-compatible Hash"
190
+ end
191
+
192
+ bytes = Phronomy::CanonicalJSON.dump(value)
193
+ snapshot = Phronomy::Agent::Immutable.copy(
194
+ Phronomy::CanonicalJSON.load(bytes)
195
+ )
196
+ config.merge(durable_context: snapshot)
197
+ end
198
+
199
+ def _required_stream_listener!(method_name)
200
+ listener = _phronomy_event_listener
201
+ return listener if listener
202
+
203
+ raise ArgumentError,
204
+ "#{method_name} requires an Agent on_event listener registered at new/create/load"
205
+ end
206
+
207
+ def _reject_removed_invocation_listener_arguments!(options, block)
208
+ if block
209
+ raise ArgumentError,
210
+ "invoke/stream blocks no longer register Agent events; register the listener at new/create/load"
211
+ end
212
+ return if options.empty?
213
+
214
+ removed = options.keys.map(&:inspect).join(", ")
215
+ raise ArgumentError,
216
+ "removed per-invocation Agent option(s): #{removed}; register on_event at new/create/load"
217
+ end
218
+
131
219
  def execution_coordinator
132
220
  @execution_coordinator ||= Agent::ExecutionCoordinator.new(self)
133
221
  end
134
222
 
135
- def resolve_event_listener(keyword_listener, block_listener)
136
- if keyword_listener && block_listener
137
- raise ArgumentError, "Provide either on_event: or a block, not both"
138
- end
139
- keyword_listener || block_listener
223
+ def execution_coordinator_for(config)
224
+ multi_agent = config.key?(:phronomy_handoff_bindings) ||
225
+ config.key?(:phronomy_handoff_context)
226
+ return execution_coordinator unless multi_agent
227
+
228
+ @multi_agent_execution_coordinator ||=
229
+ Phronomy::Agent::HandoffExecutionCoordinator.new(self)
140
230
  end
141
231
  end
142
232
  end