phronomy 0.27.0 → 0.29.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 (517) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +9 -9
  3. data/CHANGELOG.md +52 -0
  4. data/CONTRIBUTING.md +8 -8
  5. data/README.md +28 -7
  6. data/benchmark/bench_agent_invoke.rb +25 -50
  7. data/benchmark/bench_context_assembler.rb +14 -11
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/benchmark/bench_tool_schema.rb +1 -1
  10. data/docs/application-recipes.md +229 -0
  11. data/docs/architecture/agent-chat-and-state-ownership.md +7 -1
  12. data/docs/architecture/agent-transition-ownership.md +13 -89
  13. data/docs/architecture/backend-groups.md +152 -0
  14. data/docs/architecture/before-llm-input.md +3 -3
  15. data/docs/architecture/context-management.md +1 -1
  16. data/docs/architecture/dx05-agent-scope.md +119 -0
  17. data/docs/architecture/dx05-workflow-task-state.md +134 -0
  18. data/docs/architecture/error-api-review.md +245 -0
  19. data/docs/architecture/execution-abstraction-review.md +88 -0
  20. data/docs/architecture/execution-boundaries.md +28 -0
  21. data/docs/architecture/execution-metadata-and-values.md +6 -0
  22. data/docs/architecture/multi-agent-handoff.md +6 -5
  23. data/docs/architecture/persistence.md +27 -9
  24. data/docs/architecture/r8-unit1.md +104 -0
  25. data/docs/architecture/r8-unit10.md +72 -0
  26. data/docs/architecture/r8-unit11.md +71 -0
  27. data/docs/architecture/r8-unit12.md +67 -0
  28. data/docs/architecture/r8-unit13.md +61 -0
  29. data/docs/architecture/r8-unit14.md +72 -0
  30. data/docs/architecture/r8-unit15.md +65 -0
  31. data/docs/architecture/r8-unit16.md +54 -0
  32. data/docs/architecture/r8-unit17.md +73 -0
  33. data/docs/architecture/r8-unit18.md +92 -0
  34. data/docs/architecture/r8-unit19.md +61 -0
  35. data/docs/architecture/r8-unit2.md +55 -0
  36. data/docs/architecture/r8-unit20.md +51 -0
  37. data/docs/architecture/r8-unit21.md +79 -0
  38. data/docs/architecture/r8-unit22.md +74 -0
  39. data/docs/architecture/r8-unit23.md +81 -0
  40. data/docs/architecture/r8-unit3.md +130 -0
  41. data/docs/architecture/r8-unit4.md +79 -0
  42. data/docs/architecture/r8-unit5.md +115 -0
  43. data/docs/architecture/r8-unit6.md +71 -0
  44. data/docs/architecture/r8-unit7.md +70 -0
  45. data/docs/architecture/r8-unit8.md +78 -0
  46. data/docs/architecture/r8-unit9.md +73 -0
  47. data/docs/architecture/refactoring-closure.md +10 -1
  48. data/docs/architecture/remaining-refactoring-plan.md +16 -0
  49. data/docs/architecture/storage-execution-boundaries.md +54 -0
  50. data/docs/architecture/tool-schema-recording-gap.md +6 -0
  51. data/docs/architecture/tracing.md +1 -1
  52. data/docs/architecture/workflow-terminal-ownership-design.md +2 -2
  53. data/docs/architecture.md +24 -11
  54. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  55. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  56. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +2 -0
  57. data/docs/decisions/031-durable-multi-agent-coordination.md +2 -0
  58. data/docs/decisions/032-storage-backend-composition.md +5 -0
  59. data/docs/decisions/034-handoff-runner-coordination-ownership.md +2 -0
  60. data/docs/decisions/035-tool-executor-capability-ownership.md +1 -1
  61. data/docs/decisions/038-responsibility-based-source-layout.md +14 -0
  62. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +8 -0
  63. data/docs/decisions/040-configuration-default-composition.md +12 -1
  64. data/docs/decisions/045-worker-input-restriction-ownership.md +4 -0
  65. data/docs/decisions/051-execution-outcome-worker-ownership.md +2 -0
  66. data/docs/decisions/056-workflow-terminal-policy-ownership.md +1 -1
  67. data/docs/decisions/058-neutral-storage-primitives.md +5 -0
  68. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  69. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  70. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  71. data/docs/decisions/062-content-store-service.md +52 -0
  72. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  73. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  74. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  75. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  76. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  77. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  78. data/docs/decisions/README.md +18 -1
  79. data/docs/execution-boundaries-preview.md +45 -0
  80. data/docs/failure-reason-codes.md +45 -0
  81. data/docs/features.md +12 -4
  82. data/docs/getting-started.md +52 -48
  83. data/docs/migrations/0.22.md +2 -2
  84. data/docs/migrations/durable-semantic-coordination-v2.md +2 -0
  85. data/docs/migrations/handoff-runner-multi-agent.md +2 -0
  86. data/docs/migrations/persistence-failure-contracts.md +67 -0
  87. data/docs/migrations/r8-unit10.md +36 -0
  88. data/docs/migrations/r8-unit11.md +33 -0
  89. data/docs/migrations/r8-unit12.md +31 -0
  90. data/docs/migrations/r8-unit13.md +36 -0
  91. data/docs/migrations/r8-unit14.md +31 -0
  92. data/docs/migrations/r8-unit15.md +31 -0
  93. data/docs/migrations/r8-unit16.md +26 -0
  94. data/docs/migrations/r8-unit17.md +28 -0
  95. data/docs/migrations/r8-unit18.md +29 -0
  96. data/docs/migrations/r8-unit19.md +28 -0
  97. data/docs/migrations/r8-unit20.md +24 -0
  98. data/docs/migrations/r8-unit21.md +28 -0
  99. data/docs/migrations/r8-unit22.md +29 -0
  100. data/docs/migrations/r8-unit23.md +20 -0
  101. data/docs/migrations/r8-unit4.md +67 -0
  102. data/docs/migrations/r8-unit5.md +75 -0
  103. data/docs/migrations/r8-unit6.md +109 -0
  104. data/docs/migrations/r8-unit7.md +31 -0
  105. data/docs/migrations/r8-unit8.md +33 -0
  106. data/docs/migrations/r8-unit9.md +28 -0
  107. data/docs/migrations/storage-async-client.md +103 -0
  108. data/docs/migrations/vector-async-clients.md +110 -0
  109. data/docs/ordered-event-delivery.md +54 -0
  110. data/docs/persistence-backends.md +38 -21
  111. data/docs/runtime-and-concurrency.md +26 -1
  112. data/docs/workflow-completion.md +92 -0
  113. data/gemfiles/http_minimum.gemfile +5 -0
  114. data/lib/phronomy/agent/admission.rb +78 -0
  115. data/lib/phronomy/agent/{lifecycle_contract/agent_already_exists_error.rb → agent_already_exists_error.rb} +1 -1
  116. data/lib/phronomy/agent/{lifecycle_contract/agent_busy_error.rb → agent_busy_error.rb} +1 -1
  117. data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
  118. data/lib/phronomy/agent/{lifecycle_contract/agent_purged_error.rb → agent_purged_error.rb} +1 -1
  119. data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
  120. data/lib/phronomy/agent/api/agent.rb +13 -0
  121. data/lib/phronomy/{recovery → agent/api}/execution_rehydration_required_error.rb +1 -1
  122. data/lib/phronomy/agent/async_event_api.rb +132 -103
  123. data/lib/phronomy/agent/base.rb +146 -113
  124. data/lib/phronomy/agent/before_llm_input.rb +70 -0
  125. data/lib/phronomy/agent/composition/run_once.rb +1 -1
  126. data/lib/phronomy/agent/{context_assembly/context_candidate_resolver.rb → context_candidate_resolver.rb} +2 -2
  127. data/lib/phronomy/agent/{context_assembly/context_assembler.rb → context_preparation.rb} +35 -221
  128. data/lib/phronomy/agent/control_request.rb +21 -0
  129. data/lib/phronomy/agent/execution/agent_execution.rb +1 -1
  130. data/lib/phronomy/agent/execution/agent_invocation.rb +27 -53
  131. data/lib/phronomy/agent/execution/approval_resume_commit.rb +1 -1
  132. data/lib/phronomy/agent/execution/dispatch_preparation.rb +17 -127
  133. data/lib/phronomy/agent/execution/exact_execution.rb +55 -32
  134. data/lib/phronomy/agent/execution/execution_cancellation.rb +8 -4
  135. data/lib/phronomy/agent/execution/execution_coordinator.rb +229 -407
  136. data/lib/phronomy/agent/execution/execution_metadata.rb +1 -0
  137. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +71 -127
  138. data/lib/phronomy/agent/execution/execution_registry.rb +6 -2
  139. data/lib/phronomy/agent/execution/execution_session_runner.rb +22 -27
  140. data/lib/phronomy/agent/execution/initial_preparation.rb +19 -120
  141. data/lib/phronomy/agent/execution/{agent_invocation_session_builder.rb → invocation_actions.rb} +54 -194
  142. data/lib/phronomy/agent/execution/invocation_transitions.rb +39 -1
  143. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +8 -11
  144. data/lib/phronomy/agent/execution_change.rb +161 -0
  145. data/lib/phronomy/agent/execution_environment.rb +36 -0
  146. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  147. data/lib/phronomy/agent/execution_observation.rb +35 -0
  148. data/lib/phronomy/agent/filterable.rb +188 -0
  149. data/lib/phronomy/agent/handoff/{handoff_context.rb → transfer_context.rb} +4 -4
  150. data/lib/phronomy/agent/handoff/{handoff_projection.rb → transfer_projection.rb} +14 -33
  151. data/lib/phronomy/{llm_contract/transport_error.rb → agent/handoff_error.rb} +1 -1
  152. data/lib/phronomy/agent/journal/journal_projection.rb +1 -1
  153. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  154. data/lib/phronomy/agent/lifecycle/agent_root.rb +1 -1
  155. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
  156. data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
  157. data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
  158. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +117 -31
  159. data/lib/phronomy/agent/lifecycle/scope.rb +198 -0
  160. data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
  161. data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
  162. data/lib/phronomy/agent/persistence/agent_repository.rb +29 -21
  163. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  164. data/lib/phronomy/agent/persistence/codec.rb +1 -21
  165. data/lib/phronomy/agent/persistence/execution_repository.rb +57 -41
  166. data/lib/phronomy/agent/persistence/journal_repository.rb +27 -18
  167. data/lib/phronomy/agent/persistence/records.rb +33 -0
  168. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  169. data/lib/phronomy/agent/persistence/storage_schema.rb +7 -2
  170. data/lib/phronomy/agent/persistence/watermark.rb +13 -11
  171. data/lib/phronomy/agent/recovery/invocation_restorer.rb +13 -21
  172. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +13 -12
  173. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +34 -34
  174. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +33 -39
  175. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +28 -25
  176. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  177. data/lib/phronomy/agent/recovery/recovery_support.rb +10 -19
  178. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  179. data/lib/phronomy/agent/retention.rb +17 -0
  180. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  181. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  182. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  183. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  184. data/lib/phronomy/agent/{context_assembly/ruby_llm_materializer.rb → runtime_input.rb} +18 -35
  185. data/lib/phronomy/agent/{context_assembly/saved_context_reader.rb → saved_context_reader.rb} +6 -4
  186. data/lib/phronomy/agent/settings.rb +47 -0
  187. data/lib/phronomy/agent/{context_assembly/state_writer.rb → state_writer.rb} +4 -3
  188. data/lib/phronomy/agent/store.rb +253 -0
  189. data/lib/phronomy/agent/{lifecycle_contract/stream_callback_error.rb → stream_callback_error.rb} +3 -3
  190. data/lib/phronomy/agent/tool_execution/tool_binding.rb +4 -64
  191. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +50 -219
  192. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  193. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  194. data/lib/phronomy/common/error.rb +65 -1
  195. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  196. data/lib/phronomy/content_store/base.rb +7 -2
  197. data/lib/phronomy/content_store/storage_schema.rb +1 -0
  198. data/lib/phronomy/content_store/stored_contents.rb +2 -1
  199. data/lib/phronomy/context/assembly.rb +195 -0
  200. data/lib/phronomy/{llm_contract/context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +3 -1
  201. data/lib/phronomy/context/candidate.rb +51 -0
  202. data/lib/phronomy/context/constraint.rb +47 -0
  203. data/lib/phronomy/{agent/context_contract → context}/context_plan.rb +1 -1
  204. data/lib/phronomy/{agent/context_contract → context}/context_plan_validator.rb +3 -3
  205. data/lib/phronomy/{agent/context_contract → context}/context_policy.rb +1 -1
  206. data/lib/phronomy/{agent/context_contract → context}/context_policy_input.rb +1 -1
  207. data/lib/phronomy/{agent/context_assembly → context}/context_policy_input_builder.rb +1 -1
  208. data/lib/phronomy/context/default_policy.rb +140 -0
  209. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  210. data/lib/phronomy/{agent/context_contract → context}/llm_input_build_context.rb +1 -1
  211. data/lib/phronomy/{agent/context_contract → context}/llm_input_manifest.rb +19 -19
  212. data/lib/phronomy/{agent/context_contract → context}/llm_input_patch.rb +1 -1
  213. data/lib/phronomy/context/manifest_error.rb +10 -0
  214. data/lib/phronomy/context/prompt_template.rb +98 -0
  215. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  216. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  217. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  218. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  219. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  220. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  221. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  222. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  223. data/lib/phronomy/embeddings/authentication_error.rb +11 -0
  224. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +52 -0
  225. data/lib/phronomy/embeddings/base.rb +43 -0
  226. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  227. data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
  228. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  229. data/lib/phronomy/engine/concurrency/offload_pool.rb +36 -21
  230. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  231. data/lib/phronomy/engine/diagnostics.rb +1 -1
  232. data/lib/phronomy/engine/event_loop.rb +7 -7
  233. data/lib/phronomy/engine/execution_binding.rb +23 -0
  234. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  235. data/lib/phronomy/engine/fsm_protocol.rb +1 -6
  236. data/lib/phronomy/engine/fsm_session.rb +3 -3
  237. data/lib/phronomy/engine/metrics.rb +16 -14
  238. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  239. data/lib/phronomy/engine/runtime.rb +25 -12
  240. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  241. data/lib/phronomy/execution/async_operation.rb +41 -0
  242. data/lib/phronomy/{engine → execution}/blocking.rb +2 -2
  243. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +1 -1
  244. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  245. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  246. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  247. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  248. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  249. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  250. data/lib/phronomy/{engine → execution}/execution.rb +61 -27
  251. data/lib/phronomy/{engine → execution}/execution_cancellation_error.rb +2 -2
  252. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  253. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  254. data/lib/phronomy/{engine → execution}/execution_timeout_error.rb +2 -2
  255. data/lib/phronomy/{engine → execution}/invocation_context.rb +0 -15
  256. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  257. data/lib/phronomy/execution/runnable.rb +24 -0
  258. data/lib/phronomy/{engine → execution}/task_result.rb +9 -4
  259. data/lib/phronomy/execution/wait_policy.rb +24 -0
  260. data/lib/phronomy/filter/{contract/filter_block_error.rb → filter_block_error.rb} +3 -3
  261. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  262. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  263. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +2 -2
  264. data/lib/phronomy/generation/generator_verifier.rb +7 -7
  265. data/lib/phronomy/generation/low_confidence_error.rb +2 -2
  266. data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
  267. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  268. data/lib/phronomy/{llm_contract → llm_adapter}/authentication_error.rb +3 -1
  269. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  270. data/lib/phronomy/llm_adapter/base.rb +49 -68
  271. data/lib/phronomy/{llm_contract → llm_adapter}/context_length_error.rb +3 -1
  272. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  273. data/lib/phronomy/llm_adapter/message.rb +62 -0
  274. data/lib/phronomy/{llm_contract → llm_adapter}/rate_limit_error.rb +3 -1
  275. data/lib/phronomy/llm_adapter/request.rb +79 -0
  276. data/lib/phronomy/llm_adapter/response.rb +36 -0
  277. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  278. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  279. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  280. data/lib/phronomy/llm_adapter.rb +3 -16
  281. data/lib/phronomy/multi_agent/admission_registry.rb +1 -8
  282. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  283. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  284. data/lib/phronomy/{agent/handoff → multi_agent}/handoff.rb +2 -2
  285. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_capability_factory.rb +21 -2
  286. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  287. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_policy.rb +1 -1
  288. data/lib/phronomy/multi_agent/handoff_runner.rb +77 -100
  289. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_state.rb +1 -1
  290. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  291. data/lib/phronomy/multi_agent/persistence/codec.rb +20 -0
  292. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  293. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  294. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  295. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +60 -44
  296. data/lib/phronomy/multi_agent/persistence/team_repository.rb +29 -21
  297. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  298. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  299. data/lib/phronomy/multi_agent/shared_state.rb +2 -2
  300. data/lib/phronomy/multi_agent/store.rb +156 -0
  301. data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
  302. data/lib/phronomy/multi_agent/team_coordinator.rb +99 -77
  303. data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
  304. data/lib/phronomy/multi_agent/team_ownership_registry.rb +2 -12
  305. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  306. data/lib/phronomy/persistence/content_repository.rb +26 -0
  307. data/lib/phronomy/persistence/errors.rb +16 -0
  308. data/lib/phronomy/persistence/migration/initial_format_migration.rb +2 -2
  309. data/lib/phronomy/persistence/persistence.rb +23 -0
  310. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  311. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  312. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  313. data/lib/phronomy/persistence/transaction.rb +86 -0
  314. data/lib/phronomy/persistence_composition/storage_schema.rb +9 -4
  315. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  316. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  317. data/lib/phronomy/runtime_composition/agent_defaults.rb +5 -1
  318. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  319. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  320. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  321. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  322. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  323. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  324. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  325. data/lib/phronomy/storage/async/async_client.rb +60 -0
  326. data/lib/phronomy/storage/backend.rb +8 -2
  327. data/lib/phronomy/storage/condition_failed_error.rb +2 -2
  328. data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
  329. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  330. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  331. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +2 -2
  332. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +6 -6
  333. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +55 -52
  334. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +3 -3
  335. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +7 -7
  336. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +11 -11
  337. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +57 -51
  338. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +1 -1
  339. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +23 -22
  340. data/lib/phronomy/tool/authorization.rb +77 -0
  341. data/lib/phronomy/tool/base.rb +551 -9
  342. data/lib/phronomy/tool/call_request.rb +40 -0
  343. data/lib/phronomy/{agent/tool_execution/tool_definition_set.rb → tool/definition_set.rb} +7 -8
  344. data/lib/phronomy/tool/operation.rb +88 -0
  345. data/lib/phronomy/tool/schema.rb +214 -0
  346. data/lib/phronomy/tool/settings.rb +31 -0
  347. data/lib/phronomy/tool/tool_error.rb +7 -0
  348. data/lib/phronomy/tool/tool_executor.rb +58 -0
  349. data/lib/phronomy/tools/agent.rb +22 -90
  350. data/lib/phronomy/tools/mcp.rb +21 -14
  351. data/lib/phronomy/tools/vector_search.rb +4 -4
  352. data/lib/phronomy/tracing/automatic.rb +11 -6
  353. data/lib/phronomy/tracing/base.rb +36 -7
  354. data/lib/phronomy/tracing/observation.rb +37 -0
  355. data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
  356. data/lib/phronomy/tracing/settings.rb +36 -0
  357. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  358. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  359. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  360. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  361. data/lib/phronomy/vector_store/base.rb +118 -49
  362. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  363. data/lib/phronomy/vector_store.rb +2 -17
  364. data/lib/phronomy/version.rb +1 -1
  365. data/lib/phronomy/workflow/completion.rb +11 -0
  366. data/lib/phronomy/workflow/execution/workflow.rb +37 -9
  367. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  368. data/lib/phronomy/workflow/execution/workflow_context.rb +2 -2
  369. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +28 -0
  370. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +77 -8
  371. data/lib/phronomy/workflow/execution/workflow_runner.rb +105 -92
  372. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  373. data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
  374. data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
  375. data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
  376. data/lib/phronomy/workflow/persistence/state_repository.rb +28 -22
  377. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  378. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +61 -0
  379. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +158 -0
  380. data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
  381. data/lib/phronomy/workflow/{execution → runtime_binding}/workflow_terminal_policy.rb +4 -4
  382. data/lib/phronomy.rb +63 -16
  383. data/scripts/api_snapshot.rb +47 -15
  384. data/scripts/check_readme_ruby.rb +24 -29
  385. data/scripts/check_readme_runnable.rb +92 -81
  386. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  387. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  388. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  389. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  390. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  391. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  392. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  393. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  394. data/sig/phronomy/agent/admission.rbs +19 -0
  395. data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
  396. data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
  397. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  398. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  399. data/sig/phronomy/agent/store.rbs +73 -0
  400. data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
  401. data/sig/phronomy/agent.rbs +29 -135
  402. data/sig/phronomy/common.rbs +7 -0
  403. data/sig/phronomy/content_store/contracts.rbs +13 -0
  404. data/sig/phronomy/context.rbs +152 -0
  405. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  406. data/sig/phronomy/embeddings/base.rbs +23 -0
  407. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  408. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  409. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  410. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  411. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  412. data/sig/phronomy/engine/observability.rbs +13 -0
  413. data/sig/phronomy/execution/async_operation.rbs +7 -0
  414. data/sig/phronomy/execution/blocking.rbs +5 -0
  415. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  416. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  417. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  418. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  419. data/sig/phronomy/execution/errors.rbs +7 -0
  420. data/sig/phronomy/execution/execution.rbs +41 -0
  421. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  422. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  423. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  424. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  425. data/sig/phronomy/execution/task_result.rbs +29 -0
  426. data/sig/phronomy/extensions.rbs +1 -21
  427. data/sig/phronomy/filter/filter_block_error.rbs +6 -0
  428. data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
  429. data/sig/phronomy/handoff.rbs +10 -4
  430. data/sig/phronomy/integrations.rbs +14 -0
  431. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  432. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  433. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  434. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  435. data/sig/phronomy/llm_context_window.rbs +10 -0
  436. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  437. data/sig/phronomy/multi_agent/store.rbs +54 -0
  438. data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
  439. data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
  440. data/sig/phronomy/multi_agent.rbs +6 -3
  441. data/sig/phronomy/persistence/errors.rbs +18 -0
  442. data/sig/phronomy/persistence/migration.rbs +13 -0
  443. data/sig/phronomy/persistence/transaction.rbs +19 -0
  444. data/sig/phronomy/persistence.rbs +1 -99
  445. data/sig/phronomy/persistence_composition.rbs +19 -0
  446. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  447. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  448. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  449. data/sig/phronomy/{storage.rbs → storage/contracts.rbs} +4 -18
  450. data/sig/phronomy/tool.rbs +48 -4
  451. data/sig/phronomy/tracing/observation.rbs +7 -0
  452. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  453. data/sig/phronomy/vector_store/base.rbs +23 -0
  454. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +15 -0
  455. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  456. data/sig/phronomy/workflow/store.rbs +8 -0
  457. data/sig/phronomy/workflow.rbs +5 -1
  458. data/sig/phronomy.rbs +4 -49
  459. metadata +365 -117
  460. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  461. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  462. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  463. data/lib/phronomy/agent/context/capability/base.rb +0 -548
  464. data/lib/phronomy/agent/context/capability/tool_executor.rb +0 -62
  465. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  466. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +0 -36
  467. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +0 -22
  468. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  469. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  470. data/lib/phronomy/agent/execution/phase_machine_builder.rb +0 -101
  471. data/lib/phronomy/agent/execution/provider_call_outcome.rb +0 -106
  472. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +0 -15
  473. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +0 -131
  474. data/lib/phronomy/agent/handoff/handoff_request.rb +0 -45
  475. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +0 -7
  476. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +0 -58
  477. data/lib/phronomy/agent/persistence/queries.rb +0 -61
  478. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  479. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  480. data/lib/phronomy/agent/tool_execution/tool_call_intercepted.rb +0 -29
  481. data/lib/phronomy/agent/tool_execution/tool_invocation_session_builder.rb +0 -276
  482. data/lib/phronomy/configuration/configuration.rb +0 -68
  483. data/lib/phronomy/configuration/global_configuration.rb +0 -26
  484. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  485. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  486. data/lib/phronomy/engine/execution_receiver.rb +0 -65
  487. data/lib/phronomy/engine/runnable.rb +0 -54
  488. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  489. data/lib/phronomy/llm_contract/token_usage.rb +0 -85
  490. data/lib/phronomy/multi_agent/persistence/queries.rb +0 -30
  491. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +0 -15
  492. data/lib/phronomy/output_parser/contract/parse_error.rb +0 -7
  493. data/lib/phronomy/persistence/api/persistence.rb +0 -140
  494. data/lib/phronomy/persistence_composition/repositories.rb +0 -77
  495. data/lib/phronomy/recovery/recovery.rb +0 -186
  496. data/lib/phronomy/tool/contract/tool_error.rb +0 -7
  497. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  498. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  499. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  500. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +0 -9
  501. data/sig/phronomy/execution_receiver.rbs +0 -34
  502. data/sig/phronomy/llm_adapter.rbs +0 -11
  503. data/sig/phronomy/runtime.rbs +0 -76
  504. /data/lib/phronomy/agent/{context_assembly/context_importer.rb → context_importer.rb} +0 -0
  505. /data/lib/phronomy/{engine → execution}/backpressure_error.rb +0 -0
  506. /data/lib/phronomy/{engine → execution}/cancellation_error.rb +0 -0
  507. /data/lib/phronomy/{engine → execution}/concurrency/worker_input_restricted.rb +0 -0
  508. /data/lib/phronomy/{engine → execution}/event_loop_reentrancy_error.rb +0 -0
  509. /data/lib/phronomy/{engine → execution}/invalid_async_entry_action_error.rb +0 -0
  510. /data/lib/phronomy/{engine → execution}/invalid_async_transition_action_error.rb +0 -0
  511. /data/lib/phronomy/{engine → execution}/invalid_async_workflow_action_error.rb +0 -0
  512. /data/lib/phronomy/{engine → execution}/pool_shutdown_error.rb +0 -0
  513. /data/lib/phronomy/{engine → execution}/recursion_limit_error.rb +0 -0
  514. /data/lib/phronomy/{engine → execution}/runtime_shutdown_error.rb +0 -0
  515. /data/lib/phronomy/{engine → execution}/runtime_shutdown_reentrancy_error.rb +0 -0
  516. /data/lib/phronomy/{engine → execution}/scheduler_reentrancy_error.rb +0 -0
  517. /data/lib/phronomy/{engine → execution}/timeout_error.rb +0 -0
@@ -12,37 +12,34 @@ module Phronomy
12
12
  # Candidates have no side effects. Runtime retains the first registration.
13
13
  # Existing instances remain available for lookup and transition completion
14
14
  # after admission closes; their own gate rejects new ownership changes.
15
- def self.for(runtime)
16
- existing_for(runtime) || runtime.__register_shutdown_participant(
17
- key: self, participant: new(runtime: runtime)
18
- )
19
- end
20
-
21
- def self.existing_for(runtime)
22
- runtime.__shutdown_participant(key: self)
23
- end
15
+ def self.current = ExecutionEnvironment.current.ownership
16
+ def self.existing_current = ExecutionEnvironment.current.existing_ownership
17
+ def self.for(environment) = environment.ownership
18
+ def self.existing_for(environment) = environment.existing_ownership
24
19
 
25
20
  Entry = Data.define(:state, :agent, :token)
26
21
  private_constant :Entry
27
22
 
28
- def initialize(runtime:)
29
- @runtime = runtime
23
+ def initialize(environment:)
24
+ @environment = environment
30
25
  @mutex = Mutex.new
31
26
  @condition = ConditionVariable.new
32
27
  @entries = {}
28
+ @scope_owners = {}
29
+ @operations = {}
33
30
  @state = :running
34
31
  end
35
32
 
36
- def create(agent_id, expected_class:)
33
+ def create(agent_id, expected_class:, scope_owner: nil)
37
34
  key = normalize_agent_id(agent_id)
38
- token = reserve_create!(key)
35
+ token = reserve_create!(key, scope_owner: scope_owner)
39
36
 
40
37
  begin
41
- agent = yield(@runtime)
38
+ agent = yield(@environment)
42
39
  rescue Phronomy::AgentAlreadyExistsError,
43
- Phronomy::Storage::ConflictError,
44
- Phronomy::Storage::NotFoundError,
45
- Phronomy::Storage::SerializationError,
40
+ Phronomy::Persistence::ConflictError,
41
+ Phronomy::Persistence::NotFoundError,
42
+ Phronomy::Persistence::SerializationError,
46
43
  ArgumentError,
47
44
  Phronomy::ConfigurationError
48
45
  release_construction!(key, token)
@@ -64,7 +61,7 @@ module Phronomy
64
61
  )
