phronomy 0.22.0 → 0.23.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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +200 -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 +152 -0
  15. data/docs/architecture/persistence.md +175 -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 +82 -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 +112 -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/README.md +172 -0
  45. data/docs/features.md +31 -11
  46. data/docs/getting-started.md +77 -45
  47. data/docs/migrations/0.19.md +14 -7
  48. data/docs/migrations/0.22.md +390 -0
  49. data/docs/persistence-backends.md +88 -38
  50. data/docs/runtime-and-concurrency.md +227 -33
  51. data/examples/README.md +13 -0
  52. data/lib/phronomy/agent/agent_execution.rb +19 -15
  53. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  54. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  55. data/lib/phronomy/agent/agent_root.rb +3 -3
  56. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  57. data/lib/phronomy/agent/async_event_api.rb +145 -72
  58. data/lib/phronomy/agent/base.rb +388 -181
  59. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  60. data/lib/phronomy/agent/context_assembler.rb +437 -178
  61. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  62. data/lib/phronomy/agent/context_plan.rb +18 -13
  63. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  64. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  65. data/lib/phronomy/agent/context_policy.rb +109 -3
  66. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  67. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  68. data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
  69. data/lib/phronomy/agent/journal_record.rb +17 -4
  70. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  71. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  72. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  73. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  74. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  75. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
  76. data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
  77. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
  78. data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
  79. data/lib/phronomy/agent/recovery_support.rb +512 -0
  80. data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
  81. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  82. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  83. data/lib/phronomy/agent/shared_state.rb +38 -1
  84. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  85. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  86. data/lib/phronomy/agent/tool_invocation.rb +336 -102
  87. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  88. data/lib/phronomy/agent.rb +20 -2
  89. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  90. data/lib/phronomy/agent_purged_error.rb +5 -0
  91. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  92. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  93. data/lib/phronomy/engine/event_loop.rb +622 -63
  94. data/lib/phronomy/engine/fsm_session.rb +194 -21
  95. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  96. data/lib/phronomy/engine/runtime.rb +77 -20
  97. data/lib/phronomy/generator_verifier.rb +12 -14
  98. data/lib/phronomy/invocation_context.rb +9 -29
  99. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  100. data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
  101. data/lib/phronomy/multi_agent/coordinator.rb +154 -0
  102. data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
  103. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  104. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  105. data/lib/phronomy/multi_agent/handoff.rb +24 -45
  106. data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
  107. data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
  108. data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
  109. data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
  110. data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
  111. data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
  112. data/lib/phronomy/multi_agent/runner.rb +98 -0
  113. data/lib/phronomy/persistence/durable_codec.rb +646 -0
  114. data/lib/phronomy/persistence/durable_record.rb +117 -0
  115. data/lib/phronomy/persistence/in_memory.rb +210 -134
  116. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  117. data/lib/phronomy/persistence/repository_facades.rb +316 -0
  118. data/lib/phronomy/persistence.rb +81 -41
  119. data/lib/phronomy/recovery.rb +186 -0
  120. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  121. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
  122. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  123. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  124. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  125. data/lib/phronomy/tracing/automatic.rb +176 -0
  126. data/lib/phronomy/tracing/base.rb +11 -2
  127. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  128. data/lib/phronomy/version.rb +1 -1
  129. data/lib/phronomy/workflow.rb +3 -6
  130. data/lib/phronomy/workflow_context.rb +14 -5
  131. data/lib/phronomy/workflow_recovery.rb +123 -0
  132. data/lib/phronomy/workflow_runner.rb +468 -256
  133. data/lib/phronomy.rb +6 -0
  134. data/scripts/api_snapshot.rb +12 -0
  135. data/sig/phronomy/agent.rbs +209 -7
  136. data/sig/phronomy/multi_agent.rbs +39 -0
  137. data/sig/phronomy/persistence.rbs +62 -4
  138. data/sig/phronomy/runtime.rbs +1 -4
  139. data/sig/phronomy/workflow.rbs +2 -2
  140. data/sig/phronomy.rbs +10 -0
  141. metadata +65 -17
  142. data/examples/workflows/agent_event_mapping.rb +0 -101
  143. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  144. data/lib/phronomy/agent/activation_registry.rb +0 -28
  145. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  146. data/lib/phronomy/agent/context_candidate.rb +0 -47
  147. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  148. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  149. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  150. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  151. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  152. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  153. data/lib/phronomy/agent/context_request.rb +0 -35
  154. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  155. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  156. data/lib/phronomy/agent/runner.rb +0 -97
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ class RecoveryCoordinator
6
+ # Continuation portion of durable Agent Recovery.
7
+ # @api private
8
+ module Continuation
9
+ # simplecov:disable
10
+
11
+ private
12
+
13
+ def continue_provider_completed_after_resolution(
14
+ event_loop,
15
+ old_state,
16
+ execution,
17
+ completion
18
+ )
19
+ manifest_ref = execution.metadata.fetch(
20
+ "manifest_ref"
21
+ )
22
+ _manifest, projection =
23
+ RecoverySupport.materialize_projection(
24
+ agent,
25
+ manifest_ref
26
+ )
27
+ main = agent.send(:execution_coordinator)
28
+ invocation =
29
+ RecoverySupport.build_chat_for_recovery(
30
+ agent,
31
+ execution,
32
+ projection,
33
+ main,
34
+ agent.send(:_phronomy_event_listener)
35
+ )
36
+ output, usage =
37
+ RecoverySupport.provider_output_and_usage(
38
+ agent,
39
+ execution
40
+ )
41
+ invocation.output = output
42
+ invocation.usage = usage
43
+ AgentInvocationSessionBuilder.send(
44
+ :output_filtering_action,
45
+ agent,
46
+ invocation
47
+ )
48
+
49
+ event_loop.replace_agent_execution(
50
+ execution.execution_id,
51
+ execution: execution,
52
+ runtime_projection: projection,
53
+ invocation: invocation,
54
+ fsm_session_id: nil
55
+ )
56
+ event_loop.mark_agent_execution_admission(
57
+ agent.agent_id,
58
+ execution_id: execution.execution_id,
59
+ state: :executing
60
+ )
61
+ start_output_completion_session(
62
+ event_loop,
63
+ main,
64
+ execution,
65
+ invocation,
66
+ completion
67
+ )
68
+ end
69
+
70
+ def continue_tools_completed_after_resolution(
71
+ event_loop,
72
+ old_state,
73
+ execution,
74
+ completion
75
+ )
76
+ manifest_ref = execution.metadata.fetch(
77
+ "manifest_ref"
78
+ )
79
+ _manifest, projection =
80
+ RecoverySupport.materialize_projection(
81
+ agent,
82
+ manifest_ref
83
+ )
84
+ main = agent.send(:execution_coordinator)
85
+ invocation =
86
+ RecoverySupport.build_chat_for_recovery(
87
+ agent,
88
+ execution,
89
+ projection,
90
+ main,
91
+ agent.send(:_phronomy_event_listener)
92
+ )
93
+ event_loop.replace_agent_execution(
94
+ execution.execution_id,
95
+ execution: execution,
96
+ runtime_projection: projection,
97
+ invocation: invocation,
98
+ fsm_session_id: nil
99
+ )
100
+ event_loop.mark_agent_execution_admission(
101
+ agent.agent_id,
102
+ execution_id: execution.execution_id,
103
+ state: :executing
104
+ )
105
+ start_followup_session(
106
+ event_loop,
107
+ main,
108
+ execution,
109
+ invocation,
110
+ completion
111
+ )
112
+ end
113
+
114
+ def continue_failed_after_resolution(
115
+ event_loop,
116
+ old_state,
117
+ execution,
118
+ completion,
119
+ failure
120
+ )
121
+ main = agent.send(:execution_coordinator)
122
+ invocation = Phronomy::Agent::AgentInvocation.new(
123
+ agent: agent,
124
+ input: nil,
125
+ config: {
126
+ execution_id: execution.execution_id,
127
+ phronomy_execution_coordinator: main
128
+ },
129
+ event_listener:
130
+ agent.send(:_phronomy_event_listener),
131
+ mode: (
132
+ execution.metadata[
133
+ RecoverySupport::INVOCATION_MODE_KEY
134
+ ] || "invoke"
135
+ ).to_sym,
136
+ execution_id: execution.execution_id
137
+ )
138
+ event_loop.replace_agent_execution(
139
+ execution.execution_id,
140
+ execution: execution,
141
+ invocation: invocation,
142
+ fsm_session_id: nil
143
+ )
144
+ state = event_loop.agent_execution_state(
145
+ execution.execution_id
146
+ )
147
+ main.send(
148
+ :begin_terminal_commit_on_event_loop,
149
+ state,
150
+ completion,
151
+ invocation,
152
+ RecoverySupport.error_from_failure(failure),
153
+ fsm_session_id: nil
154
+ )
155
+ end
156
+
157
+ def start_output_completion_session(
158
+ event_loop,
159
+ main,
160
+ execution,
161
+ invocation,
162
+ completion
163
+ )
164
+ session =
165
+ AgentInvocationSessionBuilder.build_for_resume(
166
+ agent_invocation: invocation,
167
+ resume_event: :state_completed,
168
+ resume_phase: :output_filtering,
169
+ runtime: Phronomy::Runtime.instance
170
+ )
171
+ event_loop.replace_agent_execution(
172
+ execution.execution_id,
173
+ invocation: invocation,
174
+ fsm_session_id: session.id
175
+ )
176
+ event_loop.register_agent_completion_waiter(
177
+ execution.execution_id,
178
+ completion
179
+ )
180
+ source = Phronomy::Task.deferred(
181
+ name: "#{completion.name}-source"
182
+ )
183
+ source.on_complete do |completed, error|
184
+ main.send(
185
+ :finish_on_event_loop,
186
+ execution.execution_id,
187
+ completion,
188
+ completed || session.context,
189
+ error,
190
+ fsm_session_id: session.id
191
+ )
192
+ end
193
+ event_loop.register(session, completion: source)
194
+ end
195
+
196
+ def start_followup_session(
197
+ event_loop,
198
+ main,
199
+ execution,
200
+ invocation,
201
+ completion
202
+ )
203
+ session =
204
+ AgentInvocationSessionBuilder.build_for_resume(
205
+ agent_invocation: invocation,
206
+ resume_event: :state_completed,
207
+ resume_phase: :recording_tool_results,
208
+ runtime: Phronomy::Runtime.instance
209
+ )
210
+ event_loop.replace_agent_execution(
211
+ execution.execution_id,
212
+ invocation: invocation,
213
+ fsm_session_id: session.id
214
+ )
215
+ event_loop.register_agent_completion_waiter(
216
+ execution.execution_id,
217
+ completion
218
+ )
219
+ source = Phronomy::Task.deferred(
220
+ name: "#{completion.name}-source"
221
+ )
222
+ source.on_complete do |completed, error|
223
+ main.send(
224
+ :finish_on_event_loop,
225
+ execution.execution_id,
226
+ completion,
227
+ completed || session.context,
228
+ error,
229
+ fsm_session_id: session.id
230
+ )
231
+ end
232
+ event_loop.register(session, completion: source)
233
+ end
234
+
235
+ def deliver_resolution_required(execution, descriptor)
236
+ unless descriptor
237
+ raise Phronomy::ExecutionRehydrationRequiredError,
238
+ "Recovery state has no current unresolved subject"
239
+ end
240
+ listener = agent.send(:_phronomy_event_listener)
241
+ unless listener
242
+ raise Phronomy::ConfigurationError,
243
+ "Recovery resolution requires an Agent on_event listener"
244
+ end
245
+ payload = RecoverySupport.event_payload(
246
+ execution,
247
+ descriptor
248
+ )
249
+ callback_error = agent.send(
250
+ :_deliver_stream_event,
251
+ listener,
252
+ StreamEvent.new(
253
+ type: :recovery_resolution_required,
254
+ payload: payload
255
+ )
256
+ )
257
+ if callback_error
258
+ raise agent.send(
259
+ :_build_stream_callback_error,
260
+ event_type: :recovery_resolution_required,
261
+ callback_error: callback_error,
262
+ result: payload
263
+ )
264
+ end
265
+ nil
266
+ end
267
+ # simplecov:enable
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,427 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ class RecoveryCoordinator
6
+ # Installation portion of durable Agent Recovery.
7
+ # @api private
8
+ module Installation
9
+ private
10
+
11
+ def prepare_plan(execution)
12
+ root = agent.agent_root
13
+ manifest_ref = execution.metadata["manifest_ref"]
14
+ base_ref = execution.metadata["base_manifest_ref"] ||
15
+ manifest_ref
16
+ manifest = base_manifest = projection = nil
17
+ if manifest_ref
18
+ manifest, projection =
19
+ RecoverySupport.materialize_projection(
20
+ agent,
21
+ manifest_ref
22
+ )
23
+ base_manifest = if base_ref.to_s == manifest_ref.to_s
24
+ manifest
25
+ else
26
+ RecoverySupport.manifest_from_ref(agent, base_ref)
27
+ end
28
+ end
29
+
30
+ classification = classify(execution)
31
+ RecoveryPlan.new(
32
+ execution: execution,
33
+ root: root,
34
+ manifest: manifest,
35
+ base_manifest: base_manifest,
36
+ projection: projection,
37
+ classification: classification
38
+ )
39
+ end
40
+
41
+ def classify(execution)
42
+ if execution.status == :suspended &&
43
+ execution.phase.to_sym == :approval
44
+ return Phronomy::Recovery::Classification.new(
45
+ disposition: Phronomy::Recovery::RESUMABLE,
46
+ reason: :approval_wait,
47
+ facts: {
48
+ approval_request_id:
49
+ execution.approval_request &&
50
+ (
51
+ execution.approval_request["id"] ||
52
+ execution.approval_request[:id]
53
+ )
54
+ }.compact
55
+ )
56
+ end
57
+
58
+ case execution.phase.to_sym
59
+ when :recovery_provider_completed,
60
+ :recovery_tools_completed,
61
+ :recovery_resolved_failed
62
+ return Phronomy::Recovery::Classification.new(
63
+ disposition: Phronomy::Recovery::RESUMABLE,
64
+ reason: execution.phase
65
+ )
66
+ when :resuming
67
+ approved = execution.approval_request &&
68
+ (
69
+ execution.approval_request["approved"] ||
70
+ execution.approval_request[:approved]
71
+ )
72
+ unless approved
73
+ return Phronomy::Recovery::Classification.new(
74
+ disposition: Phronomy::Recovery::RESUMABLE,
75
+ reason: :approval_rejection_committed
76
+ )
77
+ end
78
+ end
79
+
80
+ descriptor = RecoverySupport.recovery_descriptor(
81
+ execution
82
+ )
83
+ if descriptor
84
+ return Phronomy::Recovery::Classification.new(
85
+ disposition: Phronomy::Recovery::RESOLUTION_REQUIRED,
86
+ reason: descriptor.fetch(:reason),
87
+ subject: descriptor.fetch(:subject),
88
+ allowed_outcomes:
89
+ descriptor.fetch(:allowed_outcomes),
90
+ facts: descriptor.fetch(:facts)
91
+ )
92
+ end
93
+
94
+ raise Phronomy::ExecutionRehydrationRequiredError,
95
+ "execution #{execution.execution_id} is not at an restart-safe durable continuation point " \
96
+ "(status=#{execution.status.inspect}, phase=#{execution.phase.inspect})"
97
+ end
98
+
99
+ def install_on_event_loop(command)
100
+ event_loop = Phronomy::Runtime.instance.event_loop
101
+ plan = command.plan
102
+ execution = plan.execution
103
+ main = agent.send(:execution_coordinator)
104
+ admitted = false
105
+ bound = false
106
+ installed = false
107
+
108
+ event_loop.admit_agent_execution(
109
+ agent.agent_id,
110
+ owner_token: command.owner_token
111
+ )
112
+ admitted = true
113
+ event_loop.bind_agent_execution_admission(
114
+ agent.agent_id,
115
+ owner_token: command.owner_token,
116
+ execution_id: execution.execution_id
117
+ )
118
+ bound = true
119
+
120
+ invocation = nil
121
+ if execution.status == :suspended ||
122
+ execution.phase.to_sym == :resuming
123
+ invocation =
124
+ RecoverySupport.build_invocation_for_suspended(
125
+ agent,
126
+ execution,
127
+ plan.projection,
128
+ main,
129
+ agent.send(:_phronomy_event_listener)
130
+ )
131
+ elsif execution.phase.to_sym ==
132
+ :recovery_tools_completed
133
+ invocation =
134
+ RecoverySupport.build_chat_for_recovery(
135
+ agent,
136
+ execution,
137
+ plan.projection,
138
+ main,
139
+ agent.send(:_phronomy_event_listener)
140
+ )
141
+ elsif execution.phase.to_sym ==
142
+ :recovery_provider_completed
143
+ invocation =
144
+ RecoverySupport.build_chat_for_recovery(
145
+ agent,
146
+ execution,
147
+ plan.projection,
148
+ main,
149
+ agent.send(:_phronomy_event_listener)
150
+ )
151
+ output, usage =
152
+ RecoverySupport.provider_output_and_usage(
153
+ agent,
154
+ execution
155
+ )
156
+ invocation.output = output
157
+ invocation.usage = usage
158
+ elsif execution.phase.to_sym ==
159
+ :recovery_resolved_failed
160
+ invocation = Phronomy::Agent::AgentInvocation.new(
161
+ agent: agent,
162
+ input: nil,
163
+ config: {
164
+ execution_id: execution.execution_id,
165
+ phronomy_execution_coordinator: main
166
+ },
167
+ event_listener:
168
+ agent.send(:_phronomy_event_listener),
169
+ mode: (
170
+ execution.metadata[
171
+ RecoverySupport::INVOCATION_MODE_KEY
172
+ ] || "invoke"
173
+ ).to_sym,
174
+ execution_id: execution.execution_id
175
+ )
176
+ end
177
+
178
+ event_loop.install_agent_execution(
179
+ execution_id: execution.execution_id,
180
+ agent: agent,
181
+ coordinator: main,
182
+ execution: execution,
183
+ runtime_projection: plan.projection,
184
+ base_manifest: plan.base_manifest,
185
+ invocation: invocation,
186
+ fsm_session_id: nil
187
+ )
188
+ installed = true
189
+
190
+ if execution.status == :suspended
191
+ event_loop.mark_agent_execution_admission(
192
+ agent.agent_id,
193
+ execution_id: execution.execution_id,
194
+ state: :suspended
195
+ )
196
+ request = invocation.approval_request
197
+ callback_error = agent.send(
198
+ :_deliver_stream_event,
199
+ agent.send(:_phronomy_event_listener),
200
+ StreamEvent.new(
201
+ type: :approval_required,
202
+ payload: {request: request}.freeze
203
+ )
204
+ )
205
+ if callback_error
206
+ raise agent.send(
207
+ :_build_stream_callback_error,
208
+ event_type: :approval_required,
209
+ callback_error: callback_error,
210
+ result: {
211
+ execution_id: execution.execution_id,
212
+ suspended: true
213
+ }
214
+ )
215
+ end
216
+ command.completion.complete(agent)
217
+ return
218
+ end
219
+
220
+ case plan.classification.disposition
221
+ when Phronomy::Recovery::RESOLUTION_REQUIRED
222
+ event_loop.mark_agent_execution_admission(
223
+ agent.agent_id,
224
+ execution_id: execution.execution_id,
225
+ state: :recovery_required
226
+ )
227
+ payload = RecoverySupport.event_payload(
228
+ execution,
229
+ {
230
+ reason: plan.classification.reason,
231
+ subject: plan.classification.subject,
232
+ allowed_outcomes:
233
+ plan.classification.allowed_outcomes,
234
+ facts: plan.classification.facts
235
+ }
236
+ )
237
+ callback_error = agent.send(
238
+ :_deliver_stream_event,
239
+ agent.send(:_phronomy_event_listener),
240
+ StreamEvent.new(
241
+ type: :recovery_resolution_required,
242
+ payload: payload
243
+ )
244
+ )
245
+ if callback_error
246
+ raise agent.send(
247
+ :_build_stream_callback_error,
248
+ event_type: :recovery_resolution_required,
249
+ callback_error: callback_error,
250
+ result: payload
251
+ )
252
+ end
253
+ command.completion.complete(agent)
254
+ when Phronomy::Recovery::RESUMABLE
255
+ continue_resumable_on_event_loop(
256
+ execution,
257
+ invocation,
258
+ completion: command.completion
259
+ )
260
+ else
261
+ raise Phronomy::Error,
262
+ "unsupported Recovery installation disposition: #{plan.classification.disposition.inspect}"
263
+ end
264
+ rescue => caught
265
+ # simplecov:disable
266
+ if installed
267
+ begin
268
+ event_loop.release_agent_execution(
269
+ execution.execution_id
270
+ )
271
+ rescue
272
+ nil
273
+ end
274
+ end
275
+ if admitted
276
+ if bound
277
+ begin
278
+ event_loop.release_agent_execution_admission(
279
+ agent.agent_id,
280
+ execution_id: execution&.execution_id
281
+ )
282
+ rescue
283
+ nil
284
+ end
285
+ else
286
+ begin
287
+ event_loop.release_agent_execution_admission(
288
+ agent.agent_id,
289
+ owner_token: command.owner_token
290
+ )
291
+ rescue
292
+ nil
293
+ end
294
+ end
295
+ end
296
+ command.completion.fail(caught)
297
+ # simplecov:enable
298
+ end
299
+
300
+ def observe_recovery_execution(task, execution)
301
+ Phronomy::Tracing::Automatic.observe_task(
302
+ task,
303
+ "agent.execution",
304
+ agent_id: agent.agent_id,
305
+ execution_id: execution.execution_id,
306
+ mode: (
307
+ execution.metadata[
308
+ RecoverySupport::INVOCATION_MODE_KEY
309
+ ] || "invoke"
310
+ ).to_sym
311
+ )
312
+ end
313
+
314
+ def continue_resumable_on_event_loop(
315
+ execution,
316
+ invocation,
317
+ completion:
318
+ )
319
+ # simplecov:disable
320
+ event_loop = Phronomy::Runtime.instance.event_loop
321
+ main = agent.send(:execution_coordinator)
322
+
323
+ case execution.phase.to_sym
324
+ when :resuming
325
+ approved = execution.approval_request &&
326
+ (
327
+ execution.approval_request["approved"] ||
328
+ execution.approval_request[:approved]
329
+ )
330
+ if approved
331
+ raise Phronomy::ExecutionRehydrationRequiredError,
332
+ "approved resuming execution requires Tool outcome resolution"
333
+ end
334
+ internal_task = Phronomy::Task.deferred(
335
+ name: "agent-recovery-auto:#{execution.execution_id}"
336
+ )
337
+ observe_recovery_execution(internal_task, execution)
338
+ event_loop.mark_agent_execution_admission(
339
+ agent.agent_id,
340
+ execution_id: execution.execution_id,
341
+ state: :executing
342
+ )
343
+ main.send(
344
+ :start_resume_on_event_loop,
345
+ execution.execution_id,
346
+ internal_task,
347
+ approved: false,
348
+ config: {}
349
+ )
350
+ completion.complete(agent)
351
+ when :recovery_tools_completed
352
+ internal_task = Phronomy::Task.deferred(
353
+ name: "agent-recovery-auto:#{execution.execution_id}"
354
+ )
355
+ observe_recovery_execution(internal_task, execution)
356
+ event_loop.mark_agent_execution_admission(
357
+ agent.agent_id,
358
+ execution_id: execution.execution_id,
359
+ state: :executing
360
+ )
361
+ start_followup_session(
362
+ event_loop,
363
+ main,
364
+ execution,
365
+ invocation,
366
+ internal_task
367
+ )
368
+ completion.complete(agent)
369
+ when :recovery_provider_completed
370
+ AgentInvocationSessionBuilder.send(
371
+ :output_filtering_action,
372
+ agent,
373
+ invocation
374
+ )
375
+ internal_task = Phronomy::Task.deferred(
376
+ name: "agent-recovery-auto:#{execution.execution_id}"
377
+ )
378
+ observe_recovery_execution(internal_task, execution)
379
+ event_loop.mark_agent_execution_admission(
380
+ agent.agent_id,
381
+ execution_id: execution.execution_id,
382
+ state: :executing
383
+ )
384
+ start_output_completion_session(
385
+ event_loop,
386
+ main,
387
+ execution,
388
+ invocation,
389
+ internal_task
390
+ )
391
+ completion.complete(agent)
392
+ when :recovery_resolved_failed
393
+ failure = (
394
+ execution.metadata[
395
+ RecoverySupport::RECOVERY_METADATA_KEY
396
+ ] || {}
397
+ )["failure"] || {
398
+ "class" => "Phronomy::Error",
399
+ "message" => "Recovery-resolved failure"
400
+ }
401
+ error = RecoverySupport.error_from_failure(failure)
402
+ internal_task = Phronomy::Task.deferred(
403
+ name: "agent-recovery-auto:#{execution.execution_id}"
404
+ )
405
+ observe_recovery_execution(internal_task, execution)
406
+ state = event_loop.agent_execution_state(
407
+ execution.execution_id
408
+ )
409
+ main.send(
410
+ :begin_terminal_commit_on_event_loop,
411
+ state,
412
+ internal_task,
413
+ invocation,
414
+ error,
415
+ fsm_session_id: nil
416
+ )
417
+ completion.complete(agent)
418
+ else
419
+ raise Phronomy::ExecutionRehydrationRequiredError,
420
+ "no automatic continuation for #{execution.phase.inspect}"
421
+ end
422
+ # simplecov:enable
423
+ end
424
+ end
425
+ end
426
+ end
427
+ end