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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ # Raised when a synchronous API would block the EventLoop control thread.
7
+ class EventLoopReentrancyError < Error; end
8
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Thread-free fan-out/fan-in. Each input block returns the final TaskResult for
5
+ # that JOB; its application-defined intermediate work is not discovered here.
6
+ # @api public
7
+ class Execution
8
+ # @return [InvocationContext] this execution's explicit operation context
9
+ # @api public
10
+ attr_reader :invocation_context
11
+
12
+ # @param inputs [Array] shallowly snapshotted JOB inputs
13
+ # @param timeout [Numeric, nil] whole-execution deadline in seconds
14
+ # @param cancellation_token [Concurrency::CancellationToken, nil]
15
+ # @param invocation_context [InvocationContext, nil] existing information/controls
16
+ # @yield [input, execution] starts one JOB and returns its final TaskResult
17
+ # @return [TaskResult<Array<TaskResult::Outcome>>]
18
+ # @api public
19
+ def self.run_async(inputs, timeout: nil, cancellation_token: nil,
20
+ invocation_context: nil, &block)
21
+ __run_async(inputs, timeout: timeout, cancellation_token: cancellation_token,
22
+ invocation_context: invocation_context, &block)
23
+ end
24
+
25
+ # Starts the same execution once and waits outside EventLoop.
26
+ # @return [Array<TaskResult::Outcome>]
27
+ # @see run_async
28
+ # @api public
29
+ def self.run(inputs, timeout: nil, cancellation_token: nil,
30
+ invocation_context: nil, &block)
31
+ if Runtime.in_event_loop_context?
32
+ raise EventLoopReentrancyError, "Execution.run cannot run on EventLoop; use run_async"
33
+ end
34
+ run_async(inputs, timeout: timeout, cancellation_token: cancellation_token,
35
+ invocation_context: invocation_context, &block).wait_result
36
+ end
37
+
38
+ # Existing Orchestrator concurrency policy uses the same execution engine.
39
+ # This is not an additional public execution entrance or scheduling mode.
40
+ # @api private
41
+ def self.__run_async(inputs, timeout: nil, cancellation_token: nil,
42
+ invocation_context: nil, concurrency_limit: nil, &block)
43
+ raise TypeError, "inputs must be an Array" unless inputs.is_a?(Array)
44
+ raise ArgumentError, "Execution.run_async requires a block" unless block
45
+ if !cancellation_token.nil? && !cancellation_token.is_a?(Concurrency::CancellationToken)
46
+ raise TypeError, "cancellation_token must be a Phronomy::Concurrency::CancellationToken or nil"
47
+ end
48
+ if !invocation_context.nil? && !invocation_context.is_a?(InvocationContext)
49
+ raise TypeError, "invocation_context must be a Phronomy::InvocationContext or nil"
50
+ end
51
+ unless timeout.nil?
52
+ raise TypeError, "timeout must be Numeric or nil" unless timeout.is_a?(Numeric)
53
+ if timeout.is_a?(Complex) || !timeout.finite? || !timeout.to_f.finite?
54
+ raise ArgumentError, "timeout must be a finite real number"
55
+ end
56
+ end
57
+ new(inputs.dup, timeout: timeout, cancellation_token: cancellation_token,
58
+ invocation_context: invocation_context, concurrency_limit: concurrency_limit,
59
+ &block).send(:start)
60
+ end
61
+ private_class_method :__run_async
62
+
63
+ # A scoped observation copy. The source, its controls and its physical work
64
+ # keep their original ownership. Bind before adding this run's map/flat_map.
65
+ # @param source_result [TaskResult]
66
+ # @return [TaskResult] a distinct scoped observation result
67
+ # @raise [TypeError] for a non-TaskResult source
68
+ # @api public
69
+ def observe(source_result)
70
+ unless source_result.is_a?(TaskResult)
71
+ raise TypeError, "source_result must be a Phronomy::TaskResult"
72
+ end
73
+ observed = TaskResult.deferred(name: "execution-observation").__bind_execution(self)
74
+ subscriptions = Concurrency::Subscriptions.new
75
+ subscriptions.result(observed) { subscriptions.close }
76
+ unless __open?
77
+ observed.cancel!(__cancellation_error)
78
+ return observed
79
+ end
80
+ subscriptions.result(source_result) do
81
+ status, value, error = source_result.__snapshot
82
+ if !__open? && status == :completed
83
+ observed.cancel!(__cancellation_error)
84
+ elsif status == :completed
85
+ observed.complete(value)
86
+ elsif status == :cancelled
87
+ observed.cancel!(error)
88
+ else
89
+ observed.fail(error)
90
+ end
91
+ end
92
+ subscriptions.cancellation(@token) { observed.cancel!(__cancellation_error) }
93
+ observed
94
+ end
95
+
96
+ # Scope/admission hooks shared with framework-owned operations.
97
+ # @api private
98
+ def __while_open(&block)
99
+ @collector.while_open(&block)
100
+ end
101
+
102
+ # @api private
103
+ def __open?
104
+ @collector.open?
105
+ end
106
+
107
+ # @api private
108
+ def __cancellation_token
109
+ @token
110
+ end
111
+
112
+ # @api private
113
+ def __cancellation_error
114
+ @cancellation_error
115
+ end
116
+
117
+ private
118
+
119
+ def initialize(inputs, timeout:, cancellation_token:, invocation_context:,
120
+ concurrency_limit:, &block)
121
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
122
+ @inputs = inputs
123
+ @block = block
124
+ @timeout = timeout
125
+ @parent_context = invocation_context
126
+ @parent_token = cancellation_token
127
+ @token = Concurrency::CancellationToken.new
128
+ @subscriptions = Concurrency::Subscriptions.new
129
+ @cancellation_error = CancellationError.new("Execution scope is closed")
130
+ @invocation_context = (invocation_context || InvocationContext.new)
131
+ .merge(cancellation_token: @token).__bind_execution(self)
132
+ @result = TaskResult.deferred(name: "execution")
133
+ @pump_mutex = Mutex.new
134
+ @next_index = 0
135
+ @active = 0
136
+ @draining = false
137
+ @limit = concurrency_limit || [inputs.length, 1].max
138
+ @collector = Concurrency::ResultCollector.new(inputs.length) do |kind, outcomes|
139
+ finish(kind, outcomes)
140
+ end
141
+ end
142
+
143
+ def start
144
+ # All slots and control state exist before any registration can fire.
145
+ [@parent_token, @parent_context&.cancellation_token].compact.uniq.each do |token|
146
+ @subscriptions.cancellation(token) { @collector.finish(:cancelled) }
147
+ end
148
+ if @parent_context&.__execution_scope && !@parent_context.__execution_scope.__open?
149
+ @collector.finish(:cancelled)
150
+ end
151
+ if @parent_context&.deadline
152
+ @subscriptions.after(@parent_context.deadline.remaining_seconds) { @collector.finish(:cancelled) }
153
+ end
154
+ if @timeout
155
+ remaining = @timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at)
156
+ @subscriptions.after(remaining) { @collector.finish(:timeout) }
157
+ end
158
+ @collector.finish(:completed) if @inputs.empty?
159
+ drain
160
+ @result
161
+ end
162
+
163
+ def drain
164
+ claimed = @pump_mutex.synchronize do
165
+ next false if @draining
166
+ @draining = true
167
+ end
168
+ return unless claimed
169
+
170
+ loop do
171
+ work = @collector.while_open do
172
+ @pump_mutex.synchronize do
173
+ if @active < @limit && @next_index < @inputs.length
174
+ index = @next_index
175
+ @next_index += 1
176
+ @active += 1
177
+ [index, @inputs[index], @block]
178
+ else
179
+ @draining = false
180
+ nil
181
+ end
182
+ end
183
+ end
184
+ unless work
185
+ @pump_mutex.synchronize { @draining = false } unless __open?
186
+ break
187
+ end
188
+ index, input, start_block = work
189
+ begin
190
+ result = start_block.call(input, self)
191
+ unless result.is_a?(TaskResult)
192
+ raise TypeError, "JOB at inputs[#{index}] must return a Phronomy::TaskResult"
193
+ end
194
+ rescue => error
195
+ result = TaskResult.failed(error, name: "execution-job-#{index}")
196
+ end
197
+ @collector.watch(index, result) do
198
+ @pump_mutex.synchronize { @active -= 1 }
199
+ drain
200
+ end
201
+ end
202
+ end
203
+
204
+ def finish(kind, outcomes)
205
+ @subscriptions.close
206
+ # A retained scoped result needs the closed-scope gate, not all original
207
+ # inputs and the application start closure. A claimed start keeps its own
208
+ # local copy so cancellation during admission does not invalidate it.
209
+ @pump_mutex.synchronize do
210
+ @inputs = []
211
+ @block = nil
212
+ end
213
+ case kind
214
+ when :completed
215
+ @result.complete(outcomes)
216
+ when :timeout
217
+ @cancellation_error = CancellationError.new("Execution deadline exceeded")
218
+ error = ExecutionTimeoutError.new(outcomes: outcomes)
219
+ @token.cancel!
220
+ @result.fail(error)
221
+ when :cancelled
222
+ @cancellation_error = CancellationError.new("Execution cancelled")
223
+ error = ExecutionCancellationError.new(outcomes: outcomes)
224
+ @token.cancel!
225
+ @result.cancel!(error)
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cancellation_error"
4
+
5
+ module Phronomy
6
+ class ExecutionCancellationError < CancellationError
7
+ attr_reader :outcomes
8
+
9
+ def initialize(message = "Execution cancelled", outcomes:)
10
+ @outcomes = outcomes
11
+ super(message)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Internal lifecycle/delivery contract for feature-owned execution state.
5
+ # Receivers are retained by one EventLoop, not an application callback SPI.
6
+ # Normal mutation and delivery run only on that loop. idle? runs with its
7
+ # lifecycle lock held; it must not call Runtime or acquire that lock again.
8
+ # shutdown runs on a failing loop, or exclusively after a clean loop joins.
9
+ # @api private
10
+ class ExecutionReceiver
11
+ def self.for(event_loop)
12
+ event_loop.__execution_receiver(key: self) ||
13
+ event_loop.__register_execution_receiver(key: self, receiver: new(event_loop: event_loop))
14
+ end
15
+
16
+ # Lookup only: neither a loop nor a receiver is created during inspection.
17
+ def self.existing_for(runtime)
18
+ runtime.__event_loop_if_initialized&.__execution_receiver(key: self)
19
+ end
20
+
21
+ def initialize(event_loop:)
22
+ @event_loop = event_loop
23
+ end
24
+
25
+ def __bound_to?(event_loop)
26
+ @event_loop.equal?(event_loop)
27
+ end
28
+
29
+ def idle?
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def deliver(message)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def shutdown(error:)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def session_retired(fsm_session_id, reason:)
42
+ end
43
+
44
+ private
45
+
46
+ def synchronize(&block)
47
+ @event_loop.__synchronize_execution_state(&block)
48
+ end
49
+
50
+ def admit(&block)
51
+ @event_loop.__admit_execution(self, &block)
52
+ end
53
+
54
+ def post_message(message, admission: false, completion: nil)
55
+ @event_loop.__post_execution(self, message, admission: admission, completion: completion)
56
+ end
57
+
58
+ def assert_event_loop_thread!
59
+ return if @event_loop.current?
60
+
61
+ raise Phronomy::Error,
62
+ "Phronomy-managed live execution state may only be mutated on EventLoop"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "timeout_error"
4
+
5
+ module Phronomy
6
+ class ExecutionTimeoutError < TimeoutError
7
+ attr_reader :outcomes
8
+
9
+ def initialize(message = "Execution timed out", outcomes:)
10
+ @outcomes = outcomes
11
+ super(message)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Shared terminal-state vocabulary and domain decisions for FSM execution.
5
+ # This protocol does not depend on a particular runner or session instance.
6
+ # @api private
7
+ module FSMProtocol
8
+ FINISH = :__end__
9
+
10
+ # A domain policy permits completion, reports failure, or retires a session
11
+ # without settling its caller. It never carries live session authority.
12
+ TerminalDecision = Data.define(:action, :error)
13
+ end
14
+ end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "concurrency/worker_input_restricted"
4
+
3
5
  require "securerandom"