65
62
  end
66
63
 
67
- def load(agent_id, expected_class:)
64
+ def load(agent_id, expected_class:, scope_owner: nil)
68
65
  key = normalize_agent_id(agent_id)
69
66
  token = nil
70
67
 
@@ -72,9 +69,14 @@ module Phronomy
72
69
  existing = @mutex.synchronize do
73
70
  ensure_running!
74
71
  entry = @entries[key]
72
+ if scope_owner && entry && !@scope_owners[key].equal?(scope_owner)
73
+ raise Phronomy::AgentBusyError, "Agent #{key.inspect} belongs to another live owner"
74
+ end
75
75
  case entry&.state
76
76
  when :live
77
77
  entry.agent
78
+ when :unloading
79
+ raise Phronomy::AgentBusyError, "Agent #{key.inspect} is unloading"
78
80
  when :constructing, :purging
79
81
  @condition.wait(@mutex)
80
82
  :retry
@@ -83,6 +85,7 @@ module Phronomy
83
85
  when nil
84
86
  token = Object.new.freeze
85
87
  @entries[key] = Entry.new(state: :constructing, agent: nil, token: token)
88
+ @scope_owners[key] = scope_owner if scope_owner
86
89
  nil
87
90
  # :nocov:
88
91
  else
@@ -98,7 +101,7 @@ module Phronomy
98
101
  end
