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
@@ -4,8 +4,8 @@ module Phronomy
4
4
  module Agent
5
5
  # Normal control-transfer signal raised before RubyLLM executes a Tool Call.
6
6
  #
7
- # RubyLLM >= 1.15 guarantees that the complete assistant message has already
8
- # been added to Chat#messages before before_tool_call runs. Keeping that
7
+ # RubyLLM 2.0 guarantees that the complete assistant message has already
8
+ # been added to Chat#messages before after_message runs. Keeping that
9
9
  # message here allows Phronomy to persist the complete Provider outcome even
10
10
  # though the RubyLLM call itself unwinds through this exception.
11
11
  # @api private
@@ -14,10 +14,10 @@ module Phronomy
14
14
  "name" => tool.name.to_s,
15
15
  "description" => tool.description.to_s,
16
16
  "parameters_schema" => normalize(
17
- tool.respond_to?(:parameters_schema) ? tool.parameters_schema : {}
17
+ tool.parameters_schema
18
18
  ),
19
19
  "provider_options" => normalize(
20
- tool.respond_to?(:provider_options) ? tool.provider_options : {}
20
+ tool.provider_options
21
21
  )
22
22
  }
23
23
  end.freeze
@@ -47,13 +47,13 @@ module Phronomy
47
47
 
48
48
  def initialize(runtime_tools:, definitions:)
49
49
  @runtime_tools = Array(runtime_tools).freeze
50
- @definitions = Immutable.copy(definitions)
50
+ @definitions = Phronomy::Values::Immutable.copy(definitions)
51
51
  validate_unique_names!
52
52
  freeze
53
53
  end
54
54
 
55
55
  def select_definitions(expected_definitions)
56
- expected = Immutable.copy(Array(expected_definitions))
56
+ expected = Phronomy::Values::Immutable.copy(Array(expected_definitions))
57
57
  current_by_name = definitions.each_with_index.to_h do |definition, index|
58
58
  [definition.fetch("name"), [definition, runtime_tools.fetch(index)]]
59
59
  end
@@ -70,6 +70,11 @@ module Phronomy
70
70
  raise Phronomy::ConfigurationError,
71
71
  "ContextPolicy selected Tool not present in current Agent configuration: #{name}"
72
72
  end
73
+ if definition["parameters_schema"] == {} && current_definition["parameters_schema"] != {}
74
+ raise Phronomy::ConfigurationError,
75
+ "Saved Tool definition lacks a verifiable parameters_schema: #{name}; " \
76
+ "finish this in-flight execution on its original version before upgrading"
77
+ end
73
78
  unless Phronomy::CanonicalJSON.dump(current_definition) ==
74
79
  Phronomy::CanonicalJSON.dump(definition)
75
80
  raise Phronomy::ConfigurationError,
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
5
+ require "digest"
3
6
  require "securerandom"
4
7
 
5
8
  module Phronomy
6
9
  module Agent
7
10
  class ToolInvocation
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
8
13
  AuthorizationOutcome = Data.define(
9
14
  :tool_invocation_id, :decision, :facts, :reason, :error, :cancelled
10
15
  ) do
@@ -74,6 +79,20 @@ module Phronomy
74
79
  :origin,
75
80
  :metadata
76
81
 
