phronomy 0.26.0 → 0.27.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 (304) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +4 -0
  4. data/VERIFY.sh +27 -27
  5. data/benchmark/bench_agent_invoke.rb +26 -22
  6. data/benchmark/bench_context_assembler.rb +4 -5
  7. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  8. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  9. data/docs/architecture/agent-context.md +4 -2
  10. data/docs/architecture/agent-transition-ownership.md +89 -0
  11. data/docs/architecture/before-llm-input.md +6 -0
  12. data/docs/architecture/context-management.md +30 -0
  13. data/docs/architecture/context-preparation-steps.md +90 -0
  14. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  15. data/docs/architecture/execution-metadata-and-values.md +80 -0
  16. data/docs/architecture/generator-verifier-ownership.md +111 -0
  17. data/docs/architecture/multi-agent-handoff.md +8 -2
  18. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  19. data/docs/architecture/persistence.md +27 -4
  20. data/docs/architecture/refactoring-closure.md +334 -0
  21. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  22. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  23. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  24. data/docs/architecture/tracing.md +1 -1
  25. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  26. data/docs/architecture.md +209 -0
  27. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  28. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  29. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  30. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  31. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  32. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  33. data/docs/decisions/032-storage-backend-composition.md +80 -0
  34. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  35. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  36. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  37. data/docs/decisions/036-context-contract-ownership.md +86 -0
  38. data/docs/decisions/037-common-definition-ownership.md +62 -0
  39. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  40. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  41. data/docs/decisions/040-configuration-default-composition.md +94 -0
  42. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  43. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  44. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  45. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  46. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  47. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  48. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  49. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  50. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  51. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  52. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  53. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  54. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  55. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  56. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  57. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  58. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  59. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  60. data/docs/decisions/README.md +35 -6
  61. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  62. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  63. data/docs/features.md +9 -3
  64. data/docs/getting-started.md +4 -4
  65. data/docs/migrations/0.15.md +5 -0
  66. data/docs/migrations/durable-semantic-coordination-v2.md +6 -0
  67. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  68. data/docs/migrations/neutral-storage-spi.md +43 -0
  69. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  70. data/docs/migrations/shared-state-multi-agent.md +44 -0
  71. data/docs/migrations/storage-backend-composition.md +117 -0
  72. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  73. data/docs/persistence-backends.md +162 -204
  74. data/docs/runtime-and-concurrency.md +117 -32
  75. data/lib/phronomy/agent/api/agent.rb +17 -0
  76. data/lib/phronomy/agent/async_event_api.rb +2 -2
  77. data/lib/phronomy/agent/base.rb +51 -241
  78. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  79. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  80. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  81. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  82. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  83. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  84. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  85. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  86. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  87. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  88. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  89. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  90. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  91. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  92. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  93. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  94. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  95. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +19 -95
  96. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  97. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  98. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +9 -10
  99. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  100. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  101. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  102. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  103. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  104. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  105. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  106. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  107. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  108. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +20 -71
  109. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  110. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  111. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  112. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  113. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  114. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  115. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  116. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  117. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  118. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  119. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  120. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  121. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  122. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  123. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  124. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  125. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  126. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  127. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  128. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  129. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  130. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  131. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  132. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  133. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  134. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  135. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  136. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +32 -37
  137. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  138. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +6 -10
  139. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  140. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  141. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  142. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  143. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  144. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  145. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  146. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  147. data/lib/phronomy/common/configuration_error.rb +7 -0
  148. data/lib/phronomy/common/error.rb +5 -0
  149. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  150. data/lib/phronomy/common/values/serializable.rb +32 -0
  151. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  152. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  153. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  154. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  155. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  156. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +1 -1
  157. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  158. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  159. data/lib/phronomy/engine/concurrency/offload_pool.rb +4 -0
  160. data/lib/phronomy/engine/concurrency/operation_binding.rb +1 -1
  161. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  162. data/lib/phronomy/engine/event_loop.rb +158 -624
  163. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  164. data/lib/phronomy/{execution.rb → engine/execution.rb} +0 -7
  165. data/lib/phronomy/{execution_cancellation_error.rb → engine/execution_cancellation_error.rb} +2 -0
  166. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  167. data/lib/phronomy/{execution_timeout_error.rb → engine/execution_timeout_error.rb} +2 -0
  168. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  169. data/lib/phronomy/engine/fsm_session.rb +35 -28
  170. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +2 -0
  171. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +2 -0
  172. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +2 -0
  173. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  174. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  175. data/lib/phronomy/engine/runtime.rb +80 -84
  176. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  177. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  178. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  179. data/lib/phronomy/engine/task_result.rb +4 -0
  180. data/lib/phronomy/engine/timeout_error.rb +7 -0
  181. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  182. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  183. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  184. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  185. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  186. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  187. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  188. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  189. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  190. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  191. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  192. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  193. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  194. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  195. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +5 -5
  196. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  197. data/lib/phronomy/multi_agent/orchestrator.rb +2 -2
  198. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  199. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  200. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  201. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  202. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  203. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  204. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  205. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  206. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  207. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  208. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  209. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  210. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  211. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  212. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  213. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  214. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  215. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  216. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  217. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  218. data/lib/phronomy/storage/backend.rb +101 -0
  219. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  220. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  221. data/lib/phronomy/storage/blobs.rb +31 -0
  222. data/lib/phronomy/storage/condition.rb +25 -0
  223. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  224. data/lib/phronomy/storage/conflict_error.rb +9 -0
  225. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  226. data/lib/phronomy/storage/entry.rb +33 -0
  227. data/lib/phronomy/storage/guard_ref.rb +13 -0
  228. data/lib/phronomy/storage/not_found_error.rb +9 -0
  229. data/lib/phronomy/storage/record_codec.rb +177 -0
  230. data/lib/phronomy/storage/records.rb +61 -0
  231. data/lib/phronomy/storage/resource.rb +126 -0
  232. data/lib/phronomy/storage/scope.rb +25 -0
  233. data/lib/phronomy/storage/serialization_error.rb +9 -0
  234. data/lib/phronomy/storage/streams.rb +44 -0
  235. data/lib/phronomy/storage/transaction_error.rb +10 -0
  236. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  237. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  238. data/lib/phronomy/storage/validation.rb +53 -0
  239. data/lib/phronomy/storage/view.rb +131 -0
  240. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  241. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  242. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  243. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  244. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  245. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  246. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  247. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  248. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  249. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  250. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  251. data/lib/phronomy/tools/agent.rb +1 -1
  252. data/lib/phronomy/version.rb +1 -1
  253. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +8 -6
  254. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  255. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  256. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  257. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +122 -71
  258. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  259. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  260. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  261. data/lib/phronomy/workflow/phase_machine_builder.rb +2 -2
  262. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  263. data/lib/phronomy.rb +52 -94
  264. data/scripts/api_snapshot.rb +1 -1
  265. data/scripts/storage_spi_snapshot.rb +36 -0
  266. data/sig/phronomy/agent.rbs +0 -1
  267. data/sig/phronomy/execution_receiver.rbs +34 -0
  268. data/sig/phronomy/handoff.rbs +4 -2
  269. data/sig/phronomy/multi_agent.rbs +18 -0
  270. data/sig/phronomy/persistence.rbs +7 -91
  271. data/sig/phronomy/storage.rbs +174 -0
  272. data/sig/phronomy/tool.rbs +10 -1
  273. metadata +230 -99
  274. data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
  275. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  276. data/lib/phronomy/agent/recovery_support.rb +0 -504
  277. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  278. data/lib/phronomy/agent/tool_executor.rb +0 -55
  279. data/lib/phronomy/generator_verifier.rb +0 -369
  280. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  281. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  282. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  283. data/lib/phronomy/persistence/in_memory.rb +0 -690
  284. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  285. data/lib/phronomy/persistence.rb +0 -276
  286. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  287. data/lib/phronomy/workflow_recovery.rb +0 -123
  288. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  289. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  290. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  291. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  292. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  293. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  294. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  295. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  296. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  297. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  298. /data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +0 -0
  299. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  300. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  301. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  302. /data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +0 -0
  303. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  304. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "concurrency/worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  # Runtime-owned FIFO event loop for FSMSession instances.
