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
@@ -3,231 +3,135 @@
3
3
  module Phronomy
4
4
  module Agent
5
5
  class RecoveryCoordinator
6
- # Continuation portion of durable Agent Recovery.
6
+ # Both resolution apply and restart use this interpretation of saved facts.
7
+ # Runtime tasks and sessions are projections, never continuation authority.
7
8
  # @api private
8
9
  module Continuation
9
- # simplecov:disable
10
-
11
10
  private
12
11
 
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
- )
12
+ def recovery_action(execution)
13
+ return :framework_tools if framework_batch?(execution)
48
14
 
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
- )
15
+ case execution.phase.to_sym
16
+ when :recovery_provider_completed
17
+ execution.metadata["framework_calls_pending"] ? :framework_calls : :output
18
+ when :recovery_tools_completed
19
+ execution.metadata["framework_calls_pending"] ? :framework_calls : :followup
20
+ when :recovery_resolved_failed
21
+ :failed_terminal
22
+ else
23
+ :resolution_required
24
+ end
68
25
  end
69
26
 
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
- )
27
+ # Read durable inputs before entering EventLoop. The returned messages are
28
+ # operation-local projections transferred to the new invocation, never a
29
+ # shared cache or a second source of continuation decisions.
30
+ def prepare_recovery_material(execution, projection: nil)
31
+ action = recovery_action(execution)
32
+ suspended = execution.status == :suspended || execution.phase.to_sym == :resuming
33
+ return if !suspended && %i[resolution_required failed_terminal].include?(action)
34
+
35
+ unless projection
36
+ _manifest, projection = RecoverySupport.materialize_projection(agent, execution.metadata.fetch("manifest_ref"))
37
+ end
38
+ records = execution.working_records.select { |record| %i[assistant_message tool_message].include?(record.kind.to_sym) }
39
+ materializer = RubyLLMMaterializer.new(agent: agent, persistence: agent.persistence)
40
+ messages = records.map { |record| materializer.materialize_journal_record(record) }.freeze
41
+ assistant = messages.reverse.find { |message| message.role.to_sym == :assistant }
42
+ output, usage = RecoverySupport.provider_output_and_usage(agent, execution) if execution.phase.to_sym == :recovery_provider_completed
43
+ RecoveryMaterial.new(projection: projection, messages: messages,
44
+ assistant_message: assistant, output: output, usage: usage)
112
45
  end
113
46
 
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
- )
47
+ def continue_recovery_on_event_loop(execution, completion, material:)
48
+ event_loop = @runtime.event_loop
49
+ action = recovery_action(execution)
50
+ if action == :resolution_required
51
+ event_loop.mark_agent_execution_admission(agent.agent_id,
52
+ execution_id: execution.execution_id, state: :recovery_required)
53
+ deliver_resolution_required(execution, coordination_recovery_descriptor(execution))
54
+ completion.complete({execution_id: execution.execution_id,
55
+ execution_revision: execution.execution_revision,
56
+ recovery: :resolution_required}.freeze)
57
+ return
58
+ end
59
+
60
+ observe_recovery_execution(completion, execution)
61
+ main = agent.send(:execution_coordinator_for, agent.__coordination_config)
62
+ projection = event_loop.agent_execution_state(execution.execution_id).runtime_projection
63
+ if action == :failed_terminal
64
+ invocation = build_failed_recovery_invocation(execution, main)
65
+ else
66
+ projection = material.projection
67
+ invocation = if action == :framework_tools
68
+ RecoverySupport.build_invocation_for_suspended(agent, execution, projection, main,
69
+ agent.send(:_phronomy_event_listener), assistant_message: material.assistant_message)
70
+ else
71
+ RecoverySupport.build_chat_for_recovery(agent, execution, projection, main,
72
+ agent.send(:_phronomy_event_listener), messages: material.messages)
73
+ end
74
+ if execution.phase.to_sym == :recovery_provider_completed
75
+ invocation.output, invocation.usage = material.output, material.usage
76
+ end
77
+ prepare_saved_provider_calls(execution, invocation, material.assistant_message) if action == :framework_calls
78
+ end
79
+
80
+ event_loop.replace_agent_execution(execution.execution_id, execution: execution,
81
+ runtime_projection: projection, invocation: invocation, fsm_session_id: nil)
82
+ if action == :failed_terminal
83
+ failure = execution.metadata.dig(RecoverySupport::RECOVERY_METADATA_KEY, "failure") ||
84
+ {"class" => "Phronomy::Error", "message" => "Recovery-resolved failure"}
85
+ main.send(:begin_terminal_commit_on_event_loop,
86
+ event_loop.agent_execution_state(execution.execution_id), completion, invocation,
87
+ RecoverySupport.error_from_failure(failure), fsm_session_id: nil)
88
+ return
89
+ end
90
+
91
+ event_loop.mark_agent_execution_admission(agent.agent_id,
92
+ execution_id: execution.execution_id, state: :executing)
93
+ case action
94
+ when :framework_tools
95
+ event_loop.register_agent_completion_waiter(execution.execution_id, completion)
96
+ main.send(:start_framework_tools_on_event_loop, execution.execution_id, completion)
97
+ when :framework_calls, :output
98
+ # Re-enter the ordinary FSM before output filtering so its failures
99
+ # settle through the same durable terminal barrier as a live call.
100
+ start_recovery_session(event_loop, main, execution, invocation, completion,
101
+ resume_event: :llm_completed, resume_phase: :calling_llm)
102
+ when :followup
103
+ start_recovery_session(event_loop, main, execution, invocation, completion,
104
+ resume_event: :state_completed, resume_phase: :recording_tool_results)
105
+ end
155
106
  end