99
102
 
100
103
  begin
101
- agent = yield(@runtime)
104
+ agent = yield(@environment)
102
105
  publish_constructed!(
103
106
  key,
104
107
  token,
@@ -124,8 +127,8 @@ module Phronomy
124
127
  when :recovery_required
125
128
  raise ownership_recovery_error(key)
126
129
  # :nocov:
127
- when :purging
128
- raise Phronomy::Error, "Agent #{key.inspect} is being purged"
130
+ when :purging, :unloading
131
+ raise Phronomy::AgentBusyError, "Agent #{key.inspect} is #{entry.state}"
129
132
  # :cover:
130
133
  end
131
134
  end
@@ -134,13 +137,77 @@ module Phronomy
134
137
  validate_expected_class!(agent, expected_class, key)
135
138
  end
136
139
 
140
+ # Admission and lifetime transitions share this mutex. No Runtime or Store
141
+ # operation is called while it is held.
142
+ def acquire_operation(agent, read_only: false)
143
+ token = Object.new.freeze
144
+ @mutex.synchronize do
145
+ ensure_running! unless read_only && @state == :draining
146
+ entry = @entries[agent.agent_id]
147
+ if entry&.agent&.equal?(agent) && entry.state == :unloading
148
+ raise Phronomy::AgentBusyError, "Agent #{agent.agent_id.inspect} is unloading"
149
+ end
150
+ unless entry&.state == :live && entry.agent.equal?(agent)
151
+ raise Phronomy::RuntimeShutdownError, "Agent is no longer the live Runtime owner"
152
+ end
153
+ (@operations[agent] ||= {})[token] = true
154
+ end
155
+ OperationLease.new(self, agent, token)
156
+ end
157
+
158
+ def release_operation(agent, token)
159
+ @mutex.synchronize do
160
+ operations = @operations[agent]
161
+ operations&.delete(token)
162
+ @operations.delete(agent) if operations && operations.empty?
163
+ @condition.broadcast
164
+ end
165
+ true
166
+ end
167
+
168
+ # Returns the already accepted transition for duplicate callers.
169
+ def reserve_unload(agent)
170
+ @mutex.synchronize do
171
+ ensure_running!
172
+ entry = @entries[agent.agent_id]
173
+ if entry&.state == :unloading && entry.agent.equal?(agent)
174
+ return [entry.token, false]
175
+ end
176
+ unless entry&.state == :live && entry.agent.equal?(agent)
177
+ raise Phronomy::RuntimeShutdownError, "Agent is no longer the live Runtime owner"
178
+ end
179
+ assert_no_operations!(agent)
180
+ transition = UnloadTransition.new(agent: agent, environment: @environment, ownership: self)
181
+ @entries[agent.agent_id] = Entry.new(state: :unloading, agent: agent, token: transition)
182
+ [transition, true]
183
+ end
184
+ end
185
+
186
+ def finish_unload(agent, transition, success:)
187
+ @mutex.synchronize do
188
+ entry = @entries[agent.agent_id]
189
+ unless entry&.state == :unloading && entry.agent.equal?(agent) && entry.token.equal?(transition)
190
+ raise Phronomy::RuntimeShutdownError, "Agent unload reservation is no longer authoritative"
191
+ end
192
+ if success
193
+ agent.send(:__mark_unloaded!)
194
+ @entries.delete(agent.agent_id)
195
+ @scope_owners.delete(agent.agent_id)
196
+ else
197
+ @entries[agent.agent_id] = Entry.new(state: :live, agent: agent, token: nil)
198
+ end
199
+ @condition.broadcast
200
+ end
201
+ true
202
+ end
203
+
137
204
  def owned?(agent)
138
205
  return false unless agent
139
206
 
140
207
  key = normalize_agent_id(agent.agent_id)
141
208
  @mutex.synchronize do
142
209
  entry = @entries[key]
143
- entry&.state == :live && entry.agent.equal?(agent)
210
+ %i[live unloading].include?(entry&.state) && entry.agent.equal?(agent)
144
211
  end
145
212
  rescue ArgumentError
146
213
  false
@@ -156,12 +223,16 @@ module Phronomy
156
223
  ensure_running!
157
224
  entry = @entries[key]
158
225
  unless entry&.state == :live && entry.agent.equal?(agent)
226
+ if entry&.state == :unloading && entry.agent.equal?(agent)
227
+ raise Phronomy::AgentBusyError, "Agent #{key.inspect} is unloading"
228
+ end
159
229
  raise Phronomy::RuntimeShutdownError,
160
230
  "Agent #{key.inspect} is not the live owner in this Runtime"
161
231
  end
232
+ assert_no_operations!(agent)
162
233
  @entries[key] = Entry.new(state: :purging, agent: agent, token: token)
163
234
  end
164
- agent.send(:__mark_purging!, @runtime)
235
+ agent.send(:__mark_purging!, @environment)
165
236
  token
166
237
  end
167
238
 
@@ -170,8 +241,9 @@ module Phronomy
170
241
  @mutex.synchronize do
171
242
  entry = @entries[key]
172
243
  validate_purge_entry!(entry, agent, token, key)
173
- agent.send(:__mark_purged!, @runtime)
244
+ agent.send(:__mark_purged!, @environment)
174
245
  @entries.delete(key)
246
+ @scope_owners.delete(key)
175
247
  @condition.broadcast
176
248
  end
177
249
  true
@@ -183,7 +255,7 @@ module Phronomy
183
255
  @mutex.synchronize do
184
256
  entry = @entries[key]
185
257
  validate_purge_entry!(entry, agent, token, key)
186
- agent.send(:__restore_live_after_purge_abort!, @runtime)
258
+ agent.send(:__restore_live_after_purge_abort!, @environment)
187
259
  @entries[key] = Entry.new(state: :live, agent: agent, token: nil)
188
260
  @condition.broadcast
189
261
  end
@@ -197,7 +269,7 @@ module Phronomy
197
269
  key = normalize_agent_id(agent.agent_id)
198
270
  @mutex.synchronize do
199
271
  validate_purge_entry!(@entries[key], agent, token, key)
200
- agent.send(:__mark_ownership_recovery_required!, @runtime)
272
+ agent.send(:__mark_ownership_recovery_required!, @environment)
201
273
  @entries[key] = Entry.new(
202
274
  state: :recovery_required,
203
275
  agent: agent,
@@ -220,7 +292,7 @@ module Phronomy
220
292
  # ownership transition that is still actively changing state must settle.
221
293
  def wait_until_idle(deadline)
222
294
  @mutex.synchronize do
223
- while @entries.values.any? { |entry| %i[constructing purging].include?(entry.state) }
295
+ while @entries.values.any? { |entry| %i[constructing purging unloading].include?(entry.state) }
224
296
  remaining = deadline - monotonic_now
225
297
  # :nocov:
226
298
  return false if remaining <= 0
@@ -236,26 +308,33 @@ module Phronomy
236
308
  @state = :terminated
237
309
  owned = @entries.values.filter_map(&:agent).uniq
238
310
  @entries.clear
311
+ @scope_owners.clear
312
+ @operations.clear
239
313
  @condition.broadcast
240
314
  owned
241
315
  end
242
- agents.each { |agent| agent.send(:__release_runtime_owner!, @runtime) }
316
+ agents.each { |agent| agent.send(:__release_runtime_owner!, @environment) }
243
317
  true
244
318
  end
245
319
 
246
320
  private
247
321
 
248
- def reserve_create!(key)
322
+ def reserve_create!(key, scope_owner: nil)
249
323
  token = Object.new.freeze
250
324
  @mutex.synchronize do
251
325
  ensure_running!
252
326
  if (entry = @entries[key])
253
327
  raise ownership_recovery_error(key) if entry.state == :recovery_required
254
328
 
329
+ if scope_owner
330
+ raise Phronomy::AgentBusyError, "Agent #{key.inspect} already has a Runtime owner"
331
+ end
332
+
255
333
  raise Phronomy::AgentAlreadyExistsError,
256
334
  "Agent #{key.inspect} already exists in this Runtime (#{entry.state})"
257
335
  end
258
336
  @entries[key] = Entry.new(state: :constructing, agent: nil, token: token)
337
+ @scope_owners[key] = scope_owner if scope_owner
259
338
  end
260
339
  token
261
340
  end
@@ -267,7 +346,7 @@ module Phronomy
267
346
  "constructed Agent identity mismatch: reserved #{key.inspect}, got #{agent.agent_id.inspect}"
268
347
  end
269
348
 
270
- agent.send(:__bind_runtime_owner!, @runtime)
349
+ agent.send(:__bind_runtime_owner!, @environment)
271
350
  @mutex.synchronize do
272
351
  entry = @entries[key]
273
352
  unless entry&.state == :constructing && entry.token.equal?(token)
@@ -282,7 +361,7 @@ module Phronomy
282
361
  # :nocov:
283
362
  if fail_closed
284
363
  fail_construction_closed!(key, token, agent: agent)
285
- agent&.send(:__mark_ownership_recovery_required!, @runtime)
364
+ agent&.send(:__mark_ownership_recovery_required!, @environment)
286
365
  else
287
366
  release_construction!(key, token)
288
367
  end
@@ -296,6 +375,7 @@ module Phronomy
296
375
  # :nocov:
297
376
  if entry&.state == :constructing && entry.token.equal?(token)
298
377
  @entries.delete(key)
378
+ @scope_owners.delete(key)
299
379
  @condition.broadcast
300
380
  end
301
381
  # :cover:
@@ -358,6 +438,12 @@ module Phronomy
358
438
  # :cover:
359
439
  end
360
440
 
441
+ def assert_no_operations!(agent)
442
+ return unless @operations[agent]&.any?
443
+
444
+ raise Phronomy::AgentBusyError, "Agent #{agent.agent_id.inspect} still has accepted operations"
445
+ end
446
+
361
447
  def monotonic_now
362
448
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
363
449
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Owns the Agents materialized within one application operation. A scope is
6
+ # Runtime-bound, process-local, and must not be persisted as Workflow state.
7
+ # @api public
8
+ class Scope
9
+ # @api private
10
+ def self.open
11
+ raise ArgumentError, "open_scope requires a block" unless block_given?
12
+ if Phronomy::WaitPolicy.blocking_forbidden?
13
+ raise Phronomy::EventLoopReentrancyError, "Agent.open_scope cannot block EventLoop"
14
+ end
15
+
16
+ scope = new(environment: ExecutionEnvironment.current)
17
+ result = nil
18
+ begin
19
+ result = yield(scope)
20
+ result = result.wait_result if result.is_a?(Phronomy::TaskResult)
21
+ rescue Exception => primary # standard:disable Lint/RescueException
22
+ raise
23
+ ensure
24
+ failures = scope.send(:__close)
25
+ if primary
26
+ failures.each do |failure|
27
+ error = failure.fetch(:error)
28
+ Phronomy::RuntimeSettings.current.logger&.warn(
29
+ "[Agent scope] cleanup failed agent_id=#{failure.fetch(:agent_id).inspect}: #{error.class}: #{error.message}"
30
+ )
31
+ rescue
32
+ nil
33
+ end
34
+ elsif failures.any?
35
+ raise ScopeCleanupError.new(failures: failures, result: result)
36
+ end
37
+ end
38
+ result
39
+ end
40
+ private_class_method :open
41
+
42
+ def initialize(environment:)
43
+ @environment = environment
44
+ @ownership = environment.ownership
45
+ @owner_token = Object.new.freeze
46
+ @mutex = Mutex.new
47
+ @condition = ConditionVariable.new
48
+ @state = :open
49
+ @receipts = {}
50
+ @agents = {}
51
+ end
52
+ private_class_method :new
53
+
54
+ # @return [Agent::Base]
55
+ # @api public
56
+ def create(agent_class, **options, &listener)
57
+ check_blocking!
58
+ materialize(:create, agent_class, options, listener, async: false)
59
+ end
60
+
61
+ # @return [TaskResult<Agent::Base>]
62
+ # @api public
63
+ def create_async(agent_class, **options, &listener)
64
+ materialize(:create, agent_class, options, listener, async: true)
65
+ rescue => error
66
+ Phronomy::TaskResult.failed(error)
67
+ end
68
+
69
+ # Repeated load in this scope returns the same live owner. Supplying a new
70
+ # listener or different Store for an already live instance is rejected.
71
+ # @return [Agent::Base]
72
+ # @api public
73
+ def load(agent_class, agent_id, persistence:, **options, &listener)
74
+ check_blocking!
75
+ materialize(:load, agent_class, options.merge(agent_id: agent_id, persistence: persistence), listener, async: false)
76
+ end
77
+
78
+ # @return [TaskResult<Agent::Base>]
79
+ # @api public
80
+ def load_async(agent_class, agent_id, persistence:, **options, &listener)
81
+ materialize(:load, agent_class, options.merge(agent_id: agent_id, persistence: persistence), listener, async: true)
82
+ rescue => error
83
+ Phronomy::TaskResult.failed(error)
84
+ end
85
+
86
+ # @api private
87
+ def __close
88
+ agents = @mutex.synchronize do
89
+ @state = :closing
90
+ @condition.wait(@mutex) until @receipts.empty?
91
+ @agents.values.dup
92
+ end
93
+ failures = []
94
+ agents.each do |agent|
95
+ agent.unload!
96
+ rescue => error
97
+ failures << {agent_id: agent.agent_id, error: error}.freeze
98
+ end
99
+ @mutex.synchronize do
100
+ @agents.clear
101
+ @environment = @ownership = @owner_token = nil
102
+ @state = :closed
103
+ end
104
+ failures.freeze
105
+ end
106
+ private :__close
107
+
108
+ private
109
+
110
+ def check_blocking!
111
+ return unless Phronomy::WaitPolicy.blocking_forbidden?
112
+
113
+ raise Phronomy::EventLoopReentrancyError, "Agent scope materialization cannot block EventLoop; use create_async/load_async"
114
+ end
115
+
116
+ def materialize(kind, agent_class, options, listener, async:)
117
+ unless agent_class.is_a?(Class) && agent_class <= Base
118
+ raise ArgumentError, "agent_class must be an Agent::Base subclass"
119
+ end
120
+ if options[:on_event] && listener
121
+ raise ArgumentError, "Provide either on_event: or a block, not both"
122
+ end
123
+ if kind == :load && !options[:persistence]
124
+ raise ArgumentError, "persistence is required"
125
+ end
126
+ key = (options[:agent_id] || (SecureRandom.uuid if kind == :create)).to_s.dup.freeze
127
+ raise ArgumentError, "agent_id must not be empty" if key.empty?
128
+ options = options.merge(agent_id: key)
129
+ receipt = Object.new.freeze
130
+ @mutex.synchronize do
131
+ raise Phronomy::Error, "Agent scope is #{@state}" unless @state == :open
132
+ @receipts[receipt] = key
133
+ end
134
+ operation = -> { acquire(kind, agent_class, key, options, listener) }
135
+ if async
136
+ begin
137
+ task = @environment.submit(on_full: :raise, &operation)
138
+ task.on_physical_complete { finish_receipt(receipt) }
139
+ task
140
+ rescue
141
+ finish_receipt(receipt)
142
+ raise
143
+ end
144
+ else
145
+ begin
146
+ operation.call
147
+ ensure
148
+ finish_receipt(receipt)
149
+ end
150
+ end
151
+ end
152
+
153
+ def acquire(kind, agent_class, key, options, listener)
154
+ materialized = false
155
+ agent = @ownership.public_send(kind, key, expected_class: agent_class, scope_owner: @owner_token) do |environment|
156
+ materialized = true
157
+ instance = agent_class.send(:__construct_owned_agent, environment, key,
158
+ **options, load_existing: kind == :load, &listener)
159
+ RecoveryCoordinator.new(instance).recover_on_load! if kind == :load
160
+ instance
161
+ end
162
+ # Register before any result or post-materialization check reaches the
163
+ # caller, even when scope closure began while the worker was running.
164
+ @mutex.synchronize { @agents[agent.object_id] = agent }
165
+ if kind == :load
166
+ unless agent.persistence.equal?(options[:persistence])
167
+ raise Phronomy::ConfigurationError, "Agent #{key.inspect} is already live with a different Persistence instance"
168
+ end
169
+ if !materialized && (options[:on_event] || listener)
170
+ raise Phronomy::ConfigurationError, "Agent #{key.inspect} is already live; load cannot re-bind on_event"
171
+ end
172
+ end
173
+ agent
174
+ rescue Phronomy::Persistence::ConflictError => error
175
+ raise unless kind == :create
176
+ raise Phronomy::AgentAlreadyExistsError, "Agent #{key.inspect} already exists durably: #{error.message}"
177
+ rescue => error
178
+ # A fail-closed construction remains reserved in C04. Do not purge or
179
+ # release that reservation just because no instance reached this scope.
180
+ report_materialization_error(key, error)
181
+ raise
182
+ end
183
+
184
+ def finish_receipt(receipt)
185
+ @mutex.synchronize do
186
+ @receipts.delete(receipt)
187
+ @condition.broadcast
188
+ end
189
+ end
190
+
191
+ def report_materialization_error(key, error)
192
+ Phronomy::RuntimeSettings.current.logger&.warn("[Agent scope] materialization failed agent_id=#{key.inspect}: #{error.class}: #{error.message}")
193
+ rescue
194
+ nil
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Successful application work with one or more failed Agent releases.
6
+ # Original exceptions and the successful result remain available to the caller.
7
+ # @api public
8
+ class ScopeCleanupError < Phronomy::Error
9
+ attr_reader :failures, :result
10
+
11
+ def initialize(failures:, result:)
12
+ @failures = failures.map { |failure| failure.dup.freeze }.freeze
13
+ @result = result
14
+ super("Agent scope cleanup failed for: #{@failures.map { |f| f.fetch(:agent_id) }.join(", ")}",
15
+ code: "agent.scope_cleanup_failed")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Agent-owned two-phase release. The worker only checks durable facts; the
6
+ # EventLoop completes the exact process-local reservation after physical I/O.
7
+ # @api private
8
+ class UnloadTransition
9
+ Command = Data.define(:coordinator, :phase)
10
+ private_constant :Command
11
+
12
+ attr_reader :result
13
+
14
+ def initialize(agent:, environment:, ownership:)
15
+ @agent, @environment, @ownership = agent, environment, ownership
16
+ @result = Phronomy::TaskResult.deferred(name: "agent-unload:#{agent.agent_id}")
17
+ @mutex = Mutex.new
18
+ @finished = false
19
+ end
20
+
21
+ def start
22
+ post(:check)
23
+ @result
24
+ end
25
+
26
+ def deliver_on_event_loop(command)
27
+ if command.phase == :check
28
+ if @environment.existing_registry&.agent_execution_admitted?(@agent.agent_id)
29
+ raise Phronomy::AgentBusyError, "Agent #{@agent.agent_id.inspect} has a nonterminal execution"
30
+ end
31
+ agent_id, persistence = @agent.agent_id, @agent.persistence
32
+ task = @environment.submit(on_full: :raise) do
33
+ persistence.transaction do |tx|
34
+ tx.executions.assert_idle!(agent_id)
35
+ tx.guard_agent!(agent_id)
36
+ unless tx.retentions.list(agent_id).empty?
37
+ raise Phronomy::AgentBusyError, "Agent #{agent_id.inspect} is retained by durable coordination"
38
+ end
39
+ end
40
+ end
41
+ task.on_complete do |_value, error|
42
+ @read_error = error
43
+ task.on_physical_complete { post(:apply) }
44
+ end
45
+ else
46
+ finish(@read_error)
47
+ end
48
+ rescue => error
49
+ finish(error)
50
+ end
51
+
52
+ private
53
+
54
+ def post(phase)
55
+ accepted = @environment.registry.post(Command.new(coordinator: self, phase: phase), completion: @result)
56
+ finish(Phronomy::RuntimeShutdownError.new("EventLoop rejected Agent unload #{phase}")) unless accepted
57
+ rescue => error
58
+ finish(error)
59
+ end
60
+
61
+ def finish(error)
62
+ first = @mutex.synchronize do
63
+ next false if @finished
64
+ @finished = true
65
+ end
66
+ return unless first
67
+
68
+ begin
69
+ @ownership.finish_unload(@agent, self, success: error.nil?)
70
+ rescue => ownership_error
71
+ error ||= ownership_error
72
+ end
73
+ @agent = @environment = @ownership = nil
74
+ error ? @result.fail(error) : @result.complete(true)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -9,39 +9,47 @@ module Phronomy
9
9
  def initialize(view) = @view = view
10
10
 
11
11
  def create(root)
12
- record = Codec.encode_agent_root(root)
13
- @view.atomic do |bound|
14
- entry = bound.records(StorageSchema::ROOTS).insert(key: root.agent_id.to_s,
15
- revision: Integer(root.agent_revision), attributes: {}, record: record)
16
- decode(entry, root.agent_id, revision: root.agent_revision)
12
+ Phronomy::Persistence::StorageBoundary.call do
13
+ record = Codec.encode_agent_root(root)
14
+ @view.atomic do |bound|
15
+ entry = bound.records(StorageSchema::ROOTS).insert(key: root.agent_id.to_s,
16
+ revision: Integer(root.agent_revision), attributes: {}, record: record)
17
+ decode(entry, root.agent_id, revision: root.agent_revision)
18
+ end
17
19
  end
18
20
  end
19
21
 
20
22
  def load(agent_id)
21
- value = @view.atomic do |bound|
22
- entry = bound.records(StorageSchema::ROOTS).read(agent_id.to_s)
23
- entry && decode(entry, agent_id)
23
+ Phronomy::Persistence::StorageBoundary.call do
24
+ value = @view.atomic do |bound|
25
+ entry = bound.records(StorageSchema::ROOTS).read(agent_id.to_s)
26
+ entry && decode(entry, agent_id)
27
+ end
28
+ value || raise(Phronomy::Storage::NotFoundError, "Agent not found: #{agent_id}")
24
29
  end
25
- value || raise(Phronomy::Storage::NotFoundError, "Agent not found: #{agent_id}")
26
30
  end
27
31
 
28
32
  def save(agent_id, expected_revision:, root:)
29
- expected = Integer(expected_revision)
30
- revision = Integer(root.agent_revision)
31
- raise Phronomy::Storage::ConflictError, "root save must advance revision exactly once" unless revision == expected + 1
32
- unless root.agent_id.to_s == agent_id.to_s
33
- raise Phronomy::Storage::SerializationError, "Agent root identity mismatch"
34
- end
35
- record = Codec.encode_agent_root(root)
36
- @view.atomic do |bound|
37
- entry = bound.records(StorageSchema::ROOTS).replace(key: agent_id.to_s,
38
- expected_revision: expected, next_revision: revision, attributes: {}, record: record)
39
- decode(entry, agent_id, revision: revision)
33
+ Phronomy::Persistence::StorageBoundary.call do
34
+ expected = Integer(expected_revision)
35
+ revision = Integer(root.agent_revision)
36
+ raise Phronomy::Storage::ConflictError, "root save must advance revision exactly once" unless revision == expected + 1
37
+ unless root.agent_id.to_s == agent_id.to_s
38
+ raise Phronomy::Storage::SerializationError, "Agent root identity mismatch"
39
+ end
40
+ record = Codec.encode_agent_root(root)
41
+ @view.atomic do |bound|
42
+ entry = bound.records(StorageSchema::ROOTS).replace(key: agent_id.to_s,
43
+ expected_revision: expected, next_revision: revision, attributes: {}, record: record)
44
+ decode(entry, agent_id, revision: revision)
45
+ end
40
46
  end
41
47
  end
42
48
 
43
49
  def delete(agent_id)
44
- @view.records(StorageSchema::ROOTS).delete(key: agent_id.to_s)
50
+ Phronomy::Persistence::StorageBoundary.call do
51
+ @view.records(StorageSchema::ROOTS).delete(key: agent_id.to_s)
52
+ end
45
53
  end
46
54
 
47
55
  private