5
7
  #
@@ -7,6 +9,8 @@ module Phronomy
7
9
  # lifecycle progression and Phronomy-managed live execution-state mutation
8
10
  # happens by short event dispatches on this thread.
9
11
  class EventLoop
12
+ include Phronomy::Concurrency::WorkerInputRestricted
13
+
10
14
  SYSTEM_CHANNEL_ID = "__event_loop__"
11
15
 
12
16
  QUEUE_BACKLOG_WARNING_THRESHOLD = 1_000
@@ -17,43 +21,7 @@ module Phronomy
17
21
 
18
22
  STOP = Object.new.freeze
19
23
  WAKE = Object.new.freeze
20
- UNSET = Object.new.freeze
21
- private_constant :STOP, :WAKE, :UNSET
22
-
23
- # Immutable EventLoop-owned value. The map containing these records is the
24
- # mutable authority; records are replaced rather than mutated in place.
25
- AgentExecutionState = Data.define(
26
- :execution_id,
27
- :agent,
28
- :coordinator,
29
- :execution,
30
- :runtime_projection,
31
- :base_manifest,
32
- :invocation,
33
- :fsm_session_id
34
- )
35
- private_constant :AgentExecutionState
36
-
37
- # Read-only process-local lookup view used by approval/live-owner APIs.
38
- # It intentionally exposes no mutable execution, invocation, or projection.
39
- AgentExecutionOwner = Data.define(:execution_id, :agent, :coordinator, :status)
40
- private_constant :AgentExecutionOwner
41
-
42
- # EventLoop-owned process-local top-level execution admission. This is
43
- # separate from AgentExecutionState because admission begins before the
44
- # durable AgentExecution exists. owner_token is coordination-only and is
45
- # never a semantic result-authority identifier.
46
- AgentAdmission = Data.define(:agent_id, :owner_token, :execution_id, :state)
47
- private_constant :AgentAdmission
48
-
49
- # EventLoop-owned process-local Workflow execution-segment admission. The
50
- # owner token is a Runtime coordination capability, not a domain identity or
51
- # routing identity. fsm_session_id is bound only after durable load/hydration
52
- # completes and a concrete FSMSession has been constructed.
53
- WorkflowAdmission = Data.define(
54
- :workflow_instance_id, :owner_token, :fsm_session_id, :state
55
- )
56
- private_constant :WorkflowAdmission
24
+ private_constant :STOP, :WAKE
57
25
 
58
26
  def initialize(runtime:)
59
27
  @runtime = runtime
@@ -63,15 +31,14 @@ module Phronomy
63
31
  @max_queue_depth = 0
64
32
  @last_queue_backlog_warning_at = nil
65
33
 
34
+ @execution_receivers = {}
35
+ @execution_deliveries = {}
36
+ @session_receivers = {}
37
+ @dispatching_delivery = nil
38
+ @receiver_shutdown_error = nil
66
39
  @fsms = {}
67
40
  @waiting = {}
68
41
  @admitted_fsm_session_ids = Set.new
