phronomy 0.25.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 (323) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +14 -7
  5. data/VERIFY.sh +27 -27
  6. data/benchmark/bench_agent_invoke.rb +26 -22
  7. data/benchmark/bench_context_assembler.rb +4 -5
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  10. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  11. data/docs/architecture/agent-context.md +5 -3
  12. data/docs/architecture/agent-transition-ownership.md +89 -0
  13. data/docs/architecture/before-llm-input.md +6 -0
  14. data/docs/architecture/context-management.md +30 -0
  15. data/docs/architecture/context-preparation-steps.md +90 -0
  16. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  17. data/docs/architecture/execution-metadata-and-values.md +80 -0
  18. data/docs/architecture/generator-verifier-ownership.md +111 -0
  19. data/docs/architecture/multi-agent-handoff.md +8 -2
  20. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  21. data/docs/architecture/persistence.md +29 -6
  22. data/docs/architecture/refactoring-closure.md +334 -0
  23. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  24. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  25. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  26. data/docs/architecture/tracing.md +4 -4
  27. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  28. data/docs/architecture.md +209 -0
  29. data/docs/async-composition.md +205 -0
  30. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  31. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  32. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  33. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  34. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  35. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  36. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  37. data/docs/decisions/032-storage-backend-composition.md +80 -0
  38. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  39. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  40. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  41. data/docs/decisions/036-context-contract-ownership.md +86 -0
  42. data/docs/decisions/037-common-definition-ownership.md +62 -0
  43. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  44. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  45. data/docs/decisions/040-configuration-default-composition.md +94 -0
  46. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  47. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  48. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  49. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  50. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  51. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  52. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  53. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  54. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  55. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  56. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  57. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  58. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  59. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  60. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  61. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  62. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  63. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  64. data/docs/decisions/README.md +35 -6
  65. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  66. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  67. data/docs/features.md +21 -12
  68. data/docs/getting-started.md +10 -10
  69. data/docs/migrations/0.15.md +5 -0
  70. data/docs/migrations/durable-semantic-coordination-v2.md +7 -1
  71. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  72. data/docs/migrations/neutral-storage-spi.md +43 -0
  73. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  74. data/docs/migrations/shared-state-multi-agent.md +44 -0
  75. data/docs/migrations/storage-backend-composition.md +117 -0
  76. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  77. data/docs/persistence-backends.md +162 -204
  78. data/docs/runtime-and-concurrency.md +205 -60
  79. data/lib/phronomy/agent/api/agent.rb +17 -0
  80. data/lib/phronomy/agent/async_event_api.rb +2 -2
  81. data/lib/phronomy/agent/base.rb +55 -251
  82. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  83. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  84. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  85. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  86. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  87. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  88. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  89. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  90. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  91. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  92. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  93. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  94. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  95. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  96. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  97. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  98. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  99. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +20 -96
  100. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  101. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  102. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +12 -13
  103. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  104. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  105. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  106. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  107. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  108. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  109. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  110. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  111. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  112. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +22 -73
  113. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  114. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  115. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  116. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  117. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  118. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  119. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  120. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  121. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  122. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  123. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  124. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  125. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  126. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  127. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  128. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  129. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  130. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  131. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  132. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  133. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  134. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  135. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  136. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  137. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  138. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  139. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  140. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +36 -41
  141. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  142. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +9 -13
  143. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  144. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  145. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  146. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  147. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  148. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  149. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  150. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  151. data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +2 -2
  152. data/lib/phronomy/common/configuration_error.rb +7 -0
  153. data/lib/phronomy/common/error.rb +5 -0
  154. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  155. data/lib/phronomy/common/values/serializable.rb +32 -0
  156. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  157. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  158. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  159. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  160. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  161. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +15 -8
  162. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  163. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  164. data/lib/phronomy/engine/concurrency/offload_pool.rb +29 -16
  165. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  166. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  167. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  168. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  169. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  170. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  171. data/lib/phronomy/engine/event_loop.rb +160 -626
  172. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  173. data/lib/phronomy/engine/execution.rb +229 -0
  174. data/lib/phronomy/engine/execution_cancellation_error.rb +14 -0
  175. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  176. data/lib/phronomy/engine/execution_timeout_error.rb +14 -0
  177. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  178. data/lib/phronomy/engine/fsm_session.rb +38 -31
  179. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +3 -1
  180. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +3 -1
  181. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +3 -1
  182. data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +13 -1
  183. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  184. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  185. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  186. data/lib/phronomy/engine/runtime.rb +80 -84
  187. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  188. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  189. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  190. data/lib/phronomy/engine/{task.rb → task_result.rb} +101 -42
  191. data/lib/phronomy/engine/timeout_error.rb +7 -0
  192. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  193. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  194. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  195. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  196. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  197. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  198. data/lib/phronomy/llm_adapter/base.rb +2 -2
  199. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  200. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  201. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  202. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  203. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  204. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  205. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  206. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  207. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +8 -8
  208. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  209. data/lib/phronomy/multi_agent/orchestrator.rb +36 -68
  210. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  211. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  212. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  213. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  214. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  215. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  216. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  217. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  218. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  219. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  220. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  221. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  222. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  223. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  224. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  225. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  226. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  227. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  228. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  229. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  230. data/lib/phronomy/storage/backend.rb +101 -0
  231. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  232. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  233. data/lib/phronomy/storage/blobs.rb +31 -0
  234. data/lib/phronomy/storage/condition.rb +25 -0
  235. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  236. data/lib/phronomy/storage/conflict_error.rb +9 -0
  237. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  238. data/lib/phronomy/storage/entry.rb +33 -0
  239. data/lib/phronomy/storage/guard_ref.rb +13 -0
  240. data/lib/phronomy/storage/not_found_error.rb +9 -0
  241. data/lib/phronomy/storage/record_codec.rb +177 -0
  242. data/lib/phronomy/storage/records.rb +61 -0
  243. data/lib/phronomy/storage/resource.rb +126 -0
  244. data/lib/phronomy/storage/scope.rb +25 -0
  245. data/lib/phronomy/storage/serialization_error.rb +9 -0
  246. data/lib/phronomy/storage/streams.rb +44 -0
  247. data/lib/phronomy/storage/transaction_error.rb +10 -0
  248. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  249. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  250. data/lib/phronomy/storage/validation.rb +53 -0
  251. data/lib/phronomy/storage/view.rb +131 -0
  252. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  253. data/lib/phronomy/testing/fake_clock.rb +13 -9
  254. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  255. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  256. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  257. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  258. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  259. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  260. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  261. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  262. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  263. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  264. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  265. data/lib/phronomy/tools/agent.rb +6 -6
  266. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  267. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  268. data/lib/phronomy/version.rb +1 -1
  269. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  270. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  271. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  272. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  273. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +126 -75
  274. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  275. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  276. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  277. data/lib/phronomy/workflow/phase_machine_builder.rb +8 -8
  278. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  279. data/lib/phronomy.rb +52 -94
  280. data/scripts/api_snapshot.rb +4 -2
  281. data/scripts/storage_spi_snapshot.rb +36 -0
  282. data/sig/phronomy/agent.rbs +4 -5
  283. data/sig/phronomy/execution_receiver.rbs +34 -0
  284. data/sig/phronomy/extensions.rbs +5 -5
  285. data/sig/phronomy/handoff.rbs +4 -2
  286. data/sig/phronomy/multi_agent.rbs +19 -3
  287. data/sig/phronomy/persistence.rbs +7 -91
  288. data/sig/phronomy/runtime.rbs +34 -7
  289. data/sig/phronomy/storage.rbs +174 -0
  290. data/sig/phronomy/tool.rbs +11 -2
  291. data/sig/phronomy/workflow.rbs +1 -1
  292. metadata +236 -99
  293. data/lib/phronomy/agent/execution_coordinator.rb +0 -3138
  294. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  295. data/lib/phronomy/agent/recovery_support.rb +0 -504
  296. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  297. data/lib/phronomy/agent/tool_executor.rb +0 -55
  298. data/lib/phronomy/generator_verifier.rb +0 -369
  299. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  300. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  301. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
  302. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  303. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  304. data/lib/phronomy/persistence/in_memory.rb +0 -690
  305. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  306. data/lib/phronomy/persistence.rb +0 -276
  307. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  308. data/lib/phronomy/workflow_recovery.rb +0 -123
  309. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  310. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  311. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  312. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  313. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  314. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  315. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  316. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  317. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  318. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  319. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  320. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  321. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  322. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  323. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  Phronomy uses an **EventLoop / FSMSession first** architecture for framework