82
+ # Stable identity used both before dispatch and when rebuilding facts.
83
+ # This does not construct, authorize, or execute a Tool invocation.
84
+ # @api private
85
+ def self.semantic_id(execution_id:, llm_call_id:, tool_call_id:, tool_name:)
86
+ source = [
87
+ "tool_invocation",
88
+ execution_id.to_s,
89
+ llm_call_id.to_s,
90
+ tool_call_id.to_s,
91
+ tool_name.to_s
92
+ ].join("\0")
93
+ "tool_invocation-#{Digest::SHA256.hexdigest(source)}".freeze
94
+ end
95
+
77
96
  def self.missing(
78
97
  execution_id:,
79
98
  agent:,
@@ -135,6 +154,16 @@ module Phronomy
135
154
  @phase = phase
136
155
  end
137
156
 
157
+ # Restores a newly constructed invocation from materialized saved facts.
158
+ # Recovery owns snapshot decoding and identity matching. This operation
159
+ # owns state application; it neither dispatches nor reevaluates approval.
160
+ # @api private
161
+ def restore_state!(status:, result: nil, approval_item: nil)
162
+ restore_saved_state!(status, result)
163
+ restore_approval_evidence!(approval_item)
164
+ self
165
+ end
166
+
138
167
  def handle_fsm_event(event)
139
168
  case event.type
140
169
  when :authorization_completed
@@ -215,7 +244,7 @@ module Phronomy
215
244
  cancellation_token: @config[:cancellation_token],
216
245
  on_full: :raise
217
246
  ) { evaluator.send(:evaluate_authorization_command, command) }
218
- runtime.event_loop.supervise_agent_operation(@execution_id, operation)
247
+ Phronomy::Agent::ExecutionRegistry.for(runtime.event_loop).supervise_agent_operation(@execution_id, operation)
219
248
  operation.on_complete do |outcome, error|