69
- @workflow_admissions = {}
70
- @agent_admissions = {}
71
- @agent_executions = {}
72
- @agent_completion_waiters = Hash.new { |hash, key| hash[key] = [] }
73
- @agent_inflight_work = Hash.new { |hash, key| hash[key] = {} }
74
- @agent_deferred_terminals = {}
75
42
 
76
43
  @lifecycle_mutex = Mutex.new
77
44
  @idle_cond = ConditionVariable.new
@@ -113,7 +80,93 @@ module Phronomy
113
80
  @queue_metrics_mutex.synchronize { @max_queue_depth }
114
81
  end
115
82
 
116
- def register(fsm_session, completion: nil)
83
+ # Internal receiver registration is closed before shutdown tests idleness.
84
+ # Feature constructors are side-effect free and run outside this lock.
85
+ # @api private
86
+ def __register_execution_receiver(key:, receiver:)
87
+ unless receiver.is_a?(Phronomy::ExecutionReceiver) && receiver.__bound_to?(self)
88
+ raise ArgumentError, "execution receiver must implement the Engine contract for this EventLoop"
89
+ end
90
+ @lifecycle_mutex.synchronize do
91
+ unless @state == :running
92
+ raise Phronomy::RuntimeShutdownError,
93
+ "EventLoop is #{@state}; execution receivers cannot be registered"
94
+ end
95
+ @execution_receivers[key] ||= receiver
96
+ end
97
+ end
98
+
99
+ # @api private
100
+ def __execution_receiver(key:)
101
+ @lifecycle_mutex.synchronize { @execution_receivers[key] }
102
+ end
103
+
104
+ # The same lock covers feature state and Engine's idle decision. Blocks are
105
+ # short state reads/writes, never I/O, delivery callbacks or Runtime calls.
106
+ # @api private
107
+ def __synchronize_execution_state
108
+ @lifecycle_mutex.synchronize do
109
+ yield
110
+ ensure
111
+ @idle_cond.broadcast if runtime_idle_locked?
112
+ end
113
+ end
114
+
115
+ # Admission already queued while running may establish its feature slot
116
+ # during drain. A newly requested admission may not do so.
117
+ # @api private
118
+ def __admit_execution(receiver)
119
+ assert_event_loop_thread!
120
+ __synchronize_execution_state do
121
+ accepted_delivery = @dispatching_delivery &&
122
+ @dispatching_delivery[:receiver].equal?(receiver) &&
123
+ @dispatching_delivery[:admission]
124
+ accepting = @execution_receivers.value?(receiver) &&
125
+ (@state == :running || (@state == :draining && accepted_delivery))
126
+ unless accepting
127
+ raise Phronomy::RuntimeShutdownError,
128
+ "EventLoop is #{@state}; new executions are not accepted"
129
+ end
130
+ yield
131
+ end
132
+ end
133
+
134
+ # Count accepted deliveries until dispatch finishes, including the interval
135
+ # before a feature admission or FSM exists. Continuations use registered
136
+ # receivers; new admissions require the running state.
137
+ # @api private
138
+ def __post_execution(receiver, message, admission: false, completion: nil)
139
+ token = Object.new.freeze
140
+ event = Phronomy::Event.new(type: :execution_control,
141
+ target_id: SYSTEM_CHANNEL_ID, payload: token)
142
+ queued_depth = nil
143
+ accepted = @lifecycle_mutex.synchronize do
144
+ next false unless accepting_events? && @execution_receivers.value?(receiver)
145
+ next false if admission && @state != :running
146
+
147
+ @execution_deliveries[token] = {
148
+ receiver: receiver, message: message, admission: admission, completion: completion
149
+ }.freeze
150
+ begin
151
+ queued_depth = enqueue([event, monotonic_nanoseconds])
152
+ rescue
153
+ @execution_deliveries.delete(token)
154
+ raise
155
+ end
156
+ true
157
+ end
158
+ return false unless accepted
159
+
160
+ check_queue_backlog(queued_depth, event)
161
+ true
162
+ end
163
+
164
+ # @api private
165
+ def __receiver_cleanup_complete?
166
+ @receiver_shutdown_error.nil?
167
+ end
168
+
169
+ def register(fsm_session, completion: nil, receiver: nil)
117
170
  if current? && !completion.is_a?(Phronomy::TaskResult)
118
171
  raise Phronomy::Error,
119
172
  "Cannot call a synchronous invocation API from an EventLoop action. " \
@@ -135,11 +188,16 @@ module Phronomy
135
188
  "FSMSession #{fsm_session.id.inspect} is already registered"
136
189
  end
137
190
 
191
+ if receiver && !@execution_receivers.value?(receiver)
192
+ raise ArgumentError, "execution receiver is not registered on this EventLoop"
193
+ end
194
+ @session_receivers[fsm_session.id] = receiver if receiver
138
195
  @admitted_fsm_session_ids.add(fsm_session.id)
139
196
  @outstanding_sessions += 1
140
197
  begin
141
198
  queued_depth = enqueue([event, monotonic_nanoseconds])
142
199
  rescue
200
+ @session_receivers.delete(fsm_session.id)
143
201
  @admitted_fsm_session_ids.delete(fsm_session.id)
144
202
  @outstanding_sessions -= 1
145
203
  @idle_cond.broadcast if runtime_idle_locked?
@@ -195,504 +253,32 @@ module Phronomy
195
253
  true
196
254
  end
197
255
 