4
6
 
5
7
  module Phronomy
6
8
  # Event-driven execution wrapper for a single FSM session.
7
9
  #
8
10
  # All public methods are called from the Runtime-owned EventLoop thread.
9
- # FSMSession owns FSM execution only; it does not own external Task handles,
11
+ # FSMSession owns FSM execution only; it does not own external TaskResult handles,
10
12
  # activity tokens, callback correlation, or domain-specific stale-event policy.
11
13
  class FSMSession
14
+ include Phronomy::Concurrency::WorkerInputRestricted
15
+
12
16
  class IdentityReservation
13
17
  attr_reader :fsm_session_id
14
18
 
@@ -30,6 +34,8 @@ module Phronomy
30
34
  private_constant :IdentityReservation
31
35
 
32
36
  class EventSink
37
+ include Phronomy::Concurrency::WorkerInputRestricted
38
+
33
39
  attr_reader :fsm_session_id
34
40
 
35
41
  def initialize(event_loop:)
@@ -60,8 +66,6 @@ module Phronomy
60
66
  end
61
67
  end
62
68
 
63
- FINISH = WorkflowRunner::FINISH
64
-
65
69
  attr_reader :id, :context, :event_sink
66
70
 
67
71
  # Returns the live current Workflow phase. During an active FSM transition
