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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
3
5
  require "securerandom"
4
6
 
5
7
  module Phronomy
@@ -16,9 +18,9 @@ module Phronomy
16
18
  # to the same FSMSession before that session becomes halted/completed.
17
19
  # @api private
18
20
  class WorkflowRunner
19
- include Phronomy::Runnable
21
+ include Phronomy::Concurrency::WorkerInputRestricted
20
22
 
21
- FINISH = :__end__
23
+ include Phronomy::Runnable
22
24
 
23
25
  Execution = Data.define(
24
26
  :context,
@@ -138,7 +140,7 @@ module Phronomy
138
140
  end
139
141
 
140
142
  # Posts an application-defined event to the currently live Workflow owner.
141
- # EventLoop resolves workflow_instance_id to the current concrete
143
+ # WorkflowExecutionRegistry resolves workflow_instance_id to the current concrete
142
144
  # fsm_session_id. The opaque admission owner token is deliberately not a
143
145
  # routing identity and is not exposed here.
144
146
  def signal(workflow_instance_id:, event:, payload: nil)
@@ -151,7 +153,10 @@ module Phronomy
151
153
  "Valid events: #{@external_events.keys.inspect}"
152
154
  end
153
155
 
154
- Phronomy::Runtime.instance.event_loop.post_to_workflow(
156
+ registry = Phronomy::WorkflowExecutionRegistry.existing_for(Phronomy::Runtime.instance)
157
+ return false unless registry
158
+
159
+ registry.post_to_workflow(
155
160
  workflow_instance_id: workflow_instance_id,
156
161
  event: event_name,
157
162
  payload: payload
@@ -216,7 +221,7 @@ module Phronomy
216
221
  def start_new_execution(input, config, stable_observer: nil)
217
222
  reject_legacy_workflow_identity_key!(config)
218
223
  runtime = Phronomy::Runtime.instance
219
- result_task = Phronomy::Task.deferred(name: "workflow:preparing")
224
+ result_task = Phronomy::TaskResult.deferred(name: "workflow:preparing")
220
225
  explicit_workflow_instance_id = !config[:workflow_instance_id].nil?
221
226
  workflow_instance_id = (config[:workflow_instance_id] || SecureRandom.uuid).to_s.freeze
222
227
  Phronomy::Tracing::Automatic.observe_task(
@@ -247,7 +252,7 @@ module Phronomy
247
252
  def start_resume_execution(state, input:, event_name:, current_phase:)
248
253
  runtime = Phronomy::Runtime.instance
249
254
  workflow_instance_id = state.workflow_instance_id.to_s.freeze
250
- result_task = Phronomy::Task.deferred(name: "workflow-resume:#{workflow_instance_id}")
255
+ result_task = Phronomy::TaskResult.deferred(name: "workflow-resume:#{workflow_instance_id}")
251
256
  Phronomy::Tracing::Automatic.observe_task(
252
257
  result_task,
253
258
  "workflow.execution",
@@ -277,7 +282,7 @@ module Phronomy
277
282
  runtime = Phronomy::Runtime.instance
278
283
  event_loop = runtime.event_loop
279
284
  admitted = false
280
- event_loop.admit_workflow(
285
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).admit_workflow(
281
286
  request.workflow_instance_id,
282
287
  owner_token: request.owner_token
283
288
  )
@@ -318,7 +323,7 @@ module Phronomy
318
323
  )
319
324
  rescue => error
320
325
  if admitted
321
- event_loop.release_workflow(
326
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
322
327
  request.workflow_instance_id,
323
328
  owner_token: request.owner_token
324
329
  )
@@ -330,7 +335,7 @@ module Phronomy
330
335
  runtime = Phronomy::Runtime.instance
331
336
  event_loop = runtime.event_loop
332
337
  admitted = false
333
- event_loop.admit_workflow(
338
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).admit_workflow(
334
339
  request.workflow_instance_id,
335
340
  owner_token: request.owner_token
336
341
  )
@@ -366,7 +371,7 @@ module Phronomy
366
371
  )
367
372
  rescue => error
368
373
  if admitted
369
- event_loop.release_workflow(
374
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
370
375
  request.workflow_instance_id,
371
376
  owner_token: request.owner_token
372
377
  )
@@ -503,7 +508,7 @@ module Phronomy
503
508
  local_snapshot = normalize_snapshot(snapshot_for(state))
504
509
  return durable_revision if normalized_durable == local_snapshot
505
510
 
506
- raise Phronomy::Persistence::ConflictError,
511
+ raise Phronomy::Storage::ConflictError,
507
512
  "Workflow state changed since the supplied halted context for " \
508
513
  "workflow_instance_id #{state.workflow_instance_id.inspect}; explicit reload/reconciliation is required"
509
514
  end
@@ -576,13 +581,13 @@ module Phronomy
576
581
  resume_phase: resume_phase,
577
582
  stable_observer: stable_observer
578
583
  )
579
- event_loop.bind_workflow_session(
584
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).bind_workflow_session(
580
585
  execution.workflow_instance_id,
581
586
  owner_token: execution.owner_token,
582
587
  fsm_session_id: session.id
583
588
  )
584
589
 
585
- source_task = Phronomy::Task.deferred(name: "workflow-source:#{session.id}")
590
+ source_task = Phronomy::TaskResult.deferred(name: "workflow-source:#{session.id}")
586
591
  source_task.on_complete do |result, error|
587
592
  finalize_execution(
588
593
  execution: execution,
@@ -591,11 +596,12 @@ module Phronomy
591
596
  error: error
592
597
  )
593
598
  end
594
- event_loop.register(session, completion: source_task)
599
+ event_loop.register(session, completion: source_task,
600
+ receiver: Phronomy::WorkflowExecutionRegistry.for(event_loop))
595
601
  result_task
596
602
  rescue => error
597
603
  if event_loop&.current?
598
- event_loop&.release_workflow(
604
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
599
605
  execution.workflow_instance_id,
600
606
  owner_token: execution.owner_token
601
607
  )
@@ -608,7 +614,7 @@ module Phronomy
608
614
  def finalize_execution(execution:, result_task:, result:, error:)
609
615
  event_loop = Phronomy::Runtime.instance.event_loop
610
616
  assert_event_loop!(event_loop)
611
- event_loop.release_workflow(
617
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
612
618
  execution.workflow_instance_id,
613
619
  owner_token: execution.owner_token
614
620
  )
@@ -624,73 +630,117 @@ module Phronomy
624
630
  runtime = Phronomy::Runtime.instance
625
631
  event_loop = runtime.event_loop
626
632
  assert_event_loop!(event_loop)
627
- event_loop.mark_workflow_admission(
633
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).mark_workflow_admission(
628
634
  execution.workflow_instance_id,
629
635
  owner_token: execution.owner_token,
630
636
  state: :persisting_terminal
631
637
  )
632
638
 
633
- operation = WorkflowTerminalPersistenceCommand.new(
639
+ operation = Phronomy::WorkflowRunner::WorkflowTerminalPersistenceCommand.new(
634
640
  repository: execution.repository,
635
641
  workflow_instance_id: execution.workflow_instance_id.to_s.freeze,
636
642
  expected_revision: execution.expected_revision,
637
643
  snapshot: deep_immutable_copy(snapshot_for(context))
638
644
  )
645
+
639
646
  task = runtime.offload.submit(on_full: :raise) do
640
- revision = operation.repository.save(
641
- operation.workflow_instance_id,
642
- expected_revision: operation.expected_revision,
643
- snapshot: operation.snapshot
647
+ persist_terminal_snapshot(operation)
648
+ end
649
+ task.on_complete do |result, operation_error|
650
+ deliver_terminal_persistence_result(execution, event_sink, result, operation_error)
651
+ end
652
+ terminal_type
653
+ end
654
+
655
+ def persist_terminal_snapshot(operation)
656
+ revision = operation.repository.save(
657
+ operation.workflow_instance_id,
658
+ expected_revision: operation.expected_revision,
659
+ snapshot: operation.snapshot
660
+ )
661
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
662
+ outcome: :success,
663
+ revision: revision,
664
+ error: nil
665
+ )
666
+ rescue Phronomy::Storage::ConflictError,
667
+ Phronomy::Storage::NotFoundError,
668
+ Phronomy::Storage::SerializationError,
669
+ Phronomy::Storage::UnsupportedBackendError => error
670
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
671
+ outcome: :known_failure,
672
+ revision: nil,
673
+ error: error
674
+ )
675
+ rescue => error
676
+ reconcile_workflow_terminal_f1(operation, error)
677
+ end
678
+
679
+ def deliver_terminal_persistence_result(execution, event_sink, result, operation_error)
680
+ delivery = if operation_error
681
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
682
+ outcome: :outcome_unknown,
683
+ revision: nil,
684
+ error: operation_error
644
685
  )
645
- WorkflowTerminalPersistenceResult.new(
686
+ else
687
+ result
688
+ end
689
+ accepted = event_sink.post(:workflow_terminal_persistence_result, delivery)
690
+ unless accepted
691
+ Phronomy.configuration.logger&.warn(
692
+ "[Phronomy] EventLoop rejected Workflow terminal persistence result " \
693
+ "for #{execution.workflow_instance_id.inspect}"
694
+ )
695
+ end
696
+ end
697
+
698
+ def reconcile_workflow_terminal_f1(operation, original_error)
699
+ record = operation.repository.load(operation.workflow_instance_id)
700
+ case Phronomy::Recovery.compare_revisioned_snapshot(
701
+ record: record,
702
+ expected_pre_revision: operation.expected_revision,
703
+ intended_snapshot: operation.snapshot
704
+ )
705
+ when :post_state
706
+ revision = Phronomy::Recovery.fetch_value(record, :revision)
707
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
646
708
  outcome: :success,
647
709
  revision: revision,
648
710
  error: nil
649
711
  )
650
- rescue Phronomy::Persistence::ConflictError,
651
- Phronomy::Persistence::NotFoundError,
652
- Phronomy::Persistence::SerializationError,
653
- Phronomy::Persistence::UnsupportedBackendError => error
654
- WorkflowTerminalPersistenceResult.new(
712
+ when :pre_state
713
+ # The uncertain save is now known not to have committed. Do not blindly
714
+ # retry it here; surface a known failure through the ordinary terminal
715
+ # barrier semantics.
716
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
655
717
  outcome: :known_failure,
656
718
  revision: nil,
657
- error: error
719
+ error: original_error
658
720
  )
659
- rescue => error
660
- # The Backend SPI does not currently provide a portable commit-outcome
661
- # classifier for arbitrary storage/transport failures. If non-commit is
662
- # not guaranteed by the Phronomy error contract, fail closed as F1.
663
- WorkflowTerminalPersistenceResult.new(
721
+ else
722
+ conflict = Phronomy::Storage::ConflictError.new(
723
+ "Workflow terminal Persistence outcome conflicts with both expected " \
724
+ "pre-state and intended post-state for #{operation.workflow_instance_id.inspect}"
725
+ )
726
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
664
727
  outcome: :outcome_unknown,
665
728
  revision: nil,
666
- error: error
729
+ error: conflict
667
730
  )
668
731
  end
669
- task.on_complete do |result, operation_error|
670
- delivery = if operation_error
671
- WorkflowTerminalPersistenceResult.new(
672
- outcome: :outcome_unknown,
673
- revision: nil,
674
- error: operation_error
675
- )
676
- else
677
- result
678
- end
679
- accepted = event_sink.post(:workflow_terminal_persistence_result, delivery)
680
- unless accepted
681
- Phronomy.configuration.logger&.warn(
682
- "[Phronomy] EventLoop rejected Workflow terminal persistence result " \
683
- "for #{execution.workflow_instance_id.inspect}"
684
- )
685
- end
686
- end
687
- terminal_type
732
+ rescue => reconciliation_error
733
+ Phronomy::WorkflowRunner::WorkflowTerminalPersistenceResult.new(
734
+ outcome: :outcome_unknown,
735
+ revision: nil,
736
+ error: reconciliation_error
737
+ )
688
738
  end
689
739
 
690
740
  def release_and_fail(request, error)
691
741
  event_loop = Phronomy::Runtime.instance.event_loop
692
742
  assert_event_loop!(event_loop)
693
- event_loop.release_workflow(
743
+ Phronomy::WorkflowExecutionRegistry.for(event_loop).release_workflow(
694
744
  request.workflow_instance_id,
695
745
  owner_token: request.owner_token
696
746
  )
@@ -711,17 +761,6 @@ module Phronomy
711
761
  resume_phase: nil,
712
762
  stable_observer: nil
713
763
  )
714
- terminal_barrier = if execution.repository && execution.persist
715
- ->(terminal_type:, context:, event_sink:) {
716
- begin_terminal_persistence_on_event_loop(
717
- execution,
718
- terminal_type: terminal_type,
719
- context: context,
720
- event_sink: event_sink
721
- )
722
- }
723
- end
724
-
725
764
  Phronomy::FSMSession.new(
726
765
  context: execution.context,
727
766
  context_metadata: {
@@ -739,7 +778,22 @@ module Phronomy
739
778
  resume_event: resume_event,
740
779
  resume_phase: resume_phase,
741
780
  stable_observer: stable_observer,
742
- terminal_barrier: terminal_barrier
781
+ terminal_policy: build_terminal_policy(execution)
782
+ )
783
+ end
784
+
785
+ def build_terminal_policy(execution)
786
+ return unless execution.repository && execution.persist
787
+
788
+ Phronomy::WorkflowTerminalPolicy.new(
789
+ persist: ->(terminal_type:, context:, event_sink:) {
790
+ begin_terminal_persistence_on_event_loop(
791
+ execution,
792
+ terminal_type: terminal_type,
793
+ context: context,
794
+ event_sink: event_sink
795
+ )
796
+ }
743
797
  )
744
798
  end
745
799
 
@@ -766,13 +820,10 @@ module Phronomy
766
820
  end
767
821
 
768
822
  def post_control(runtime, command)
769
- runtime.event_loop.post(
770
- Phronomy::Event.new(
771
- type: :workflow_control,
772
- target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
773
- payload: {command: command}
774
- )
775
- )
823
+ admission = command.is_a?(StartCommand) || command.is_a?(ResumeCommand)
824
+ completion = admission ? command.result_task : command.request.result_task
825
+ Phronomy::WorkflowExecutionRegistry.for(runtime.event_loop).post(command,
826
+ admission: admission, completion: completion)
776
827
  end
777
828
 
778
829
  def runtime_rejected_error(action)
@@ -797,7 +848,7 @@ module Phronomy
797
848
  end
798
849
 
799
850
  def failed_task(name, error)
800
- task = Phronomy::Task.deferred(name: name)
851
+ task = Phronomy::TaskResult.deferred(name: name)
801
852
  fail_task(task, error)
802
853
  task
803
854
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Interprets durable Workflow results for the shared FSM terminal gate.
5
+ # The session owns pending state; Runner owns persistence and reconciliation.
6
+ # @api private
7
+ class WorkflowTerminalPolicy
8
+ include Phronomy::Concurrency::WorkerInputRestricted
9
+
10
+ def initialize(persist:)
11
+ @persist = persist
12
+ end
13
+
14
+ def start(terminal_type:, context:, event_sink:)
15
+ @persist.call(terminal_type: terminal_type, context: context, event_sink: event_sink)
16
+ end
17
+
18
+ def handles?(event)
19
+ event.type == :workflow_terminal_persistence_result
20
+ end
21
+
22
+ def decision_for(event)
23
+ result = event.payload
24
+ case result.outcome
25
+ when :success
26
+ FSMProtocol::TerminalDecision.new(action: :complete, error: nil)
27
+ when :known_failure
28
+ FSMProtocol::TerminalDecision.new(
29
+ action: :fail,
30
+ error: result.error || Phronomy::Error.new("Workflow terminal persistence failed")
31
+ )
32
+ when :outcome_unknown
33
+ FSMProtocol::TerminalDecision.new(action: :retire, error: result.error)
34
+ else
35
+ raise Phronomy::Error,
36
+ "unknown Workflow terminal persistence outcome: #{result.outcome.inspect}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Workflow
5
+ module Persistence
6
+ # Current-format record schemas and conversion for this domain.
7
+ # @api private
8
+ module Codec
9
+ extend Phronomy::Storage::RecordCodec
10
+
11
+ WORKFLOW_STATE_RECORD_TYPE = "phronomy.workflow_state"
12
+ WORKFLOW_STATE_FORMAT_VERSION = "0.1"
13
+ WORKFLOW_STATE_KEYS = %w[
14
+ workflow_instance_id workflow_revision snapshot
15
+ ].freeze
16
+
17
+ WORKFLOW_SNAPSHOT_KEYS = %w[fields phase].freeze
18
+
19
+ module_function
20
+
21
+ def encode_workflow_state(workflow_instance_id:, workflow_revision:, snapshot:)
22
+ normalized_snapshot = canonicalize_workflow_snapshot(snapshot)
23
+ validate_workflow_snapshot!(normalized_snapshot)
24
+ revision = Integer(workflow_revision)
25
+ unless revision.positive?
26
+ raise Phronomy::Storage::SerializationError,
27
+ "Workflow durable revision must be positive"
28
+ end
29
+
30
+ payload = {
31
+ "workflow_instance_id" => String(workflow_instance_id),
32
+ "workflow_revision" => revision,
33
+ "snapshot" => normalized_snapshot
34
+ }
35
+ validate_workflow_state_payload!(payload)
36
+ build_record(WORKFLOW_STATE_RECORD_TYPE, WORKFLOW_STATE_FORMAT_VERSION, payload)
37
+ rescue Phronomy::Storage::SerializationError
38
+ raise
39
+ rescue => error
40
+ serialization_error("cannot encode Workflow state", error)
41
+ end
42
+
43
+ def decode_workflow_state(record, expected_workflow_instance_id: nil)
44
+ payload = current_payload!(
45
+ record,
46
+ record_type: WORKFLOW_STATE_RECORD_TYPE,
47
+ format_version: WORKFLOW_STATE_FORMAT_VERSION,
48
+ keys: WORKFLOW_STATE_KEYS,
49
+ label: "Workflow state payload"
50
+ )
51
+ validate_workflow_state_payload!(payload)
52
+ workflow_instance_id = payload.fetch("workflow_instance_id")
53
+ if expected_workflow_instance_id &&
54
+ workflow_instance_id != expected_workflow_instance_id.to_s
55
+ raise Phronomy::Storage::SerializationError,
56
+ "Workflow state identity mismatch: #{workflow_instance_id.inspect} != " \
57
+ "#{expected_workflow_instance_id.to_s.inspect}"
58
+ end
59
+
60
+ {
61
+ snapshot: immutable_copy(payload.fetch("snapshot")),
62
+ revision: payload.fetch("workflow_revision")
63
+ }.freeze
64
+ rescue Phronomy::Storage::SerializationError
65
+ raise
66
+ rescue => error
67
+ serialization_error("cannot decode Workflow state", error)
68
+ end
69
+
70
+ def validate_workflow_state_payload!(payload)
71
+ validate_exact_keys!(payload, WORKFLOW_STATE_KEYS, label: "Workflow state payload")
72
+ require_nonempty_string!(payload, "workflow_instance_id", label: "Workflow state payload")
73
+ require_positive_integer!(payload, "workflow_revision", label: "Workflow state payload")
74
+ validate_workflow_snapshot!(payload.fetch("snapshot"))
75
+ payload
76
+ end
77
+
78
+ def validate_workflow_snapshot!(snapshot)
79
+ validate_exact_keys!(snapshot, WORKFLOW_SNAPSHOT_KEYS, label: "Workflow snapshot")
80
+ unless snapshot.fetch("fields").is_a?(Hash)
81
+ raise Phronomy::Storage::SerializationError,
82
+ "Workflow snapshot fields must be a Hash"
83
+ end
84
+ phase = snapshot.fetch("phase")
85
+ unless phase.nil? || phase.is_a?(String)
86
+ raise Phronomy::Storage::SerializationError,
87
+ "Workflow snapshot phase must be a String or nil"
88
+ end
89
+ Phronomy::CanonicalJSON.dump(snapshot)
90
+ snapshot
91
+ rescue ArgumentError => error
92
+ raise Phronomy::Storage::SerializationError,
93
+ "Workflow snapshot is not canonical JSON compatible: #{error.message}"
94
+ end
95
+
96
+ def canonicalize_workflow_snapshot(snapshot)
97
+ source = top_level_string_keys(snapshot, label: "Workflow snapshot")
98
+ fields = source.fetch("fields")
99
+ unless fields.is_a?(Hash)
100
+ raise Phronomy::Storage::SerializationError,
101
+ "Workflow snapshot fields must be a Hash"
102
+ end
103
+ {
104
+ "fields" => canonicalize_workflow_value(fields),
105
+ "phase" => source["phase"]&.to_s
106
+ }
107
+ end
108
+
109
+ def canonicalize_workflow_value(value)
110
+ case value
111
+ when Hash
112
+ value.each_with_object({}) do |(key, child), result|
113
+ unless key.is_a?(String) || key.is_a?(Symbol)
114
+ raise Phronomy::Storage::SerializationError,
115
+ "Workflow field key must be String or Symbol, got #{key.class}"
116
+ end
117
+ string_key = key.to_s
118
+ if result.key?(string_key)
119
+ raise Phronomy::Storage::SerializationError,
120
+ "duplicate Workflow field key after normalization: #{string_key.inspect}"
121
+ end
122
+ result[string_key] = canonicalize_workflow_value(child)
123
+ end
124
+ when Array
125
+ value.map { |child| canonicalize_workflow_value(child) }
126
+ when Symbol
127
+ value.to_s
128
+ when String, Integer, Float, TrueClass, FalseClass, NilClass
129
+ value
130
+ else
131
+ raise Phronomy::Storage::SerializationError,
132
+ "unsupported Workflow durable value: #{value.class}"
133
+ end
134
+ end
135
+
136
+ def immutable_copy(value)
137
+ case value
138
+ when Hash
139
+ value.each_with_object({}) do |(key, child), result|
140
+ result[key.dup.freeze] = immutable_copy(child)
141
+ end.freeze
142
+ when Array
143
+ value.map { |child| immutable_copy(child) }.freeze
144
+ when String
145
+ value.dup.freeze
146
+ else
147
+ value
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Workflow
5
+ module Persistence
6
+ # Owns Workflow snapshot encoding and the initial revision.
7
+ # @api private
8
+ class StateRepository
9
+ def initialize(view) = @view = view
10
+
11
+ def load(workflow_instance_id)
12
+ @view.atomic do |bound|
13
+ entry = bound.records(Phronomy::WorkflowStorageSchema::STATES).read(workflow_instance_id.to_s)
14
+ entry && decode(entry, workflow_instance_id)
15
+ end
16
+ end
17
+
18
+ def save(workflow_instance_id, expected_revision:, snapshot:)
19
+ expected = expected_revision.nil? ? nil : Integer(expected_revision)
20
+ revision = expected.nil? ? 1 : expected + 1
21
+ record = Codec.encode_workflow_state(workflow_instance_id: workflow_instance_id,
22
+ workflow_revision: revision, snapshot: snapshot)
23
+ @view.atomic do |bound|
24
+ records = bound.records(Phronomy::WorkflowStorageSchema::STATES)
25
+ values = {key: workflow_instance_id.to_s, attributes: {}, record: record}
26
+ entry = if expected.nil?
27
+ records.insert(**values, revision: revision)
28
+ else
29
+ records.replace(**values, expected_revision: expected, next_revision: revision)
30
+ end
31
+ decoded = decode(entry, workflow_instance_id)
32
+ unless decoded.fetch(:revision) == revision
33
+ raise Phronomy::Storage::SerializationError, "backend returned another Workflow revision"
34
+ end
35
+ revision
36
+ end
37
+ rescue Phronomy::Storage::NotFoundError => error
38
+ raise Phronomy::Storage::ConflictError, error.message
39
+ end
40
+
41
+ def delete(workflow_instance_id, expected_revision:)
42
+ @view.records(Phronomy::WorkflowStorageSchema::STATES).delete(key: workflow_instance_id.to_s, expected_revision: Integer(expected_revision))
43
+ end
44
+
45
+ private
46
+
47
+ def decode(entry, workflow_instance_id)
48
+ decoded = Codec.decode_workflow_state(entry.record, expected_workflow_instance_id: workflow_instance_id)
49
+ unless entry.key == workflow_instance_id.to_s && decoded.fetch(:revision) == entry.revision
50
+ raise Phronomy::Storage::SerializationError, "backend returned another Workflow identity/revision"
51
+ end
52
+ decoded
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -32,7 +32,7 @@ module Phronomy
32
32
 
33
33
  def build
34
34
  entry = @entry_point
35
- all_states = (@declared_states + @wait_state_names + [:__end__]).uniq
35
+ all_states = (@declared_states + @wait_state_names + [FSMProtocol::FINISH]).uniq
36
36
  auto_transitions = @auto_transitions
37
37
  external_events = @external_events
38
38
  entry_actions = @entry_actions
@@ -139,7 +139,7 @@ module Phronomy
139
139
  machine.context,
140
140
  machine.current_event
141
141
  )
142
- if result.is_a?(Phronomy::Task)
142
+ if result.is_a?(Phronomy::TaskResult)
143
143
  raise Phronomy::InvalidAsyncTransitionActionError,
144
144
  transition_task_error_message(metadata)
145
145
  end
@@ -151,9 +151,9 @@ module Phronomy
151
151
  def build_entry_callback(callable, state_name)
152
152
  ->(machine) {
153
153
  result = callable.call(machine.context)
154
- if result.is_a?(Phronomy::Task)
154
+ if result.is_a?(Phronomy::TaskResult)
155
155
  raise Phronomy::InvalidAsyncEntryActionError,
156
- "Entry action for state #{state_name.inspect} returned Phronomy::Task. " \
156
+ "Entry action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
157
157
  "Start the asynchronous operation, register its callback/listener, " \
158
158
  "and return the WorkflowContext or nil."
159
159
  end
@@ -164,9 +164,9 @@ module Phronomy
164
164
  def build_exit_callback(callable, state_name)
165
165
  ->(machine) {
166
166
  result = callable.call(machine.context)
167
- if result.is_a?(Phronomy::Task)
167
+ if result.is_a?(Phronomy::TaskResult)
168
168
  raise Phronomy::InvalidAsyncEntryActionError,
169
- "Exit action for state #{state_name.inspect} returned Phronomy::Task. " \
169
+ "Exit action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
170
170
  "Exit actions are synchronous Run-to-Completion callbacks."
171
171
  end
172
172
  }
@@ -188,7 +188,7 @@ module Phronomy
188
188
  end
189
189
 
190
190
  def public_destination(destination)
191
- return :__finish__ if destination == Phronomy::WorkflowRunner::FINISH
191
+ return :__finish__ if destination == FSMProtocol::FINISH
192
192
 
193
193
  destination
194
194
  end
@@ -196,7 +196,7 @@ module Phronomy
196
196
  def transition_task_error_message(metadata)
197
197
  "Transition action " \
198
198
  "#{metadata[:from].inspect} --#{metadata[:event].inspect}--> " \
199
- "#{metadata[:to].inspect} returned Phronomy::Task. " \
199
+ "#{metadata[:to].inspect} returned Phronomy::TaskResult. " \
200
200
  "Start the asynchronous operation, register its callback/listener, " \
201
201
  "and return the WorkflowContext or nil."
202
202
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Domain-owned Workflow metadata, loadable without the Workflow runtime.
5
+ # @api private
6
+ module WorkflowStorageSchema
7
+ STATES = Phronomy::Storage::Resource.new(id: "workflow.states", kind: :records)
8
+ end
9
+ end