156
107
 
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
- )
108
+ def prepare_saved_provider_calls(execution, invocation, message)
109
+ record = RecoverySupport.latest_assistant_record(execution)
110
+ calls = message.tool_calls.respond_to?(:values) ? message.tool_calls.values : Array(message.tool_calls)
111
+ framework_calls = calls.select { |call| agent.__framework_call?(call.name) }
112
+ if framework_calls.empty?
113
+ raise Phronomy::ExecutionRehydrationRequiredError, "Saved framework call wiring is missing"
192
114
  end
193
- event_loop.register(session, completion: source)
115
+ invocation.accept_tool_calls!(framework_calls, llm_call_id: record.llm_call_id)
194
116
  end
195
117
 
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
- )
118
+ def build_failed_recovery_invocation(execution, main)
119
+ Phronomy::Agent::AgentInvocation.new(agent: agent, input: nil,
120
+ config: {execution_id: execution.execution_id, phronomy_execution_coordinator: main},
121
+ event_listener: agent.send(:_phronomy_event_listener),
122
+ mode: (execution.metadata[RecoverySupport::INVOCATION_MODE_KEY] || "invoke").to_sym,
123
+ execution_id: execution.execution_id)
124
+ end
125
+
126
+ def start_recovery_session(event_loop, main, execution, invocation, completion, resume_event:, resume_phase:)
127
+ session = AgentInvocationSessionBuilder.build_for_resume(agent_invocation: invocation,
128
+ resume_event: resume_event, resume_phase: resume_phase, runtime: @runtime)
129
+ event_loop.replace_agent_execution(execution.execution_id, invocation: invocation, fsm_session_id: session.id)
130
+ event_loop.register_agent_completion_waiter(execution.execution_id, completion)
131
+ source = Phronomy::Task.deferred(name: "#{completion.name}-source")
222
132
  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
- )
133
+ main.send(:finish_on_event_loop, execution.execution_id, completion,
134
+ completed || session.context, error, fsm_session_id: session.id)
231
135
  end
232
136
  event_loop.register(session, completion: source)
233
137
  end
@@ -264,7 +168,6 @@ module Phronomy
264
168
  end
265
169
  nil
266
170
  end
267
- # simplecov:enable
268
171
  end
269
172
  end
270
173
  end
@@ -8,6 +8,35 @@ module Phronomy
8
8
  module Installation
9
9
  private
10
10
 
11
+ def framework_batch?(execution)
12
+ return false unless %i[dispatching_tools resuming recovery_tools].include?(execution.phase.to_sym)
13
+ return false if execution.status == :suspended
14
+ entries = Array(execution.metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY])
15
+ pending = entries.reject { |entry| %w[completed rejected failed cancelled].include?(entry.fetch("status")) }
16
+ !pending.empty? && pending.all? do |entry|
17
+ entry.fetch("status") == "authorized" && agent.__framework_tool_replayable?(entry.fetch("tool_name"))
18
+ end
19
+ end
20
+
21
+ # Framework operations never become Application factual questions. In a
22
+ # mixed batch, external facts are resolved first; authorized framework
23
+ # slots then resume through their normal Tool sessions.
24
+ def coordination_recovery_descriptor(execution)
25
+ metadata = execution.metadata.dup
26
+ if metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY]
27
+ metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY] = metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY].sort_by do |entry|
28
+ agent.__framework_tool_replayable?(entry.fetch("tool_name")) ? 1 : 0
29
+ end
30
+ end
31
+ if (recovery = metadata[RecoverySupport::RECOVERY_METADATA_KEY]) && recovery["subjects"]
32
+ metadata[RecoverySupport::RECOVERY_METADATA_KEY] = recovery.merge("subjects" => recovery["subjects"].sort_by do |entry|
33
+ agent.__framework_tool_replayable?(entry.fetch("tool_name")) ? 1 : 0
34
+ end)
35
+ end
36
+ RecoverySupport.recovery_descriptor(execution.with(execution_revision: execution.execution_revision,
37
+ updated_at: execution.updated_at, metadata: metadata))
38
+ end
39
+
11
40
  def prepare_plan(execution)
12
41
  root = agent.agent_root
13
42
  manifest_ref = execution.metadata["manifest_ref"]