198
- # Process-local read-only admission check used by destructive Agent lifecycle
199
- # operations. The mutable admission map itself remains EventLoop-owned.
200
- def agent_execution_admitted?(agent_id)
201
- @lifecycle_mutex.synchronize { @agent_admissions.key?(agent_id.to_s) }
202
- end
203
-
204
- # Reserves the one top-level logical execution slot for agent_id before any
205
- # Persistence execution admission is attempted.
256
+ # Resolve feature identity and enqueue to a live FSM under one lock. The
257
+ # receiver supplies only a short lookup; dispatch still uses the FIFO.
206
258
  # @api private
207
- def admit_agent_execution(agent_id, owner_token:)
208
- assert_event_loop_thread!
209
- key = agent_id.to_s
210
- raise ArgumentError, "agent_id must not be empty" if key.empty?
211
- raise ArgumentError, "owner_token is required" unless owner_token
212
-
213
- @lifecycle_mutex.synchronize do
214
- ensure_accepting_registrations!
215
- if @agent_admissions.key?(key)
216
- raise Phronomy::AgentBusyError,
217
- "Agent #{key.inspect} already has a nonterminal top-level execution"
218
- end
219
- @agent_admissions[key] = AgentAdmission.new(
220
- agent_id: key.freeze,
221
- owner_token: owner_token,
222
- execution_id: nil,
223
- state: :admitting
224
- )
225
- end
226
- true
227
- end
228
-
229
- # Binds a successful durable AgentExecution identity to the earlier
230
- # process-local admission.
231
- # @api private
232
- def bind_agent_execution_admission(agent_id, owner_token:, execution_id:)
233
- assert_event_loop_thread!
234
- key = agent_id.to_s
235
- execution_key = execution_id.to_s
236
- @lifecycle_mutex.synchronize do
237
- current = @agent_admissions.fetch(key) do
238
- raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
239
- end
240
- unless current.owner_token.equal?(owner_token) && current.execution_id.nil?
241
- raise Phronomy::Error, "stale Agent admission bind for #{key.inspect}"
242
- end
243
- @agent_admissions[key] = AgentAdmission.new(
244
- agent_id: current.agent_id,
245
- owner_token: current.owner_token,
246
- execution_id: execution_key.freeze,
247
- state: :executing
248
- )
249
- end
250
- true
251
- end
252
-
253
- # @api private
254
- def mark_agent_execution_admission(agent_id, execution_id:, state:)
255
- assert_event_loop_thread!
256
- key = agent_id.to_s
257
- execution_key = execution_id.to_s
258
- next_state = state.to_sym
259
- unless %i[executing suspended resuming cancelling terminalizing recovery_required].include?(next_state)
260
- raise ArgumentError, "unsupported Agent admission state: #{next_state.inspect}"
261
- end
262
-
263
- @lifecycle_mutex.synchronize do
264
- current = @agent_admissions.fetch(key) do
265
- raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
266
- end
267
- unless current.execution_id.to_s == execution_key
268
- raise Phronomy::Error, "stale Agent admission state update for #{key.inspect}"
269
- end
270
- @agent_admissions[key] = AgentAdmission.new(
271
- agent_id: current.agent_id,
272
- owner_token: current.owner_token,
273
- execution_id: current.execution_id,
274
- state: next_state
275
- )
276
- @idle_cond.broadcast if runtime_idle_locked?
277
- end
278
- true
279
- end
280
-
281
- # @api private
282
- def mark_agent_admission_recovery_required(agent_id, owner_token:)
283
- assert_event_loop_thread!
284
- key = agent_id.to_s
285
- @lifecycle_mutex.synchronize do
286
- current = @agent_admissions.fetch(key) do
287
- raise Phronomy::Error, "Agent #{key.inspect} has no Runtime admission"
288
- end
289
- unless current.owner_token.equal?(owner_token)
290
- raise Phronomy::Error, "stale Agent admission recovery update for #{key.inspect}"
291
- end
292
- @agent_admissions[key] = AgentAdmission.new(
293
- agent_id: current.agent_id,
294
- owner_token: current.owner_token,
295
- execution_id: current.execution_id,
296
- state: :recovery_required
297
- )
298
- @idle_cond.broadcast if runtime_idle_locked?
299
- end
300
- true
301
- end
302
-
303
- # Owner-aware release. Pre-durable failures release by owner_token; durable
304
- # terminal outcomes release by execution_id.
305
- # @api private
306
- def release_agent_execution_admission(agent_id, owner_token: nil, execution_id: nil)
307
- assert_event_loop_thread!
308
- key = agent_id.to_s
309
- @lifecycle_mutex.synchronize do
310
- current = @agent_admissions[key]
311
- next false unless current
312
-
313
- authoritative = if execution_id
314
- current.execution_id.to_s == execution_id.to_s
315
- elsif owner_token
316
- current.owner_token.equal?(owner_token)
317
- else
318
- false
319
- end
320
- next false unless authoritative
259
+ def __route_execution_event(receiver)
260
+ event = nil
261
+ queued_depth = nil
262
+ accepted = @lifecycle_mutex.synchronize do
263
+ next false unless accepting_events? && @execution_receivers.value?(receiver)
264
+ event = yield
265
+ next false unless event && @admitted_fsm_session_ids.include?(event.target_id)
321
266
 
322
- @agent_admissions.delete(key)
323
- @idle_cond.broadcast if runtime_idle_locked?
267
+ queued_depth = enqueue([event, monotonic_nanoseconds])
324
268
  true
325
269
  end