4
4
  lifecycle coordination. `FSMSession` represents explicit lifecycle state and
5
- events. `Phronomy::Task` is the common caller-facing completion handle, not an
5
+ events. `Phronomy::TaskResult` is the common caller-facing completion handle, not an
6
6
  execution backend. Synchronous work that must stay off EventLoop is isolated in
7
7
  the bounded `OffloadPool`.
8
8
 
@@ -23,37 +23,38 @@ are defined by
23
23
 
24
24
  ## Runtime model
25
25
 
26
- ```text
27
- Runtime
28
- ├─ Agent ownership registry
29
- │ └─ agent_id -> one mutable live Agent instance
30
- ├─ EventLoop (one control-plane operating-system Thread)
31
- │ ├─ FSMSession
32
- │ │ ├─ Agent
33
- │ │ ├─ Workflow
34
- │ │ ├─ ToolInvocation
35
- │ │ └─ MultiAgent fan-out
36
- │ ├─ Agent top-level admission
37
- │ │ └─ agent_id -> one nonterminal logical Execution admission
38
- │ └─ Agent execution directory
39
- │ └─ execution_id -> immutable live-state record
40
- ├─ OffloadPool (bounded operating-system Threads)
41
- │ ├─ private Operation records
42
- │ ├─ blocking input/output (I/O)
43
- │ ├─ central-processing-unit (CPU)-bound synchronous work
44
- │ └─ operation-specific durable Agent/Workflow work
45
- ├─ named OffloadPools
46
- └─ EventLoop-driven timers
47
-
48
- EventLoop / FSMSession ─┐
49
- ├─> Task = completion handle
50
- OffloadPool ────────────┘
51
- ```
26
+ | Runtime-retained component | Implementation owner and responsibility |
27
+ |---|---|
28
+ | Shutdown participants | Feature-owned registries: Agent identity, Team identity, and MultiAgent synchronous-call admission |
29
+ | EventLoop | Engine dispatcher thread, FSMSessions and registered feature receivers |
30
+ | Execution receivers | Agent/Workflow own execution admission, state and idle policies; mutations run on EventLoop |
31
+ | OffloadPool and named pools | Engine bounded workers and private physical-operation records |
32
+ | Timers | Engine EventLoop-driven scheduling |
33
+
34
+ Runtime retains participants through a generic lifecycle protocol; it does not
35
+ construct Agent or Team identity registries. Each feature registers its own
36
+ instance. See [ADR-041](decisions/041-feature-owned-identity-registries.md).
52
37
 