@@ -34,11 +63,28 @@ module Phronomy
34
63
  manifest: manifest,
35
64
  base_manifest: base_manifest,
36
65
  projection: projection,
37
- classification: classification
66
+ classification: classification,
67
+ material: prepare_recovery_material(execution, projection: projection)
38
68
  )
39
69
  end
40
70
 
41
71
  def classify(execution)
72
+ if framework_batch?(execution)
73
+ return Phronomy::Recovery::Classification.new(disposition: Phronomy::Recovery::RESUMABLE, reason: :framework_tools)
74
+ end
75
+ if execution.status == :preparing &&
76
+ execution.phase.to_sym == :preparing
77
+ unless execution.metadata["preparation_replayable"] == true
78
+ raise Phronomy::ExecutionRehydrationRequiredError,
79
+ "execution #{execution.execution_id} is :preparing but replay safety " \
80
+ "was not durably established"
81
+ end
82
+ return Phronomy::Recovery::Classification.new(
83
+ disposition: Phronomy::Recovery::RESUMABLE,
84
+ reason: :initial_preparation
85
+ )
86
+ end
87
+
42
88
  if execution.status == :suspended &&
43
89
  execution.phase.to_sym == :approval
44
90
  return Phronomy::Recovery::Classification.new(
@@ -77,7 +123,7 @@ module Phronomy
77
123
  end
78
124
  end
79
125
 
80
- descriptor = RecoverySupport.recovery_descriptor(
126
+ descriptor = coordination_recovery_descriptor(
81
127
  execution
82
128
  )
83
129
  if descriptor
@@ -97,10 +143,10 @@ module Phronomy
97
143
  end
98
144
 
99
145
  def install_on_event_loop(command)
100
- event_loop = Phronomy::Runtime.instance.event_loop
146
+ event_loop = @runtime.event_loop
101
147
  plan = command.plan
102
148
  execution = plan.execution
103
- main = agent.send(:execution_coordinator)
149
+ main = agent.send(:execution_coordinator_for, agent.__coordination_config)
104
150
  admitted = false
105
151
  bound = false
106
152
  installed = false
@@ -118,60 +164,10 @@ module Phronomy
118
164
  bound = true
119
165
 
120
166
  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
167
+ if execution.status == :suspended || execution.phase.to_sym == :resuming
168
+ invocation = RecoverySupport.build_invocation_for_suspended(
169
+ agent, execution, plan.projection, main, agent.send(:_phronomy_event_listener),
170
+ assistant_message: plan.material.assistant_message
175
171
  )
176
172
  end
177
173
 
@@ -254,8 +250,7 @@ module Phronomy
254
250
  when Phronomy::Recovery::RESUMABLE
255
251
  continue_resumable_on_event_loop(
256
252
  execution,
257
- invocation,
258
- completion: command.completion
253
+ completion: command.completion, material: plan.material
259
254
  )
260
255
  else
261
256
  raise Phronomy::Error,
@@ -313,14 +308,36 @@ module Phronomy
313
308
 
314
309
  def continue_resumable_on_event_loop(
315
310
  execution,
316
- invocation,
317
- completion:
311
+ completion:, material:
318
312
  )
319
313
  # simplecov:disable
320
- event_loop = Phronomy::Runtime.instance.event_loop
321
- main = agent.send(:execution_coordinator)
314
+ event_loop = @runtime.event_loop
315
+ main = agent.send(:execution_coordinator_for, agent.__coordination_config)
316
+
317
+ if framework_batch?(execution)
318
+ internal = Phronomy::Task.deferred(name: "framework-tool-recovery:#{execution.execution_id}")
319
+ continue_recovery_on_event_loop(execution, internal, material: material)
320
+ completion.complete(agent)
321
+ return
322
+ end
322
323
 
323
324
  case execution.phase.to_sym
325
+ when :preparing
326
+ internal_task = Phronomy::Task.deferred(
327
+ name: "agent-recovery-auto:#{execution.execution_id}"
328
+ )
329
+ observe_recovery_execution(internal_task, execution)
330
+ event_loop.mark_agent_execution_admission(
331
+ agent.agent_id,
332
+ execution_id: execution.execution_id,
333
+ state: :executing
334
+ )
335
+ main.send(
336
+ :start_initial_preparation_recovery_on_event_loop,
337
+ execution,
338
+ internal_task,
339
+ load_completion: completion
340
+ )
324
341
  when :resuming
325
342
  approved = execution.approval_request &&
326
343
  (
@@ -348,72 +365,9 @@ module Phronomy
348
365
  config: {}
349
366
  )
350
367
  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
- )
368
+ when :recovery_tools_completed, :recovery_provider_completed, :recovery_resolved_failed
369
+ internal_task = Phronomy::Task.deferred(name: "agent-recovery-auto:#{execution.execution_id}")
370
+ continue_recovery_on_event_loop(execution, internal_task, material: material)
417
371
  completion.complete(agent)
418
372
  else
419
373
  raise Phronomy::ExecutionRehydrationRequiredError,