@@ -95,7 +99,7 @@ module Phronomy
95
99
  context_metadata: {},
96
100
  event_sink: nil,
97
101
  identity_reservation: nil,
98
- terminal_barrier: nil
102
+ terminal_policy: nil
99
103
  )
100
104
  @id = if identity_reservation
101
105
  unless identity_reservation.is_a?(IdentityReservation)
@@ -123,7 +127,7 @@ module Phronomy
123
127
  @resume_event = resume_event
124
128
  @resume_phase = resume_phase
125
129
  @stable_observer = stable_observer
126
- @terminal_barrier = terminal_barrier
130
+ @terminal_policy = terminal_policy
127
131
  @terminal_lifecycle_state = :running
128
132
  @pending_terminal_type = nil
129
133
  @pending_terminal_notify_stable = false
@@ -160,14 +164,13 @@ module Phronomy
160
164
  def handle(event)
161
165
  return if @done
162
166
 
163
- if event.type == :workflow_terminal_persistence_result
164
- handle_terminal_persistence_result(event.payload)
167
+ if @terminal_policy&.handles?(event)
168
+ handle_terminal_event(event)
165
169
  return
166
170
  end
167
171
 
168
- # Once terminal persistence begins, ordinary Workflow events no longer
169
- # have result authority. Only the persistence completion for this concrete
170
- # FSMSession can advance its terminal lifecycle.
172
+ # Once a terminal decision is pending, ordinary events cannot advance
173
+ # the state machine. Only the injected policy can permit terminalization.
171
174
  return unless @terminal_lifecycle_state == :running