220
249
  callback.call(
221
250
  error ? evaluator.send(:authorization_failure_result, tool_invocation_id, error) : outcome
@@ -259,7 +288,7 @@ module Phronomy
259
288
  raise Phronomy::ToolError,
260
289
  "Tool #{@tool.class.name}#call_async must return a completion handle"
261
290
  end
262
- runtime.event_loop.supervise_agent_operation(@execution_id, operation)
291
+ Phronomy::Agent::ExecutionRegistry.for(runtime.event_loop).supervise_agent_operation(@execution_id, operation)
263
292
 
264
293
  evaluator = self.class
265
294
  tool_invocation_id = @id.to_s.freeze
@@ -332,7 +361,7 @@ module Phronomy
332
361
  end
333
362
 
334
363
  def tool_schema
335
- @tool&.respond_to?(:params_schema) ? @tool.params_schema : {}
364
+ @tool&.respond_to?(:parameters_schema) ? @tool.parameters_schema : {}
336
365
  end
337
366
 
338
367
  def display_arguments
@@ -345,6 +374,40 @@ module Phronomy
345
374
 
346
375
  private
347
376
 
377
+ def restore_saved_state!(status, result)
378
+ case status
379
+ when :awaiting_approval
380
+ validate! unless terminal?
381
+ @final_decision = :require_approval
382
+ mark_awaiting_approval!
383
+ when :authorized
384
+ validate! unless terminal?
385
+ @final_decision = :allow
386
+ mark_authorized!
387
+ when :completed
388
+ @result = result
389
+ @status = :completed
390
+ when :rejected
391
+ mark_rejected!
392
+ when :failed
393
+ mark_framework_failed!(
394
+ Phronomy::ToolError.new("durably restored Tool preflight failure")
395
+ )
396
+ when :cancelled
397
+ mark_cancelled!
398
+ else
399
+ raise Phronomy::ExecutionRehydrationRequiredError,
400
+ "unsupported durable Tool snapshot state: #{status.inspect}"
401
+ end
402
+ end
403
+
404
+ def restore_approval_evidence!(item)
405
+ return unless item
406
+
407
+ @facts = immutable_copy(item.facts)
408
+ @authorization_reason = item.reason
409
+ end
410
+
348
411
  def authorization_command
349
412
  definition = @agent.class.agent_definition
350
413
 
@@ -490,24 +553,7 @@ module Phronomy
490
553
  private_class_method :immutable_command_copy
491
554
 
492
555
  def self.phronomy_managed_live_domain_object?(value)
493
- value.is_a?(Phronomy::Agent::Base) ||
494
- value.is_a?(Phronomy::Agent::AgentRoot) ||
495
- value.is_a?(Phronomy::Agent::AgentExecution) ||
496
- value.is_a?(Phronomy::Agent::AgentInvocation) ||
497
- value.is_a?(Phronomy::Agent::ToolInvocation) ||
498
- value.is_a?(Phronomy::Agent::JournalProjection) ||
499
- value.is_a?(Phronomy::Agent::ExecutionCoordinator) ||
500
- value.is_a?(Phronomy::Agent::Context::Capability::Base) ||
501
- value.is_a?(Phronomy::Workflow) ||
502
- value.is_a?(Phronomy::WorkflowRunner) ||
503
- value.is_a?(Phronomy::WorkflowContext) ||
504
- value.is_a?(Phronomy::Runtime) ||
505
- value.is_a?(Phronomy::Task) ||
506
- value.is_a?(Phronomy::EventLoop) ||
507
- value.is_a?(Phronomy::FSMSession) ||
508
- value.is_a?(Phronomy::FSMSession::EventSink) ||
509
- value.is_a?(Phronomy::Concurrency::CancellationToken) ||
510
- value.is_a?(Phronomy::Concurrency::OffloadPool)
556
+ value.is_a?(Phronomy::Concurrency::WorkerInputRestricted)
511
557
  end
512
558
  private_class_method :phronomy_managed_live_domain_object?
513
559
 
@@ -551,7 +597,7 @@ module Phronomy
551
597
 
552
598
  def start_async_tool_operation(runtime)
553
599
  if uses_default_call_async?
554
- Phronomy::Agent::ToolExecutor.call_async(
600
+ Phronomy::Agent::Context::Capability::ToolExecutor.call_async(
555
601
  tool: @tool,
556
602
  args: @arguments,
557
603
  cancellation_token: @config[:cancellation_token],
@@ -665,18 +711,7 @@ module Phronomy
665
711
  end
666
712
 
667
713
  def immutable_copy(value)
668
- case value
669
- when Hash
670
- value.each_with_object({}) do |(key, item), result|
671
- result[immutable_copy(key)] = immutable_copy(item)
672
- end.freeze
673
- when Array
674
- value.map { |item| immutable_copy(item) }.freeze
675
- when String
676
- value.dup.freeze
677
- else
678
- value
679
- end
714
+ Phronomy::Values::Immutable.copy(value)
680
715
  end
681
716
 
682
717
  def redact_for_display(value)
@@ -181,9 +181,9 @@ module Phronomy
181
181
  def self.build_entry_callback(callable, state_name)
182
182
  ->(machine) {
183
183
  result = callable.call(machine.context)
184
- if result.is_a?(Phronomy::Task)
184
+ if result.is_a?(Phronomy::TaskResult)
185
185
  raise Phronomy::InvalidAsyncEntryActionError,
186
- "Tool entry action for #{state_name.inspect} returned Phronomy::Task"
186
+ "Tool entry action for #{state_name.inspect} returned Phronomy::TaskResult"
187
187
  end
188
188
  machine.context = result if result.respond_to?(:set_graph_metadata)
189
189
  }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Phronomy
6
+ class ConfigurationError < Error; end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Error < StandardError; end
5
+ end
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../canonical_json"
4
+
3
5
  module Phronomy
4
- module Agent
6
+ module Values
7
+ # Shared value operations for snapshots, recovery facts, and result views.
8
+ # Hash, Array, and String trees are copied and frozen; other values retain
9
+ # their identity. Canonical JSON validation is a separate operation.
10
+ # @api private
5
11
  module Immutable
6
12
  module_function
7
13
 
14
+ # @api private
8
15
  def copy(value)
9
16
  case value
10
17
  when Hash
@@ -20,6 +27,7 @@ module Phronomy
20
27
  end
21
28
  end
22
29
 
30
+ # @api private
23
31
  def validate_canonical_json!(value, label: "value")
24
32
  Phronomy::CanonicalJSON.dump(value)
25
33
  true
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Values
5
+ # Prepares a Ruby value tree for JSON serialization. Hash keys and Symbol
6
+ # values become Strings; containers are rebuilt and scalar objects retained.
7
+ # This neither freezes values nor validates canonical JSON numbers/encoding.
8
+ # @api private
9
+ module Serializable
10
+ module_function
11
+
12
+ def convert(value, unsupported_message:)
13
+ case value
14
+ when Hash
15
+ value.to_h { |key, child| [key.to_s, convert(child, unsupported_message: unsupported_message)] }
16
+ when Array
17
+ value.map { |child| convert(child, unsupported_message: unsupported_message) }
18
+ when String, Integer, Float, TrueClass, FalseClass, NilClass
19
+ value
20
+ when Symbol
21
+ value.to_s
22
+ else
23
+ if value.respond_to?(:to_h)
24
+ convert(value.to_h, unsupported_message: unsupported_message)
25
+ else
26
+ raise ArgumentError, "#{unsupported_message}: #{value.class}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,13 +6,23 @@ module Phronomy
6
6
  STREAM_CALLBACK_ERROR_POLICIES = %i[report fail_task].freeze
7
7
  private_constant :STREAM_CALLBACK_ERROR_POLICIES
8
8
 
9
+ DEFAULT_FACTORIES = {}
10
+ private_constant :DEFAULT_FACTORIES
11
+
12
+ # Bind fresh-instance factories once during application loading. Concrete
13
+ # component selection belongs to runtime_composition, not configuration.
14
+ # Applications replace instances through the existing configuration writers.
15
+ # @api private
16
+ def self.install_default_factories(tracer:, llm_adapter:)
17
+ DEFAULT_FACTORIES.replace(tracer: tracer, llm_adapter: llm_adapter).freeze
18
+ nil
19
+ end
20
+
9
21
  attr_accessor :default_model
10
22
  attr_accessor :default_embedding_model
11
23
  attr_accessor :tracer
12
24
  attr_accessor :before_llm_input
13
- attr_accessor :default_output_reserve
14
25
  attr_accessor :recursion_limit
15
- attr_accessor :parallel_tool_execution
16
26
  attr_accessor :trace_pii
17
27
  attr_accessor :logger
18
28
  attr_accessor :event_loop_stop_grace_seconds
@@ -40,11 +50,10 @@ module Phronomy
40
50
 
41
51
  def initialize
42
52
  @recursion_limit = 25
43
- @tracer = Phronomy::Tracing::NullTracer.new
53
+ @tracer = DEFAULT_FACTORIES.fetch(:tracer).call
44
54
  @trace_pii = false
45
- @parallel_tool_execution = false
46
55
  @event_loop_stop_grace_seconds = 5
47
- @llm_adapter = Phronomy::LLMAdapter::RubyLLM.new
56
+ @llm_adapter = DEFAULT_FACTORIES.fetch(:llm_adapter).call
48
57
  @event_loop_starvation_threshold_seconds = nil
49
58
  @event_loop_dispatch_threshold_seconds = nil
50
59
  @stream_callback_error_policy = :report
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global configuration access, replacement, and scoped overrides.
4
+ # Explicitly loaded by the application entry point; not a common definition.
5
+ module Phronomy
6
+ class << self
7
+ def configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+
11
+ def configure
12
+ yield configuration
13
+ end
14
+
15
+ def reset_configuration!
16
+ @configuration = Configuration.new
17
+ end
18
+
19
+ def with_configuration
20
+ original = @configuration&.dup
21
+ yield configuration
22
+ ensure
23
+ @configuration = original
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module ContentStore
5
+ # @api private
6
+ module StorageSchema
7
+ CONTENTS = Phronomy::Storage::Resource.new(id: "content.blobs", kind: :blobs,
8
+ attributes: {canonicalization_version: :integer})
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module ContentStore
5
+ # Content identity and digest validation belong to ContentStore, not Blobs.
6
+ # @api private
7
+ class StoredContents < Base
8
+ def initialize(view) = @view = view
9
+
10
+ def put(bytes, canonicalization_version:)
11
+ value = String(bytes).b
12
+ id = content_id_for(value)
13
+ @view.atomic do |bound|
14
+ blob = bound.blobs(StorageSchema::CONTENTS).put_if_absent(key: id, bytes: value,
15
+ attributes: {canonicalization_version: Integer(canonicalization_version)})
16
+ verify!(blob, id)
17
+ id
18
+ end
19
+ rescue Phronomy::Storage::BlobConflictError => error
20
+ raise IntegrityError, error.message
21
+ end
22
+
23
+ def fetch(content_id)
24
+ id = content_id.to_s
25
+ @view.atomic do |bound|
26
+ blob = bound.blobs(StorageSchema::CONTENTS).fetch(id)
27
+ verify!(blob, id)
28
+ blob.bytes.dup
29
+ end
30
+ end
31
+
32
+ def exist?(content_id) = @view.blobs(StorageSchema::CONTENTS).exist?(content_id.to_s)
33
+
34
+ private
35
+
36
+ def verify!(blob, id)
37
+ unless blob.key == id && content_id_for(blob.bytes) == id
38
+ raise IntegrityError, "content digest mismatch: #{id}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class BackpressureError < Error; end
7
+ end
@@ -7,27 +7,34 @@ module Phronomy
7
7
  module Blocking
8
8
  # Submits synchronous application work to the default Runtime OffloadPool.
9
9
  # Admission never waits for queue space. Admission StandardError failures
10
- # become failed Tasks; accepted work retains the pool's Task unchanged.
11
- # A timeout/cancellation can settle that Task before a running worker exits.
12
- # The block must not wait for another Agent, Workflow, or Task.
10
+ # become failed results; accepted work retains the pool's TaskResult unchanged.
11
+ # A timeout/cancellation can settle that TaskResult before a running worker exits.
12
+ # The block must not wait for another Agent, Workflow, or TaskResult.
13
13
  # @param timeout [Numeric, nil] operation deadline, including queue time
14
14
  # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
15
+ # @param invocation_context [Phronomy::InvocationContext, nil] explicit context/scope
15
16
  # @yield synchronous application work
16
- # @return [Phronomy::Task] original accepted Task or failed admission Task
17
+ # @return [Phronomy::TaskResult] original accepted TaskResult or failed admission TaskResult
17
18
  # @raise [ArgumentError] if no block is supplied
18
19
  # @api public
19
- def self.call_async(timeout: nil, cancellation_token: nil, &block)
20
+ def self.call_async(timeout: nil, cancellation_token: nil, invocation_context: nil, &block)
20
21
  raise ArgumentError, "Blocking.call_async requires a block" unless block
21
22
 
22
23
  begin
23
- Phronomy::Runtime.instance.offload.submit(
24
+ unless invocation_context.nil?
25
+ binding = Concurrency::OperationBinding.new(invocation_context: invocation_context,
26
+ cancellation_token: cancellation_token)
27
+ end
28
+ result = Phronomy::Runtime.instance.offload.submit(
24
29
  on_full: :raise,
25
30
  timeout: timeout,
26
- cancellation_token: cancellation_token,
31
+ cancellation_token: binding ? binding.token : cancellation_token,
27
32
  &block
28
33
  )
34
+ binding ? binding.bind(result) : result
29
35
  rescue => error
30
- Phronomy::Task.failed(error, name: "blocking-admission-failed")
36
+ result = Phronomy::TaskResult.failed(error, name: "blocking-admission-failed")
37
+ binding ? binding.bind(result) : result
31
38
  end
32
39
  end
33
40
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class CancellationError < Error; end
7
+ end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  module Concurrency
5
7
  # Cooperative cancellation token for Agent/Tool work.
6
8
  class CancellationToken
9
+ include Phronomy::Concurrency::WorkerInputRestricted
10
+
7
11
  # Creates a token that expires after +seconds+ measured with the monotonic clock.
8
12
  # @api public
9
13
  def self.timeout_after(seconds)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  module Concurrency
5
7
  # A bounded, observable thread pool for synchronous work that must not run on
@@ -13,7 +15,7 @@ module Phronomy
13
15
  # synchronous call. Phronomy deliberately does not classify the workload by
14
16
  # cause; the application decides whether a unit of work is EventLoop-safe.
15
17
  #
16
- # Logical waits are different. Waiting for another Phronomy Task, Agent,
18
+ # Logical waits are different. Waiting for another Phronomy TaskResult, Agent,
17
19
  # Workflow, ToolInvocation, timer, or FSMSession must remain an explicit
18
20
  # EventLoop/FSMSession continuation and must not consume an OffloadPool worker.
19
21
  # See ADR-010.
@@ -22,7 +24,7 @@ module Phronomy
22
24
  #
23
25
  # 1. The total number of worker OS threads is capped.
24
26
  # 2. Queue depth is bounded (backpressure when the pool is saturated).
25
- # 3. Per-operation timeouts and cancellation settle the caller-facing Task.
27
+ # 3. Per-operation timeouts and cancellation settle the caller-facing TaskResult.
26
28
  # 4. Operations that settle after worker execution has started are tracked as
27
29
  # abandoned until that worker returns.
28
30
  # 5. Metrics expose active work, queue depth, cumulative abandonment,
@@ -41,9 +43,11 @@ module Phronomy
41
43
  # task = pool.submit(timeout: 30, cancellation_token: token) { expensive_call }
42
44
  # result = task.wait_result
43
45
  class OffloadPool
46
+ include Phronomy::Concurrency::WorkerInputRestricted
47
+
44
48
  # Private execution record for one submitted synchronous operation.
45
49
  #
46
- # Caller-facing completion is represented exclusively by {Phronomy::Task}.
50
+ # Caller-facing completion is represented exclusively by {Phronomy::TaskResult}.
47
51
  # This object owns only OffloadPool-specific execution state: queue timing,
48
52
  # worker-start linearization, submit timeout/cancellation, abandonment, and
49
53
  # the submitted block itself.
@@ -77,6 +81,7 @@ module Phronomy
77
81
  @submitted_at = submitted_at ||
78
82
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
83
  @mutex = Mutex.new
84
+ @timer_subscriptions = Subscriptions.new
80
85
 
81
86
  # Explicit submit cancellation is operation-wide. Deadline-only tokens are
82
87
  # promoted to cancel! by OffloadPool#submit using the Runtime timer queue.
@@ -92,19 +97,26 @@ module Phronomy
92
97
  @mutex.synchronize { @settled }
93
98
  end
94
99
 
95
- # @return [Boolean] true when the submit-time deadline settled the Task
100
+ # Registration can race logical completion. Dispose the timer even when
101
+ # settlement occurred while schedule was returning.
102
+ def schedule(timer_queue, seconds:, &callback)
103
+ timer_queue.schedule(seconds: seconds, &callback)
104
+ @timer_subscriptions.add { timer_queue.cancel(callback) }
105
+ end
106
+
107
+ # @return [Boolean] true when the submit-time deadline settled the TaskResult
96
108
  # @api private
97
109
  def timed_out?
98
110
  @mutex.synchronize { @timed_out }
99
111
  end
100
112
 
101
- # @return [Boolean] true when submit cancellation settled the Task
113
+ # @return [Boolean] true when submit cancellation settled the TaskResult
102
114
  # @api private
103
115
  def cancelled?
104
116
  @mutex.synchronize { @cancelled }
105
117
  end
106
118
 
107
- # @return [Boolean] true when timeout/cancellation settled the Task after
119
+ # @return [Boolean] true when timeout/cancellation settled the TaskResult after
108
120
  # worker execution had started. The worker is not forcibly interrupted.
109
121
  # @api private
110
122
  def abandoned?
@@ -117,7 +129,7 @@ module Phronomy
117
129
  @wait_time || 0.0
118
130
  end
119
131
 
120
- # Settles the caller-facing Task with a submit-time timeout.
132
+ # Settles the caller-facing TaskResult with a submit-time timeout.
121
133
  #
122
134
  # The worker is not interrupted. If execution has already started, the
123
135
  # operation is marked abandoned and the worker's eventual result is discarded.
@@ -132,7 +144,7 @@ module Phronomy
132
144
  end
133
145
  end
134
146
 
135
- # Settles the caller-facing Task because its submit cancellation token was
147
+ # Settles the caller-facing TaskResult because its submit cancellation token was
136
148
  # cancelled. Cancellation never injects Thread#raise into the worker.
137
149
  #
138
150
  # @return [Boolean] true when this call won settlement
@@ -201,7 +213,7 @@ module Phronomy
201
213
  complete_with_value!(@block.call)
202
214
  rescue Exception => e # rubocop:disable Lint/RescueException
203
215
  # Rescue all Exception subclasses so non-StandardError raises still
204
- # settle the Task and unblock waiters.
216
+ # settle the TaskResult and unblock waiters.
205
217
  complete_with_error!(e)
206
218
  raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
207
219
  end
@@ -280,6 +292,7 @@ module Phronomy
280
292
  end
281
293
 
282
294
  def detach_submit_cancellation
295
+ @timer_subscriptions.close
283
296
  return unless @cancellation_token && @cancellation_callback
284
297
 
285
298
  @cancellation_token.send(
@@ -325,20 +338,20 @@ module Phronomy
325
338
 
326
339
  # Submits synchronous off-EventLoop work to the pool.
327
340
  #
328
- # Returns a {Phronomy::Task} immediately after queue admission; the block
341
+ # Returns a {Phronomy::TaskResult} immediately after queue admission; the block
329
342
  # runs on a worker thread. Do not submit logical waits (for example waiting
330
- # for a child Agent Task) merely to make them asynchronous; those belong to
343
+ # for a child Agent TaskResult) merely to make them asynchronous; those belong to
331
344
  # FSMSession/EventLoop completion events.
332
345
  #
333
346
  # A submit-time +timeout+ is an operation-wide deadline measured from the
334
- # start of this method, including queue wait. The timer settles the Task and
347
+ # start of this method, including queue wait. The timer settles the TaskResult and
335
348
  # notifies +on_complete+ without forcibly interrupting a running worker. If
336
349
  # the deadline fires before worker execution starts, the block is skipped.
337
350
  # If it fires after execution starts, the private Operation is marked
338
351
  # abandoned and the eventual worker result is discarded.
339
352
  #
340
353
  # The submit +cancellation_token+ is also operation-wide. Explicit
341
- # cancellation settles the Task immediately. A token with a monotonic
354
+ # cancellation settles the TaskResult immediately. A token with a monotonic
342
355
  # deadline is attached to the Runtime timer queue so deadline expiry becomes
343
356
  # explicit cancellation without adding a polling Thread. Cancellation before
344
357
  # execution skips the block; cancellation after execution starts abandons
@@ -356,7 +369,7 @@ module Phronomy
356
369
  # @param on_full [Symbol] +:wait+, +:raise+, or +:timeout+
357
370
  # @param full_timeout [Numeric, nil] queue-admission timeout for +on_full: :timeout+
358
371
  # @yield block containing synchronous work
359
- # @return [Phronomy::Task]
372
+ # @return [Phronomy::TaskResult]
360
373
  # @raise [Phronomy::ConfigurationError] when a timer is required but no
361
374
  # timer queue provider is configured
362
375
  # @raise [Phronomy::PoolShutdownError] when the pool has been shut down
@@ -421,7 +434,7 @@ module Phronomy
421
434
 
422
435
  # Arm before queue admission so the deadline includes time spent waiting
423
436
  # for a queue slot.
424
- timer_queue.schedule(seconds: remaining) { operation.fire_timeout! }
437
+ operation.schedule(timer_queue, seconds: remaining) { operation.fire_timeout! }
425
438
  end
426
439
 
427
440
  if cancellation_remaining
@@ -431,7 +444,7 @@ module Phronomy
431
444
  cancellation_token.cancel!
432
445
  return task
433
446
  end
434
- timer_queue.schedule(seconds: remaining) { cancellation_token.cancel! }
447
+ operation.schedule(timer_queue, seconds: remaining) { cancellation_token.cancel! }
435
448
  end
436
449
 
437
450
  # Cancellation/timeout can race with timer registration. Do not enqueue