326
- end
327
-
328
- # Process-local read-only owner lookup. Mutable Agent execution state never
329
- # crosses this boundary; external callers receive only routing/ownership data.
330
- def agent_execution_owner(execution_id)
331
- key = execution_id.to_s
332
- @lifecycle_mutex.synchronize do
333
- state = @agent_executions[key]
334
- next nil unless state
335
-
336
- AgentExecutionOwner.new(
337
- execution_id: key.freeze,
338
- agent: state.agent,
339
- coordinator: state.coordinator,
340
- status: state.execution.status
341
- )
342
- end
343
- end
344
-
345
- # EventLoop-only accessors below form the live Agent execution authority.
346
- # Offload workers receive operation-specific immutable snapshots instead.
347
- # @api private
348
- def agent_execution_state(execution_id)
349
- assert_event_loop_thread!
350
- @agent_executions[execution_id.to_s]
351
- end
352
-
353
- # @api private
354
- def install_agent_execution(
355
- execution_id:,
356
- agent:,
357
- coordinator:,
358
- execution:,
359
- runtime_projection:,
360
- base_manifest:,
361
- invocation:,
362
- fsm_session_id:
363
- )
364
- assert_event_loop_thread!
365
- key = execution_id.to_s
366
- state = AgentExecutionState.new(
367
- execution_id: key.freeze,
368
- agent: agent,
369
- coordinator: coordinator,
370
- execution: execution,
371
- runtime_projection: runtime_projection,
372
- base_manifest: base_manifest,
373
- invocation: invocation,
374
- fsm_session_id: fsm_session_id&.to_s&.freeze
375
- )
376
- @lifecycle_mutex.synchronize do
377
- if @agent_executions.key?(key)
378
- raise Phronomy::Error, "Agent execution #{key.inspect} is already live"
379
- end
380
- @agent_executions[key] = state
381
- end
382
- state
383
- end
384
-
385
- # @api private
386
- def replace_agent_execution(
387
- execution_id,
388
- execution: UNSET,
389
- runtime_projection: UNSET,
390
- invocation: UNSET,
391
- fsm_session_id: UNSET
392
- )
393
- assert_event_loop_thread!
394
- key = execution_id.to_s
395
- @lifecycle_mutex.synchronize do
396
- current = @agent_executions.fetch(key) do
397
- raise Phronomy::Error, "Agent execution #{key.inspect} is not live"
398
- end
399
- updated = AgentExecutionState.new(
400
- execution_id: current.execution_id,
401
- agent: current.agent,
402
- coordinator: current.coordinator,
403
- execution: execution.equal?(UNSET) ? current.execution : execution,
404
- runtime_projection: runtime_projection.equal?(UNSET) ?
405
- current.runtime_projection : runtime_projection,
406
- base_manifest: current.base_manifest,
407
- invocation: invocation.equal?(UNSET) ? current.invocation : invocation,
408
- fsm_session_id: fsm_session_id.equal?(UNSET) ?
409
- current.fsm_session_id : fsm_session_id&.to_s&.freeze
410
- )
411
- @agent_executions[key] = updated
412
- updated
413
- end
414
- end
415
-
416
- # Registers a caller-facing TaskResult that observes the authoritative terminal
417
- # outcome of one logical Agent execution. Waiters are Runtime-only and are
418
- # never persisted or rehydrated.
419
- # @api private
420
- def register_agent_completion_waiter(execution_id, task)
421
- assert_event_loop_thread!
422
- unless task.is_a?(Phronomy::TaskResult)
423
- raise ArgumentError, "Agent completion waiter must be a Phronomy::TaskResult"
424
- end
425
-
426
- key = execution_id.to_s
427
- @lifecycle_mutex.synchronize do
428
- waiters = @agent_completion_waiters[key]
429
- waiters << task unless waiters.include?(task)
430
- end
431
- task
432
- end
433
-
434
- # Atomically detaches all process-local completion waiters at authoritative
435
- # terminal delivery. A fallback TaskResult is included for pre-install terminal
436
- # paths that never acquired a live execution directory entry.
437
- # @api private
438
- def take_agent_completion_waiters(execution_id, fallback: nil)
439
- assert_event_loop_thread!
440
- key = execution_id.to_s
441
- @lifecycle_mutex.synchronize do
442
- waiters = @agent_completion_waiters.delete(key) || []
443
- waiters << fallback if fallback && !waiters.include?(fallback)
444
- waiters
445
- end
446
- end
447
-
448
- # Registers one execution-owned asynchronous operation for physical
449
- # quiescence supervision. OffloadPool tasks expose a private physical
450
- # completion signal; custom asynchronous handles are required to make their
451
- # ordinary completion mean that no residual execution-affecting work remains.
452
- # @api private
453
- def supervise_agent_operation(execution_id, operation)
454
- assert_event_loop_thread!
455
- key = execution_id.to_s
456
- unless operation.respond_to?(:on_complete)
457
- raise ArgumentError, "supervised operation must expose on_complete"
458
- end
459
-
460
- physically_done = if operation.respond_to?(:physical_complete?)
461
- operation.physical_complete?
462
- elsif operation.respond_to?(:done?)
463
- operation.done?
464
- else
465
- false
466
- end
467
- return operation if physically_done
468
-
469
- token = Object.new.freeze
470
- @lifecycle_mutex.synchronize do
471
- unless @agent_executions.key?(key)
472
- raise Phronomy::Error, "Agent execution #{key.inspect} is not live"
473
- end
474
- @agent_inflight_work[key][token] = true
475
- end
476
-
477
- callback = lambda do
478
- accepted = post(
479
- Phronomy::Event.new(
480
- type: :agent_physical_work_completed,
481
- target_id: SYSTEM_CHANNEL_ID,
482
- payload: {execution_id: key, token: token}.freeze
483
- )
484
- )
485
- unless accepted
486
- Phronomy.configuration.logger&.warn(
487
- "[Phronomy] EventLoop rejected physical-completion delivery for #{key}"
488
- )
489
- end
490
- end
491
-
492
- if operation.respond_to?(:on_physical_complete)
493
- operation.on_physical_complete(&callback)
494
- else
495
- operation.on_complete { |_value, _error| callback.call }
496
- end
497
- operation
498
- end
499
-
500
- # @api private
501
- def agent_execution_quiescent?(execution_id)
502
- assert_event_loop_thread!
503
- key = execution_id.to_s
504
- @lifecycle_mutex.synchronize do
505
- work = @agent_inflight_work.fetch(key, nil)
506
- work.nil? || work.empty?
507
- end
508
- end
270
+ return false unless accepted
509
271
 