172
175
 
173
176
  context_disposition = apply_context_event(event)
@@ -195,9 +198,9 @@ module Phronomy
195
198
  end
196
199
 
197
200
  def apply_synchronous_action_result!(result, state_name)
198
- if result.is_a?(Phronomy::Task)
201
+ if result.is_a?(Phronomy::TaskResult)
199
202
  raise Phronomy::InvalidAsyncEntryActionError,
200
- "Entry action for state #{state_name.inspect} returned Phronomy::Task. " \
203
+ "Entry action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
201
204
  "Start the asynchronous operation, register its callback/listener, " \
202
205
  "and return the WorkflowContext or nil."
203
206
  end
@@ -261,7 +264,7 @@ module Phronomy
261
264
  end
262
265
 
263
266
  def advance_or_halt
264
- return finish! if @current_state == FINISH
267
+ return finish! if @current_state == FSMProtocol::FINISH
265
268
 
266
269
  # A wait state is the logical end of this Workflow execution segment. Its
267
270
  # public stable-state notification is therefore part of terminalization
@@ -324,7 +327,7 @@ module Phronomy
324
327
  def finish!(notify_stable: false)
325
328
  request_terminal!(
326
329
  :finished,
327
- phase: :__end__,
330
+ phase: FSMProtocol::FINISH,
328
331
  notify_stable: notify_stable
329
332
  )
330
333
  end
@@ -339,13 +342,13 @@ module Phronomy
339
342
  apply_context_metadata!(phase: phase)
340
343
  @pending_terminal_type = terminal_type
341
344
  @pending_terminal_notify_stable = notify_stable
342
- unless @terminal_barrier
345
+ unless @terminal_policy
343
346
  complete_terminal!(terminal_type)
344
347
  return
345
348
  end
346
349
 