53
38
  The framework does not allocate one operating-system Thread per logical
54
39
  Agent/Workflow/Tool lifecycle. Logical waits remain explicit states plus later
55
40
  EventLoop events.
56
41
 
42
+ ## Feature execution receivers
43
+
44
+ [ADR-042](decisions/042-feature-owned-execution-state.md) separates implementation
45
+ ownership from the single-writer execution context. Agent and Workflow create
46
+ `Agent::ExecutionRegistry` and `WorkflowExecutionRegistry` respectively; Engine
47
+ retains registered receivers without selecting concrete feature types.
48
+
49
+ Receiver registration and new request admission close before idle checks.
50
+ Already queued requests, pending durable operations and supervised physical
51
+ work are counted until their feature continuation settles. Worker callbacks
52
+ only enqueue results. Workflow instance routing and generic FSM enqueue share
53
+ one lock. After a clean join, receiver cleanup invalidates retained state;
54
+ a failing receiver makes Runtime cleanup incomplete. Dispatcher failure instead
55
+ notifies receivers on the failing loop thread with the original error.
56
+
57
+
57
58
  ## Live state and durable state
58
59
 
59
60
  A live Agent or Workflow owns its current logical state. `Persistence` is the
@@ -61,7 +62,7 @@ last committed durable representation and recovery source; it is not reloaded at
61
62
  every semantic boundary.
62
63
 
63
64
  For active Agents, **EventLoop is the single writer of Phronomy-managed live
64
- execution state**. EventLoop owns a process-local execution directory keyed by
65
+ execution state**. `Agent::ExecutionRegistry` owns the process-local directory keyed by
65
66
  canonical `execution_id`. Each directory value is immutable and is replaced on
66
67
  EventLoop when the current AgentExecution, RuntimeProjection, AgentInvocation, or
67
68
  owning FSMSession changes. The former mutex-protected
@@ -75,7 +76,7 @@ handling; workers do not receive AgentInvocation as a mutable state authority.
75
76
  Mutable Agent/Execution/Journal state is not automatically reloaded before every
76
77
  LLM or Tool step. Durable writes use optimistic revision/position guardrails; an
77
78
  external writer that advances the durable base causes
78
- `Persistence::ConflictError` rather than automatic reload or merge.
79
+ `Storage::ConflictError` rather than automatic reload or merge.
79
80
 
80
81
  For Workflows, the current `WorkflowContext` and FSMSession own the active
