phronomy 0.26.0 → 0.28.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 (557) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +9 -9
  3. data/CHANGELOG.md +92 -0
  4. data/CONTRIBUTING.md +8 -8
  5. data/README.md +13 -3
  6. data/VERIFY.sh +27 -27
  7. data/benchmark/bench_agent_invoke.rb +26 -47
  8. data/benchmark/bench_context_assembler.rb +17 -15
  9. data/benchmark/bench_regression.rb +1 -1
  10. data/benchmark/bench_tool_schema.rb +1 -1
  11. data/docs/architecture/agent-chat-and-state-ownership.md +153 -0
  12. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  13. data/docs/architecture/agent-context.md +4 -2
  14. data/docs/architecture/agent-transition-ownership.md +13 -0
  15. data/docs/architecture/backend-groups.md +152 -0
  16. data/docs/architecture/before-llm-input.md +9 -3
  17. data/docs/architecture/context-management.md +31 -1
  18. data/docs/architecture/context-preparation-steps.md +90 -0
  19. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  20. data/docs/architecture/execution-abstraction-review.md +88 -0
  21. data/docs/architecture/execution-boundaries.md +28 -0
  22. data/docs/architecture/execution-metadata-and-values.md +86 -0
  23. data/docs/architecture/generator-verifier-ownership.md +111 -0
  24. data/docs/architecture/multi-agent-handoff.md +11 -4
  25. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  26. data/docs/architecture/persistence.md +45 -4
  27. data/docs/architecture/r8-unit1.md +104 -0
  28. data/docs/architecture/r8-unit10.md +72 -0
  29. data/docs/architecture/r8-unit11.md +71 -0
  30. data/docs/architecture/r8-unit12.md +67 -0
  31. data/docs/architecture/r8-unit13.md +61 -0
  32. data/docs/architecture/r8-unit14.md +72 -0
  33. data/docs/architecture/r8-unit15.md +65 -0
  34. data/docs/architecture/r8-unit16.md +54 -0
  35. data/docs/architecture/r8-unit17.md +73 -0
  36. data/docs/architecture/r8-unit18.md +92 -0
  37. data/docs/architecture/r8-unit19.md +61 -0
  38. data/docs/architecture/r8-unit2.md +55 -0
  39. data/docs/architecture/r8-unit20.md +51 -0
  40. data/docs/architecture/r8-unit21.md +79 -0
  41. data/docs/architecture/r8-unit22.md +74 -0
  42. data/docs/architecture/r8-unit23.md +81 -0
  43. data/docs/architecture/r8-unit3.md +130 -0
  44. data/docs/architecture/r8-unit4.md +79 -0
  45. data/docs/architecture/r8-unit5.md +115 -0
  46. data/docs/architecture/r8-unit6.md +71 -0
  47. data/docs/architecture/r8-unit7.md +70 -0
  48. data/docs/architecture/r8-unit8.md +78 -0
  49. data/docs/architecture/r8-unit9.md +73 -0
  50. data/docs/architecture/refactoring-closure.md +343 -0
  51. data/docs/architecture/remaining-refactoring-plan.md +390 -0
  52. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  53. data/docs/architecture/storage-execution-boundaries.md +54 -0
  54. data/docs/architecture/tool-schema-recording-gap.md +56 -0
  55. data/docs/architecture/tracing.md +2 -2
  56. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  57. data/docs/architecture.md +222 -0
  58. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  59. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  60. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  61. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  62. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  63. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  64. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +7 -0
  65. data/docs/decisions/031-durable-multi-agent-coordination.md +7 -0
  66. data/docs/decisions/032-storage-backend-composition.md +85 -0
  67. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  68. data/docs/decisions/034-handoff-runner-coordination-ownership.md +78 -0
  69. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  70. data/docs/decisions/036-context-contract-ownership.md +86 -0
  71. data/docs/decisions/037-common-definition-ownership.md +62 -0
  72. data/docs/decisions/038-responsibility-based-source-layout.md +145 -0
  73. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +82 -0
  74. data/docs/decisions/040-configuration-default-composition.md +105 -0
  75. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  76. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  77. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  78. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  79. data/docs/decisions/045-worker-input-restriction-ownership.md +104 -0
  80. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  81. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  82. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  83. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  84. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  85. data/docs/decisions/051-execution-outcome-worker-ownership.md +153 -0
  86. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  87. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  88. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  89. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  90. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  91. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  92. data/docs/decisions/058-neutral-storage-primitives.md +83 -0
  93. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  94. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  95. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  96. data/docs/decisions/062-content-store-service.md +52 -0
  97. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  98. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  99. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  100. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  101. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  102. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  103. data/docs/decisions/README.md +52 -6
  104. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  105. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  106. data/docs/execution-boundaries-preview.md +45 -0
  107. data/docs/features.md +16 -5
  108. data/docs/getting-started.md +11 -8
  109. data/docs/migrations/0.15.md +5 -0
  110. data/docs/migrations/0.22.md +2 -2
  111. data/docs/migrations/durable-semantic-coordination-v2.md +8 -0
  112. data/docs/migrations/handoff-runner-multi-agent.md +37 -0
  113. data/docs/migrations/neutral-storage-spi.md +43 -0
  114. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  115. data/docs/migrations/persistence-failure-contracts.md +67 -0
  116. data/docs/migrations/r8-unit10.md +36 -0
  117. data/docs/migrations/r8-unit11.md +33 -0
  118. data/docs/migrations/r8-unit12.md +31 -0
  119. data/docs/migrations/r8-unit13.md +36 -0
  120. data/docs/migrations/r8-unit14.md +31 -0
  121. data/docs/migrations/r8-unit15.md +31 -0
  122. data/docs/migrations/r8-unit16.md +26 -0
  123. data/docs/migrations/r8-unit17.md +28 -0
  124. data/docs/migrations/r8-unit18.md +29 -0
  125. data/docs/migrations/r8-unit19.md +28 -0
  126. data/docs/migrations/r8-unit20.md +24 -0
  127. data/docs/migrations/r8-unit21.md +28 -0
  128. data/docs/migrations/r8-unit22.md +29 -0
  129. data/docs/migrations/r8-unit23.md +20 -0
  130. data/docs/migrations/r8-unit4.md +67 -0
  131. data/docs/migrations/r8-unit5.md +75 -0
  132. data/docs/migrations/r8-unit6.md +109 -0
  133. data/docs/migrations/r8-unit7.md +31 -0
  134. data/docs/migrations/r8-unit8.md +33 -0
  135. data/docs/migrations/r8-unit9.md +28 -0
  136. data/docs/migrations/shared-state-multi-agent.md +44 -0
  137. data/docs/migrations/storage-async-client.md +103 -0
  138. data/docs/migrations/storage-backend-composition.md +117 -0
  139. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  140. data/docs/migrations/vector-async-clients.md +110 -0
  141. data/docs/persistence-backends.md +190 -215
  142. data/docs/runtime-and-concurrency.md +142 -32
  143. data/lib/phronomy/agent/admission.rb +78 -0
  144. data/lib/phronomy/{agent_already_exists_error.rb → agent/agent_already_exists_error.rb} +2 -0
  145. data/lib/phronomy/{agent_busy_error.rb → agent/agent_busy_error.rb} +2 -0
  146. data/lib/phronomy/{agent_purged_error.rb → agent/agent_purged_error.rb} +2 -0
  147. data/lib/phronomy/agent/api/agent.rb +17 -0
  148. data/lib/phronomy/{execution_rehydration_required_error.rb → agent/api/execution_rehydration_required_error.rb} +2 -0
  149. data/lib/phronomy/agent/async_event_api.rb +38 -33
  150. data/lib/phronomy/agent/base.rb +106 -294
  151. data/lib/phronomy/agent/before_llm_input.rb +64 -0
  152. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  153. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  154. data/lib/phronomy/agent/context_importer.rb +2 -2
  155. data/lib/phronomy/agent/{context_assembler.rb → context_preparation.rb} +157 -288
  156. data/lib/phronomy/agent/control_request.rb +21 -0
  157. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  158. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +32 -63
  159. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  160. data/lib/phronomy/agent/execution/dispatch_preparation.rb +195 -0
  161. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +43 -23
  162. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +8 -5
  163. data/lib/phronomy/agent/execution/execution_coordinator.rb +1744 -0
  164. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  165. data/lib/phronomy/agent/execution/execution_metadata.rb +54 -0
  166. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +288 -0
  167. data/lib/phronomy/agent/execution/execution_registry.rb +463 -0
  168. data/lib/phronomy/agent/execution/execution_session_runner.rb +113 -0
  169. data/lib/phronomy/agent/execution/initial_preparation.rb +323 -0
  170. data/lib/phronomy/agent/execution/invocation_actions.rb +368 -0
  171. data/lib/phronomy/agent/execution/invocation_transitions.rb +124 -0
  172. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  173. data/lib/phronomy/agent/execution_change.rb +161 -0
  174. data/lib/phronomy/agent/execution_environment.rb +36 -0
  175. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  176. data/lib/phronomy/agent/execution_observation.rb +35 -0
  177. data/lib/phronomy/agent/filterable.rb +182 -0
  178. data/lib/phronomy/agent/{handoff_context.rb → handoff/transfer_context.rb} +6 -6
  179. data/lib/phronomy/agent/{handoff_projection.rb → handoff/transfer_projection.rb} +14 -33
  180. data/lib/phronomy/agent/handoff_error.rb +7 -0
  181. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  182. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  183. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  184. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  185. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  186. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  187. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -15
  188. data/lib/phronomy/agent/persistence/agent_repository.rb +69 -0
  189. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  190. data/lib/phronomy/agent/persistence/codec.rb +338 -0
  191. data/lib/phronomy/agent/persistence/execution_repository.rb +124 -0
  192. data/lib/phronomy/agent/persistence/journal_repository.rb +63 -0
  193. data/lib/phronomy/agent/persistence/records.rb +33 -0
  194. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  195. data/lib/phronomy/agent/persistence/storage_schema.rb +29 -0
  196. data/lib/phronomy/agent/persistence/watermark.rb +29 -0
  197. data/lib/phronomy/agent/recovery/invocation_restorer.rb +124 -0
  198. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +28 -51
  199. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +50 -55
  200. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +38 -43
  201. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +26 -23
  202. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  203. data/lib/phronomy/agent/recovery/recovery_support.rb +224 -0
  204. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  205. data/lib/phronomy/agent/retention.rb +17 -0
  206. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  207. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  208. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  209. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  210. data/lib/phronomy/agent/{ruby_llm_materializer.rb → runtime_input.rb} +19 -39
  211. data/lib/phronomy/agent/saved_context_reader.rb +55 -0
  212. data/lib/phronomy/agent/settings.rb +47 -0
  213. data/lib/phronomy/agent/state_writer.rb +166 -0
  214. data/lib/phronomy/agent/store.rb +253 -0
  215. data/lib/phronomy/{stream_callback_error.rb → agent/stream_callback_error.rb} +2 -0
  216. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  217. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  218. data/lib/phronomy/agent/tool_execution/tool_binding.rb +30 -0
  219. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +114 -248
  220. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  221. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  222. data/lib/phronomy/common/configuration_error.rb +7 -0
  223. data/lib/phronomy/common/error.rb +5 -0
  224. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  225. data/lib/phronomy/common/values/serializable.rb +32 -0
  226. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  227. data/lib/phronomy/content_store/storage_schema.rb +12 -0
  228. data/lib/phronomy/content_store/stored_contents.rb +44 -0
  229. data/lib/phronomy/context/assembly.rb +202 -0
  230. data/lib/phronomy/{context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +5 -1
  231. data/lib/phronomy/context/candidate.rb +51 -0
  232. data/lib/phronomy/context/constraint.rb +47 -0
  233. data/lib/phronomy/{agent → context}/context_plan.rb +2 -2
  234. data/lib/phronomy/{agent → context}/context_plan_validator.rb +3 -3
  235. data/lib/phronomy/{agent → context}/context_policy.rb +1 -1
  236. data/lib/phronomy/{agent → context}/context_policy_input.rb +7 -7
  237. data/lib/phronomy/{agent → context}/context_policy_input_builder.rb +1 -1
  238. data/lib/phronomy/context/default_policy.rb +140 -0
  239. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  240. data/lib/phronomy/{agent → context}/llm_input_build_context.rb +2 -2
  241. data/lib/phronomy/{agent → context}/llm_input_manifest.rb +21 -21
  242. data/lib/phronomy/{agent → context}/llm_input_patch.rb +3 -3
  243. data/lib/phronomy/context/manifest_error.rb +10 -0
  244. data/lib/phronomy/context/prompt_template.rb +98 -0
  245. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  246. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  247. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  248. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  249. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  250. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  251. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  252. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  253. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
  254. data/lib/phronomy/embeddings/base.rb +43 -0
  255. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  256. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  257. data/lib/phronomy/engine/concurrency/offload_pool.rb +39 -20
  258. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  259. data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +1 -1
  260. data/lib/phronomy/engine/event_loop.rb +164 -630
  261. data/lib/phronomy/engine/execution_binding.rb +23 -0
  262. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  263. data/lib/phronomy/engine/fsm_protocol.rb +9 -0
  264. data/lib/phronomy/engine/fsm_session.rb +37 -30
  265. data/lib/phronomy/engine/metrics.rb +28 -0
  266. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  267. data/lib/phronomy/engine/runtime.rb +104 -95
  268. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  269. data/lib/phronomy/execution/async_operation.rb +41 -0
  270. data/lib/phronomy/execution/backpressure_error.rb +7 -0
  271. data/lib/phronomy/{blocking.rb → execution/blocking.rb} +2 -2
  272. data/lib/phronomy/execution/cancellation_error.rb +7 -0
  273. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +5 -1
  274. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  275. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  276. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  277. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  278. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  279. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  280. data/lib/phronomy/execution/concurrency/worker_input_restricted.rb +14 -0
  281. data/lib/phronomy/execution/event_loop_reentrancy_error.rb +8 -0
  282. data/lib/phronomy/{execution.rb → execution/execution.rb} +57 -30
  283. data/lib/phronomy/{execution_cancellation_error.rb → execution/execution_cancellation_error.rb} +2 -0
  284. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  285. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  286. data/lib/phronomy/{execution_timeout_error.rb → execution/execution_timeout_error.rb} +2 -0
  287. data/lib/phronomy/{invalid_async_entry_action_error.rb → execution/invalid_async_entry_action_error.rb} +2 -0
  288. data/lib/phronomy/{invalid_async_transition_action_error.rb → execution/invalid_async_transition_action_error.rb} +2 -0
  289. data/lib/phronomy/{invalid_async_workflow_action_error.rb → execution/invalid_async_workflow_action_error.rb} +2 -0
  290. data/lib/phronomy/{invocation_context.rb → execution/invocation_context.rb} +0 -15
  291. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  292. data/lib/phronomy/execution/pool_shutdown_error.rb +7 -0
  293. data/lib/phronomy/execution/recursion_limit_error.rb +7 -0
  294. data/lib/phronomy/execution/runnable.rb +24 -0
  295. data/lib/phronomy/execution/runtime_shutdown_error.rb +7 -0
  296. data/lib/phronomy/execution/runtime_shutdown_reentrancy_error.rb +7 -0
  297. data/lib/phronomy/execution/scheduler_reentrancy_error.rb +9 -0
  298. data/lib/phronomy/{engine → execution}/task_result.rb +7 -3
  299. data/lib/phronomy/execution/timeout_error.rb +7 -0
  300. data/lib/phronomy/execution/wait_policy.rb +24 -0
  301. data/lib/phronomy/filter/filter_block_error.rb +14 -0
  302. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  303. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  304. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  305. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  306. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  307. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  308. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  309. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  310. data/lib/phronomy/llm_adapter/authentication_error.rb +9 -0
  311. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  312. data/lib/phronomy/llm_adapter/base.rb +49 -68
  313. data/lib/phronomy/llm_adapter/context_length_error.rb +9 -0
  314. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  315. data/lib/phronomy/llm_adapter/message.rb +62 -0
  316. data/lib/phronomy/llm_adapter/rate_limit_error.rb +9 -0
  317. data/lib/phronomy/llm_adapter/request.rb +79 -0
  318. data/lib/phronomy/llm_adapter/response.rb +36 -0
  319. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  320. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  321. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  322. data/lib/phronomy/llm_adapter.rb +3 -16
  323. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  324. data/lib/phronomy/multi_agent/admission_registry.rb +15 -2
  325. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  326. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  327. data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
  328. data/lib/phronomy/{agent → multi_agent}/handoff_capability_factory.rb +21 -2
  329. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  330. data/lib/phronomy/{agent → multi_agent}/handoff_policy.rb +1 -1
  331. data/lib/phronomy/multi_agent/handoff_runner.rb +156 -0
  332. data/lib/phronomy/{agent → multi_agent}/handoff_state.rb +2 -2
  333. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  334. data/lib/phronomy/multi_agent/persistence/codec.rb +75 -0
  335. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  336. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  337. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  338. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +124 -0
  339. data/lib/phronomy/multi_agent/persistence/team_repository.rb +69 -0
  340. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  341. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  342. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  343. data/lib/phronomy/multi_agent/store.rb +156 -0
  344. data/lib/phronomy/multi_agent/team_coordinator.rb +87 -75
  345. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  346. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +5 -5
  347. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  348. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  349. data/lib/phronomy/persistence/content_repository.rb +26 -0
  350. data/lib/phronomy/persistence/errors.rb +16 -0
  351. data/lib/phronomy/persistence/migration/initial_format_migration.rb +21 -21
  352. data/lib/phronomy/persistence/persistence.rb +23 -0
  353. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  354. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  355. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  356. data/lib/phronomy/persistence/transaction.rb +86 -0
  357. data/lib/phronomy/persistence_composition/storage_schema.rb +29 -0
  358. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  359. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  360. data/lib/phronomy/runtime_composition/agent_defaults.rb +11 -0
  361. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  362. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  363. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  364. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  365. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  366. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  367. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  368. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  369. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  370. data/lib/phronomy/storage/async/async_client.rb +60 -0
  371. data/lib/phronomy/storage/backend.rb +107 -0
  372. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  373. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  374. data/lib/phronomy/storage/blobs.rb +31 -0
  375. data/lib/phronomy/storage/condition.rb +25 -0
  376. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  377. data/lib/phronomy/storage/conflict_error.rb +9 -0
  378. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  379. data/lib/phronomy/storage/entry.rb +33 -0
  380. data/lib/phronomy/storage/guard_ref.rb +13 -0
  381. data/lib/phronomy/storage/not_found_error.rb +9 -0
  382. data/lib/phronomy/storage/record_codec.rb +177 -0
  383. data/lib/phronomy/storage/records.rb +61 -0
  384. data/lib/phronomy/storage/resource.rb +126 -0
  385. data/lib/phronomy/storage/scope.rb +25 -0
  386. data/lib/phronomy/storage/serialization_error.rb +9 -0
  387. data/lib/phronomy/storage/streams.rb +44 -0
  388. data/lib/phronomy/storage/transaction_error.rb +10 -0
  389. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  390. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  391. data/lib/phronomy/storage/validation.rb +53 -0
  392. data/lib/phronomy/storage/view.rb +131 -0
  393. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  394. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  395. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  396. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  397. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +54 -48
  398. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +1 -1
  399. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +1 -1
  400. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  401. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +52 -46
  402. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  403. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +124 -0
  404. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  405. data/lib/phronomy/tool/authorization.rb +77 -0
  406. data/lib/phronomy/tool/base.rb +549 -9
  407. data/lib/phronomy/tool/call_request.rb +40 -0
  408. data/lib/phronomy/{agent/tool_definition_set.rb → tool/definition_set.rb} +16 -12
  409. data/lib/phronomy/tool/operation.rb +88 -0
  410. data/lib/phronomy/tool/schema.rb +214 -0
  411. data/lib/phronomy/tool/settings.rb +31 -0
  412. data/lib/phronomy/tool/tool_error.rb +7 -0
  413. data/lib/phronomy/{agent → tool}/tool_executor.rb +10 -7
  414. data/lib/phronomy/tools/agent.rb +22 -90
  415. data/lib/phronomy/tools/mcp.rb +21 -14
  416. data/lib/phronomy/tools/vector_search.rb +4 -4
  417. data/lib/phronomy/tracing/automatic.rb +11 -6
  418. data/lib/phronomy/tracing/base.rb +1 -1
  419. data/lib/phronomy/tracing/observation.rb +37 -0
  420. data/lib/phronomy/tracing/settings.rb +36 -0
  421. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  422. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  423. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  424. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  425. data/lib/phronomy/vector_store/base.rb +118 -49
  426. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  427. data/lib/phronomy/vector_store.rb +2 -17
  428. data/lib/phronomy/version.rb +1 -1
  429. data/lib/phronomy/workflow/completion.rb +11 -0
  430. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  431. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  432. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +5 -1
  433. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  434. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
  435. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +186 -0
  436. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +145 -115
  437. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  438. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  439. data/lib/phronomy/workflow/persistence/state_repository.rb +63 -0
  440. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  441. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
  442. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
  443. data/lib/phronomy/workflow/runtime_binding/workflow_terminal_policy.rb +40 -0
  444. data/lib/phronomy.rb +97 -98
  445. data/scripts/api_snapshot.rb +36 -16
  446. data/scripts/storage_spi_snapshot.rb +36 -0
  447. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  448. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  449. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  450. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  451. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  452. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  453. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  454. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  455. data/sig/phronomy/agent/admission.rbs +19 -0
  456. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  457. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  458. data/sig/phronomy/agent/store.rbs +73 -0
  459. data/sig/phronomy/agent.rbs +14 -139
  460. data/sig/phronomy/common.rbs +7 -0
  461. data/sig/phronomy/content_store/contracts.rbs +13 -0
  462. data/sig/phronomy/context.rbs +152 -0
  463. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  464. data/sig/phronomy/embeddings/base.rbs +17 -0
  465. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  466. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  467. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  468. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  469. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  470. data/sig/phronomy/engine/observability.rbs +13 -0
  471. data/sig/phronomy/execution/async_operation.rbs +7 -0
  472. data/sig/phronomy/execution/blocking.rbs +5 -0
  473. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  474. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  475. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  476. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  477. data/sig/phronomy/execution/errors.rbs +7 -0
  478. data/sig/phronomy/execution/execution.rbs +41 -0
  479. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  480. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  481. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  482. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  483. data/sig/phronomy/execution/task_result.rbs +29 -0
  484. data/sig/phronomy/extensions.rbs +0 -21
  485. data/sig/phronomy/handoff.rbs +12 -4
  486. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  487. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  488. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  489. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  490. data/sig/phronomy/llm_context_window.rbs +10 -0
  491. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  492. data/sig/phronomy/multi_agent/store.rbs +54 -0
  493. data/sig/phronomy/multi_agent.rbs +24 -3
  494. data/sig/phronomy/persistence/errors.rbs +18 -0
  495. data/sig/phronomy/persistence/migration.rbs +13 -0
  496. data/sig/phronomy/persistence/transaction.rbs +19 -0
  497. data/sig/phronomy/persistence.rbs +3 -185
  498. data/sig/phronomy/persistence_composition.rbs +19 -0
  499. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  500. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  501. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  502. data/sig/phronomy/storage/contracts.rbs +160 -0
  503. data/sig/phronomy/tool.rbs +58 -5
  504. data/sig/phronomy/tracing/observation.rbs +7 -0
  505. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  506. data/sig/phronomy/vector_store/base.rbs +23 -0
  507. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
  508. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  509. data/sig/phronomy/workflow/store.rbs +8 -0
  510. data/sig/phronomy/workflow.rbs +1 -1
  511. data/sig/phronomy.rbs +0 -49
  512. metadata +462 -130
  513. data/lib/phronomy/agent/agent_invocation_session_builder.rb +0 -584
  514. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  515. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  516. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  517. data/lib/phronomy/agent/context/capability/base.rb +0 -507
  518. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  519. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  520. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  521. data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
  522. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  523. data/lib/phronomy/agent/handoff_request.rb +0 -45
  524. data/lib/phronomy/agent/handoff_runner.rb +0 -178
  525. data/lib/phronomy/agent/phase_machine_builder.rb +0 -152
  526. data/lib/phronomy/agent/provider_call_outcome.rb +0 -106
  527. data/lib/phronomy/agent/recovery_support.rb +0 -504
  528. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  529. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  530. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  531. data/lib/phronomy/agent/tool_call_intercepted.rb +0 -29
  532. data/lib/phronomy/agent/tool_invocation_session_builder.rb +0 -276
  533. data/lib/phronomy/configuration.rb +0 -59
  534. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  535. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  536. data/lib/phronomy/generator_verifier.rb +0 -369
  537. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  538. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  539. data/lib/phronomy/metrics.rb +0 -26
  540. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  541. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  542. data/lib/phronomy/persistence/in_memory.rb +0 -690
  543. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  544. data/lib/phronomy/persistence.rb +0 -276
  545. data/lib/phronomy/recovery.rb +0 -186
  546. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  547. data/lib/phronomy/runnable.rb +0 -54
  548. data/lib/phronomy/token_usage.rb +0 -85
  549. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  550. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  551. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  552. data/lib/phronomy/workflow_recovery.rb +0 -123
  553. data/sig/phronomy/llm_adapter.rbs +0 -11
  554. data/sig/phronomy/runtime.rbs +0 -76
  555. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  556. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  557. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
@@ -0,0 +1,1744 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../execution/concurrency/worker_input_restricted"
4
+
5
+ require "time"
6
+ require "securerandom"
7
+
8
+ module Phronomy
9
+ module Agent
10
+ class ExecutionCoordinator
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
13
+ # External/API -> EventLoop control messages.
14
+ StartCommand = Data.define(
15
+ :coordinator, :input, :config, :mode,
16
+ :approval_policy, :approval_listener, :on_event, :result_task,
17
+ :admission_token, :controls
18
+ )
19
+ ResumeCommand = Data.define(
20
+ :coordinator, :execution_id, :approval_request_id,
21
+ :approved, :config, :result_task
22
+ )
23
+
24
+ # Recovery -> execution-owner commands. These stay on EventLoop and do
25
+ # not carry persistence readers or arbitrary private method names.
26
+ RecoverPreparationCommand = Data.define(
27
+ :coordinator, :execution_id, :expected_execution_revision,
28
+ :result_task, :load_completion
29
+ )
30
+ ContinueRecoveredCommand = Data.define(
31
+ :coordinator, :execution_id, :expected_execution_revision,
32
+ :continuation, :invocation, :runtime_projection, :result_task, :error
33
+ )
34
+ SessionFinishedCommand = Data.define(
35
+ :coordinator, :execution_id, :result_task, :invocation, :error, :fsm_session_id
36
+ )
37
+
38
+ # EventLoop -> Offload operation-specific immutable snapshots.
39
+ InitialPreparationCommand = InitialPreparation::Command
40
+ ProviderDispatchPreparationCommand = DispatchPreparation::ProviderCommand
41
+ ToolDispatchPreparationCommand = DispatchPreparation::ToolCommand
42
+ ResumeCommitCommand = ApprovalResumeCommit::Command
43
+ TerminalView = ExecutionOutcomeCommitter::TerminalView
44
+ TerminalCommitCommand = ExecutionOutcomeCommitter::Command
45
+ TerminalDelivery = Data.define(
46
+ :result_task, :application_listener, :approval_listener,
47
+ :handoff_request, :handoff_manifest
48
+ )
49
+
50
+ # Offload -> EventLoop operation results. No live Agent/Invocation object is
51
+ # mutated by a worker; these values are validated and applied on EventLoop.
52
+ InitialPreparationResult = InitialPreparation::Result
53
+ ProviderDispatchPreparationResult = DispatchPreparation::ProviderResult
54
+ ToolDispatchPreparationResult = DispatchPreparation::ToolResult
55
+ ResumeCommitResult = ApprovalResumeCommit::Result
56
+ TerminalOutcome = ExecutionOutcomeCommitter::Outcome
57
+
58
+ InitialPreparationReady = Data.define(
59
+ :coordinator, :request, :result, :error
60
+ )
61
+ InitialPreparationRecoveryReady = Data.define(
62
+ :coordinator, :execution_id, :expected_execution_revision,
63
+ :result_task, :load_completion, :result, :error
64
+ )
65
+ ProviderDispatchPreparationReady = Data.define(
66
+ :coordinator, :operation, :result, :error
67
+ )
68
+ ToolDispatchPreparationReady = Data.define(
69
+ :coordinator, :operation, :result, :error
70
+ )
71
+ ResumeCommitReady = Data.define(
72
+ :coordinator, :request, :operation, :result, :error
73
+ )
74
+ TerminalCommitReady = Data.define(
75
+ :coordinator, :operation, :delivery, :outcome, :error
76
+ )
77
+ DeferredTerminalCommand = Data.define(
78
+ :coordinator, :execution_id, :result_task, :invocation,
79
+ :source_error, :fsm_session_id
80
+ )
81
+
82
+ def initialize(agent)
83
+ @agent = agent
84
+ @initial_preparation = InitialPreparation.new(agent: agent, persistence: agent.persistence)
85
+ @dispatch_preparation = DispatchPreparation.new(agent: agent, persistence: agent.persistence)
86
+ @approval_resume_commit = ApprovalResumeCommit.new(agent_id: agent.agent_id, persistence: agent.persistence)
87
+ @outcome_committer = build_outcome_committer
88
+ end
89
+
90
+ def start(
91
+ input, config: {}, mode: :invoke,
92
+ approval_policy: nil, approval_listener: nil, on_event: nil
93
+ )
94
+ @agent.send(:__assert_live_agent!)
95
+ @agent.send(:_reject_removed_generic_identity_keys!, config)
96
+ result_task = Phronomy::TaskResult.deferred(name: "agent-#{@agent.agent_id}-#{mode}")
97
+ controls = Phronomy::InvocationControls.attach(result_task,
98
+ invocation_context: config[:invocation_context],
99
+ cancellation_token: config[:cancellation_token])
100
+ config = config.merge(cancellation_token: controls.token)
101
+ command = StartCommand.new(
102
+ coordinator: self,
103
+ input: input,
104
+ config: @agent.__invocation_config(config).dup.freeze,
105
+ mode: mode.to_sym,
106
+ approval_policy: approval_policy,
107
+ approval_listener: approval_listener,
108
+ on_event: on_event,
109
+ result_task: result_task,
110
+ admission_token: Object.new.freeze,
111
+ controls: controls
112
+ )
113
+ unless post_control(@agent.__execution_environment, command)
114
+ fail_task(result_task, runtime_rejected_error(:start))
115
+ end
116
+ result_task
117
+ rescue => error
118
+ fail_task(result_task, error) if defined?(result_task) && result_task
119
+ result_task
120
+ end
121
+
122
+ def resume(
123
+ execution_id,
124
+ approval_request_id:,
125
+ approved:,
126
+ config: {}
127
+ )
128
+ @agent.send(:__assert_live_agent!)
129
+ @agent.send(:_reject_removed_generic_identity_keys!, config)
130
+ result_task = Phronomy::TaskResult.deferred(
131
+ name: "agent-approval-resume:#{execution_id}"
132
+ )
133
+ command = ResumeCommand.new(
134
+ coordinator: self,
135
+ execution_id: execution_id.to_s.freeze,
136
+ approval_request_id: approval_request_id.to_s.freeze,
137
+ approved: !!approved,
138
+ config: config.dup.freeze,
139
+ result_task: result_task
140
+ )
141
+ unless post_control(@agent.__execution_environment, command)
142
+ fail_task(result_task, runtime_rejected_error(:resume))
143
+ end
144
+ result_task
145
+ rescue => error
146
+ fail_task(result_task, error) if defined?(result_task) && result_task
147
+ result_task
148
+ end
149
+
150
+ # Called only from an Agent FSMSession external-operation dispatch state on
151
+ # EventLoop. Each method captures one operation-specific immutable snapshot
152
+ # and sends only that snapshot to OffloadPool.
153
+ # @api private
154
+ def prepare_provider_dispatch(invocation, event_sink:, streaming:)
155
+ state = dispatch_preparation_state!(
156
+ invocation, event_sink, phase: :calling_llm, kind: "Provider"
157
+ )
158
+ operation = capture_provider_preparation(
159
+ state, invocation, fsm_session_id: event_sink.fsm_session_id, streaming: streaming
160
+ )
161
+ submit_provider_dispatch_preparation(operation)
162
+ nil
163
+ rescue => error
164
+ event_sink.post(:llm_setup_failed, error)
165
+ nil
166
+ end
167
+
168
+ # @api private
169
+ def prepare_tool_dispatch(invocation, event_sink:)
170
+ state = dispatch_preparation_state!(
171
+ invocation, event_sink, phase: :dispatching_tools, kind: "Tool"
172
+ )
173
+ operation = capture_tool_preparation(
174
+ state, invocation, fsm_session_id: event_sink.fsm_session_id
175
+ )
176
+ submit_tool_dispatch_preparation(operation)
177
+ nil
178
+ rescue => error
179
+ event_sink.post(:tool_setup_failed, error)
180
+ nil
181
+ end
182
+
183
+ def submit_provider_dispatch_preparation(operation)
184
+ environment = @agent.__execution_environment
185
+ task = environment.submit(on_full: :raise) do
186
+ @dispatch_preparation.prepare_provider(operation)
187
+ end
188
+ task.on_complete do |result, error|
189
+ ready = ProviderDispatchPreparationReady.new(
190
+ coordinator: self,
191
+ operation: operation,
192
+ result: result,
193
+ error: error
194
+ )
195
+ unless post_control(environment, ready)
196
+ Phronomy::RuntimeSettings.current.logger&.warn(
197
+ "[Phronomy] EventLoop rejected Provider dispatch preparation result for " \
198
+ "#{operation.execution_id}"
199
+ )
200
+ end
201
+ end
202
+ end
203
+
204
+ def submit_tool_dispatch_preparation(operation)
205
+ environment = @agent.__execution_environment
206
+ task = environment.submit(on_full: :raise) do
207
+ @dispatch_preparation.prepare_tools(operation)
208
+ end
209
+ task.on_complete do |result, error|
210
+ ready = ToolDispatchPreparationReady.new(
211
+ coordinator: self,
212
+ operation: operation,
213
+ result: result,
214
+ error: error
215
+ )
216
+ unless post_control(environment, ready)
217
+ Phronomy::RuntimeSettings.current.logger&.warn(
218
+ "[Phronomy] EventLoop rejected Tool dispatch preparation result for " \
219
+ "#{operation.execution_id}"
220
+ )
221
+ end
222
+ end
223
+ end
224
+
225
+ # Every control message is delivered by EventLoop. This method is the only
226
+ # coordinator entry point allowed to advance Phronomy-managed live state.
227
+ # @api private
228
+ def deliver_on_event_loop(command)
229
+ environment = @agent.__execution_environment
230
+ assert_event_loop!(environment)
231
+
232
+ case command
233
+ when StartCommand
234
+ begin_start_on_event_loop(command)
235
+ when RecoverPreparationCommand
236
+ recover_preparation_on_event_loop(command)
237
+ when ContinueRecoveredCommand
238
+ continue_recovered_on_event_loop(command)
239
+ when SessionFinishedCommand
240
+ finish_on_event_loop(command.execution_id, command.result_task,
241
+ command.invocation, command.error, fsm_session_id: command.fsm_session_id)
242
+ when InitialPreparationReady
243
+ apply_initial_preparation_on_event_loop(command)
244
+ when InitialPreparationRecoveryReady
245
+ apply_initial_preparation_recovery_on_event_loop(command)
246
+ when ProviderDispatchPreparationReady
247
+ apply_provider_dispatch_preparation_on_event_loop(command)
248
+ when ToolDispatchPreparationReady
249
+ apply_tool_dispatch_preparation_on_event_loop(command)
250
+ when ResumeCommand
251
+ begin_resume_on_event_loop(command)
252
+ when ResumeCommitReady
253
+ apply_resume_commit_on_event_loop(command)
254
+ when TerminalCommitReady
255
+ apply_terminal_commit_on_event_loop(command)
256
+ when DeferredTerminalCommand
257
+ resume_deferred_terminal_on_event_loop(command)
258
+ else
259
+ raise Phronomy::Error, "unknown Agent control command: #{command.class}"
260
+ end
261
+ end
262
+
263
+ private
264
+
265
+ # ----------------------------------------------------------------------
266
+ # Initial preparation
267
+ # ----------------------------------------------------------------------
268
+
269
+ def begin_start_on_event_loop(request)
270
+ environment = @agent.__execution_environment
271
+ request.controls.check_start!
272
+ root = @agent.agent_root
273
+ if root.lifecycle_status == :closed
274
+ deliver_start_failure_on_event_loop(
275
+ request,
276
+ Phronomy::Error.new("agent is closed: #{@agent.agent_id}")
277
+ )
278
+ return
279
+ end
280
+
281
+ @agent.send(:__assert_live_agent!)
282
+ admitted = false
283
+ submitted = false
284
+ environment.registry.admit_agent_execution(
285
+ @agent.agent_id,
286
+ owner_token: request.admission_token
287
+ )
288
+ admitted = true
289
+
290
+ # Ownership purge may begin on an application thread between the first
291
+ # live-owner check and EventLoop admission. Re-check after the slot is
292
+ # installed: once this check succeeds, purge observes the admission and
293
+ # must abort instead of deleting the Agent underneath this start.
294
+ @agent.send(:__assert_live_agent!)
295
+
296
+ operation = capture_initial_preparation(request, root)
297
+ task = environment.submit(on_full: :raise) do
298
+ @initial_preparation.prepare(operation)
299
+ end
300
+ submitted = true
301
+ task.on_complete do |result, error|
302
+ ready = InitialPreparationReady.new(
303
+ coordinator: self,
304
+ request: request,
305
+ result: result,
306
+ error: error
307
+ )
308
+ fail_task(request.result_task, runtime_rejected_error(:initial_preparation)) unless
309
+ post_control(environment, ready)
310
+ end
311
+ rescue => error
312
+ if admitted
313
+ if submitted
314
+ environment.registry.mark_agent_admission_recovery_required(
315
+ @agent.agent_id,
316
+ owner_token: request.admission_token
317
+ )
318
+ else
319
+ environment.registry.release_agent_execution_admission(
320
+ @agent.agent_id,
321
+ owner_token: request.admission_token
322
+ )
323
+ end
324
+ end
325
+ deliver_start_failure_on_event_loop(request, error)
326
+ end
327
+
328
+ def capture_initial_preparation(request, root)
329
+ InitialPreparationCommand.new(
330
+ root: root,
331
+ journal_records: @agent.send(:_journal_records_snapshot),
332
+ input: request.input,
333
+ config: request.config,
334
+ preparation_replayable: initial_preparation_replayable?(
335
+ request.input,
336
+ request.config,
337
+ request.approval_policy
338
+ )
339
+ )
340
+ end
341
+
342
+ def initial_preparation_replayable?(input, config, approval_policy)
343
+ return false unless input.is_a?(String)
344
+ return false unless approval_policy.nil?
345
+ if config.key?(:phronomy_control_bindings) ||
346
+ config.key?(:phronomy_transfer_context)
347
+ return false unless config[:phronomy_execution_participant]
348
+ end
349
+
350
+ invocation_context = config[:invocation_context]
351
+ return true unless invocation_context
352
+
353
+ %i[approval_policy redaction_policy token_budget].none? do |name|
354
+ invocation_context.respond_to?(name) &&
355
+ !invocation_context.public_send(name).nil?
356
+ end
357
+ end
358
+
359
+ def apply_initial_preparation_on_event_loop(ready)
360
+ request = ready.request
361
+ environment = @agent.__execution_environment
362
+ if ready.error
363
+ environment.registry.mark_agent_admission_recovery_required(
364
+ @agent.agent_id,
365
+ owner_token: request.admission_token
366
+ )
367
+ deliver_start_failure_on_event_loop(request, ready.error)
368
+ return
369
+ end
370
+
371
+ result = ready.result
372
+ case result.admission_outcome
373
+ when :not_established
374
+ environment.registry.release_agent_execution_admission(
375
+ @agent.agent_id,
376
+ owner_token: request.admission_token
377
+ )
378
+ deliver_start_failure_on_event_loop(request, result.error)
379
+ return
380
+ when :outcome_unknown, :recovery_required
381
+ environment.registry.mark_agent_admission_recovery_required(
382
+ @agent.agent_id,
383
+ owner_token: request.admission_token
384
+ )
385
+ deliver_start_failure_on_event_loop(request, result.error)
386
+ return
387
+ when :active, :terminal
388
+ environment.registry.bind_agent_execution_admission(
389
+ @agent.agent_id,
390
+ owner_token: request.admission_token,
391
+ execution_id: result.execution.execution_id
392
+ )
393
+ else
394
+ environment.registry.mark_agent_admission_recovery_required(
395
+ @agent.agent_id,
396
+ owner_token: request.admission_token
397
+ )
398
+ raise Phronomy::Error,
399
+ "unknown initial admission outcome: #{result.admission_outcome.inspect}"
400
+ end
401
+
402
+ apply_agent_live_state(
403
+ root: result.root,
404
+ appended_records: result.appended_records
405
+ )
406
+ if result.admission_outcome == :terminal
407
+ environment.registry.release_agent_execution_admission(
408
+ @agent.agent_id,
409
+ execution_id: result.execution.execution_id
410
+ )
411
+ deliver_start_failure_on_event_loop(request, result.error)
412
+ return
413
+ end
414
+
415
+ register_initial_session_on_event_loop(request, result)
416
+ rescue => error
417
+ # Preparation may already be durably active. Terminalize through another
418
+ # offloaded durable operation rather than releasing the admission blindly.
419
+ begin_terminal_without_session_on_event_loop(
420
+ request: request,
421
+ prepared: ready.result,
422
+ error: error
423
+ )
424
+ end
425
+
426
+ def register_initial_session_on_event_loop(request, prepared)
427
+ environment = @agent.__execution_environment
428
+ Phronomy::Tracing::Automatic.observe_task(
429
+ request.result_task,
430
+ "agent.execution",
431
+ input: request.input,
432
+ agent_id: @agent.agent_id,
433
+ execution_id: prepared.execution.execution_id,
434
+ mode: request.mode,
435
+ **@agent.send(:_build_caller_meta, prepared.config)
436
+ )
437
+ effective_config = prepared.config.merge(
438
+ phronomy_execution_coordinator: self,
439
+ phronomy_runtime_projection: prepared.runtime_projection,
440
+ phronomy_filtered_input: prepared.filtered_input,
441
+ execution_id: prepared.execution.execution_id
442
+ )
443
+ invocation = Agent::AgentInvocation.new(
444
+ agent: @agent,
445
+ input: prepared.filtered_input,
446
+ config: effective_config,
447
+ approval_policy: request.approval_policy,
448
+ approval_listener: request.approval_listener,
449
+ mode: request.mode,
450
+ event_listener: request.on_event
451
+ )
452
+ session = environment.build_agent_session(invocation: invocation)
453
+ environment.registry.install_agent_execution(
454
+ execution_id: prepared.execution.execution_id,
455
+ agent: @agent,
456
+ coordinator: self,
457
+ execution: prepared.execution,
458
+ runtime_projection: prepared.runtime_projection,
459
+ base_manifest: prepared.runtime_projection.manifest,
460
+ invocation: session.context,
461
+ fsm_session_id: session.id
462
+ )
463
+ environment.registry.register_agent_completion_waiter(
464
+ prepared.execution.execution_id,
465
+ request.result_task
466
+ )
467
+ execution_session_runner(environment).register(
468
+ session, request.result_task
469
+ )
470
+ # simplecov:disable
471
+ rescue => _error
472
+ environment.registry.release_agent_execution(prepared.execution.execution_id) if
473
+ environment&.executing? && environment.registry.agent_execution_state(prepared.execution.execution_id)
474
+ raise
475
+ end
476
+ # simplecov:enable
477
+
478
+ def deliver_start_failure_on_event_loop(request, error)
479
+ callback_error = @agent.send(
480
+ :_deliver_stream_event,
481
+ request.on_event,
482
+ StreamEvent.new(
483
+ type: terminal_event_type(error),
484
+ payload: {error: error}
485
+ )
486
+ )
487
+ settle_after_terminal(
488
+ request.result_task,
489
+ callback_error,
490
+ terminal_event_type(error),
491
+ nil,
492
+ error
493
+ )
494
+ end
495
+
496
+ # Restarts only the replay-safe initial preparation region of an already
497
+ # durably admitted logical Execution. Runtime-only caller state is not
498
+ # reconstructed here; the durable preparation inputs are authoritative.
499
+ # @api private
500
+ def start_initial_preparation_recovery_on_event_loop(
501
+ execution,
502
+ result_task,
503
+ load_completion:
504
+ )
505
+ environment = @agent.__execution_environment
506
+ assert_event_loop!(environment)
507
+ state = environment.registry.agent_execution_state(execution.execution_id)
508
+ unless state && state.agent.equal?(@agent) &&
509
+ state.execution.execution_revision == execution.execution_revision &&
510
+ state.execution.status == :preparing &&
511
+ state.execution.phase.to_sym == :preparing
512
+ error = Phronomy::ExecutionRehydrationRequiredError.new(
513
+ "stale :preparing Recovery installation for #{execution.execution_id}"
514
+ )
515
+ fail_task(result_task, error)
516
+ load_completion.fail(error)
517
+ return
518
+ end
519
+
520
+ operation = InitialPreparation::RecoveryCommand.new(
521
+ execution: execution,
522
+ root: @agent.agent_root,
523
+ journal_records: @agent.send(:_journal_records_snapshot)
524
+ )
525
+ task = environment.submit(on_full: :raise) do
526
+ @initial_preparation.recover(operation)
527
+ end
528
+ task.on_complete do |result, error|
529
+ ready = InitialPreparationRecoveryReady.new(
530
+ coordinator: self,
531
+ execution_id: execution.execution_id.to_s.freeze,
532
+ expected_execution_revision: execution.execution_revision,
533
+ result_task: result_task,
534
+ load_completion: load_completion,
535
+ result: result,
536
+ error: error
537
+ )
538
+ unless post_control(environment, ready)
539
+ rejected = runtime_rejected_error(:initial_preparation_recovery)
540
+ fail_task(result_task, rejected)
541
+ load_completion.fail(rejected)
542
+ end
543
+ end
544
+ rescue => error
545
+ # simplecov:disable
546
+ begin
547
+ if defined?(environment) && environment.executing?
548
+ release_initial_preparation_recovery_runtime_state(
549
+ environment,
550
+ execution.execution_id
551
+ )
552
+ end
553
+ rescue
554
+ nil
555
+ end
556
+ translated_error = error
557
+ fail_task(result_task, translated_error)
558
+ load_completion.fail(translated_error)
559
+ # simplecov:enable
560
+ end
561
+
562
+ def apply_initial_preparation_recovery_on_event_loop(ready)
563
+ environment = @agent.__execution_environment
564
+ state = environment.registry.agent_execution_state(ready.execution_id)
565
+ unless state && state.agent.equal?(@agent) &&
566
+ state.execution.execution_revision == ready.expected_execution_revision &&
567
+ state.execution.status == :preparing &&
568
+ state.execution.phase.to_sym == :preparing
569
+ error = Phronomy::ExecutionRehydrationRequiredError.new(
570
+ "stale initial preparation Recovery result for #{ready.execution_id}"
571
+ )
572
+ fail_task(ready.result_task, error)
573
+ ready.load_completion.fail(error)
574
+ return
575
+ end
576
+
577
+ if ready.error
578
+ release_initial_preparation_recovery_runtime_state(
579
+ environment,
580
+ ready.execution_id
581
+ )
582
+ error = ready.error
583
+ fail_task(ready.result_task, error)
584
+ ready.load_completion.fail(error)
585
+ return
586
+ end
587
+
588
+ result = ready.result
589
+ case result.admission_outcome
590
+ when :terminal
591
+ settle_failed_preparation_recovery_on_event_loop(ready, environment)
592
+ ready.load_completion.complete(@agent)
593
+ when :active
594
+ restart_prepared_execution_on_event_loop(ready, environment)
595
+ ready.load_completion.complete(@agent)
596
+ else
597
+ error = Phronomy::ExecutionRehydrationRequiredError.new(
598
+ "unexpected initial preparation Recovery outcome: #{result.admission_outcome.inspect}"
599
+ )
600
+ release_initial_preparation_recovery_runtime_state(
601
+ environment,
602
+ ready.execution_id
603
+ )
604
+ fail_task(ready.result_task, error)
605
+ ready.load_completion.fail(error)
606
+ end
607
+ rescue => error
608
+ # simplecov:disable
609
+ begin
610
+ release_initial_preparation_recovery_runtime_state(
611
+ environment,
612
+ ready.execution_id
613
+ )
614
+ rescue
615
+ nil
616
+ end
617
+ translated_error = error
618
+ fail_task(ready.result_task, translated_error)
619
+ ready.load_completion.fail(translated_error)
620
+ # simplecov:enable
621
+ end
622
+
623
+ def settle_failed_preparation_recovery_on_event_loop(ready, environment)
624
+ result = ready.result
625
+ apply_agent_live_state(
626
+ root: result.root,
627
+ appended_records: result.appended_records
628
+ )
629
+ environment.registry.replace_agent_execution(
630
+ ready.execution_id,
631
+ execution: result.execution
632
+ )
633
+ environment.registry.release_agent_execution(ready.execution_id)
634
+ environment.registry.release_agent_execution_admission(
635
+ @agent.agent_id,
636
+ execution_id: ready.execution_id
637
+ )
638
+ event_type = terminal_event_type(result.error)
639
+ callback_error = deliver_terminal(
640
+ @agent.send(:_phronomy_event_listener),
641
+ event_type,
642
+ error: result.error
643
+ )
644
+ settle_after_terminal(
645
+ ready.result_task,
646
+ callback_error,
647
+ event_type,
648
+ nil,
649
+ result.error
650
+ )
651
+ end
652
+
653
+ def restart_prepared_execution_on_event_loop(ready, environment)
654
+ result = ready.result
655
+ apply_agent_live_state(
656
+ root: result.root,
657
+ appended_records: result.appended_records
658
+ )
659
+ environment.registry.release_agent_execution(ready.execution_id)
660
+ mode = (
661
+ result.execution.metadata[ExecutionMetadata::INVOCATION_MODE_KEY] ||
662
+ "invoke"
663
+ ).to_sym
664
+ request = StartCommand.new(
665
+ coordinator: self,
666
+ input: result.filtered_input,
667
+ config: result.config,
668
+ mode: mode,
669
+ approval_policy: nil,
670
+ approval_listener: nil,
671
+ on_event: @agent.send(:_phronomy_event_listener),
672
+ result_task: ready.result_task,
673
+ admission_token: Object.new.freeze,
674
+ controls: nil
675
+ )
676
+ begin
677
+ register_initial_session_on_event_loop(request, result)
678
+ rescue => error
679
+ begin_terminal_without_session_on_event_loop(
680
+ request: request,
681
+ prepared: result,
682
+ error: error
683
+ )
684
+ end
685
+ end
686
+
687
+ def release_initial_preparation_recovery_runtime_state(
688
+ environment,
689
+ execution_id
690
+ )
691
+ state = environment.registry.agent_execution_state(execution_id)
692
+ environment.registry.release_agent_execution(execution_id) if state
693
+ environment.registry.release_agent_execution_admission(
694
+ @agent.agent_id,
695
+ execution_id: execution_id
696
+ )
697
+ end
698
+
699
+ # ----------------------------------------------------------------------
700
+ # External-operation causal durable barriers
701
+ # ----------------------------------------------------------------------
702
+
703
+ def dispatch_preparation_state!(invocation, event_sink, phase:, kind:)
704
+ environment = @agent.__execution_environment
705
+ assert_event_loop!(environment)
706
+ state = environment.registry.agent_execution_state(invocation.execution_id)
707
+ validate_live_session!(state, invocation, event_sink.fsm_session_id)
708
+ unless environment.session_phase(event_sink.fsm_session_id) == phase
709
+ raise Phronomy::Error,
710
+ "#{kind} dispatch preparation requires the owning FSMSession to be in #{phase.inspect}"
711
+ end
712
+ state
713
+ end
714
+
715
+ def capture_provider_preparation(state, invocation, fsm_session_id:, streaming:)
716
+ ProviderDispatchPreparationCommand.new(
717
+ execution_id: state.execution_id,
718
+ fsm_session_id: fsm_session_id.to_s.freeze,
719
+ expected_execution_revision: state.execution.execution_revision,
720
+ root: @agent.agent_root,
721
+ journal_records: @agent.send(:_journal_records_snapshot),
722
+ execution: state.execution,
723
+ base_manifest: state.base_manifest,
724
+ invocation_config: invocation.config.dup.freeze,
725
+ runtime_snapshot: invocation.runtime_snapshot,
726
+ streaming: !!streaming,
727
+ pending_llm_call_id: SecureRandom.uuid.to_s.freeze,
728
+ pending_llm_started_at: Time.now.utc.iso8601(6).freeze
729
+ )
730
+ end
731
+
732
+ def capture_tool_preparation(state, invocation, fsm_session_id:)
733
+ tool_batch_snapshot = ExecutionMetadata.build_tool_batch_snapshot(invocation)
734
+ if tool_batch_snapshot.empty?
735
+ raise Phronomy::Error, "Tool dispatch preparation requires a non-empty Tool batch"
736
+ end
737
+
738
+ ToolDispatchPreparationCommand.new(
739
+ execution_id: state.execution_id,
740
+ fsm_session_id: fsm_session_id.to_s.freeze,
741
+ expected_execution_revision: state.execution.execution_revision,
742
+ root: @agent.agent_root,
743
+ execution: state.execution,
744
+ runtime_snapshot: invocation.runtime_snapshot,
745
+ tool_batch_snapshot: tool_batch_snapshot,
746
+ invocation_config: invocation.config.dup.freeze
747
+ )
748
+ end
749
+
750
+ def apply_provider_dispatch_preparation_on_event_loop(ready)
751
+ operation = ready.operation
752
+ state = authoritative_state_for_operation(
753
+ execution_id: operation.execution_id,
754
+ fsm_session_id: operation.fsm_session_id,
755
+ expected_execution_revision: operation.expected_execution_revision,
756
+ expected_fsm_state: :calling_llm
757
+ )
758
+ return unless state
759
+
760
+ if ready.error
761
+ if ready.error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
762
+ mark_barrier_recovery_required(operation, ready.error)
763
+ else
764
+ state.invocation.event_sink.post(:llm_setup_failed, ready.error)
765
+ end
766
+ return
767
+ end
768
+
769
+ apply_confirmed_provider_dispatch_preparation_on_event_loop(
770
+ operation,
771
+ ready.result,
772
+ state
773
+ )
774
+ rescue => error
775
+ state&.invocation&.event_sink&.post(:llm_setup_failed, error)
776
+ end
777
+
778
+ def apply_tool_dispatch_preparation_on_event_loop(ready)
779
+ operation = ready.operation
780
+ state = authoritative_state_for_operation(
781
+ execution_id: operation.execution_id,
782
+ fsm_session_id: operation.fsm_session_id,
783
+ expected_execution_revision: operation.expected_execution_revision,
784
+ expected_fsm_state: :dispatching_tools
785
+ )
786
+ return unless state
787
+
788
+ if ready.error
789
+ if ready.error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
790
+ mark_barrier_recovery_required(operation, ready.error)
791
+ else
792
+ state.invocation.event_sink.post(:tool_setup_failed, ready.error)
793
+ end
794
+ return
795
+ end
796
+
797
+ apply_confirmed_tool_dispatch_preparation_on_event_loop(
798
+ operation,
799
+ ready.result,
800
+ state
801
+ )
802
+ rescue => error
803
+ state&.invocation&.event_sink&.post(:tool_setup_failed, error)
804
+ end
805
+
806
+ def apply_confirmed_provider_dispatch_preparation_on_event_loop(
807
+ operation,
808
+ result,
809
+ state
810
+ )
811
+ environment = @agent.__execution_environment
812
+ if result.runtime_projection
813
+ environment.registry.replace_agent_execution(
814
+ operation.execution_id,
815
+ execution: result.execution,
816
+ runtime_projection: result.runtime_projection
817
+ )
818
+ else
819
+ environment.registry.replace_agent_execution(
820
+ operation.execution_id,
821
+ execution: result.execution
822
+ )
823
+ end
824
+ state.invocation.acknowledge_runtime_snapshot(operation.runtime_snapshot)
825
+
826
+ if result.error
827
+ state.invocation.event_sink.post(:llm_setup_failed, result.error)
828
+ return
829
+ end
830
+
831
+ Agent::InvocationActions.start_prepared_provider_call(
832
+ agent: @agent,
833
+ environment: @agent.__execution_environment,
834
+ event_sink: state.invocation.event_sink,
835
+ invocation: state.invocation,
836
+ projection: result.runtime_projection,
837
+ streaming: operation.streaming
838
+ )
839
+ end
840
+
841
+ def apply_confirmed_tool_dispatch_preparation_on_event_loop(
842
+ operation,
843
+ result,
844
+ state
845
+ )
846
+ environment = @agent.__execution_environment
847
+ environment.registry.replace_agent_execution(
848
+ operation.execution_id,
849
+ execution: result.execution
850
+ )
851
+ state.invocation.acknowledge_runtime_snapshot(operation.runtime_snapshot)
852
+ Agent::InvocationActions.start_prepared_tool_dispatch(
853
+ environment: @agent.__execution_environment,
854
+ event_sink: state.invocation.event_sink,
855
+ invocation: state.invocation
856
+ )
857
+ end
858
+
859
+ def mark_barrier_recovery_required(operation, error)
860
+ environment = @agent.__execution_environment
861
+ environment.registry.mark_agent_execution_admission(
862
+ @agent.agent_id,
863
+ execution_id: operation.execution_id,
864
+ state: :recovery_required
865
+ )
866
+ Phronomy::RuntimeSettings.current.logger&.warn(
867
+ "[Phronomy] causal durable barrier requires recovery for " \
868
+ "#{operation.execution_id}: #{error.class}: #{error.message}"
869
+ )
870
+ end
871
+
872
+ # ----------------------------------------------------------------------
873
+ # Approval resume
874
+ # ----------------------------------------------------------------------
875
+
876
+ def begin_resume_on_event_loop(request)
877
+ environment = @agent.__execution_environment
878
+ state = environment.registry.agent_execution_state(request.execution_id)
879
+ unless state
880
+ fail_task(
881
+ request.result_task,
882
+ Phronomy::ExecutionRehydrationRequiredError.new(
883
+ "no live execution owner for #{request.execution_id}; durable rehydration is required"
884
+ )
885
+ )
886
+ return
887
+ end
888
+
889
+ unless state.agent.equal?(@agent) && state.execution.status == :suspended
890
+ fail_task(
891
+ request.result_task,
892
+ ArgumentError.new(
893
+ "execution is not a suspended execution of this agent: #{request.execution_id}"
894
+ )
895
+ )
896
+ return
897
+ end
898
+
899
+ approval = state.execution.approval_request || {}
900
+ current_request_id = approval["id"] || approval[:id]
901
+ unless current_request_id.to_s == request.approval_request_id
902
+ fail_task(
903
+ request.result_task,
904
+ ArgumentError.new(
905
+ "approval request does not match execution #{request.execution_id}"
906
+ )
907
+ )
908
+ return
909
+ end
910
+
911
+ operation = capture_approval_resume(state, request)
912
+ resume_transition_started = false
913
+ submitted = false
914
+ environment.registry.mark_agent_execution_admission(
915
+ @agent.agent_id,
916
+ execution_id: state.execution_id,
917
+ state: :resuming
918
+ )
919
+ resume_transition_started = true
920
+ task = environment.submit(on_full: :raise) do
921
+ @approval_resume_commit.commit(operation)
922
+ end
923
+ submitted = true
924
+ task.on_complete do |result, error|
925
+ ready = ResumeCommitReady.new(
926
+ coordinator: self,
927
+ request: request,
928
+ operation: operation,
929
+ result: result,
930
+ error: error
931
+ )
932
+ fail_task(request.result_task, runtime_rejected_error(:resume_commit)) unless
933
+ post_control(environment, ready)
934
+ end
935
+ rescue => error
936
+ if resume_transition_started
937
+ environment.registry.mark_agent_execution_admission(
938
+ @agent.agent_id,
939
+ execution_id: state.execution_id,
940
+ state: submitted ? :recovery_required : :suspended
941
+ )
942
+ end
943
+ fail_task(request.result_task, error)
944
+ raise unless resume_transition_started
945
+ end
946
+
947
+ def capture_approval_resume(state, request)
948
+ snapshot = if state.invocation
949
+ Phronomy::Values::Immutable.copy(
950
+ ExecutionMetadata.build_tool_batch_snapshot(state.invocation)
951
+ )
952
+ end
953
+ ResumeCommitCommand.new(
954
+ execution_id: state.execution_id,
955
+ expected_execution_revision: state.execution.execution_revision,
956
+ root: @agent.agent_root,
957
+ execution: state.execution,
958
+ approval_request_id: request.approval_request_id,
959
+ approved: request.approved,
960
+ tool_batch_snapshot: snapshot
961
+ )
962
+ end
963
+
964
+ def apply_resume_commit_on_event_loop(ready)
965
+ request = ready.request
966
+ operation = ready.operation
967
+ environment = @agent.__execution_environment
968
+ state = environment.registry.agent_execution_state(operation.execution_id)
969
+ unless state && state.agent.equal?(@agent) &&
970
+ state.execution.execution_revision == operation.expected_execution_revision &&
971
+ state.execution.status == :suspended
972
+ fail_task(
973
+ request.result_task,
974
+ Phronomy::Error.new(
975
+ "stale approval resume result for #{operation.execution_id}"
976
+ )
977
+ )
978
+ return
979
+ end
980
+
981
+ if ready.error
982
+ environment.registry.mark_agent_execution_admission(
983
+ @agent.agent_id,
984
+ execution_id: operation.execution_id,
985
+ state: :recovery_required
986
+ )
987
+ fail_task(request.result_task, ready.error)
988
+ return
989
+ end
990
+
991
+ install_resumed_execution_on_event_loop(ready, environment)
992
+ observe_resumed_execution(ready, state)
993
+ execution_session_runner(@agent.__execution_environment).resume_approval(
994
+ state.invocation, request.result_task,
995
+ approved: request.approved, config: request.config
996
+ )
997
+ rescue => error
998
+ state = environment.registry.agent_execution_state(operation.execution_id) if environment&.executing?
999
+ if state
1000
+ begin_terminal_commit_on_event_loop(
1001
+ state,
1002
+ request.result_task,
1003
+ state.invocation,
1004
+ error,
1005
+ fsm_session_id: state.fsm_session_id
1006
+ )
1007
+ else
1008
+ fail_task(request.result_task, error)
1009
+ end
1010
+ end
1011
+
1012
+ def install_resumed_execution_on_event_loop(ready, environment)
1013
+ request = ready.request
1014
+ operation = ready.operation
1015
+ apply_agent_live_state(root: ready.result.root, appended_records: [])
1016
+ environment.registry.mark_agent_execution_admission(
1017
+ @agent.agent_id,
1018
+ execution_id: operation.execution_id,
1019
+ state: :executing
1020
+ )
1021
+ environment.registry.replace_agent_execution(
1022
+ operation.execution_id,
1023
+ execution: ready.result.execution
1024
+ )
1025
+ environment.registry.register_agent_completion_waiter(
1026
+ operation.execution_id,
1027
+ request.result_task
1028
+ )
1029
+ end
1030
+
1031
+ def observe_resumed_execution(ready, state)
1032
+ request = ready.request
1033
+ operation = ready.operation
1034
+ trace_config = state.invocation ?
1035
+ state.invocation.config.merge(request.config) : request.config
1036
+ trace_mode = state.invocation&.mode ||
1037
+ (
1038
+ ready.result.execution.metadata[
1039
+ ExecutionMetadata::INVOCATION_MODE_KEY
1040
+ ] || "invoke"
1041
+ ).to_sym
1042
+ Phronomy::Tracing::Automatic.observe_task(
1043
+ request.result_task,
1044
+ "agent.execution",
1045
+ agent_id: @agent.agent_id,
1046
+ execution_id: operation.execution_id,
1047
+ mode: trace_mode,
1048
+ **@agent.send(:_build_caller_meta, trace_config)
1049
+ )
1050
+ end
1051
+
1052
+ # Recovery chooses a semantic continuation after resolving saved facts.
1053
+ # Execution owns validation, admission, live installation and FSM entry.
1054
+ def recover_preparation_on_event_loop(command)
1055
+ state = recovered_execution_state!(command)
1056
+ unless state.execution.status == :preparing && state.execution.phase.to_sym == :preparing
1057
+ raise Phronomy::ExecutionRehydrationRequiredError,
1058
+ "initial preparation continuation requires a :preparing execution"
1059
+ end
1060
+ @agent.__execution_environment.registry.mark_agent_execution_admission(
1061
+ @agent.agent_id, execution_id: state.execution_id, state: :executing
1062
+ )
1063
+ start_initial_preparation_recovery_on_event_loop(
1064
+ state.execution, command.result_task, load_completion: command.load_completion
1065
+ )
1066
+ rescue => error
1067
+ fail_task(command.result_task, error)
1068
+ command.load_completion.fail(error)
1069
+ end
1070
+
1071
+ def continue_recovered_on_event_loop(command)
1072
+ environment = @agent.__execution_environment
1073
+ registry = environment.registry
1074
+ state = recovered_execution_state!(command)
1075
+ validate_recovered_continuation!(state.execution, command.continuation)
1076
+ invocation = command.invocation
1077
+ unless invocation && invocation.agent.equal?(@agent) &&
1078
+ invocation.execution_id.to_s == state.execution_id.to_s
1079
+ raise Phronomy::ExecutionRehydrationRequiredError,
1080
+ "recovered invocation does not belong to execution #{state.execution_id}"
1081
+ end
1082
+ if command.continuation == :failed_terminal && !command.error.is_a?(Exception)
1083
+ raise Phronomy::ExecutionRehydrationRequiredError,
1084
+ "failed recovery continuation requires a failure"
1085
+ end
1086
+
1087
+ state = registry.replace_agent_execution(state.execution_id,
1088
+ runtime_projection: command.runtime_projection, invocation: invocation, fsm_session_id: nil)
1089
+ if command.continuation == :failed_terminal
1090
+ begin_terminal_commit_on_event_loop(state, command.result_task, invocation,
1091
+ command.error, fsm_session_id: nil)
1092
+ return
1093
+ end
1094
+
1095
+ registry.mark_agent_execution_admission(@agent.agent_id,
1096
+ execution_id: state.execution_id, state: :executing)
1097
+ registry.register_agent_completion_waiter(state.execution_id, command.result_task)
1098
+ runner = execution_session_runner(environment)
1099
+ case command.continuation
1100
+ when :approval_rejection
1101
+ runner.resume_approval(invocation, command.result_task, approved: false, config: {})
1102
+ when :framework_tools
1103
+ runner.resume_framework_tools(invocation, command.result_task)
1104
+ when :framework_calls, :output
1105
+ runner.resume(invocation, command.result_task,
1106
+ resume_event: :llm_completed, resume_phase: :calling_llm)
1107
+ when :followup
1108
+ runner.resume(invocation, command.result_task,
1109
+ resume_event: :state_completed, resume_phase: :recording_tool_results)
1110
+ end
1111
+ end
1112
+
1113
+ def recovered_execution_state!(command)
1114
+ state = @agent.__execution_environment.registry.agent_execution_state(command.execution_id)
1115
+ unless command.coordinator.equal?(self) && state && state.agent.equal?(@agent) &&
1116
+ state.coordinator.equal?(self) && state.execution.active? &&
1117
+ state.execution.execution_revision == command.expected_execution_revision &&
1118
+ state.fsm_session_id.nil?
1119
+ raise Phronomy::ExecutionRehydrationRequiredError,
1120
+ "stale recovered execution continuation for #{command.execution_id}"
1121
+ end
1122
+ state
1123
+ end
1124
+
1125
+ def execution_session_runner(environment)
1126
+ ExecutionSessionRunner.new(environment: environment, on_complete: ->(**result) {
1127
+ deliver_on_event_loop(SessionFinishedCommand.new(coordinator: self, **result))
1128
+ })
1129
+ end
1130
+
1131
+ def validate_recovered_continuation!(execution, continuation)
1132
+ phase = execution.phase.to_sym
1133
+ valid = case continuation
1134
+ when :framework_tools
1135
+ %i[dispatching_tools resuming recovery_tools].include?(phase) && execution.status != :suspended
1136
+ when :framework_calls
1137
+ %i[recovery_provider_completed recovery_tools_completed].include?(phase) &&
1138
+ execution.metadata["framework_calls_pending"]
1139
+ when :output
1140
+ phase == :recovery_provider_completed && !execution.metadata["framework_calls_pending"]
1141
+ when :followup
1142
+ phase == :recovery_tools_completed && !execution.metadata["framework_calls_pending"]
1143
+ when :failed_terminal
1144
+ phase == :recovery_resolved_failed
1145
+ when :approval_rejection
1146
+ approval = execution.approval_request
1147
+ phase == :resuming && !(approval && (approval["approved"] || approval[:approved]))
1148
+ end
1149
+ return if valid
1150
+
1151
+ raise Phronomy::ExecutionRehydrationRequiredError,
1152
+ "invalid recovered continuation #{continuation.inspect} for #{phase.inspect}"
1153
+ end
1154
+
1155
+ # ----------------------------------------------------------------------
1156
+ # Terminal durable barrier
1157
+ # ----------------------------------------------------------------------
1158
+
1159
+ def finish_on_event_loop(execution_id, result_task, invocation, error, fsm_session_id:)
1160
+ environment = @agent.__execution_environment
1161
+ assert_event_loop!(environment)
1162
+ state = environment.registry.agent_execution_state(execution_id)
1163
+ return fail_task(result_task, runtime_rejected_error(:terminal)) unless state
1164
+
1165
+ unless state.agent.equal?(@agent) && state.coordinator.equal?(self) &&
1166
+ state.fsm_session_id.to_s == fsm_session_id.to_s &&
1167
+ state.invocation.equal?(invocation)
1168
+ return fail_task(
1169
+ result_task,
1170
+ Phronomy::Error.new("stale Agent terminal callback for #{execution_id}")
1171
+ )
1172
+ end
1173
+
1174
+ terminal_error = error || invocation&.error
1175
+ unless environment.registry.agent_execution_quiescent?(execution_id)
1176
+ wait_state = quiescence_sensitive_terminal_error?(terminal_error) ?
1177
+ :cancelling : :terminalizing
1178
+ environment.registry.mark_agent_execution_admission(
1179
+ @agent.agent_id,
1180
+ execution_id: execution_id,
1181
+ state: wait_state
1182
+ )
1183
+ environment.registry.defer_agent_terminal_until_quiescent(
1184
+ execution_id,
1185
+ DeferredTerminalCommand.new(
1186
+ coordinator: self,
1187
+ execution_id: execution_id.to_s.freeze,
1188
+ result_task: result_task,
1189
+ invocation: invocation,
1190
+ source_error: error,
1191
+ fsm_session_id: fsm_session_id.to_s.freeze
1192
+ )
1193
+ )
1194
+ return
1195
+ end
1196
+
1197
+ begin_terminal_commit_on_event_loop(
1198
+ state,
1199
+ result_task,
1200
+ invocation,
1201
+ error,
1202
+ fsm_session_id: fsm_session_id
1203
+ )
1204
+ end
1205
+
1206
+ def resume_deferred_terminal_on_event_loop(command)
1207
+ environment = @agent.__execution_environment
1208
+ state = environment.registry.agent_execution_state(command.execution_id)
1209
+ unless state && state.agent.equal?(@agent) &&
1210
+ state.fsm_session_id.to_s == command.fsm_session_id.to_s &&
1211
+ state.invocation.equal?(command.invocation)
1212
+ Phronomy::RuntimeSettings.current.logger&.warn(
1213
+ "[Phronomy] Dropped stale deferred terminal for #{command.execution_id}"
1214
+ )
1215
+ return
1216
+ end
1217
+
1218
+ unless environment.registry.agent_execution_quiescent?(command.execution_id)
1219
+ raise Phronomy::Error,
1220
+ "deferred terminal resumed before quiescence for #{command.execution_id}"
1221
+ end
1222
+
1223
+ begin_terminal_commit_on_event_loop(
1224
+ state,
1225
+ command.result_task,
1226
+ command.invocation,
1227
+ command.source_error,
1228
+ fsm_session_id: command.fsm_session_id
1229
+ )
1230
+ end
1231
+
1232
+ def quiescence_sensitive_terminal_error?(error)
1233
+ error.is_a?(Phronomy::CancellationError) ||
1234
+ error.is_a?(Phronomy::TimeoutError)
1235
+ end
1236
+
1237
+ def begin_terminal_commit_on_event_loop(
1238
+ state,
1239
+ result_task,
1240
+ invocation,
1241
+ source_error,
1242
+ fsm_session_id:
1243
+ )
1244
+ if invocation&.phase == :suspended
1245
+ snapshot = ExecutionMetadata.build_tool_batch_snapshot(invocation)
1246
+ staged_execution = ExecutionMetadata.with_values(
1247
+ state.execution,
1248
+ ExecutionMetadata::TOOL_BATCH_METADATA_KEY => snapshot
1249
+ )
1250
+ state = state.class.new(
1251
+ **state.to_h.merge(execution: staged_execution)
1252
+ )
1253
+ end
1254
+
1255
+ operation = build_terminal_operation(
1256
+ execution: state.execution,
1257
+ root: @agent.agent_root,
1258
+ journal_records: @agent.send(:_journal_records_snapshot),
1259
+ invocation: invocation,
1260
+ source_error: source_error,
1261
+ fsm_session_id: fsm_session_id,
1262
+ state_required: true
1263
+ )
1264
+ delivery = TerminalDelivery.new(
1265
+ result_task: result_task,
1266
+ application_listener: invocation&.event_listener,
1267
+ approval_listener: invocation&.approval_listener,
1268
+ handoff_request: invocation&.handoff_request,
1269
+ handoff_manifest: state.runtime_projection&.manifest
1270
+ )
1271
+ submit_terminal_operation(operation, delivery)
1272
+ end
1273
+
1274
+ def begin_terminal_without_session_on_event_loop(request:, prepared:, error:)
1275
+ return deliver_start_failure_on_event_loop(request, error) unless prepared&.execution
1276
+
1277
+ operation = build_terminal_operation(
1278
+ execution: prepared.execution,
1279
+ root: prepared.root,
1280
+ journal_records: @agent.send(:_journal_records_snapshot),
1281
+ invocation: nil,
1282
+ source_error: error,
1283
+ fsm_session_id: nil,
1284
+ state_required: false
1285
+ )
1286
+ delivery = TerminalDelivery.new(
1287
+ result_task: request.result_task,
1288
+ application_listener: request.on_event,
1289
+ approval_listener: request.approval_listener,
1290
+ handoff_request: nil,
1291
+ handoff_manifest: nil
1292
+ )
1293
+ submit_terminal_operation(operation, delivery)
1294
+ end
1295
+
1296
+ def build_terminal_operation(
1297
+ execution:,
1298
+ root:,
1299
+ journal_records:,
1300
+ invocation:,
1301
+ source_error:,
1302
+ fsm_session_id:,
1303
+ state_required:
1304
+ )
1305
+ TerminalCommitCommand.new(
1306
+ execution_id: execution.execution_id.to_s.freeze,
1307
+ fsm_session_id: fsm_session_id&.to_s&.freeze,
1308
+ expected_execution_revision: execution.execution_revision,
1309
+ root: root,
1310
+ journal_records: journal_records,
1311
+ execution: execution,
1312
+ runtime_snapshot: invocation ? invocation.runtime_snapshot : empty_runtime_snapshot,
1313
+ terminal_view: terminal_view(invocation, source_error),
1314
+ state_required: state_required,
1315
+ wiring: (invocation ? invocation.config : @agent.__execution_wiring).dup.freeze
1316
+ )
1317
+ end
1318
+
1319
+ def build_outcome_committer
1320
+ ExecutionOutcomeCommitter.new(agent: @agent, persistence: @agent.persistence)
1321
+ end
1322
+
1323
+ def submit_terminal_operation(operation, delivery)
1324
+ environment = @agent.__execution_environment
1325
+ terminal_transition_started = false
1326
+ environment.registry.mark_agent_execution_admission(
1327
+ @agent.agent_id,
1328
+ execution_id: operation.execution_id,
1329
+ state: :terminalizing
1330
+ )
1331
+ terminal_transition_started = true
1332
+ environment.registry.register_agent_completion_waiter(
1333
+ operation.execution_id,
1334
+ delivery.result_task
1335
+ )
1336
+ task = environment.submit(on_full: :raise) do
1337
+ # Only the operation-specific immutable durable snapshot crosses the
1338
+ # worker boundary. TaskResult/listener delivery state stays outside it.
1339
+ @outcome_committer.commit_outcome(operation)
1340
+ end
1341
+ task.on_complete do |outcome, error|
1342
+ ready = TerminalCommitReady.new(
1343
+ coordinator: self,
1344
+ operation: operation,
1345
+ delivery: delivery,
1346
+ outcome: outcome,
1347
+ error: error
1348
+ )
1349
+ fail_task(delivery.result_task, runtime_rejected_error(:terminal_apply)) unless
1350
+ post_control(environment, ready)
1351
+ end
1352
+ rescue => error
1353
+ if terminal_transition_started
1354
+ environment.registry.mark_agent_execution_admission(
1355
+ @agent.agent_id,
1356
+ execution_id: operation.execution_id,
1357
+ state: :recovery_required
1358
+ )
1359
+ Phronomy::RuntimeSettings.current.logger&.warn(
1360
+ "[Phronomy] Agent terminal transition requires recovery for " \
1361
+ "#{operation.execution_id}: #{error.class}: #{error.message}"
1362
+ )
1363
+ return
1364
+ end
1365
+
1366
+ fail_task(delivery.result_task, error)
1367
+ raise
1368
+ end
1369
+
1370
+ def terminal_view(invocation, source_error)
1371
+ TerminalView.new(
1372
+ phase: invocation&.phase,
1373
+ output: invocation&.output,
1374
+ usage: invocation&.usage,
1375
+ approval_request: invocation&.approval_request,
1376
+ rejected: invocation ? !!invocation.rejected : false,
1377
+ input_blocked: invocation ? invocation.input_blocked? : false,
1378
+ output_blocked: invocation ? invocation.output_blocked? : false,
1379
+ block_error: invocation&.block_error,
1380
+ invocation_error: invocation&.error,
1381
+ handoff: handoff_terminal_view(invocation&.handoff_request),
1382
+ callback_failure: invocation&.callback_failure,
1383
+ source_error: source_error,
1384
+ cancel_requested: invocation&.config&.fetch(:cancellation_token, nil)&.cancelled? || false
1385
+ )
1386
+ end
1387
+
1388
+ def handoff_terminal_view(request) = request
1389
+
1390
+ def empty_runtime_snapshot
1391
+ {llm_results: [].freeze, runtime_events: [].freeze, active_call: nil}.freeze
1392
+ end
1393
+
1394
+ def apply_terminal_commit_on_event_loop(ready)
1395
+ operation = ready.operation
1396
+ delivery = ready.delivery
1397
+ environment = @agent.__execution_environment
1398
+ state = environment.registry.agent_execution_state(operation.execution_id)
1399
+
1400
+ if operation.state_required
1401
+ unless state && state.agent.equal?(@agent) &&
1402
+ state.execution.execution_revision == operation.expected_execution_revision &&
1403
+ state.fsm_session_id.to_s == operation.fsm_session_id.to_s
1404
+ Phronomy::RuntimeSettings.current.logger&.warn(
1405
+ "[Phronomy] Dropped stale Agent terminal result for #{operation.execution_id}"
1406
+ )
1407
+ return
1408
+ end
1409
+ end
1410
+
1411
+ if ready.error
1412
+ handle_terminal_commit_error_on_event_loop(ready, state, environment)
1413
+ return
1414
+ end
1415
+
1416
+ apply_terminal_outcome_state_on_event_loop(ready, state, environment)
1417
+ outcome = ready.outcome
1418
+ case outcome.type
1419
+ when :coordination_wait
1420
+ release_coordination_wait_on_event_loop(ready, state, environment)
1421
+ when :suspended
1422
+ suspend_execution_on_event_loop(ready, state, environment)
1423
+ when :completed
1424
+ deliver_completed_execution_on_event_loop(ready, state, environment)
1425
+ when :handed_off
1426
+ deliver_handoff_on_event_loop(ready, state, environment)
1427
+ when :failed
1428
+ deliver_failed_execution_on_event_loop(ready, state, environment)
1429
+ else
1430
+ fail_task(
1431
+ delivery.result_task,
1432
+ Phronomy::Error.new("unknown terminal outcome: #{outcome.type.inspect}")
1433
+ )
1434
+ end
1435
+ end
1436
+
1437
+ def handle_terminal_commit_error_on_event_loop(ready, state, environment)
1438
+ operation = ready.operation
1439
+ delivery = ready.delivery
1440
+ if operation.execution.metadata["reservation"] || operation.execution.metadata["execution_extension"]
1441
+ release_terminal_ownership(environment, operation.execution_id, state)
1442
+ environment.registry.take_agent_completion_waiters(operation.execution_id, fallback: delivery.result_task).each do |task|
1443
+ task.fail(ready.error)
1444
+ end
1445
+ return
1446
+ end
1447
+ environment.registry.mark_agent_execution_admission(
1448
+ @agent.agent_id,
1449
+ execution_id: operation.execution_id,
1450
+ state: :recovery_required
1451
+ )
1452
+ Phronomy::RuntimeSettings.current.logger&.warn(
1453
+ "[Phronomy] Agent terminal durable outcome requires recovery for " \
1454
+ "#{operation.execution_id}: #{ready.error.class}: #{ready.error.message}"
1455
+ )
1456
+ end
1457
+
1458
+ def apply_terminal_outcome_state_on_event_loop(ready, state, environment)
1459
+ operation = ready.operation
1460
+ outcome = ready.outcome
1461
+ apply_agent_live_state(
1462
+ root: outcome.root,
1463
+ appended_records: outcome.appended_records
1464
+ )
1465
+ if state
1466
+ state.invocation&.acknowledge_runtime_snapshot(operation.runtime_snapshot)
1467
+ environment.registry.replace_agent_execution(
1468
+ operation.execution_id,
1469
+ execution: outcome.execution,
1470
+ fsm_session_id: (outcome.type == :suspended) ? nil : state.fsm_session_id
1471
+ )
1472
+ end
1473
+ end
1474
+
1475
+ def release_coordination_wait_on_event_loop(ready, state, environment)
1476
+ operation = ready.operation
1477
+ delivery = ready.delivery
1478
+ outcome = ready.outcome
1479
+ release_terminal_ownership(environment, operation.execution_id, state)
1480
+ environment.registry.take_agent_completion_waiters(operation.execution_id, fallback: delivery.result_task).each { |task| task.fail(outcome.error) }
1481
+ end
1482
+
1483
+ def suspend_execution_on_event_loop(ready, state, environment)
1484
+ operation = ready.operation
1485
+ delivery = ready.delivery
1486
+ outcome = ready.outcome
1487
+ environment.registry.mark_agent_execution_admission(
1488
+ @agent.agent_id,
1489
+ execution_id: operation.execution_id,
1490
+ state: :suspended
1491
+ )
1492
+ dispatch_approval_listener(delivery.approval_listener, outcome.approval_request)
1493
+ callback_error = deliver_terminal(
1494
+ delivery.application_listener,
1495
+ :approval_required,
1496
+ request: outcome.approval_request
1497
+ )
1498
+ report_nonterminal_callback_error(
1499
+ callback_error,
1500
+ :approval_required,
1501
+ outcome.result
1502
+ )
1503
+ Array(state&.invocation&.config&.fetch(:phronomy_exact_observers, [])).each do |task|
1504
+ task.fail(Phronomy::ExecutionRehydrationRequiredError.new("Execution #{operation.execution_id} requires approval"))
1505
+ end
1506
+ end
1507
+
1508
+ def deliver_completed_execution_on_event_loop(ready, state, environment)
1509
+ operation = ready.operation
1510
+ delivery = ready.delivery
1511
+ outcome = ready.outcome
1512
+ release_terminal_ownership(environment, operation.execution_id, state)
1513
+ callback_error = deliver_terminal(delivery.application_listener, :done, outcome.result)
1514
+ settle_execution_waiters(
1515
+ environment, operation.execution_id, delivery.result_task,
1516
+ callback_error, :done, outcome.result
1517
+ )
1518
+ end
1519
+
1520
+ def deliver_handoff_on_event_loop(ready, state, environment)
1521
+ operation = ready.operation
1522
+ delivery = ready.delivery
1523
+ outcome = ready.outcome
1524
+ release_terminal_ownership(environment, operation.execution_id, state)
1525
+ result = outcome.result.merge(
1526
+ handoff_request: delivery.handoff_request,
1527
+ _phronomy_handoff_manifest: delivery.handoff_manifest
1528
+ ).freeze
1529
+ callback_error = deliver_terminal(delivery.application_listener, :handoff, result)
1530
+ settle_execution_waiters(
1531
+ environment, operation.execution_id, delivery.result_task,
1532
+ callback_error, :handoff, result
1533
+ )
1534
+ end
1535
+
1536
+ def deliver_failed_execution_on_event_loop(ready, state, environment)
1537
+ operation = ready.operation
1538
+ delivery = ready.delivery
1539
+ outcome = ready.outcome
1540
+ release_terminal_ownership(environment, operation.execution_id, state)
1541
+ type = terminal_event_type(outcome.error)
1542
+ callback_error = deliver_terminal(
1543
+ delivery.application_listener,
1544
+ type,
1545
+ error: outcome.error
1546
+ )
1547
+ settle_execution_waiters(
1548
+ environment, operation.execution_id, delivery.result_task,
1549
+ callback_error, type, nil, outcome.error
1550
+ )
1551
+ end
1552
+
1553
+ def release_terminal_ownership(environment, execution_id, state)
1554
+ registry = environment.registry
1555
+ registry.release_agent_execution(execution_id) if state
1556
+ registry.release_agent_execution_admission(@agent.agent_id, execution_id: execution_id)
1557
+ end
1558
+
1559
+ # ----------------------------------------------------------------------
1560
+ # EventLoop validation/apply helpers
1561
+ # ----------------------------------------------------------------------
1562
+
1563
+ def authoritative_state_for_operation(
1564
+ execution_id:,
1565
+ fsm_session_id:,
1566
+ expected_execution_revision:,
1567
+ expected_fsm_state:
1568
+ )
1569
+ environment = @agent.__execution_environment
1570
+ state = environment.registry.agent_execution_state(execution_id)
1571
+ authoritative = state &&
1572
+ state.agent.equal?(@agent) &&
1573
+ state.execution.execution_revision == expected_execution_revision &&
1574
+ state.fsm_session_id.to_s == fsm_session_id.to_s &&
1575
+ environment.session_phase(fsm_session_id) == expected_fsm_state
1576
+ return state if authoritative
1577
+
1578
+ Phronomy::RuntimeSettings.current.logger&.warn(
1579
+ "[Phronomy] Dropped stale Agent operation result: " \
1580
+ "execution_id=#{execution_id} fsm_session_id=#{fsm_session_id}"
1581
+ )
1582
+ nil
1583
+ rescue
1584
+ nil
1585
+ end
1586
+
1587
+ def validate_live_session!(state, invocation, fsm_session_id)
1588
+ unless state && state.agent.equal?(@agent) &&
1589
+ state.invocation.equal?(invocation) &&
1590
+ state.fsm_session_id.to_s == fsm_session_id.to_s
1591
+ raise Phronomy::Error,
1592
+ "Agent operation does not belong to the current live FSMSession"
1593
+ end
1594
+ end
1595
+
1596
+ def apply_agent_live_state(root:, appended_records:)
1597
+ environment = @agent.__execution_environment
1598
+ assert_event_loop!(environment)
1599
+ @agent.send(:_append_journal_records, appended_records)
1600
+ @agent.__replace_root(root)
1601
+ end
1602
+
1603
+ def assert_event_loop!(environment)
1604
+ return if environment.executing?
1605
+
1606
+ raise Phronomy::Error,
1607
+ "ExecutionCoordinator live-state apply must run on EventLoop"
1608
+ end
1609
+
1610
+ def deliver_terminal(listener, type, payload)
1611
+ return nil unless listener
1612
+
1613
+ @agent.send(
1614
+ :_deliver_stream_event,
1615
+ listener,
1616
+ StreamEvent.new(type: type, payload: payload)
1617
+ )
1618
+ end
1619
+
1620
+ def dispatch_approval_listener(listener, request)
1621
+ return unless listener
1622
+
1623
+ Phronomy::Blocking.call_async do
1624
+ listener.call(request)
1625
+ end.on_complete do |_value, error|
1626
+ report_approval_dispatch_failure(error) if error
1627
+ end
1628
+ rescue => error
1629
+ report_approval_dispatch_failure(error)
1630
+ end
1631
+
1632
+ def report_approval_dispatch_failure(error)
1633
+ Phronomy::RuntimeSettings.current.logger&.warn(
1634
+ "[Phronomy] approval listener dispatch failed: #{error.class}: #{error.message}"
1635
+ )
1636
+ end
1637
+
1638
+ def settle_execution_waiters(
1639
+ environment, execution_id, fallback_task, callback_error, event_type,
1640
+ result, execution_error = nil
1641
+ )
1642
+ waiters = environment.registry.take_agent_completion_waiters(
1643
+ execution_id,
1644
+ fallback: fallback_task
1645
+ )
1646
+ waiters.each do |task|
1647
+ settle_after_terminal(
1648
+ task, callback_error, event_type, result, execution_error
1649
+ )
1650
+ end
1651
+ end
1652
+
1653
+ def report_nonterminal_callback_error(callback_error, event_type, result)
1654
+ return unless callback_error
1655
+
1656
+ @agent.send(
1657
+ :_report_stream_callback_error,
1658
+ callback_error,
1659
+ event: StreamEvent.new(type: event_type, payload: result),
1660
+ execution_id: result&.fetch(:execution_id, nil),
1661
+ callback_error_policy: Phronomy::Agent::Settings.current.stream_callback_error_policy
1662
+ )
1663
+ end
1664
+
1665
+ def settle_after_terminal(
1666
+ result_task,
1667
+ callback_error,
1668
+ event_type,
1669
+ result,
1670
+ execution_error = nil
1671
+ )
1672
+ if callback_error
1673
+ policy = Phronomy::Agent::Settings.current.stream_callback_error_policy
1674
+ @agent.send(
1675
+ :_report_stream_callback_error,
1676
+ callback_error,
1677
+ event: StreamEvent.new(
1678
+ type: event_type,
1679
+ payload: result || {error: execution_error}
1680
+ ),
1681
+ execution_id: result&.fetch(:execution_id, nil),
1682
+ callback_error_policy: policy
1683
+ )
1684
+ if policy == :fail_task && execution_error.nil?
1685
+ return fail_task(
1686
+ result_task,
1687
+ @agent.send(
1688
+ :_build_stream_callback_error,
1689
+ event_type: event_type,
1690
+ callback_error: callback_error,
1691
+ result: result
1692
+ )
1693
+ )
1694
+ end
1695
+ end
1696
+ if execution_error
1697
+ if execution_error.is_a?(Phronomy::CancellationError)
1698
+ result_task.cancel!(execution_error)
1699
+ else
1700
+ fail_task(result_task, execution_error)
1701
+ end
1702
+ else
1703
+ complete_task(result_task, result)
1704
+ end
1705
+ end
1706
+
1707
+ def terminal_event_type(error)
1708
+ return :timeout if error.is_a?(Phronomy::TimeoutError)
1709
+ return :cancelled if error.is_a?(Phronomy::CancellationError)
1710
+
1711
+ :error
1712
+ end
1713
+
1714
+ def post_control(environment, command)
1715
+ admission = command.is_a?(StartCommand) || command.is_a?(ResumeCommand)
1716
+ completion = case command
1717
+ when StartCommand, ResumeCommand then command.result_task
1718
+ when InitialPreparationReady, ResumeCommitReady then command.request.result_task
1719
+ when InitialPreparationRecoveryReady then command.load_completion
1720
+ when TerminalCommitReady then command.delivery.result_task
1721
+ when DeferredTerminalCommand then command.result_task
1722
+ end
1723
+ environment.registry.post(command,
1724
+ admission: admission, completion: completion)
1725
+ rescue Phronomy::RuntimeShutdownError
1726
+ false
1727
+ end
1728
+
1729
+ def runtime_rejected_error(operation)
1730
+ Phronomy::RuntimeShutdownError.new(
1731
+ "EventLoop is not accepting Agent #{operation} control delivery"
1732
+ )
1733
+ end
1734
+
1735
+ def complete_task(task, result)
1736
+ @agent.send(:_complete_result_task, task, result)
1737
+ end
1738
+
1739
+ def fail_task(task, error)
1740
+ @agent.send(:_fail_result_task, task, error)
1741
+ end
1742
+ end
1743
+ end
1744
+ end