phronomy 0.26.0 → 0.28.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 (557) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +9 -9
  3. data/CHANGELOG.md +92 -0
  4. data/CONTRIBUTING.md +8 -8
  5. data/README.md +13 -3
  6. data/VERIFY.sh +27 -27
  7. data/benchmark/bench_agent_invoke.rb +26 -47
  8. data/benchmark/bench_context_assembler.rb +17 -15
  9. data/benchmark/bench_regression.rb +1 -1
  10. data/benchmark/bench_tool_schema.rb +1 -1
  11. data/docs/architecture/agent-chat-and-state-ownership.md +153 -0
  12. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  13. data/docs/architecture/agent-context.md +4 -2
  14. data/docs/architecture/agent-transition-ownership.md +13 -0
  15. data/docs/architecture/backend-groups.md +152 -0
  16. data/docs/architecture/before-llm-input.md +9 -3
  17. data/docs/architecture/context-management.md +31 -1
  18. data/docs/architecture/context-preparation-steps.md +90 -0
  19. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  20. data/docs/architecture/execution-abstraction-review.md +88 -0
  21. data/docs/architecture/execution-boundaries.md +28 -0
  22. data/docs/architecture/execution-metadata-and-values.md +86 -0
  23. data/docs/architecture/generator-verifier-ownership.md +111 -0
  24. data/docs/architecture/multi-agent-handoff.md +11 -4
  25. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  26. data/docs/architecture/persistence.md +45 -4
  27. data/docs/architecture/r8-unit1.md +104 -0
  28. data/docs/architecture/r8-unit10.md +72 -0
  29. data/docs/architecture/r8-unit11.md +71 -0
  30. data/docs/architecture/r8-unit12.md +67 -0
  31. data/docs/architecture/r8-unit13.md +61 -0
  32. data/docs/architecture/r8-unit14.md +72 -0
  33. data/docs/architecture/r8-unit15.md +65 -0
  34. data/docs/architecture/r8-unit16.md +54 -0
  35. data/docs/architecture/r8-unit17.md +73 -0
  36. data/docs/architecture/r8-unit18.md +92 -0
  37. data/docs/architecture/r8-unit19.md +61 -0
  38. data/docs/architecture/r8-unit2.md +55 -0
  39. data/docs/architecture/r8-unit20.md +51 -0
  40. data/docs/architecture/r8-unit21.md +79 -0
  41. data/docs/architecture/r8-unit22.md +74 -0
  42. data/docs/architecture/r8-unit23.md +81 -0
  43. data/docs/architecture/r8-unit3.md +130 -0
  44. data/docs/architecture/r8-unit4.md +79 -0
  45. data/docs/architecture/r8-unit5.md +115 -0
  46. data/docs/architecture/r8-unit6.md +71 -0
  47. data/docs/architecture/r8-unit7.md +70 -0
  48. data/docs/architecture/r8-unit8.md +78 -0
  49. data/docs/architecture/r8-unit9.md +73 -0
  50. data/docs/architecture/refactoring-closure.md +343 -0
  51. data/docs/architecture/remaining-refactoring-plan.md +390 -0
  52. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  53. data/docs/architecture/storage-execution-boundaries.md +54 -0
  54. data/docs/architecture/tool-schema-recording-gap.md +56 -0
  55. data/docs/architecture/tracing.md +2 -2
  56. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  57. data/docs/architecture.md +222 -0
  58. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  59. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  60. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  61. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  62. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  63. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  64. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +7 -0
  65. data/docs/decisions/031-durable-multi-agent-coordination.md +7 -0
  66. data/docs/decisions/032-storage-backend-composition.md +85 -0
  67. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  68. data/docs/decisions/034-handoff-runner-coordination-ownership.md +78 -0
  69. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  70. data/docs/decisions/036-context-contract-ownership.md +86 -0
  71. data/docs/decisions/037-common-definition-ownership.md +62 -0
  72. data/docs/decisions/038-responsibility-based-source-layout.md +145 -0
  73. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +82 -0
  74. data/docs/decisions/040-configuration-default-composition.md +105 -0
  75. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  76. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  77. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  78. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  79. data/docs/decisions/045-worker-input-restriction-ownership.md +104 -0
  80. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  81. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  82. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  83. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  84. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  85. data/docs/decisions/051-execution-outcome-worker-ownership.md +153 -0
  86. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  87. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  88. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  89. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  90. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  91. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  92. data/docs/decisions/058-neutral-storage-primitives.md +83 -0
  93. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  94. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  95. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  96. data/docs/decisions/062-content-store-service.md +52 -0
  97. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  98. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  99. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  100. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  101. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  102. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  103. data/docs/decisions/README.md +52 -6
  104. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  105. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  106. data/docs/execution-boundaries-preview.md +45 -0
  107. data/docs/features.md +16 -5
  108. data/docs/getting-started.md +11 -8
  109. data/docs/migrations/0.15.md +5 -0
  110. data/docs/migrations/0.22.md +2 -2
  111. data/docs/migrations/durable-semantic-coordination-v2.md +8 -0
  112. data/docs/migrations/handoff-runner-multi-agent.md +37 -0
  113. data/docs/migrations/neutral-storage-spi.md +43 -0
  114. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  115. data/docs/migrations/persistence-failure-contracts.md +67 -0
  116. data/docs/migrations/r8-unit10.md +36 -0
  117. data/docs/migrations/r8-unit11.md +33 -0
  118. data/docs/migrations/r8-unit12.md +31 -0
  119. data/docs/migrations/r8-unit13.md +36 -0
  120. data/docs/migrations/r8-unit14.md +31 -0
  121. data/docs/migrations/r8-unit15.md +31 -0
  122. data/docs/migrations/r8-unit16.md +26 -0
  123. data/docs/migrations/r8-unit17.md +28 -0
  124. data/docs/migrations/r8-unit18.md +29 -0
  125. data/docs/migrations/r8-unit19.md +28 -0
  126. data/docs/migrations/r8-unit20.md +24 -0
  127. data/docs/migrations/r8-unit21.md +28 -0
  128. data/docs/migrations/r8-unit22.md +29 -0
  129. data/docs/migrations/r8-unit23.md +20 -0
  130. data/docs/migrations/r8-unit4.md +67 -0
  131. data/docs/migrations/r8-unit5.md +75 -0
  132. data/docs/migrations/r8-unit6.md +109 -0
  133. data/docs/migrations/r8-unit7.md +31 -0
  134. data/docs/migrations/r8-unit8.md +33 -0
  135. data/docs/migrations/r8-unit9.md +28 -0
  136. data/docs/migrations/shared-state-multi-agent.md +44 -0
  137. data/docs/migrations/storage-async-client.md +103 -0
  138. data/docs/migrations/storage-backend-composition.md +117 -0
  139. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  140. data/docs/migrations/vector-async-clients.md +110 -0
  141. data/docs/persistence-backends.md +190 -215
  142. data/docs/runtime-and-concurrency.md +142 -32
  143. data/lib/phronomy/agent/admission.rb +78 -0
  144. data/lib/phronomy/{agent_already_exists_error.rb → agent/agent_already_exists_error.rb} +2 -0
  145. data/lib/phronomy/{agent_busy_error.rb → agent/agent_busy_error.rb} +2 -0
  146. data/lib/phronomy/{agent_purged_error.rb → agent/agent_purged_error.rb} +2 -0
  147. data/lib/phronomy/agent/api/agent.rb +17 -0
  148. data/lib/phronomy/{execution_rehydration_required_error.rb → agent/api/execution_rehydration_required_error.rb} +2 -0
  149. data/lib/phronomy/agent/async_event_api.rb +38 -33
  150. data/lib/phronomy/agent/base.rb +106 -294
  151. data/lib/phronomy/agent/before_llm_input.rb +64 -0
  152. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  153. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  154. data/lib/phronomy/agent/context_importer.rb +2 -2
  155. data/lib/phronomy/agent/{context_assembler.rb → context_preparation.rb} +157 -288
  156. data/lib/phronomy/agent/control_request.rb +21 -0
  157. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  158. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +32 -63
  159. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  160. data/lib/phronomy/agent/execution/dispatch_preparation.rb +195 -0
  161. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +43 -23
  162. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +8 -5
  163. data/lib/phronomy/agent/execution/execution_coordinator.rb +1744 -0
  164. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  165. data/lib/phronomy/agent/execution/execution_metadata.rb +54 -0
  166. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +288 -0
  167. data/lib/phronomy/agent/execution/execution_registry.rb +463 -0
  168. data/lib/phronomy/agent/execution/execution_session_runner.rb +113 -0
  169. data/lib/phronomy/agent/execution/initial_preparation.rb +323 -0
  170. data/lib/phronomy/agent/execution/invocation_actions.rb +368 -0
  171. data/lib/phronomy/agent/execution/invocation_transitions.rb +124 -0
  172. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  173. data/lib/phronomy/agent/execution_change.rb +161 -0
  174. data/lib/phronomy/agent/execution_environment.rb +36 -0
  175. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  176. data/lib/phronomy/agent/execution_observation.rb +35 -0
  177. data/lib/phronomy/agent/filterable.rb +182 -0
  178. data/lib/phronomy/agent/{handoff_context.rb → handoff/transfer_context.rb} +6 -6
  179. data/lib/phronomy/agent/{handoff_projection.rb → handoff/transfer_projection.rb} +14 -33
  180. data/lib/phronomy/agent/handoff_error.rb +7 -0
  181. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  182. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  183. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  184. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  185. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  186. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  187. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -15
  188. data/lib/phronomy/agent/persistence/agent_repository.rb +69 -0
  189. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  190. data/lib/phronomy/agent/persistence/codec.rb +338 -0
  191. data/lib/phronomy/agent/persistence/execution_repository.rb +124 -0
  192. data/lib/phronomy/agent/persistence/journal_repository.rb +63 -0
  193. data/lib/phronomy/agent/persistence/records.rb +33 -0
  194. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  195. data/lib/phronomy/agent/persistence/storage_schema.rb +29 -0
  196. data/lib/phronomy/agent/persistence/watermark.rb +29 -0
  197. data/lib/phronomy/agent/recovery/invocation_restorer.rb +124 -0
  198. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +28 -51
  199. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +50 -55
  200. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +38 -43
  201. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +26 -23
  202. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  203. data/lib/phronomy/agent/recovery/recovery_support.rb +224 -0
  204. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  205. data/lib/phronomy/agent/retention.rb +17 -0
  206. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  207. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  208. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  209. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  210. data/lib/phronomy/agent/{ruby_llm_materializer.rb → runtime_input.rb} +19 -39
  211. data/lib/phronomy/agent/saved_context_reader.rb +55 -0
  212. data/lib/phronomy/agent/settings.rb +47 -0
  213. data/lib/phronomy/agent/state_writer.rb +166 -0
  214. data/lib/phronomy/agent/store.rb +253 -0
  215. data/lib/phronomy/{stream_callback_error.rb → agent/stream_callback_error.rb} +2 -0
  216. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  217. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  218. data/lib/phronomy/agent/tool_execution/tool_binding.rb +30 -0
  219. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +114 -248
  220. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  221. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  222. data/lib/phronomy/common/configuration_error.rb +7 -0
  223. data/lib/phronomy/common/error.rb +5 -0
  224. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  225. data/lib/phronomy/common/values/serializable.rb +32 -0
  226. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  227. data/lib/phronomy/content_store/storage_schema.rb +12 -0
  228. data/lib/phronomy/content_store/stored_contents.rb +44 -0
  229. data/lib/phronomy/context/assembly.rb +202 -0
  230. data/lib/phronomy/{context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +5 -1
  231. data/lib/phronomy/context/candidate.rb +51 -0
  232. data/lib/phronomy/context/constraint.rb +47 -0
  233. data/lib/phronomy/{agent → context}/context_plan.rb +2 -2
  234. data/lib/phronomy/{agent → context}/context_plan_validator.rb +3 -3
  235. data/lib/phronomy/{agent → context}/context_policy.rb +1 -1
  236. data/lib/phronomy/{agent → context}/context_policy_input.rb +7 -7
  237. data/lib/phronomy/{agent → context}/context_policy_input_builder.rb +1 -1
  238. data/lib/phronomy/context/default_policy.rb +140 -0
  239. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  240. data/lib/phronomy/{agent → context}/llm_input_build_context.rb +2 -2
  241. data/lib/phronomy/{agent → context}/llm_input_manifest.rb +21 -21
  242. data/lib/phronomy/{agent → context}/llm_input_patch.rb +3 -3
  243. data/lib/phronomy/context/manifest_error.rb +10 -0
  244. data/lib/phronomy/context/prompt_template.rb +98 -0
  245. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  246. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  247. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  248. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  249. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  250. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  251. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  252. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  253. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
  254. data/lib/phronomy/embeddings/base.rb +43 -0
  255. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  256. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  257. data/lib/phronomy/engine/concurrency/offload_pool.rb +39 -20
  258. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  259. data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +1 -1
  260. data/lib/phronomy/engine/event_loop.rb +164 -630
  261. data/lib/phronomy/engine/execution_binding.rb +23 -0
  262. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  263. data/lib/phronomy/engine/fsm_protocol.rb +9 -0
  264. data/lib/phronomy/engine/fsm_session.rb +37 -30
  265. data/lib/phronomy/engine/metrics.rb +28 -0
  266. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  267. data/lib/phronomy/engine/runtime.rb +104 -95
  268. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  269. data/lib/phronomy/execution/async_operation.rb +41 -0
  270. data/lib/phronomy/execution/backpressure_error.rb +7 -0
  271. data/lib/phronomy/{blocking.rb → execution/blocking.rb} +2 -2
  272. data/lib/phronomy/execution/cancellation_error.rb +7 -0
  273. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +5 -1
  274. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  275. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  276. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  277. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  278. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  279. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  280. data/lib/phronomy/execution/concurrency/worker_input_restricted.rb +14 -0
  281. data/lib/phronomy/execution/event_loop_reentrancy_error.rb +8 -0
  282. data/lib/phronomy/{execution.rb → execution/execution.rb} +57 -30
  283. data/lib/phronomy/{execution_cancellation_error.rb → execution/execution_cancellation_error.rb} +2 -0
  284. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  285. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  286. data/lib/phronomy/{execution_timeout_error.rb → execution/execution_timeout_error.rb} +2 -0
  287. data/lib/phronomy/{invalid_async_entry_action_error.rb → execution/invalid_async_entry_action_error.rb} +2 -0
  288. data/lib/phronomy/{invalid_async_transition_action_error.rb → execution/invalid_async_transition_action_error.rb} +2 -0
  289. data/lib/phronomy/{invalid_async_workflow_action_error.rb → execution/invalid_async_workflow_action_error.rb} +2 -0
  290. data/lib/phronomy/{invocation_context.rb → execution/invocation_context.rb} +0 -15
  291. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  292. data/lib/phronomy/execution/pool_shutdown_error.rb +7 -0
  293. data/lib/phronomy/execution/recursion_limit_error.rb +7 -0
  294. data/lib/phronomy/execution/runnable.rb +24 -0
  295. data/lib/phronomy/execution/runtime_shutdown_error.rb +7 -0
  296. data/lib/phronomy/execution/runtime_shutdown_reentrancy_error.rb +7 -0
  297. data/lib/phronomy/execution/scheduler_reentrancy_error.rb +9 -0
  298. data/lib/phronomy/{engine → execution}/task_result.rb +7 -3
  299. data/lib/phronomy/execution/timeout_error.rb +7 -0
  300. data/lib/phronomy/execution/wait_policy.rb +24 -0
  301. data/lib/phronomy/filter/filter_block_error.rb +14 -0
  302. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  303. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  304. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  305. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  306. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  307. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  308. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  309. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  310. data/lib/phronomy/llm_adapter/authentication_error.rb +9 -0
  311. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  312. data/lib/phronomy/llm_adapter/base.rb +49 -68
  313. data/lib/phronomy/llm_adapter/context_length_error.rb +9 -0
  314. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  315. data/lib/phronomy/llm_adapter/message.rb +62 -0
  316. data/lib/phronomy/llm_adapter/rate_limit_error.rb +9 -0
  317. data/lib/phronomy/llm_adapter/request.rb +79 -0
  318. data/lib/phronomy/llm_adapter/response.rb +36 -0
  319. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  320. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  321. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  322. data/lib/phronomy/llm_adapter.rb +3 -16
  323. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  324. data/lib/phronomy/multi_agent/admission_registry.rb +15 -2
  325. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  326. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  327. data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
  328. data/lib/phronomy/{agent → multi_agent}/handoff_capability_factory.rb +21 -2
  329. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  330. data/lib/phronomy/{agent → multi_agent}/handoff_policy.rb +1 -1
  331. data/lib/phronomy/multi_agent/handoff_runner.rb +156 -0
  332. data/lib/phronomy/{agent → multi_agent}/handoff_state.rb +2 -2
  333. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  334. data/lib/phronomy/multi_agent/persistence/codec.rb +75 -0
  335. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  336. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  337. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  338. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +124 -0
  339. data/lib/phronomy/multi_agent/persistence/team_repository.rb +69 -0
  340. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  341. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  342. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  343. data/lib/phronomy/multi_agent/store.rb +156 -0
  344. data/lib/phronomy/multi_agent/team_coordinator.rb +87 -75
  345. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  346. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +5 -5
  347. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  348. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  349. data/lib/phronomy/persistence/content_repository.rb +26 -0
  350. data/lib/phronomy/persistence/errors.rb +16 -0
  351. data/lib/phronomy/persistence/migration/initial_format_migration.rb +21 -21
  352. data/lib/phronomy/persistence/persistence.rb +23 -0
  353. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  354. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  355. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  356. data/lib/phronomy/persistence/transaction.rb +86 -0
  357. data/lib/phronomy/persistence_composition/storage_schema.rb +29 -0
  358. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  359. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  360. data/lib/phronomy/runtime_composition/agent_defaults.rb +11 -0
  361. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  362. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  363. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  364. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  365. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  366. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  367. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  368. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  369. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  370. data/lib/phronomy/storage/async/async_client.rb +60 -0
  371. data/lib/phronomy/storage/backend.rb +107 -0
  372. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  373. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  374. data/lib/phronomy/storage/blobs.rb +31 -0
  375. data/lib/phronomy/storage/condition.rb +25 -0
  376. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  377. data/lib/phronomy/storage/conflict_error.rb +9 -0
  378. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  379. data/lib/phronomy/storage/entry.rb +33 -0
  380. data/lib/phronomy/storage/guard_ref.rb +13 -0
  381. data/lib/phronomy/storage/not_found_error.rb +9 -0
  382. data/lib/phronomy/storage/record_codec.rb +177 -0
  383. data/lib/phronomy/storage/records.rb +61 -0
  384. data/lib/phronomy/storage/resource.rb +126 -0
  385. data/lib/phronomy/storage/scope.rb +25 -0
  386. data/lib/phronomy/storage/serialization_error.rb +9 -0
  387. data/lib/phronomy/storage/streams.rb +44 -0
  388. data/lib/phronomy/storage/transaction_error.rb +10 -0
  389. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  390. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  391. data/lib/phronomy/storage/validation.rb +53 -0
  392. data/lib/phronomy/storage/view.rb +131 -0
  393. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  394. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  395. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  396. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  397. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +54 -48
  398. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +1 -1
  399. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +1 -1
  400. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  401. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +52 -46
  402. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  403. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +124 -0
  404. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  405. data/lib/phronomy/tool/authorization.rb +77 -0
  406. data/lib/phronomy/tool/base.rb +549 -9
  407. data/lib/phronomy/tool/call_request.rb +40 -0
  408. data/lib/phronomy/{agent/tool_definition_set.rb → tool/definition_set.rb} +16 -12
  409. data/lib/phronomy/tool/operation.rb +88 -0
  410. data/lib/phronomy/tool/schema.rb +214 -0
  411. data/lib/phronomy/tool/settings.rb +31 -0
  412. data/lib/phronomy/tool/tool_error.rb +7 -0
  413. data/lib/phronomy/{agent → tool}/tool_executor.rb +10 -7
  414. data/lib/phronomy/tools/agent.rb +22 -90
  415. data/lib/phronomy/tools/mcp.rb +21 -14
  416. data/lib/phronomy/tools/vector_search.rb +4 -4
  417. data/lib/phronomy/tracing/automatic.rb +11 -6
  418. data/lib/phronomy/tracing/base.rb +1 -1
  419. data/lib/phronomy/tracing/observation.rb +37 -0
  420. data/lib/phronomy/tracing/settings.rb +36 -0
  421. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  422. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  423. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  424. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  425. data/lib/phronomy/vector_store/base.rb +118 -49
  426. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  427. data/lib/phronomy/vector_store.rb +2 -17
  428. data/lib/phronomy/version.rb +1 -1
  429. data/lib/phronomy/workflow/completion.rb +11 -0
  430. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  431. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  432. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +5 -1
  433. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  434. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
  435. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +186 -0
  436. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +145 -115
  437. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  438. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  439. data/lib/phronomy/workflow/persistence/state_repository.rb +63 -0
  440. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  441. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
  442. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
  443. data/lib/phronomy/workflow/runtime_binding/workflow_terminal_policy.rb +40 -0
  444. data/lib/phronomy.rb +97 -98
  445. data/scripts/api_snapshot.rb +36 -16
  446. data/scripts/storage_spi_snapshot.rb +36 -0
  447. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  448. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  449. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  450. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  451. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  452. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  453. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  454. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  455. data/sig/phronomy/agent/admission.rbs +19 -0
  456. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  457. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  458. data/sig/phronomy/agent/store.rbs +73 -0
  459. data/sig/phronomy/agent.rbs +14 -139
  460. data/sig/phronomy/common.rbs +7 -0
  461. data/sig/phronomy/content_store/contracts.rbs +13 -0
  462. data/sig/phronomy/context.rbs +152 -0
  463. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  464. data/sig/phronomy/embeddings/base.rbs +17 -0
  465. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  466. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  467. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  468. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  469. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  470. data/sig/phronomy/engine/observability.rbs +13 -0
  471. data/sig/phronomy/execution/async_operation.rbs +7 -0
  472. data/sig/phronomy/execution/blocking.rbs +5 -0
  473. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  474. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  475. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  476. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  477. data/sig/phronomy/execution/errors.rbs +7 -0
  478. data/sig/phronomy/execution/execution.rbs +41 -0
  479. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  480. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  481. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  482. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  483. data/sig/phronomy/execution/task_result.rbs +29 -0
  484. data/sig/phronomy/extensions.rbs +0 -21
  485. data/sig/phronomy/handoff.rbs +12 -4
  486. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  487. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  488. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  489. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  490. data/sig/phronomy/llm_context_window.rbs +10 -0
  491. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  492. data/sig/phronomy/multi_agent/store.rbs +54 -0
  493. data/sig/phronomy/multi_agent.rbs +24 -3
  494. data/sig/phronomy/persistence/errors.rbs +18 -0
  495. data/sig/phronomy/persistence/migration.rbs +13 -0
  496. data/sig/phronomy/persistence/transaction.rbs +19 -0
  497. data/sig/phronomy/persistence.rbs +3 -185
  498. data/sig/phronomy/persistence_composition.rbs +19 -0
  499. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  500. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  501. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  502. data/sig/phronomy/storage/contracts.rbs +160 -0
  503. data/sig/phronomy/tool.rbs +58 -5
  504. data/sig/phronomy/tracing/observation.rbs +7 -0
  505. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  506. data/sig/phronomy/vector_store/base.rbs +23 -0
  507. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
  508. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  509. data/sig/phronomy/workflow/store.rbs +8 -0
  510. data/sig/phronomy/workflow.rbs +1 -1
  511. data/sig/phronomy.rbs +0 -49
  512. metadata +462 -130
  513. data/lib/phronomy/agent/agent_invocation_session_builder.rb +0 -584
  514. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  515. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  516. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  517. data/lib/phronomy/agent/context/capability/base.rb +0 -507
  518. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  519. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  520. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  521. data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
  522. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  523. data/lib/phronomy/agent/handoff_request.rb +0 -45
  524. data/lib/phronomy/agent/handoff_runner.rb +0 -178
  525. data/lib/phronomy/agent/phase_machine_builder.rb +0 -152
  526. data/lib/phronomy/agent/provider_call_outcome.rb +0 -106
  527. data/lib/phronomy/agent/recovery_support.rb +0 -504
  528. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  529. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  530. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  531. data/lib/phronomy/agent/tool_call_intercepted.rb +0 -29
  532. data/lib/phronomy/agent/tool_invocation_session_builder.rb +0 -276
  533. data/lib/phronomy/configuration.rb +0 -59
  534. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  535. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  536. data/lib/phronomy/generator_verifier.rb +0 -369
  537. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  538. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  539. data/lib/phronomy/metrics.rb +0 -26
  540. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  541. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  542. data/lib/phronomy/persistence/in_memory.rb +0 -690
  543. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  544. data/lib/phronomy/persistence.rb +0 -276
  545. data/lib/phronomy/recovery.rb +0 -186
  546. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  547. data/lib/phronomy/runnable.rb +0 -54
  548. data/lib/phronomy/token_usage.rb +0 -85
  549. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  550. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  551. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  552. data/lib/phronomy/workflow_recovery.rb +0 -123
  553. data/sig/phronomy/llm_adapter.rbs +0 -11
  554. data/sig/phronomy/runtime.rbs +0 -76
  555. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  556. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  557. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- class Persistence
4
+ module Storage
5
5
  # Immutable logical record exchanged across the Persistence Backend SPI.
6
6
  #
7
7
  # Phronomy owns +record_type+, +format_version+, and the semantic meaning of
@@ -28,20 +28,20 @@ module Phronomy
28
28
  # @api public
29
29
  def initialize(record_type: MISSING, format_version: MISSING, payload: MISSING)
30
30
  if record_type.equal?(MISSING)
31
- raise Phronomy::Persistence::SerializationError, "durable record_type is missing"
31
+ raise Phronomy::Storage::SerializationError, "durable record_type is missing"
32
32
  end
33
33
  if format_version.equal?(MISSING)
34
- raise Phronomy::Persistence::SerializationError, "durable format_version is missing"
34
+ raise Phronomy::Storage::SerializationError, "durable format_version is missing"
35
35
  end
36
36
  if payload.equal?(MISSING)
37
- raise Phronomy::Persistence::SerializationError, "durable payload is missing"
37
+ raise Phronomy::Storage::SerializationError, "durable payload is missing"
38
38
  end
39
39
  unless record_type.is_a?(String)
40
- raise Phronomy::Persistence::SerializationError,
40
+ raise Phronomy::Storage::SerializationError,
41
41
  "durable record_type must be a String"
42
42
  end
43
43
  unless format_version.is_a?(String)
44
- raise Phronomy::Persistence::SerializationError,
44
+ raise Phronomy::Storage::SerializationError,
45
45
  "durable format_version must be a String"
46
46
  end
47
47
 
@@ -51,10 +51,10 @@ module Phronomy
51
51
  validate_payload!(payload)
52
52
  @payload = immutable_copy(payload)
53
53
  freeze
54
- rescue Phronomy::Persistence::SerializationError
54
+ rescue Phronomy::Storage::SerializationError
55
55
  raise
56
56
  rescue => error
57
- raise Phronomy::Persistence::SerializationError, error.message
57
+ raise Phronomy::Storage::SerializationError, error.message
58
58
  end
59
59
 
60
60
  # Returns an independent immutable record with the same logical value.
@@ -75,26 +75,26 @@ module Phronomy
75
75
 
76
76
  def validate_header!
77
77
  if record_type.empty?
78
- raise Phronomy::Persistence::SerializationError,
78
+ raise Phronomy::Storage::SerializationError,
79
79
  "durable record_type must not be empty"
80
80
  end
81
81
 
82
82
  return if FORMAT_VERSION_PATTERN.match?(format_version)
83
83
 
84
- raise Phronomy::Persistence::SerializationError,
84
+ raise Phronomy::Storage::SerializationError,
85
85
  "invalid durable format_version: #{format_version.inspect}"
86
86
  end
87
87
 
88
88
  def validate_payload!(payload)
89
89
  unless payload.is_a?(Hash)
90
- raise Phronomy::Persistence::SerializationError,
90
+ raise Phronomy::Storage::SerializationError,
91
91
  "durable payload must be a Hash"
92
92
  end
93
93
 
94
94
  Phronomy::CanonicalJSON.dump(payload)
95
95
  true
96
96
  rescue ArgumentError => error
97
- raise Phronomy::Persistence::SerializationError,
97
+ raise Phronomy::Storage::SerializationError,
98
98
  "durable payload is not canonical JSON compatible: #{error.message}"
99
99
  end
100
100
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # Values crossing the driver boundary never expose mutable storage state.
6
+ # @api public
7
+ module Entry
8
+ Record = Data.define(:key, :revision, :attributes, :record) do
9
+ def initialize(key:, revision:, attributes:, record:)
10
+ super(key: Validation.key(key), revision: Validation.revision(revision),
11
+ attributes: Validation.attributes(attributes), record: Validation.record(record))
12
+ end
13
+ end
14
+ Append = Data.define(:id, :record) do
15
+ def initialize(id:, record:)
16
+ super(id: Validation.key(id), record: Validation.record(record))
17
+ end
18
+ end
19
+ Stream = Data.define(:position, :id, :record) do
20
+ def initialize(position:, id:, record:)
21
+ super(position: Validation.revision(position), id: Validation.key(id), record: Validation.record(record))
22
+ end
23
+ end
24
+ Blob = Data.define(:key, :bytes, :attributes) do
25
+ def initialize(key:, bytes:, attributes:)
26
+ raise ArgumentError, "blob bytes must be a String" unless bytes.is_a?(String)
27
+ super(key: Validation.key(key), bytes: bytes.dup.force_encoding(Encoding::BINARY).freeze,
28
+ attributes: Validation.attributes(attributes))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # A stable existing record used as a transaction lock anchor.
6
+ # @api public
7
+ GuardRef = Data.define(:resource, :key) do
8
+ def initialize(resource:, key:)
9
+ super(resource: Resource.normalize_reference(resource), key: Validation.key(key))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # A requested stored record does not exist.
6
+ # @api public
7
+ class NotFoundError < Phronomy::Error; end
8
+ end
9
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # Shared record-envelope and scalar validation; domain schemas live with their domains.
6
+ # @api private
7
+ module RecordCodec
8
+ private
9
+
10
+ def current_payload!(record, record_type:, format_version:, keys:, label:)
11
+ unless record.is_a?(Phronomy::Storage::DurableRecord)
12
+ raise Phronomy::Storage::SerializationError,
13
+ "backend returned #{record.class}; expected Storage::DurableRecord"
14
+ end
15
+ unless record.record_type == record_type
16
+ raise Phronomy::Storage::SerializationError,
17
+ "durable record type mismatch: expected #{record_type.inspect}, " \
18
+ "got #{record.record_type.inspect}"
19
+ end
20
+ unless record.format_version == format_version
21
+ raise Phronomy::Storage::SerializationError,
22
+ "unsupported #{record_type} format version: #{record.format_version.inspect}; " \
23
+ "current version is #{format_version.inspect}"
24
+ end
25
+ validate_exact_keys!(record.payload, keys, label: label)
26
+ record.payload
27
+ end
28
+
29
+ def validate_allowed_keys!(hash, required_keys:, optional_keys:, label:)
30
+ unless hash.is_a?(Hash)
31
+ raise Phronomy::Storage::SerializationError, "#{label} must be a Hash"
32
+ end
33
+ unless hash.keys.all? { |key| key.is_a?(String) }
34
+ raise Phronomy::Storage::SerializationError,
35
+ "#{label} keys must all be String"
36
+ end
37
+
38
+ actual = hash.keys.sort
39
+ missing = required_keys.sort - actual
40
+ unknown = actual - (required_keys + optional_keys).sort
41
+ return hash if missing.empty? && unknown.empty?
42
+
43
+ details = []
44
+ details << "missing=#{missing.inspect}" unless missing.empty?
45
+ details << "unknown=#{unknown.inspect}" unless unknown.empty?
46
+ raise Phronomy::Storage::SerializationError,
47
+ "#{label} schema mismatch (#{details.join(", ")})"
48
+ end
49
+
50
+ def validate_exact_keys!(hash, expected_keys, label:)
51
+ unless hash.is_a?(Hash)
52
+ raise Phronomy::Storage::SerializationError, "#{label} must be a Hash"
53
+ end
54
+ unless hash.keys.all? { |key| key.is_a?(String) }
55
+ raise Phronomy::Storage::SerializationError,
56
+ "#{label} keys must all be String"
57
+ end
58
+
59
+ actual = hash.keys.sort
60
+ expected = expected_keys.sort
61
+ return hash if actual == expected
62
+
63
+ missing = expected - actual
64
+ unknown = actual - expected
65
+ details = []
66
+ details << "missing=#{missing.inspect}" unless missing.empty?
67
+ details << "unknown=#{unknown.inspect}" unless unknown.empty?
68
+ raise Phronomy::Storage::SerializationError,
69
+ "#{label} schema mismatch (#{details.join(", ")})"
70
+ end
71
+
72
+ def top_level_string_keys(value, label:)
73
+ unless value.is_a?(Hash)
74
+ raise Phronomy::Storage::SerializationError, "#{label} must be a Hash"
75
+ end
76
+ value.each_with_object({}) do |(key, child), result|
77
+ unless key.is_a?(String) || key.is_a?(Symbol)
78
+ raise Phronomy::Storage::SerializationError,
79
+ "#{label} key must be String or Symbol, got #{key.class}"
80
+ end
81
+ string_key = key.to_s
82
+ if result.key?(string_key)
83
+ raise Phronomy::Storage::SerializationError,
84
+ "#{label} contains duplicate key after normalization: #{string_key.inspect}"
85
+ end
86
+ result[string_key] = child
87
+ end
88
+ end
89
+
90
+ def require_nonempty_string!(hash, key, label:)
91
+ value = hash.fetch(key)
92
+ return value if value.is_a?(String) && !value.empty?
93
+
94
+ raise Phronomy::Storage::SerializationError,
95
+ "#{label} #{key} must be a non-empty String"
96
+ end
97
+
98
+ def require_optional_string!(hash, key, label:)
99
+ value = hash.fetch(key)
100
+ return value if value.nil? || value.is_a?(String)
101
+
102
+ raise Phronomy::Storage::SerializationError,
103
+ "#{label} #{key} must be a String or nil"
104
+ end
105
+
106
+ def require_integer!(hash, key, label:)
107
+ value = hash.fetch(key)
108
+ return value if value.is_a?(Integer)
109
+
110
+ raise Phronomy::Storage::SerializationError,
111
+ "#{label} #{key} must be an Integer"
112
+ end
113
+
114
+ def require_positive_integer!(hash, key, label:)
115
+ value = hash.fetch(key)
116
+ return value if value.is_a?(Integer) && value.positive?
117
+
118
+ raise Phronomy::Storage::SerializationError,
119
+ "#{label} #{key} must be a positive Integer"
120
+ end
121
+
122
+ def require_nonnegative_integer!(hash, key, label:)
123
+ value = hash.fetch(key)
124
+ return value if value.is_a?(Integer) && value >= 0
125
+
126
+ raise Phronomy::Storage::SerializationError,
127
+ "#{label} #{key} must be a non-negative Integer"
128
+ end
129
+
130
+ def require_boolean!(hash, key, label:)
131
+ value = hash.fetch(key)
132
+ return value if boolean?(value)
133
+
134
+ raise Phronomy::Storage::SerializationError,
135
+ "#{label} #{key} must be true or false"
136
+ end
137
+
138
+ def require_enum_string!(hash, key, allowed, label:)
139
+ value = hash.fetch(key)
140
+ return value if value.is_a?(String) && allowed.include?(value)
141
+
142
+ raise Phronomy::Storage::SerializationError,
143
+ "#{label} #{key} must be one of #{allowed.inspect}"
144
+ end
145
+
146
+ def require_canonical_hash!(hash, key, label:)
147
+ value = hash.fetch(key)
148
+ unless value.is_a?(Hash)
149
+ raise Phronomy::Storage::SerializationError,
150
+ "#{label} #{key} must be a Hash"
151
+ end
152
+ Phronomy::CanonicalJSON.dump(value)
153
+ value
154
+ rescue ArgumentError => error
155
+ raise Phronomy::Storage::SerializationError,
156
+ "#{label} #{key} is not canonical JSON compatible: #{error.message}"
157
+ end
158
+
159
+ def boolean?(value)
160
+ value.equal?(true) || value.equal?(false)
161
+ end
162
+
163
+ def build_record(record_type, format_version, payload)
164
+ Phronomy::Storage::DurableRecord.new(
165
+ record_type: record_type,
166
+ format_version: format_version,
167
+ payload: payload
168
+ )
169
+ end
170
+
171
+ def serialization_error(prefix, error)
172
+ raise Phronomy::Storage::SerializationError,
173
+ "#{prefix}: #{error.class}: #{error.message}"
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # Compare-and-write records and declared equality indexes.
6
+ # @api public
7
+ class Records
8
+ UNCHECKED = Object.new.freeze
9
+ attr_reader :resource
10
+ def initialize(view, resource) = (@view, @resource = view, resource)
11
+
12
+ # @api public
13
+ def insert(key:, revision:, attributes:, record:)
14
+ @view.validate!
15
+ entry = Entry::Record.new(key: key, revision: revision, attributes: resource.validate_attributes(attributes), record: record)
16
+ @view.perform(:insert_record, resource, entry: entry)
17
+ end
18
+
19
+ # @api public
20
+ def read(key)
21
+ @view.validate!
22
+ @view.perform(:read_record, resource, key: Validation.key(key))
23
+ end
24
+
25
+ # @api public
26
+ def fetch(key)
27
+ read(key) || raise(NotFoundError, "record not found: #{resource.id}/#{key}")
28
+ end
29
+
30
+ # @api public
31
+ def replace(key:, expected_revision:, next_revision:, attributes:, record:, expected_attributes: {})
32
+ @view.validate!
33
+ expected = Validation.revision(expected_revision)
34
+ entry = Entry::Record.new(key: key, revision: next_revision, attributes: resource.validate_attributes(attributes), record: record)
35
+ raise ConflictError, "replace must advance revision exactly once" unless next_revision == expected + 1
36
+ expected_values = resource.validate_attributes(expected_attributes, partial: true)
37
+ @view.perform(:replace_record, resource, entry: entry, expected_revision: expected, expected_attributes: expected_values)
38
+ end
39
+
40
+ # @api public
41
+ def delete(key:, expected_revision: UNCHECKED)
42
+ @view.validate!
43
+ expected = expected_revision.equal?(UNCHECKED) ? UNCHECKED : Validation.revision(expected_revision)
44
+ @view.perform(:delete_record, resource, key: Validation.key(key), expected_revision: expected)
45
+ end
46
+
47
+ # @api public
48
+ def scan(index:, equals:, after: nil, limit: nil)
49
+ @view.validate!
50
+ values = resource.index_values(index, equals)
51
+ @view.perform(:scan_records, resource, equals: values, after: after.nil? ? nil : Validation.key(after), limit: Validation.limit(limit))
52
+ end
53
+
54
+ # @api public
55
+ def delete_matching(index:, equals:)
56
+ @view.validate!
57
+ @view.perform(:delete_matching_records, resource, equals: resource.index_values(index, equals))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # Immutable logical schema. Physical drivers receive a separate mapping.
6
+ # @api public
7
+ class Resource
8
+ TYPES = [:string, :integer, :boolean, :nullable_string, :nullable_integer, :nullable_boolean].freeze
9
+ attr_reader :id, :kind, :attributes, :immutable_attributes, :indexes, :unique, :guard
10
+
11
+ # Normalize a schema object or ID before constructing guards and conditions.
12
+ # @api private
13
+ def self.normalize_reference(value)
14
+ value.is_a?(Resource) ? value : Validation.key(value)
15
+ end
16
+
17
+ # @api public
18
+ def initialize(id:, kind:, attributes: {}, immutable_attributes: [], indexes: {}, unique: [], guard: nil)
19
+ @id = Validation.key(id)
20
+ @kind = kind
21
+ @attributes = Validation.immutable(attributes)
22
+ @immutable_attributes = Validation.immutable(immutable_attributes)
23
+ @indexes = Validation.immutable(indexes)
24
+ @unique = Validation.immutable(unique)
25
+ @guard = Validation.immutable(guard)
26
+ validate_shape!
27
+ validate_indexes!
28
+ validate_guard!
29
+ validate_unique!
30
+ freeze
31
+ end
32
+
33
+ # @api public
34
+ def validate_attributes(values, partial: false)
35
+ valid = values.is_a?(Hash) && (values.keys - attributes.keys).empty? && (partial || values.keys.sort == attributes.keys.sort)
36
+ unless valid
37
+ raise ArgumentError, "attributes do not match #{id}"
38
+ end
39
+ values = values.to_h do |name, value|
40
+ type = attributes.fetch(name)
41
+ if type.to_s.start_with?("nullable_") && value.nil?
42
+ [name, nil]
43
+ else
44
+ [name, typed_value(name, type, value)]
45
+ end
46
+ end
47
+ if guard && values.key?(guard[:via])
48
+ values[guard[:via]] = Validation.key(values.fetch(guard[:via]))
49
+ end
50
+ Validation.attributes(values)
51
+ end
52
+
53
+ # @api public
54
+ def index_values(index, equals)
55
+ fields = indexes.fetch(index) { raise ArgumentError, "unknown index: #{index}" }
56
+ raise ArgumentError, "index attributes must match exactly" unless equals.is_a?(Hash) && equals.keys.sort == fields.sort
57
+ validate_attributes(equals, partial: true)
58
+ end
59
+
60
+ private
61
+
62
+ def validate_shape!
63
+ raise ArgumentError, "unsupported resource kind" unless [:records, :streams, :blobs].include?(kind)
64
+ unless attributes.is_a?(Hash) && attributes.keys.all? { |key| key.is_a?(Symbol) } && attributes.values.all? { |type| TYPES.include?(type) }
65
+ raise ArgumentError, "invalid attribute declarations"
66
+ end
67
+ unless field_list?(immutable_attributes, empty: true) && indexes.is_a?(Hash) && unique.is_a?(Array)
68
+ raise ArgumentError, "invalid resource declarations"
69
+ end
70
+ if kind != :records && (!immutable_attributes.empty? || !indexes.empty? || !unique.empty?)
71
+ raise ArgumentError, "only records declare indexes, uniqueness and immutable attributes"
72
+ end
73
+ raise ArgumentError, "streams have no indexed attributes" if kind == :streams && !attributes.empty?
74
+ end
75
+
76
+ def validate_indexes!
77
+ unless indexes.all? { |name, fields| name.is_a?(Symbol) && field_list?(fields) }
78
+ raise ArgumentError, "indexes need names and known distinct fields"
79
+ end
80
+ end
81
+
82
+ def validate_unique!
83
+ names = unique.map do |constraint|
84
+ unless constraint.is_a?(Hash) && constraint.keys.sort == [:fields, :name, :where] &&
85
+ constraint[:name].is_a?(Symbol) && field_list?(constraint[:fields])
86
+ raise ArgumentError, "invalid unique constraint"
87
+ end
88
+ validate_attributes(constraint.fetch(:where), partial: true)
89
+ constraint[:name]
90
+ end
91
+ raise ArgumentError, "unique constraints need distinct names" unless names.uniq == names
92
+ end
93
+
94
+ def validate_guard!
95
+ return unless guard
96
+ unless guard.is_a?(Hash) && guard.keys.sort == [:resource, :via] && guard[:resource].is_a?(String)
97
+ raise ArgumentError, "invalid guard declaration"
98
+ end
99
+ Validation.key(guard[:resource])
100
+ allowed = case kind
101
+ when :records then [:key, *attributes.select { |_, type| type == :string }.keys]
102
+ when :streams then [:stream]
103
+ else []
104
+ end
105
+ raise ArgumentError, "invalid guard key" unless allowed.include?(guard[:via])
106
+ if kind == :records && guard[:via] != :key && !immutable_attributes.include?(guard[:via])
107
+ raise ArgumentError, "guard attributes must be immutable"
108
+ end
109
+ end
110
+
111
+ def field_list?(values, empty: false)
112
+ values.is_a?(Array) && (empty || !values.empty?) && values.uniq == values && (values - attributes.keys).empty?
113
+ end
114
+
115
+ def typed_value(name, type, value)
116
+ valid = case type.to_s.delete_prefix("nullable_")
117
+ when "string" then value.is_a?(String) && value.valid_encoding? && !value.include?("\0")
118
+ when "integer" then value.is_a?(Integer)
119
+ when "boolean" then value.equal?(true) || value.equal?(false)
120
+ end
121
+ raise ArgumentError, "invalid #{id}.#{name}" unless valid
122
+ value.is_a?(String) ? value.encode(Encoding::UTF_8) : value
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # One synchronous transaction lifetime, independent of physical rollback.
6
+ # @api private
7
+ class Scope
8
+ def initialize
9
+ @thread = Thread.current
10
+ @active = true
11
+ @failed = false
12
+ end
13
+
14
+ def check!
15
+ raise TransactionError, "transaction belongs to another execution context" unless @thread.equal?(Thread.current)
16
+ raise TransactionError, "transaction scope is closed" unless @active
17
+ raise TransactionError, "transaction scope has failed; leave it before continuing" if @failed
18
+ true
19
+ end
20
+
21
+ def fail! = @failed = true
22
+ def close! = @active = false
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # A value cannot be represented by the storage record contract.
6
+ # @api public
7
+ class SerializationError < Phronomy::Error; end
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # Atomic ordered append with a per-stream head and entry identity.
6
+ # @api public
7
+ class Streams
8
+ attr_reader :resource
9
+ def initialize(view, resource) = (@view, @resource = view, resource)
10
+
11
+ # @api public
12
+ def append(stream:, expected_head:, entries:)
13
+ @view.validate!
14
+ key = Validation.key(stream)
15
+ expected = Validation.revision(expected_head)
16
+ raise ArgumentError, "entries must be an Array" unless entries.is_a?(Array)
17
+ values = entries.map do |entry|
18
+ raise SerializationError, "expected Entry::Append" unless entry.is_a?(Entry::Append)
19
+ Entry::Append.new(id: entry.id, record: entry.record)
20
+ end
21
+ raise ConflictError, "duplicate stream entry identity" unless values.map(&:id).uniq.length == values.length
22
+ @view.perform(:append_stream, resource, stream: key, expected_head: expected, entries: values.freeze)
23
+ end
24
+
25
+ # @api public
26
+ def read(stream:, after: 0, limit: nil)
27
+ @view.validate!
28
+ @view.perform(:read_stream, resource, stream: Validation.key(stream), after: Validation.revision(after), limit: Validation.limit(limit))
29
+ end
30
+
31
+ # @api public
32
+ def head(stream:)
33
+ @view.validate!
34
+ @view.perform(:stream_head, resource, stream: Validation.key(stream))
35
+ end
36
+
37
+ # @api public
38
+ def delete(stream:)
39
+ @view.validate!
40
+ @view.perform(:delete_stream, resource, stream: Validation.key(stream))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # A neutral storage contract failure.
6
+ # @api public
7
+ class TransactionError < Phronomy::Error
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # A neutral storage contract failure.
6
+ # @api public
7
+ class UniqueConstraintError < ConflictError
8
+ attr_reader :resource, :constraint
9
+ # @api public
10
+ def initialize(resource:, constraint:)
11
+ @resource = resource.id
12
+ @constraint = constraint
13
+ super("unique constraint #{resource.id}.#{constraint} failed")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # A backend does not provide a required storage capability.
6
+ # @api public
7
+ class UnsupportedBackendError < Phronomy::Error; end
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Storage
5
+ # Input validation shared by storage handles; no physical work happens here.
6
+ # @api private
7
+ module Validation
8
+ module_function
9
+
10
+ def key(value)
11
+ raise ArgumentError, "key must be a non-empty String" unless value.is_a?(String) && !value.empty?
12
+ result = value.encode(Encoding::UTF_8)
13
+ raise ArgumentError, "key must be valid UTF-8" unless result.valid_encoding? && !result.include?("\0")
14
+ result.dup.freeze
15
+ rescue EncodingError
16
+ raise ArgumentError, "key must be valid UTF-8"
17
+ end
18
+
19
+ def revision(value)
20
+ raise ArgumentError, "revision must be a non-negative Integer" unless value.is_a?(Integer) && value >= 0
21
+ value
22
+ end
23
+
24
+ def limit(value)
25
+ return nil if value.nil?
26
+ raise ArgumentError, "limit must be a positive Integer" unless value.is_a?(Integer) && value.positive?
27
+ value
28
+ end
29
+
30
+ def record(value)
31
+ raise SerializationError, "expected Storage::DurableRecord" unless value.is_a?(DurableRecord)
32
+ value.copy
33
+ end
34
+
35
+ def attributes(value)
36
+ unless value.is_a?(Hash) && value.all? { |name, item| name.is_a?(Symbol) && [String, Integer, TrueClass, FalseClass, NilClass].any? { |type| item.is_a?(type) } }
37
+ raise ArgumentError, "attributes must contain named scalar values"
38
+ end
39
+ immutable(value)
40
+ end
41
+
42
+ def immutable(value)
43
+ case value
44
+ when Hash then value.to_h { |k, v| [immutable(k), immutable(v)] }.freeze
45
+ when Array then value.map { |v| immutable(v) }.freeze
46
+ when String then value.dup.freeze
47
+ when Symbol, Integer, TrueClass, FalseClass, NilClass then value
48
+ else raise ArgumentError, "declarations must contain immutable scalar values"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end