81
82
  logical state. A durable Workflow hydrates once at invocation/resume and saves at
@@ -88,8 +89,9 @@ content reference is value materialization rather than mutable state refresh.
88
89
 
89
90
  `agent_id` identifies one logical Agent, not a reusable lookup key for independent
90
91
  mutable objects. One Runtime therefore publishes at most one mutable live Agent
91
- instance for a given `agent_id`. The Runtime-owned registry is an authority, not a
92
- cache, and reserves the identity before create/load materialization.
92
+ instance for a given `agent_id`. The Agent-owned registry, strongly retained by
93
+ Runtime, is an authority, not a cache, and reserves the identity before
94
+ create/load materialization.
93
95
 
94
96
  The application-facing identity operations are distinct:
95
97
 
@@ -100,7 +102,7 @@ new / create
100
102
  load(agent_id, persistence:)
101
103
  live -> exact same Ruby object, with no Persistence reload
102
104
  durable-only -> hydrate and publish once
103
- missing -> Persistence::NotFoundError
105
+ missing -> Storage::NotFoundError
104
106
 
105
107
  get(agent_id)
106
108
  live Runtime lookup only; missing -> nil
@@ -114,7 +116,8 @@ invalidates the old object, deletes durable state, releases the process-local
114
116
  identity, and allows a later new Agent to reuse the textual ID.
115
117
 
116
118
  Live Agent ownership and top-level Execution admission are separate lifetimes.
117
- For one live Agent, EventLoop admits at most one nonterminal top-level Execution.
119
+ For one live Agent, `Agent::ExecutionRegistry` admits at most one nonterminal
120
+ top-level Execution on EventLoop.
118
121
  Admission is acquired **before** the initial Offload/Persistence operation:
119
122
 
120
123
  ```text
@@ -195,6 +198,15 @@ Phronomy-managed live domain objects are rejected from that value data. A comple
195
198
  value-type/serialization contract for arbitrary Application-owned opaque objects is
196
199
  deferred; such objects remain Application-owned and must be worker-safe.
197
200
 
201
+ The existing restricted types declare the internal methodless
202
+ `Concurrency::WorkerInputRestricted` marker at their own definitions.
203
+ ToolInvocation checks that execution-boundary contract instead of enumerating
204
+ Agent/Workflow/Runtime classes. Frozen marked values and subclasses remain
205
+ restricted; Hash keys and values, nested Arrays and behavior handles are all
206
+ checked. The marker does not inspect opaque application fields or closure
207
+ captures and is not a general restriction on every OffloadPool command.
208
+ See [ADR-045](decisions/045-worker-input-restriction-ownership.md).
209
+
198
210
  Worker authorization/execution outcomes return as values carrying
199
211
  `tool_invocation_id`; the Tool FSMSession consumes a mismatched semantic result
200
212
  without advancing its current state.
@@ -254,7 +266,7 @@ lifecycle. Logical halt/completion first enters a private
254
266
  `persisting_terminal` lifecycle condition; the FSMSession remains nonterminal
255
267
  while WorkflowRunner saves the terminal snapshot through OffloadPool. Only a
256
268
  known-successful save result returned to that same FSMSession permits
257
- `HALTED`/`COMPLETED`, admission release, and caller Task settlement.
269
+ `HALTED`/`COMPLETED`, admission release, and caller TaskResult settlement.
258
270
 
259
271
  ```text
260
272
  RUNNING
@@ -279,13 +291,39 @@ remain durable conflict defense rather than distributed ownership.
279
291
 
280
292
  ## Tool execution modes
281
293
 
294
+ The default dispatch helper is the private
295
+ `Phronomy::Agent::Context::Capability::ToolExecutor`, colocated with Capability
296
+ Base. Agent ToolInvocation supplies Runtime and admission policy for the
297
+ standard path, and owns authorization and logical result handling. Custom
298
+ `call_async` implementations keep the public Tool protocol. This ownership is
299
+ defined by [ADR-035](decisions/035-tool-executor-capability-ownership.md); the
300
+ public `Phronomy::Tool::Base` facade and its Class identity remain unchanged.
301
+
302
+ Agent uses ordinary `RubyLLM::Chat` for both complete and streaming Provider
303
+ calls. The `before_tool_call` callback takes the complete assistant Tool-call
304
+ batch before RubyLLM executes the first Tool body. `AgentInvocation` then owns
305
+ authorization, dispatch, approval suspension and result collection. Offloaded
306
+ Tools can overlap within Runtime capacity; short cooperative Tools run according
307
+ to their existing execution contract.
308
+
309
+ On successful batch completion, Agent records one Tool result per call ID in
310
+ request order and includes the whole exchange in the next Provider request.
311
+ Worker completion order does not split the conversation or trigger a partial
312
+ Provider continuation. Failure, cancellation and approval keep their existing
313
+ Agent lifecycle semantics.
314
+
315
+ There is no separate Chat execution path or `parallel_tool_execution` switch.
316
+ The removed internal `MultiAgent::ParallelToolChat` class is not replaced by an
317
+ alias. See the [Chat migration guide](migrations/parallel-tool-chat-removal.md)
318
+ for application configuration and existing stored model-config records.
319
+
282
320
  Phronomy exposes two execution modes for capabilities:
283
321
 
284
322
  - `:cooperative` — short EventLoop-safe work, or specialized asynchronous work
285
- that starts another Phronomy lifecycle and returns a Task immediately;
323
+ that starts another Phronomy lifecycle and returns a TaskResult immediately;
286
324
  - `:offloaded` — synchronous work that must not run to completion on EventLoop.
287
325
 
288
- Both paths return `Phronomy::Task`. The execution mechanism differs; the
326
+ Both paths return `Phronomy::TaskResult`. The execution mechanism differs; the
289
327
  completion abstraction does not.
290
328
 
291
329
  Phronomy does not classify application work into framework-level I/O/CPU/process
@@ -310,7 +348,7 @@ Correct shape:
310
348
  parent FSMSession
311
349
  → start child lifecycle
312
350
  → return immediately
313
- → child Task settles
351
+ → child TaskResult settles
314
352
  → post parent EventLoop event
315
353
  ```
316
354
 
@@ -326,7 +364,7 @@ Agent preparation/commit and Workflow hydrate/save operations are submitted to
326
364
 
327
365
  A durable barrier may pause one logical lifecycle without blocking EventLoop.
328
366
  Persistence does not implement async repository variants and must not depend on
329
- EventLoop, FSMSession, Task settlement internals, or private OffloadPool operation
367
+ EventLoop, FSMSession, TaskResult settlement internals, or private OffloadPool operation
330
368
  records.
331
369
 
332
370
  ## Sync versus async application APIs
@@ -336,7 +374,7 @@ records.
336
374
  | Top-level application code | `agent.invoke(...)` when blocking the caller is acceptable |
337
375
  | Top-level explicit async | `task = agent.invoke_async(...)`; optionally `task.wait_result` outside EventLoop |
338
376
  | Workflow entry/transition action | Start async work and continue through `Workflow#signal` |
339
- | EventLoop callback | Never block waiting for a Task that requires EventLoop progress |
377
+ | EventLoop callback | Never block waiting for a TaskResult that requires EventLoop progress |
340
378
  | Top-level streaming | `agent.stream(...)` |
341
379
  | Non-blocking streaming | `agent.stream_async(...)` |
342
380
  | Approval from EventLoop callback | Resolve with `live_for_execution`, call `agent.approve_async(...)`, and return immediately |
@@ -345,37 +383,88 @@ Blocking synchronous APIs reject EventLoop re-entry with
345
383
  `Phronomy::EventLoopReentrancyError` when waiting would stall the same EventLoop
346
384
  needed for progress.
347
385
 
348
- ## Task
386
+ ## TaskResult
349
387
 
350
- `Phronomy::Task` is thread-free. It represents one terminal result:
388
+ `Phronomy::TaskResult` is thread-free. It represents one terminal result:
351
389
 
352
390
  - completed value;
353
391
  - failure;
354
392
  - cancellation.
355
393
 
356
- Task is the common completion abstraction for logical EventLoop/FSMSession
394
+ TaskResult is the common completion abstraction for logical EventLoop/FSMSession
357
395
  lifecycles and OffloadPool-backed synchronous work.
358
396
 
359
- `Task#wait_result(timeout:)` is a bridge for external synchronous callers. Its
360
- timeout is waiter-local: it does not settle/cancel the Task or alter OffloadPool
397
+ `TaskResult#wait_result(timeout:)` is a bridge for external synchronous callers. Its
398
+ timeout is waiter-local: it does not settle/cancel the TaskResult or alter OffloadPool
361
399
  abandonment state.
362
400
 
363
- `Task#on_complete` registers an independent notification callback. Callback
401
+ `TaskResult#on_complete` registers an independent notification callback. Callback
364
402
  execution thread is not guaranteed. A callback may be delivered by an OffloadPool