347
- @terminal_lifecycle_state = :persisting_terminal
348
- @terminal_barrier.call(
350
+ @terminal_lifecycle_state = :awaiting_terminal
351
+ @terminal_policy.start(
349
352
  terminal_type: terminal_type,
350
353
  context: @ctx,
351
354
  event_sink: @event_sink
@@ -354,31 +357,35 @@ module Phronomy
354
357
  finish_with_error(error)
355
358
  end
356
359
 
357
- def handle_terminal_persistence_result(result)
358
- return unless @terminal_lifecycle_state == :persisting_terminal
360
+ def handle_terminal_event(event)
361
+ return unless @terminal_lifecycle_state == :awaiting_terminal
359
362
 
360
- case result.outcome
361
- when :success
363
+ decision = @terminal_policy.decision_for(event)
364
+ case decision.action
365
+ when :complete
362
366
  complete_terminal!(@pending_terminal_type)
363
- when :known_failure
364
- finish_with_error(
365
- result.error || Phronomy::Error.new("Workflow terminal persistence failed")
366
- )
367
- when :outcome_unknown
368
- @done = true
369
- @terminal_lifecycle_state = :recovery_required
370
- post_recovery_required_event(result.error)
367
+ when :fail
368
+ finish_with_error(decision.error)
369
+ when :retire
370
+ retire_without_result!(decision.error)
371
371
  else
372
372
  raise Phronomy::Error,
373
- "unknown Workflow terminal persistence outcome: #{result.outcome.inspect}"
373
+ "unknown FSM terminal decision: #{decision.action.inspect}"
374
374
  end
375
375
  end
376
376
 
377
- def complete_terminal!(terminal_type)
377
+ def retire_without_result!(error)
378
378
  @done = true
379
+ @terminal_lifecycle_state = :retired
380
+ post_recovery_required_event(error)
381
+ end
382
+
383
+ def complete_terminal!(terminal_type)
379
384
  @terminal_lifecycle_state =
380
385
  (terminal_type == :halted) ? :halted : :completed
386
+ # Keep the error path open until the observer returns successfully.
381
387
  notify_stable_state! if @pending_terminal_notify_stable
388
+ @done = true
382
389
  post_terminal_event(terminal_type, @ctx)
383
390
  end
384
391
 
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "invalid_async_workflow_action_error"
4
+
3
5
  module Phronomy
4
- # Raised when a synchronous FSM entry action returns Phronomy::Task.
6
+ # Raised when a synchronous FSM entry action returns Phronomy::TaskResult.
5
7
  #
6
8
  # Entry actions are Run-to-Completion callbacks. They may start asynchronous
7
9
  # work, but completion must return through a later explicit event.
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "invalid_async_workflow_action_error"
4
+
3
5
  module Phronomy
4
- # Raised when a synchronous Workflow transition action returns Phronomy::Task.
6
+ # Raised when a synchronous Workflow transition action returns Phronomy::TaskResult.
5
7
  #
6
8
  # Transition actions are Run-to-Completion callbacks. They may start
7
9
  # asynchronous work, but completion must return through a later explicit event.
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../common/error"
4
+
3
5
  module Phronomy
4
- # Base error for synchronous Workflow callbacks that return Phronomy::Task.
6
+ # Base error for synchronous Workflow callbacks that return Phronomy::TaskResult.
5
7
  #
6
8
  # Workflow callbacks may start asynchronous work, but they must return
7
9
  # synchronously and deliver completion through a later explicit event.
@@ -56,7 +56,19 @@ module Phronomy
56
56
  redaction_policy: overrides.fetch(:redaction_policy, @redaction_policy),
57
57
  task_id: overrides.fetch(:task_id, @task_id),
58
58
  parent_task_id: overrides.fetch(:parent_task_id, @parent_task_id)
59
- )
59
+ ).__bind_execution(@execution_scope)
60
+ end
61
+
62
+ # Framework-owned metadata; not inferred from the executing Ruby thread.
63
+ # @api private
64
+ def __bind_execution(execution)
65
+ @execution_scope = execution
66
+ self
67
+ end
68
+
69
+ # @api private
70
+ def __execution_scope
71
+ @execution_scope
60
72
  end
61
73
 
62
74
  # @api private
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class PoolShutdownError < Error; end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class RecursionLimitError < Error; end
7
+ end
@@ -44,6 +44,17 @@ module Phronomy
44
44
  end
45
45
  end
46
46
 
47
+ # Remove this registration without changing another user's timers. A
48
+ # callback already taken by fire_due may still run; owners arbitrate once.
49
+ # @api private
50
+ def cancel(callback)
51
+ @mutex.synchronize do
52
+ before = @heap.length
53
+ @heap.delete_if { |(_, registered)| registered == callback }
54
+ before != @heap.length
55
+ end
56
+ end
57
+
47
58
  # Executes all callbacks whose deadline is due. Must be called by EventLoop.
48
59
  def fire_due
49
60
  callbacks = @mutex.synchronize do