510
- # Holds exactly one terminal continuation while cancellation/deadline has
511
- # revoked result authority but execution-owned physical work is still live.
512
- # @api private
513
- def defer_agent_terminal_until_quiescent(execution_id, command)
514
- assert_event_loop_thread!
515
- key = execution_id.to_s
516
- @lifecycle_mutex.synchronize do
517
- if @agent_deferred_terminals.key?(key)
518
- raise Phronomy::Error, "Agent execution #{key.inspect} already has a deferred terminal"
519
- end
520
- @agent_deferred_terminals[key] = command
521
- end
272
+ check_queue_backlog(queued_depth, event)
522
273
  true
523
274
  end
524
275
 
525
- # @api private
526
- def agent_inflight_work_count(execution_id)
527
- key = execution_id.to_s
528
- @lifecycle_mutex.synchronize do
529
- (@agent_inflight_work.fetch(key, nil) || {}).size
530
- end
531
- end
532
-
533
- # @api private
534
- def release_agent_execution(execution_id)
535
- assert_event_loop_thread!
536
- key = execution_id.to_s
537
- @lifecycle_mutex.synchronize do
538
- work = @agent_inflight_work.fetch(key, nil)
539
- unless work.nil? || work.empty?
540
- raise Phronomy::Error,
541
- "cannot release non-quiescent Agent execution #{key.inspect}"
542
- end
543
- if @agent_deferred_terminals.key?(key)
544
- raise Phronomy::Error,
545
- "cannot release Agent execution #{key.inspect} with deferred terminal work"
546
- end
547
- @agent_inflight_work.delete(key)
548
- @agent_executions.delete(key)
549
- end
550
- end
551
-
552
276
  # @api private
553
277
  def fsm_session_state(fsm_session_id)
554
278
  assert_event_loop_thread!
555
279
  @fsms[fsm_session_id.to_s]&.current_state
556
280
  end
557
281
 
558
- # Reserves one logical Workflow execution segment before durable load or
559
- # hydration. The owner token is independent from any later FSMSession id.
560
- # @api private
561
- def admit_workflow(workflow_instance_id, owner_token:)
562
- assert_event_loop_thread!
563
- key = workflow_instance_id.to_s
564
- raise ArgumentError, "workflow_instance_id must not be empty" if key.empty?
565
- raise ArgumentError, "owner_token is required" unless owner_token
566
-
567
- @lifecycle_mutex.synchronize do
568
- ensure_accepting_registrations!
569
- if @workflow_admissions.key?(key)
570
- raise Phronomy::Error,
571
- "Workflow instance #{key.inspect} already has a live execution segment"
572
- end
573
- @workflow_admissions[key] = WorkflowAdmission.new(
574
- workflow_instance_id: key.freeze,
575
- owner_token: owner_token,
576
- fsm_session_id: nil,
577
- state: :admitting
578
- )
579
- end
580
- true
581
- end
582
-
583
- # Binds the concrete routing identity after admission and durable hydration.
584
- # @api private
585
- def bind_workflow_session(workflow_instance_id, owner_token:, fsm_session_id:)
586
- assert_event_loop_thread!
587
- key = workflow_instance_id.to_s
588
- fsm_key = fsm_session_id.to_s
589
- raise ArgumentError, "fsm_session_id must not be empty" if fsm_key.empty?
590
-
591
- @lifecycle_mutex.synchronize do
592
- current = @workflow_admissions.fetch(key) do
593
- raise Phronomy::Error, "Workflow instance #{key.inspect} has no Runtime admission"
594
- end
595
- unless current.owner_token.equal?(owner_token) && current.fsm_session_id.nil?
596
- raise Phronomy::Error, "stale Workflow admission bind for #{key.inspect}"
597
- end
598
- @workflow_admissions[key] = WorkflowAdmission.new(
599
- workflow_instance_id: current.workflow_instance_id,
600
- owner_token: current.owner_token,
601
- fsm_session_id: fsm_key.freeze,
602
- state: :executing
603
- )
604
- end
605
- true
606
- end
607
-
608
- # @api private
609
- def mark_workflow_admission(workflow_instance_id, owner_token:, state:)
610
- assert_event_loop_thread!
611
- key = workflow_instance_id.to_s
612
- next_state = state.to_sym
613
- unless %i[executing persisting_terminal recovery_required].include?(next_state)
614
- raise ArgumentError, "unsupported Workflow admission state: #{next_state.inspect}"
615
- end
616
-
617
- @lifecycle_mutex.synchronize do
618
- current = @workflow_admissions.fetch(key) do
619
- raise Phronomy::Error, "Workflow instance #{key.inspect} has no Runtime admission"
620
- end
621
- unless current.owner_token.equal?(owner_token)
622
- raise Phronomy::Error, "stale Workflow admission update for #{key.inspect}"
623
- end
624
- @workflow_admissions[key] = WorkflowAdmission.new(
625
- workflow_instance_id: current.workflow_instance_id,
626
- owner_token: current.owner_token,
627
- fsm_session_id: current.fsm_session_id,
628
- state: next_state
629
- )
630
- @idle_cond.broadcast if runtime_idle_locked?
631
- end
632
- true
633
- end
634
-
635
- # Owner-aware release. A competing or stale attempt cannot release the
636
- # current Workflow execution segment.
637
- # @api private
638
- def release_workflow(workflow_instance_id, owner_token:)
639
- assert_event_loop_thread!
640
- key = workflow_instance_id.to_s
641
- @lifecycle_mutex.synchronize do
642
- current = @workflow_admissions[key]
643
- next false unless current&.owner_token&.equal?(owner_token)
644
-
645
- @workflow_admissions.delete(key)
646
- @idle_cond.broadcast if runtime_idle_locked?
647
- true
648
- end
649
- end
650
-
651
- # Read-only diagnostics used by internal tests and routing assertions.
652
- def workflow_admission_owner(workflow_instance_id)
653
- @lifecycle_mutex.synchronize do
654
- @workflow_admissions[workflow_instance_id.to_s]&.owner_token
655
- end
656
- end
657
-
658
- def workflow_admission_fsm_session_id(workflow_instance_id)
659
- @lifecycle_mutex.synchronize do
660
- @workflow_admissions[workflow_instance_id.to_s]&.fsm_session_id
661
- end
662
- end
663
-
664
- def workflow_admission_state(workflow_instance_id)
665
- @lifecycle_mutex.synchronize do
666
- @workflow_admissions[workflow_instance_id.to_s]&.state
667
- end
668
- end
669
-
670
- def post_to_workflow(workflow_instance_id:, event:, payload: nil)
671
- queued_depth = nil
672
- posted_event = nil
673
- accepted = @lifecycle_mutex.synchronize do
674
- next false unless accepting_events?
675
-
676
- admission = @workflow_admissions[workflow_instance_id.to_s]
677
- next false unless admission&.state == :executing
678
- fsm_session_id = admission.fsm_session_id
679
- next false unless fsm_session_id
680
- next false unless @admitted_fsm_session_ids.include?(fsm_session_id)
681
-
682
- posted_event = Phronomy::Event.new(
683
- type: event.to_sym,
684
- target_id: fsm_session_id,
685
- payload: payload
686
- )
687
- queued_depth = enqueue([posted_event, monotonic_nanoseconds])
688
- true
689
- end
690
- return false unless accepted
691
-
692
- check_queue_backlog(queued_depth, posted_event)
693
- true
694
- end
695
-
696
282
  def wake