365
403
  worker, a timer/cancellation caller, an EventLoop-related control path, or the
366
404
  thread that registers after settlement. Callbacks must therefore be thread-safe
367
405
  and should complete quickly. Framework lifecycle code normally converts worker
368
406
  completion into an explicit EventLoop event before applying live state.
369
407
 
370
- `Task#map` is application-level composition. A transformation exception settles
371
- the mapped Task as failed.
408
+ `TaskResult#map` is application-level composition. A transformation exception settles
409
+ the mapped TaskResult as failed.
372
410
 
373
- Framework components own Task settlement. Application code should not use
374
- `Task#complete`, `Task#fail`, or `Task#cancel!` as operation-control APIs. Request
411
+ `flat_map` connects a returned TaskResult's completion, while `all_settled`
412
+ observes an input-order list of results. `Execution.run_async` starts and joins
413
+ JOBs under whole-execution controls. See [Result composition and Execution](async-composition.md)
414
+ for the complete contracts, ownership boundaries and development-release migration.
415
+
416
+ Framework components own TaskResult settlement. Application code should not use
417
+ `TaskResult#complete`, `TaskResult#fail`, or `TaskResult#cancel!` as operation-control APIs. Request
375
418
  operation-wide cancellation through the `CancellationToken` accepted by the API
376
- that created the Task. Task settlement never propagates backwards to cancel a
419
+ that created the TaskResult. TaskResult settlement never propagates backwards to cancel a
377
420
  shared CancellationToken.
378
421
 
422
+ ### Settled TaskResult factories
423
+
424
+ `TaskResult.completed(value)` and `TaskResult.failed(error)` create results that are already
425
+ settled. They are useful when application code has a result or an error in hand
426
+ and needs to present it through the same `TaskResult` contract as a lifecycle-backed
427
+ completion:
428
+
429
+ ```ruby
430
+ # Already-computed value — no execution started.
431
+ task = Phronomy::TaskResult.completed("cached result")
432
+
433
+ # Known failure — no execution started.
434
+ task = Phronomy::TaskResult.failed(StandardError.new("precondition not met"))
435
+ ```
436
+
437
+ Settled results satisfy `TaskResult#wait_result`, `TaskResult#on_complete`, and `TaskResult#map` with
438
+ the same contract as a normally-completed TaskResult.
439
+
440
+ ## Blocking
441
+
442
+ `Phronomy::Blocking.call_async(&block)` submits a synchronous block to the
443
+ existing default Runtime OffloadPool and returns a `TaskResult`. It is the public
444
+ application entry point for one-off synchronous work that should not block the
445
+ caller:
446
+
447
+ ```ruby
448
+ task = Phronomy::Blocking.call_async { expensive_io_call }
449
+ result = task.wait_result
450
+ ```
451
+
452
+ The optional `invocation_context:` connects this operation to an Execution
453
+ scope or an existing invocation context. Context controls and the optional
454
+ individual `cancellation_token:` both apply, without reverse cancellation of
455
+ shared controls. The application chooses when synchronous work needs OffloadPool.
456
+
457
+ The method uses the existing bounded OffloadPool. It does not create a new thread
458
+ or scheduler. Capacity and backpressure semantics are inherited from the pool
459
+ configuration (`offload_pool_size` / `offload_queue_size`).
460
+
461
+ `Blocking.call_async` always uses non-waiting admission (`on_full: :raise`) and can
462
+ be called from within the EventLoop thread. Admission `StandardError` failures (for
463
+ example `BackpressureError` when the queue is full) are caught and returned as a
464
+ failed `TaskResult`. Accepted work retains the pool's original `TaskResult` unchanged. Do not
465
+ call `TaskResult#wait_result` on the returned `TaskResult` from within the EventLoop — waiting
466
+ for a `TaskResult` that requires EventLoop progress will deadlock.
467
+
379
468
  ## OffloadPool
380
469
 
381
470
  `OffloadPool` is a bounded worker pool for synchronous work that must not execute
@@ -391,7 +480,7 @@ Its guarantees include:
391
480
  - runtime metrics;
392
481
  - shutdown/drain behavior.
393
482
 
394
- `OffloadPool#submit` returns a `Phronomy::Task`. OffloadPool does not expose its
483
+ `OffloadPool#submit` returns a `Phronomy::TaskResult`. OffloadPool does not expose its
395
484
  execution record as a caller-facing future/promise. Its private `Operation` owns:
396
485
 
397
486
  - the submitted block;
@@ -403,27 +492,36 @@ execution record as a caller-facing future/promise. Its private `Operation` owns
403
492
  - metrics state needed by the pool.
404
493
 
405
494
  This separation keeps execution details private while allowing every asynchronous
406
- Phronomy API to expose the same Task completion contract.
495
+ Phronomy API to expose the same TaskResult completion contract.
407
496
 
408
497
  ### EventLoop queue admission
409
498
 
410
499
  Framework-owned EventLoop-origin submissions must not wait for a free worker
411
500
  queue slot. They use non-blocking admission (`on_full: :raise`) and route
412
- `BackpressureError` through the ordinary FSM/Task completion path.
501
+ `BackpressureError` through the ordinary FSM/TaskResult completion path.
502
+
503
+ Any internal `OffloadPool#submit` call that requests waiting admission
504
+ (`on_full: :wait` or `:timeout`) is **rejected before the Operation is created**
505
+ when the caller is on the EventLoop thread. The rejection raises
506
+ `Phronomy::EventLoopReentrancyError` directly; it does not create an Operation,
507
+ timer, cancellation subscription, or TaskResult. The public `Phronomy::Blocking.call_async`
508
+ always uses `on_full: :raise` and therefore does not trigger this guard; it converts
509
+ admission `StandardError` (such as `BackpressureError`) into a failed `TaskResult` at the
510
+ public-API layer.
413
511
 
414
512
  External management threads may choose a blocking admission policy when blocking
415
513
  the caller is acceptable.
416
514
 
417
515
  ## Submit timeout and cancellation
418
516
 
419
- Submit-time timeout and submit cancellation settle the caller-facing Task. They
517
+ Submit-time timeout and submit cancellation settle the caller-facing TaskResult. They
420
518
  do **not** asynchronously interrupt an already-running synchronous worker.
421
519
 
422
520
  ### Before worker start
423
521
 
424
522
  If timeout/cancellation wins before execution starts:
425
523
 
426
- - the Task settles (`TimeoutError` failure or cancellation);
524
+ - the TaskResult settles (`TimeoutError` failure or cancellation);
427
525
  - the submitted block does not run;
428
526
  - the private Operation is not counted as abandoned.
429
527
 
@@ -431,7 +529,7 @@ If timeout/cancellation wins before execution starts:
431
529
 
432
530
  If timeout/cancellation wins after execution starts:
433
531
 
434
- - the Task settles immediately;
532
+ - the TaskResult settles immediately;
435
533
  - the private Operation is marked abandoned;
436
534
  - the worker continues until its synchronous call returns;
437
535
  - the eventual worker result is discarded.
@@ -447,7 +545,7 @@ requiring callback delivery promote the deadline to explicit `cancel!` through
447
545
  the Runtime timer queue. OffloadPool does this for its submit cancellation token.
448
546
 
449
547
  A CancellationToken may be shared by multiple operations. For that reason,
450
- settling or cancelling one Task does not cancel the token in the reverse
548
+ settling or cancelling one TaskResult does not cancel the token in the reverse
451
549
  direction.
452
550
 
453
551
  ## Native async boundary
@@ -455,7 +553,7 @@ direction.
455
553
  A genuine native-async driver that does not create a Phronomy-owned OS Thread and
456
554
  does not block EventLoop need not consume an OffloadPool worker. If Phronomy
457
555
  formally exposes such an extension point, it must adapt completion to
458
- `Phronomy::Task` rather than exposing a provider-specific future or a private
556
+ `Phronomy::TaskResult` rather than exposing a provider-specific future or a private
459
557
  Runtime type.
460
558
 
461
559
  The current Persistence, VectorStore, Embeddings, and LLM call-extension
@@ -473,8 +571,8 @@ Two metrics answer different operational questions:
473
571
  - `offload_pool_abandoned_active` — current number of abandoned operations whose
474
572
  synchronous workers still occupy pool capacity.
475
573
 
476
- The abandonment state belongs to the private OffloadPool Operation, not to Task.
477
- Task reports only caller-facing settlement.
574
+ The abandonment state belongs to the private OffloadPool Operation, not to TaskResult.
575
+ TaskResult reports only caller-facing settlement.
478
576
 
479
577
  ## EventLoop metrics
480
578
 
@@ -487,6 +585,53 @@ Use these to distinguish worker saturation from EventLoop backlog/latency.
487
585
  Runtime-owned EventLoop, then closes pools and timers according to the Runtime
488
586
  shutdown contract.
489
587
 