697
283
  @queue.push(WAKE)
698
284
  true
@@ -770,6 +356,23 @@ module Phronomy
770
356
 
771
357
  private
772
358
 
359
+ def dispatch_execution(token)
360
+ delivery = @lifecycle_mutex.synchronize { @execution_deliveries[token] }
361
+ return unless delivery
362
+
363
+ @dispatching_delivery = delivery
364
+ delivery[:receiver].deliver(delivery[:message])
365
+ rescue => error
366
+ complete_waiter(delivery[:completion], error) if delivery
367
+ raise
368
+ ensure
369
+ @dispatching_delivery = nil
370
+ @lifecycle_mutex.synchronize do
371
+ @execution_deliveries.delete(token)
372
+ @idle_cond.broadcast if runtime_idle_locked?
373
+ end
374
+ end
375
+
773
376
  def run_loop
774
377
  loop do
775
378
  fire_due_timers
@@ -824,6 +427,7 @@ module Phronomy
824
427
  session = @fsms.delete(fsm_session_id)
825
428
  waiter = @waiting.delete(fsm_session_id)
826
429
  decrement_outstanding if session
430
+ @lifecycle_mutex.synchronize { @session_receivers.delete(fsm_session_id) }
827
431
  complete_waiter(waiter, event.payload.fetch(:result))
828
432
  when :start
829
433
  session = event.payload.fetch(:session)
@@ -831,21 +435,14 @@ module Phronomy
831
435
  @fsms[session.id] = session
832
436
  @waiting[session.id] = waiter if waiter
833
437
  session.start
834
- when :agent_control, :agent_terminal_ready
835
- # :agent_terminal_ready is retained as an internal migration-compatible
836
- # dispatch name; ACS-11 emits the operation-neutral :agent_control event.
837
- cmd = event.payload.fetch(:command)
838
- cmd.coordinator.deliver_on_event_loop(cmd)
839
- when :agent_physical_work_completed
840
- complete_agent_physical_work(event.payload)
841
- when :workflow_control
842
- cmd = event.payload.fetch(:command)
843
- cmd.runner.deliver_on_event_loop(cmd)
438
+ when :execution_control
439
+ dispatch_execution(event.payload)
844
440
  when :recovery_required
845
441
  fsm_session_id = event.payload.fetch(:fsm_session_id)
846
442
  session = @fsms.delete(fsm_session_id)
847
443
  decrement_outstanding if session
848
- mark_workflow_recovery_required_for_session(fsm_session_id)
444
+ receiver = @lifecycle_mutex.synchronize { @session_receivers.delete(fsm_session_id) }
445
+ receiver&.session_retired(fsm_session_id, reason: :recovery_required)
849
446
  end
850
447
  end
851
448
 
@@ -856,42 +453,6 @@ module Phronomy
856
453
  event.payload.key?(:fsm_session_id)
857
454
  end
858
455
 