588
+ Internal subsystems can register one shutdown participant per Runtime-local key
589
+ through `Runtime#__register_shutdown_participant`. This is an internal lifecycle
590
+ contract, not a public extension API or an execution registry. Runtime does not
591
+ construct participants or interpret their admission rules.
592
+
593
+ Each participant implements two required operations:
594
+
595
+ - `begin_draining` closes admission under the participant's admission lock. It
596
+ must be idempotent, short and nonblocking, and must not call back into Runtime.
597
+ Runtime invokes it under its lifecycle lock when shutdown starts or EventLoop
598
+ fails, so no participant can be registered after closure begins.
599
+ - `wait_until_idle(deadline)` returns whether admitted calls have finished before
600
+ the shared absolute monotonic deadline. Runtime closes every participant before
601
+ waiting, and waits without holding its lifecycle lock. A timeout or failed hook
602
+ makes cleanup incomplete; other participants, pools and timers still receive
603
+ their shutdown calls. Incomplete cleanup prevents default Runtime replacement.
604
+
605
+ Participants may also implement `after_runtime_shutdown`. Runtime calls it
606
+ outside its lifecycle lock after every wait succeeds, EventLoop has stopped,
607
+ and pools/timers have shut down successfully. The hook must be idempotent,
608
+ short, and perform no I/O; its return value is ignored. A hook exception marks
609
+ cleanup incomplete, retains the first failure, and does not skip later hooks.
610
+ Completed releases are not rolled back; repeated shutdown returns its cached
611
+ result without retrying hooks. A prior closure/wait failure, timeout, or worker
612
+ shutdown failure skips finalization entirely.
613
+
614
+ `Runtime#__shutdown_participant(key:)` only retrieves an existing participant,
615
+ including during/after shutdown. Registration still requires a running Runtime,
616
+ even for an existing key. Agent/Team `get` uses lookup without registration;
617
+ feature gates reject new ownership changes through retained registries after
618
+ closure. Already admitted transitions may finish. Identity registries wait only
619
+ for active construction/purge, then detach Agent references or clear Team
620
+ owners at finalization; live idle objects do not themselves prolong shutdown.
621
+
622
+ `MultiAgent::AdmissionRegistry.for(runtime)` constructs and registers the shared
623
+ coordination admission registry. MultiAgent::HandoffRunner and TeamCoordinator retain that
624
+ registry, admit each synchronous call directly and release it in `ensure` after
625
+ successful admission. The registry closes admission atomically with respect to
626
+ `admit!`; a call admitted before closure is included in the wait, and a call after
627
+ closure raises `RuntimeShutdownError`, including after EventLoop failure. The
628
+ existing `HandoffError` for duplicate admission remains unchanged.
629
+
630
+ This tracks the current synchronous Handoff/Team call, not the lifetime or
631
+ completion of a durable AgentExecution or TeamExecution. Multi-Agent policy stays
632
+ in `multi_agent`; Runtime depends only on the shutdown operations. Application
633
+ authors do not register these participants themselves.
634
+
490
635
  Workflow durable admission participates in EventLoop idleness: a Workflow whose
491
636
  FSMSession has ended but whose durable save is still in flight remains owned until
492
637
  that save completes and owner-aware admission is released.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ StreamEvent = Data.define(:type, :payload)
6
+ end
7
+ end
8
+
9
+ require_relative "../async_event_api"
10
+
11
+ unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
12
+ Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
13
+ end
14
+
15
+ # Durable Agent Recovery and Agent-incarnation event binding.
16
+ require_relative "../recovery/recovery_support"
17
+ require_relative "../recovery/recovery_coordinator"
@@ -96,7 +96,7 @@ module Phronomy
96
96
 
97
97
  def approve_async(execution_id, approval_request_id:, approved: true, config: {})
98
98
  _reject_removed_generic_identity_keys!(config)
99
- owner = Phronomy::Runtime.instance.__agent_execution_owner(execution_id)
99
+ owner = Phronomy::Agent::ExecutionRegistry.existing_for(Phronomy::Runtime.instance)&.agent_execution_owner(execution_id)
100
100
  coordinator = if owner&.agent&.equal?(self)
101
101
  owner.coordinator
102
102
  else
@@ -190,7 +190,7 @@ module Phronomy
190
190
  end
191
191
 
192
192
  bytes = Phronomy::CanonicalJSON.dump(value)
193
- snapshot = Phronomy::Agent::Immutable.copy(
193
+ snapshot = Phronomy::Values::Immutable.copy(
194
194
  Phronomy::CanonicalJSON.load(bytes)
195
195
  )
196
196
  config.merge(durable_context: snapshot)