859
- def complete_agent_physical_work(payload)
860
- key = payload.fetch(:execution_id).to_s
861
- token = payload.fetch(:token)
862
- deferred = @lifecycle_mutex.synchronize do
863
- work = @agent_inflight_work.fetch(key, nil)
864
- next nil unless work&.delete(token)
865
-
866
- if work.empty?
867
- @agent_inflight_work.delete(key)
868
- command = @agent_deferred_terminals.delete(key)
869
- @idle_cond.broadcast if runtime_idle_locked?
870
- command
871
- end
872
- end
873
- deferred&.coordinator&.deliver_on_event_loop(deferred)
874
- true
875
- end
876
-
877
- def mark_workflow_recovery_required_for_session(fsm_session_id)
878
- @lifecycle_mutex.synchronize do
879
- key, admission = @workflow_admissions.find do |_workflow_instance_id, candidate|
880
- candidate.fsm_session_id == fsm_session_id.to_s
881
- end
882
- return false unless admission
883
-
884
- @workflow_admissions[key] = WorkflowAdmission.new(
885
- workflow_instance_id: admission.workflow_instance_id,
886
- owner_token: admission.owner_token,
887
- fsm_session_id: nil,
888
- state: :recovery_required
889
- )
890
- @idle_cond.broadcast if runtime_idle_locked?
891
- end
892
- true
893
- end
894
-
895
456
  def begin_stopping_if_idle
896
457
  @lifecycle_mutex.synchronize do
897
458
  return false unless @state == :draining
@@ -916,22 +477,17 @@ module Phronomy
916
477
  @waiting.values.each { |waiter| complete_waiter(waiter, error) }
917
478
  @waiting.clear
918
479
  @fsms.clear
919
- completion_waiters = @lifecycle_mutex.synchronize do
920
- waiters = @agent_completion_waiters.values.flatten
921
- @agent_completion_waiters.clear
922
- @agent_inflight_work.clear
923
- @agent_deferred_terminals.clear
924
- waiters
925
- end
926
- completion_waiters.each { |waiter| complete_waiter(waiter, error) }
927
- @lifecycle_mutex.synchronize do
480
+ deliveries = @lifecycle_mutex.synchronize do
481
+ pending = @execution_deliveries.values
482
+ @execution_deliveries.clear
483
+ @session_receivers.clear
928
484
  @admitted_fsm_session_ids.clear
929
- @workflow_admissions.clear
930
- @agent_admissions.clear
931
- @agent_executions.clear
932
485
  @outstanding_sessions = 0
933
486
  @idle_cond.broadcast
487
+ pending
934
488
  end
489
+ deliveries.each { |delivery| complete_waiter(delivery[:completion], error) }
490
+ shutdown_receivers(error)
935
491
  end
936
492
 
937
493
  def drain_queued_items
@@ -948,9 +504,6 @@ module Phronomy
948
504
  @lifecycle_mutex.synchronize do
949
505
  @state = :failed
950
506
  @admitted_fsm_session_ids.clear
951
- @workflow_admissions.clear
952
- @agent_admissions.clear
953
- @agent_executions.clear
954
507
  @idle_cond.broadcast
955
508
  end
956
509
  cleanup_abandoned_work(error)
@@ -982,23 +535,8 @@ module Phronomy
982
535
  end
983
536
 
984
537
  def runtime_idle_locked?
985
- @outstanding_sessions.zero? &&
986
- @agent_inflight_work.values.all?(&:empty?) &&
987
- @agent_deferred_terminals.empty? &&
988
- !workflow_admission_transition_in_progress_locked? &&
989
- !agent_admission_transition_in_progress_locked?
990
- end
991
-
992
- def workflow_admission_transition_in_progress_locked?
993
- @workflow_admissions.values.any? do |admission|
994
- %i[admitting executing persisting_terminal].include?(admission.state)
995
- end
996
- end
997
-
998
- def agent_admission_transition_in_progress_locked?
999
- @agent_admissions.values.any? do |admission|
1000
- %i[admitting executing resuming cancelling terminalizing].include?(admission.state)
1001
- end
538
+ @outstanding_sessions.zero? && @execution_deliveries.empty? &&
539
+ @execution_receivers.values.all?(&:idle?)
1002
540
  end
1003
541
 
1004
542
  def join_until(deadline)
@@ -1010,28 +548,24 @@ module Phronomy
1010
548
  end
1011
549
 
1012
550
  def finalize_terminated(status)
1013
- pending_waiters = @lifecycle_mutex.synchronize do
1014
- waiters = @agent_completion_waiters.values.flatten
1015
- @agent_completion_waiters.clear
1016
- @agent_inflight_work.clear
1017
- @agent_deferred_terminals.clear
551
+ @lifecycle_mutex.synchronize do
1018
552
  @state = :terminated
1019
553
  @admitted_fsm_session_ids.clear
1020
- @workflow_admissions.clear
1021
- @agent_admissions.clear
1022
- @agent_executions.clear
1023
- @thread = nil unless @thread&.alive?
554
+ @session_receivers.clear
555
+ @thread = nil
1024
556
  @idle_cond.broadcast
1025
- waiters
1026
557
  end
1027
- if pending_waiters.any?
1028
- error = Phronomy::ExecutionRehydrationRequiredError.new(
1029
- "Runtime terminated while Agent execution remained nonterminal; " \
1030
- "process-local TaskResult handles are not rehydrated"
1031
- )
1032
- pending_waiters.each { |waiter| complete_waiter(waiter, error) }
558
+ shutdown_receivers(nil)
559
+ @receiver_shutdown_error ? :failed : status
560
+ end
561
+
562
+ def shutdown_receivers(error)
563
+ @execution_receivers.each_value do |receiver|
564
+ receiver.shutdown(error: error)
565
+ rescue => caught
566
+ @receiver_shutdown_error ||= caught
1033
567
  end
1034
- status
568
+ @runtime.__event_loop_failed(@receiver_shutdown_error) if @receiver_shutdown_error
1035
569
  end
1036
570
 
1037
571
  def complete_waiter(waiter, payload)