phronomy 0.26.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 (304) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +4 -0
  4. data/VERIFY.sh +27 -27
  5. data/benchmark/bench_agent_invoke.rb +26 -22
  6. data/benchmark/bench_context_assembler.rb +4 -5
  7. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  8. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  9. data/docs/architecture/agent-context.md +4 -2
  10. data/docs/architecture/agent-transition-ownership.md +89 -0
  11. data/docs/architecture/before-llm-input.md +6 -0
  12. data/docs/architecture/context-management.md +30 -0
  13. data/docs/architecture/context-preparation-steps.md +90 -0
  14. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  15. data/docs/architecture/execution-metadata-and-values.md +80 -0
  16. data/docs/architecture/generator-verifier-ownership.md +111 -0
  17. data/docs/architecture/multi-agent-handoff.md +8 -2
  18. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  19. data/docs/architecture/persistence.md +27 -4
  20. data/docs/architecture/refactoring-closure.md +334 -0
  21. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  22. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  23. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  24. data/docs/architecture/tracing.md +1 -1
  25. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  26. data/docs/architecture.md +209 -0
  27. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  28. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  29. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  30. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  31. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  32. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  33. data/docs/decisions/032-storage-backend-composition.md +80 -0
  34. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  35. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  36. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  37. data/docs/decisions/036-context-contract-ownership.md +86 -0
  38. data/docs/decisions/037-common-definition-ownership.md +62 -0
  39. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  40. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  41. data/docs/decisions/040-configuration-default-composition.md +94 -0
  42. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  43. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  44. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  45. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  46. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  47. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  48. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  49. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  50. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  51. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  52. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  53. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  54. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  55. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  56. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  57. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  58. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  59. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  60. data/docs/decisions/README.md +35 -6
  61. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  62. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  63. data/docs/features.md +9 -3
  64. data/docs/getting-started.md +4 -4
  65. data/docs/migrations/0.15.md +5 -0
  66. data/docs/migrations/durable-semantic-coordination-v2.md +6 -0
  67. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  68. data/docs/migrations/neutral-storage-spi.md +43 -0
  69. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  70. data/docs/migrations/shared-state-multi-agent.md +44 -0
  71. data/docs/migrations/storage-backend-composition.md +117 -0
  72. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  73. data/docs/persistence-backends.md +162 -204
  74. data/docs/runtime-and-concurrency.md +117 -32
  75. data/lib/phronomy/agent/api/agent.rb +17 -0
  76. data/lib/phronomy/agent/async_event_api.rb +2 -2
  77. data/lib/phronomy/agent/base.rb +51 -241
  78. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  79. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  80. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  81. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  82. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  83. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  84. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  85. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  86. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  87. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  88. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  89. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  90. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  91. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  92. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  93. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  94. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  95. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +19 -95
  96. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  97. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  98. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +9 -10
  99. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  100. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  101. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  102. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  103. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  104. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  105. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  106. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  107. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  108. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +20 -71
  109. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  110. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  111. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  112. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  113. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  114. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  115. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  116. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  117. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  118. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  119. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  120. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  121. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  122. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  123. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  124. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  125. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  126. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  127. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  128. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  129. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  130. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  131. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  132. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  133. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  134. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  135. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  136. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +32 -37
  137. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  138. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +6 -10
  139. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  140. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  141. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  142. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  143. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  144. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  145. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  146. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  147. data/lib/phronomy/common/configuration_error.rb +7 -0
  148. data/lib/phronomy/common/error.rb +5 -0
  149. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  150. data/lib/phronomy/common/values/serializable.rb +32 -0
  151. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  152. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  153. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  154. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  155. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  156. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +1 -1
  157. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  158. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  159. data/lib/phronomy/engine/concurrency/offload_pool.rb +4 -0
  160. data/lib/phronomy/engine/concurrency/operation_binding.rb +1 -1
  161. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  162. data/lib/phronomy/engine/event_loop.rb +158 -624
  163. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  164. data/lib/phronomy/{execution.rb → engine/execution.rb} +0 -7
  165. data/lib/phronomy/{execution_cancellation_error.rb → engine/execution_cancellation_error.rb} +2 -0
  166. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  167. data/lib/phronomy/{execution_timeout_error.rb → engine/execution_timeout_error.rb} +2 -0
  168. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  169. data/lib/phronomy/engine/fsm_session.rb +35 -28
  170. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +2 -0
  171. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +2 -0
  172. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +2 -0
  173. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  174. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  175. data/lib/phronomy/engine/runtime.rb +80 -84
  176. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  177. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  178. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  179. data/lib/phronomy/engine/task_result.rb +4 -0
  180. data/lib/phronomy/engine/timeout_error.rb +7 -0
  181. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  182. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  183. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  184. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  185. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  186. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  187. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  188. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  189. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  190. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  191. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  192. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  193. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  194. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  195. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +5 -5
  196. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  197. data/lib/phronomy/multi_agent/orchestrator.rb +2 -2
  198. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  199. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  200. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  201. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  202. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  203. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  204. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  205. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  206. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  207. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  208. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  209. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  210. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  211. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  212. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  213. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  214. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  215. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  216. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  217. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  218. data/lib/phronomy/storage/backend.rb +101 -0
  219. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  220. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  221. data/lib/phronomy/storage/blobs.rb +31 -0
  222. data/lib/phronomy/storage/condition.rb +25 -0
  223. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  224. data/lib/phronomy/storage/conflict_error.rb +9 -0
  225. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  226. data/lib/phronomy/storage/entry.rb +33 -0
  227. data/lib/phronomy/storage/guard_ref.rb +13 -0
  228. data/lib/phronomy/storage/not_found_error.rb +9 -0
  229. data/lib/phronomy/storage/record_codec.rb +177 -0
  230. data/lib/phronomy/storage/records.rb +61 -0
  231. data/lib/phronomy/storage/resource.rb +126 -0
  232. data/lib/phronomy/storage/scope.rb +25 -0
  233. data/lib/phronomy/storage/serialization_error.rb +9 -0
  234. data/lib/phronomy/storage/streams.rb +44 -0
  235. data/lib/phronomy/storage/transaction_error.rb +10 -0
  236. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  237. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  238. data/lib/phronomy/storage/validation.rb +53 -0
  239. data/lib/phronomy/storage/view.rb +131 -0
  240. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  241. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  242. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  243. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  244. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  245. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  246. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  247. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  248. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  249. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  250. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  251. data/lib/phronomy/tools/agent.rb +1 -1
  252. data/lib/phronomy/version.rb +1 -1
  253. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +8 -6
  254. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  255. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  256. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  257. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +122 -71
  258. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  259. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  260. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  261. data/lib/phronomy/workflow/phase_machine_builder.rb +2 -2
  262. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  263. data/lib/phronomy.rb +52 -94
  264. data/scripts/api_snapshot.rb +1 -1
  265. data/scripts/storage_spi_snapshot.rb +36 -0
  266. data/sig/phronomy/agent.rbs +0 -1
  267. data/sig/phronomy/execution_receiver.rbs +34 -0
  268. data/sig/phronomy/handoff.rbs +4 -2
  269. data/sig/phronomy/multi_agent.rbs +18 -0
  270. data/sig/phronomy/persistence.rbs +7 -91
  271. data/sig/phronomy/storage.rbs +174 -0
  272. data/sig/phronomy/tool.rbs +10 -1
  273. metadata +230 -99
  274. data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
  275. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  276. data/lib/phronomy/agent/recovery_support.rb +0 -504
  277. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  278. data/lib/phronomy/agent/tool_executor.rb +0 -55
  279. data/lib/phronomy/generator_verifier.rb +0 -369
  280. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  281. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  282. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  283. data/lib/phronomy/persistence/in_memory.rb +0 -690
  284. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  285. data/lib/phronomy/persistence.rb +0 -276
  286. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  287. data/lib/phronomy/workflow_recovery.rb +0 -123
  288. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  289. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  290. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  291. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  292. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  293. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  294. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  295. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  296. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  297. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  298. /data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +0 -0
  299. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  300. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  301. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  302. /data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +0 -0
  303. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  304. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -40,7 +40,7 @@ RSpec.shared_examples "an Execution repository" do
40
40
  it "raises NotFoundError for a missing execution" do
41
41
  expect do
42
42
  execution_repository.load("missing-#{SecureRandom.uuid}")
43
- end.to raise_error(Phronomy::Persistence::NotFoundError)
43
+ end.to raise_error(Phronomy::Storage::NotFoundError)
44
44
  end
45
45
 
46
46
  it "rejects a duplicate execution_id" do
@@ -49,7 +49,7 @@ RSpec.shared_examples "an Execution repository" do
49
49
 
50
50
  expect do
51
51
  execution_repository.create_active(execution)
52
- end.to raise_error(Phronomy::Persistence::ConflictError)
52
+ end.to raise_error(Phronomy::Storage::ConflictError)
53
53
  end
54
54
 
55
55
  it "admits at most one active or suspended execution for one Agent" do
@@ -107,7 +107,7 @@ RSpec.shared_examples "an Execution repository" do
107
107
  expected_revision: 0,
108
108
  execution: updated
109
109
  )
110
- end.to raise_error(Phronomy::Persistence::ConflictError)
110
+ end.to raise_error(Phronomy::Storage::ConflictError)
111
111
  end
112
112
 
113
113
  it "rejects an execution identity mismatch" do
@@ -131,7 +131,7 @@ RSpec.shared_examples "an Execution repository" do
131
131
  expected_revision: 0,
132
132
  execution: other
133
133
  )
134
- end.to raise_error(Phronomy::Persistence::SerializationError)
134
+ end.to raise_error(Phronomy::Storage::SerializationError)
135
135
  end
136
136
 
137
137
  it "requires execution revision to advance exactly once" do
@@ -149,7 +149,7 @@ RSpec.shared_examples "an Execution repository" do
149
149
  expected_revision: 0,
150
150
  execution: skipped
151
151
  )
152
- end.to raise_error(Phronomy::Persistence::ConflictError)
152
+ end.to raise_error(Phronomy::Storage::ConflictError)
153
153
  end
154
154
 
155
155
  it "lists active executions for one Agent" do
@@ -179,7 +179,7 @@ RSpec.shared_examples "an Execution repository" do
179
179
 
180
180
  expect do
181
181
  execution_repository.load(execution.execution_id)
182
- end.to raise_error(Phronomy::Persistence::NotFoundError)
182
+ end.to raise_error(Phronomy::Storage::NotFoundError)
183
183
  end
184
184
 
185
185
  it "deletes all executions for one Agent without deleting other Agents' executions" do
@@ -43,14 +43,14 @@ RSpec.shared_examples "a Handoff state repository" do
43
43
  persistence.handoff_states.save(value.main_agent_id, expected_revision: 1, state: updated)
44
44
  expect do
45
45
  persistence.handoff_states.save(value.main_agent_id, expected_revision: 1, state: updated)
46
- end.to raise_error(Phronomy::Persistence::ConflictError)
46
+ end.to raise_error(Phronomy::Storage::ConflictError)
47
47
  expect(persistence.handoff_states.load(value.main_agent_id).handoff_revision).to eq(2)
48
48
  end
49
49
 
50
50
  it "rejects a snapshot for a different routing anchor" do
51
51
  expect do
52
52
  persistence.handoff_states.save("other", expected_revision: nil, state: coordination_handoff)
53
- end.to raise_error(Phronomy::Persistence::SerializationError)
53
+ end.to raise_error(Phronomy::Storage::SerializationError)
54
54
  end
55
55
  end
56
56
 
@@ -60,7 +60,7 @@ RSpec.shared_examples "a Team repository" do
60
60
  it "round-trips a Team lineage and rejects duplicate creation" do
61
61
  persistence.teams.create(coordination_team)
62
62
  expect(persistence.teams.load(coordination_team.team_id).to_h).to eq(coordination_team.to_h)
63
- expect { persistence.teams.create(coordination_team) }.to raise_error(Phronomy::Persistence::ConflictError)
63
+ expect { persistence.teams.create(coordination_team) }.to raise_error(Phronomy::Storage::ConflictError)
64
64
  end
65
65
 
66
66
  it "rejects a stale Team revision" do
@@ -69,11 +69,11 @@ RSpec.shared_examples "a Team repository" do
69
69
  persistence.teams.save(coordination_team.team_id, expected_revision: 0, root: updated)
70
70
  expect do
71
71
  persistence.teams.save(coordination_team.team_id, expected_revision: 0, root: updated)
72
- end.to raise_error(Phronomy::Persistence::ConflictError)
72
+ end.to raise_error(Phronomy::Storage::ConflictError)
73
73
  end
74
74
 
75
75
  it "raises NotFoundError for an absent Team" do
76
- expect { persistence.teams.load(SecureRandom.uuid) }.to raise_error(Phronomy::Persistence::NotFoundError)
76
+ expect { persistence.teams.load(SecureRandom.uuid) }.to raise_error(Phronomy::Storage::NotFoundError)
77
77
  end
78
78
  end
79
79
 
@@ -100,11 +100,11 @@ RSpec.shared_examples "a Team execution repository" do
100
100
  expect(persistence.team_executions.list("other-team")).to be_empty
101
101
  expect do
102
102
  persistence.team_executions.save(completed.team_execution_id, expected_revision: 0, execution: completed)
103
- end.to raise_error(Phronomy::Persistence::ConflictError)
103
+ end.to raise_error(Phronomy::Storage::ConflictError)
104
104
  expect do
105
105
  persistence.team_executions.save(completed.team_execution_id, expected_revision: 1,
106
106
  execution: completed.with(status: "active", phase: "coordinator"))
107
- end.to raise_error(Phronomy::Persistence::ConflictError)
107
+ end.to raise_error(Phronomy::Storage::ConflictError)
108
108
  end
109
109
 
110
110
  it "rolls the new three repositories back with the existing transaction domain" do
@@ -129,7 +129,7 @@ RSpec.shared_examples "a Team execution repository" do
129
129
  expect(persistence.teams.load(coordination_team.team_id).team_revision).to eq(0)
130
130
  expect(persistence.handoff_states.load(handoff.main_agent_id)).to be_nil
131
131
  expect(persistence.team_executions.list(coordination_team.team_id)).to be_empty
132
- expect { persistence.agents.load(root_id) }.to raise_error(Phronomy::Persistence::NotFoundError)
132
+ expect { persistence.agents.load(root_id) }.to raise_error(Phronomy::Storage::NotFoundError)
133
133
  expect(persistence.executions.list(root_id)).to be_empty
134
134
  expect(persistence.workflow_states.load(root_id)).to be_nil
135
135
  expect(persistence.contents.exist?(content_ref)).to be(false)
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ # This same opaque-record suite runs against InMemory, SQLite and PostgreSQL.
6
+ RSpec.shared_examples "neutral storage primitives" do
7
+ let(:raw_backend) { persistence.backend }
8
+ let(:raw_view) { raw_backend.view }
9
+ let(:raw_schema) { Phronomy::Agent::Persistence::StorageSchema }
10
+ let(:raw_record) { Phronomy::Storage::DurableRecord.new(record_type: "unrelated.envelope", format_version: "0.1", payload: {"status" => "ignored"}) }
11
+ let(:raw_owner) { "owner-#{SecureRandom.uuid}" }
12
+ let(:raw_key) { "key-#{SecureRandom.uuid}" }
13
+ let(:raw_records) { raw_view.records(raw_schema::EXECUTIONS) }
14
+ let(:raw_stream) { raw_view.streams(raw_schema::JOURNAL) }
15
+ let(:raw_blobs) { raw_view.blobs(Phronomy::ContentStore::StorageSchema::CONTENTS) }
16
+
17
+ def raw_insert(key = raw_key, active: true, owner: raw_owner)
18
+ raw_records.insert(key: key, revision: 0, attributes: {owner: owner, active: active}, record: raw_record)
19
+ end
20
+
21
+ before do
22
+ raw_view.records(raw_schema::ROOTS).insert(key: raw_owner, revision: 7, attributes: {}, record: raw_record)
23
+ end
24
+
25
+ it "keeps payload opaque and returns independent immutable entries" do
26
+ inserted = raw_insert
27
+ loaded = raw_records.fetch(raw_key)
28
+ expect(loaded).not_to equal(inserted)
29
+ expect(loaded.record).not_to equal(inserted.record)
30
+ expect(loaded.to_h.except(:record)).to eq(inserted.to_h.except(:record))
31
+ expect(loaded).to be_frozen
32
+ expect(loaded.attributes).to be_frozen
33
+ expect(loaded.record.payload).to eq("status" => "ignored")
34
+ end
35
+
36
+ it "reports a named conditional unique constraint separately from primary identity" do
37
+ raw_insert
38
+ expect { raw_insert("other") }.to raise_error(Phronomy::Storage::UniqueConstraintError) { |error|
39
+ expect(error.resource).to eq(raw_schema::EXECUTIONS.id)
40
+ expect(error.constraint).to eq(:one_active_owner)
41
+ }
42
+ expect { raw_insert }.to raise_error(Phronomy::Storage::ConflictError) { |error|
43
+ expect(error).not_to be_a(Phronomy::Storage::UniqueConstraintError)
44
+ }
45
+ end
46
+
47
+ it "checks revision before active-owner exclusion and retains failed writes" do
48
+ raw_insert
49
+ expect do
50
+ raw_records.replace(key: raw_key, expected_revision: 9, next_revision: 10,
51
+ attributes: {owner: raw_owner, active: false}, record: raw_record)
52
+ end.to raise_error(Phronomy::Storage::ConflictError)
53
+ expect(raw_records.fetch(raw_key).revision).to eq(0)
54
+ end
55
+
56
+ it "checks immutable owner and explicit expected attributes" do
57
+ raw_insert
58
+ expect do
59
+ raw_records.replace(key: raw_key, expected_revision: 0, next_revision: 1,
60
+ attributes: {owner: "another", active: true}, record: raw_record)
61
+ end.to raise_error(Phronomy::Storage::ConflictError)
62
+ expect do
63
+ raw_records.replace(key: raw_key, expected_revision: 0, next_revision: 1,
64
+ expected_attributes: {active: false}, attributes: {owner: raw_owner, active: true}, record: raw_record)
65
+ end.to raise_error(Phronomy::Storage::ConflictError)
66
+ expect(raw_records.fetch(raw_key).revision).to eq(0)
67
+ end
68
+
69
+ it "keeps active semantics outside Records while enforcing the declared unique rule" do
70
+ raw_insert(active: false)
71
+ raw_insert("competitor")
72
+ expect do
73
+ raw_records.replace(key: raw_key, expected_revision: 0, next_revision: 1,
74
+ attributes: {owner: raw_owner, active: true}, record: raw_record)
75
+ end.to raise_error(Phronomy::Storage::UniqueConstraintError)
76
+ raw_records.replace(key: "competitor", expected_revision: 0, next_revision: 1,
77
+ attributes: {owner: raw_owner, active: false}, record: raw_record)
78
+ expect(raw_records.replace(key: raw_key, expected_revision: 0, next_revision: 1,
79
+ attributes: {owner: raw_owner, active: true}, record: raw_record).revision).to eq(1)
80
+ end
81
+
82
+ it "orders equality-index pages by UTF-8 bytes with an exclusive cursor" do
83
+ keys = ["z", "é", "Z", "a"]
84
+ keys.each { |key| raw_insert(key, active: false) }
85
+ sorted = keys.sort_by(&:b)
86
+ first = raw_records.scan(index: :owner, equals: {owner: raw_owner}, limit: 2)
87
+ rest = raw_records.scan(index: :owner, equals: {owner: raw_owner}, after: first.last.key)
88
+ expect((first + rest).map(&:key)).to eq(sorted)
89
+ expect(raw_records.scan(index: :owner_active, equals: {owner: raw_owner, active: true})).to be_empty
90
+ end
91
+
92
+ it "validates index shape, limits, attributes and revisions before writing" do
93
+ expect { raw_records.scan(index: :missing, equals: {}) }.to raise_error(ArgumentError)
94
+ expect { raw_records.scan(index: :owner, equals: {active: true}) }.to raise_error(ArgumentError)
95
+ expect { raw_records.scan(index: :owner, equals: {owner: raw_owner}, limit: 0) }.to raise_error(ArgumentError)
96
+ expect { raw_records.insert(key: raw_key, revision: -1, attributes: {owner: raw_owner, active: true}, record: raw_record) }.to raise_error(ArgumentError)
97
+ expect { raw_records.insert(key: raw_key, revision: 0, attributes: {owner: raw_owner, active: "true"}, record: raw_record) }.to raise_error(ArgumentError)
98
+ expect(raw_records.read(raw_key)).to be_nil
99
+ end
100
+
101
+ it "requires a parent anchor for child creation, stream append and bulk deletion" do
102
+ expect { raw_insert(owner: "missing-parent") }.to raise_error(Phronomy::Storage::NotFoundError)
103
+ expect { raw_stream.append(stream: "missing-parent", expected_head: 0, entries: []) }.to raise_error(Phronomy::Storage::NotFoundError)
104
+ expect { raw_records.delete_matching(index: :owner, equals: {owner: "missing-parent"}) }.to raise_error(Phronomy::Storage::NotFoundError)
105
+ expect(raw_records.read(raw_key)).to be_nil
106
+ end
107
+
108
+ it "separates optional reads, required fetches and revision-checked deletion" do
109
+ expect(raw_records.read(raw_key)).to be_nil
110
+ expect { raw_records.fetch(raw_key) }.to raise_error(Phronomy::Storage::NotFoundError)
111
+ expect { raw_records.delete(key: raw_key, expected_revision: 0) }.to raise_error(Phronomy::Storage::ConflictError)
112
+ expect(raw_records.delete(key: raw_key)).to be_nil
113
+ raw_insert
114
+ expect { raw_records.delete(key: raw_key, expected_revision: 1) }.to raise_error(Phronomy::Storage::ConflictError)
115
+ raw_records.delete(key: raw_key, expected_revision: 0)
116
+ expect(raw_records.read(raw_key)).to be_nil
117
+ end
118
+
119
+ it "deletes only the equality-index matches" do
120
+ raw_insert(active: false)
121
+ raw_insert("active")
122
+ raw_records.delete_matching(index: :owner_active, equals: {owner: raw_owner, active: false})
123
+ expect(raw_records.read(raw_key)).to be_nil
124
+ expect(raw_records.fetch("active").attributes[:active]).to be(true)
125
+ end
126
+
127
+ it "keeps ordered stream positions, entry IDs and head in one atomic append" do
128
+ entries = %w[first second].map { |id| Phronomy::Storage::Entry::Append.new(id: id, record: raw_record) }
129
+ appended = raw_stream.append(stream: raw_owner, expected_head: 0, entries: entries)
130
+ expect(appended.map(&:position)).to eq([1, 2])
131
+ expect(raw_stream.head(stream: raw_owner)).to eq(2)
132
+ expect(raw_stream.read(stream: raw_owner, after: 1, limit: 1).map(&:id)).to eq(["second"])
133
+ expect { raw_stream.append(stream: raw_owner, expected_head: 2, entries: [entries.first]) }.to raise_error(Phronomy::Storage::ConflictError)
134
+ expect(raw_stream.head(stream: raw_owner)).to eq(2)
135
+ expect(raw_stream.read(stream: raw_owner).map(&:id)).to eq(%w[first second])
136
+ end
137
+
138
+ it "checks the head for an empty append and validates duplicate IDs before writing" do
139
+ expect(raw_stream.append(stream: raw_owner, expected_head: 0, entries: [])).to be_empty
140
+ expect { raw_stream.append(stream: raw_owner, expected_head: 1, entries: []) }.to raise_error(Phronomy::Storage::ConflictError)
141
+ entry = Phronomy::Storage::Entry::Append.new(id: "same", record: raw_record)
142
+ expect { raw_stream.append(stream: raw_owner, expected_head: 0, entries: [entry, entry]) }.to raise_error(Phronomy::Storage::ConflictError)
143
+ expect(raw_stream.head(stream: raw_owner)).to eq(0)
144
+ end
145
+
146
+ it "deletes a stream's entries and head so identities and positions can be reused" do
147
+ entries = [Phronomy::Storage::Entry::Append.new(id: "reusable", record: raw_record)]
148
+ raw_stream.append(stream: raw_owner, expected_head: 0, entries: entries)
149
+ raw_stream.delete(stream: raw_owner)
150
+ expect(raw_stream.head(stream: raw_owner)).to eq(0)
151
+ expect(raw_stream.read(stream: raw_owner)).to be_empty
152
+ expect(raw_stream.append(stream: raw_owner, expected_head: 0, entries: entries).first.position).to eq(1)
153
+ end
154
+
155
+ it "uses arbitrary blob keys, preserves binary bytes and retains first metadata" do
156
+ bytes = "\x00\xffbinary".b
157
+ first = raw_blobs.put_if_absent(key: raw_key, bytes: bytes, attributes: {canonicalization_version: 1})
158
+ second = raw_blobs.put_if_absent(key: raw_key, bytes: bytes, attributes: {canonicalization_version: 99})
159
+ expect(first.attributes).to eq(second.attributes)
160
+ expect(raw_blobs.fetch(raw_key).bytes).to eq(bytes)
161
+ expect { raw_blobs.put_if_absent(key: raw_key, bytes: "other", attributes: {canonicalization_version: 1}) }.to raise_error(Phronomy::Storage::BlobConflictError)
162
+ expect(raw_blobs.fetch(raw_key).bytes).to eq(bytes)
163
+ end
164
+
165
+ it "evaluates guarded revision, stream head and no-rows conditions atomically" do
166
+ guard = Phronomy::Storage::GuardRef.new(resource: raw_schema::ROOTS, key: raw_owner)
167
+ revision = Phronomy::Storage::Condition::RevisionIs.new(resource: raw_schema::ROOTS, key: raw_owner, expected: 7)
168
+ head = Phronomy::Storage::Condition::StreamHeadIs.new(resource: raw_schema::JOURNAL, stream: raw_owner, expected: 0)
169
+ idle = Phronomy::Storage::Condition::NoRows.new(resource: raw_schema::EXECUTIONS, index: :owner_active, equals: {owner: raw_owner, active: true})
170
+ expect(raw_view.check!(guards: [guard], conditions: [revision, head, idle])).to be(true)
171
+ expect { raw_view.check!(guards: [], conditions: [head]) }.to raise_error(ArgumentError)
172
+ raw_insert
173
+ expect { raw_view.check!(guards: [guard], conditions: [idle]) }.to raise_error(Phronomy::Storage::ConditionFailedError) { |error| expect(error.condition).to equal(idle) }
174
+ end
175
+
176
+ it "rejects a retained bound view and retained handle after commit" do
177
+ saved_view = saved_records = nil
178
+ raw_backend.transaction { |view|
179
+ saved_view = view
180
+ saved_records = view.records(raw_schema::EXECUTIONS)
181
+ }
182
+ expect { saved_view.records(raw_schema::EXECUTIONS) }.to raise_error(Phronomy::Storage::TransactionError)
183
+ expect { saved_records.read(raw_key) }.to raise_error(Phronomy::Storage::TransactionError)
184
+ expect(raw_records.read(raw_key)).to be_nil
185
+ end
186
+
187
+ it "rejects a retained bound handle after rollback" do
188
+ handle = nil
189
+ expect do
190
+ raw_backend.transaction { |view|
191
+ handle = view.records(raw_schema::EXECUTIONS)
192
+ raise "abort"
193
+ }
194
+ end.to raise_error("abort")
195
+ expect { handle.read(raw_key) }.to raise_error(Phronomy::Storage::TransactionError)
196
+ end
197
+
198
+ it "rejects use of a bound view from another thread" do
199
+ raw_backend.transaction do |view|
200
+ result = Thread.new do
201
+ view.records(raw_schema::EXECUTIONS).read(raw_key)
202
+ rescue => error
203
+ error
204
+ end.value
205
+ expect(result).to be_a(Phronomy::Storage::TransactionError)
206
+ expect(view.records(raw_schema::EXECUTIONS).read(raw_key)).to be_nil
207
+ end
208
+ end
209
+
210
+ it "routes cached root handles into the current transaction and rolls back their writes" do
211
+ handle = raw_records
212
+ expect do
213
+ raw_backend.transaction do
214
+ handle.insert(key: raw_key, revision: 0, attributes: {owner: raw_owner, active: true}, record: raw_record)
215
+ raise "rollback"
216
+ end
217
+ end.to raise_error("rollback")
218
+ expect(handle.read(raw_key)).to be_nil
219
+ end
220
+
221
+ it "marks a failed physical scope unusable even when the operation error is caught" do
222
+ expect do
223
+ raw_backend.transaction do |view|
224
+ handle = view.records(raw_schema::EXECUTIONS)
225
+ handle.insert(key: raw_key, revision: 0, attributes: {owner: raw_owner, active: true}, record: raw_record)
226
+ expect { handle.insert(key: "collision", revision: 0, attributes: {owner: raw_owner, active: true}, record: raw_record) }.to raise_error(Phronomy::Storage::UniqueConstraintError)
227
+ expect { handle.read(raw_key) }.to raise_error(Phronomy::Storage::TransactionError)
228
+ end
229
+ end.to raise_error(Phronomy::Storage::TransactionError)
230
+ expect(raw_records.read(raw_key)).to be_nil
231
+ end
232
+
233
+ [:break, :throw, :return].each do |exit_kind|
234
+ it "rolls back a non-local #{exit_kind} instead of committing" do
235
+ operation = lambda do
236
+ case exit_kind
237
+ when :break
238
+ raw_backend.transaction {
239
+ raw_insert
240
+ break :escaped
241
+ }
242
+ when :throw
243
+ catch(:escape) {
244
+ raw_backend.transaction {
245
+ raw_insert
246
+ throw :escape
247
+ }
248
+ }
249
+ when :return
250
+ returner = -> {
251
+ raw_backend.transaction {
252
+ raw_insert
253
+ return :escaped
254
+ }
255
+ }
256
+ returner.call
257
+ end
258
+ end
259
+ expect { operation.call }.to raise_error(Phronomy::Storage::TransactionError)
260
+ expect(raw_records.read(raw_key)).to be_nil
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ RSpec.shared_examples "storage transaction boundaries" do
6
+ [:backend, :persistence].each do |entry_point|
7
+ context "through #{entry_point} transactions" do
8
+ def transaction_contents(view)
9
+ view.is_a?(Phronomy::Storage::View) ? Phronomy::ContentStore::StoredContents.new(view) : view.contents
10
+ end
11
+
12
+ let(:transaction_service) { (entry_point == :backend) ? persistence.backend : persistence }
13
+
14
+ it "rolls back a failed inner scope and re-raises the same error before the outer scope continues" do
15
+ outer_id = inner_id = later_id = nil
16
+ workflow_id = "nested-#{SecureRandom.uuid}"
17
+ failure = RuntimeError.new("inner failure")
18
+
19
+ result = transaction_service.transaction do |outer|
20
+ outer_id = transaction_contents(outer).put_text("outer-#{workflow_id}")
21
+ expect do
22
+ transaction_service.transaction do |inner|
23
+ inner_id = transaction_contents(inner).put_text("inner-#{workflow_id}")
24
+ if entry_point == :backend
25
+ inner.records(Phronomy::WorkflowStorageSchema::STATES).insert(key: workflow_id,
26
+ revision: 1, attributes: {}, record: Phronomy::Storage::DurableRecord.new(
27
+ record_type: "opaque.transaction", format_version: "0.1", payload: {"value" => "inner"}
28
+ ))
29
+ else
30
+ inner.workflow_states.save(workflow_id,
31
+ expected_revision: nil, snapshot: {fields: {value: "inner"}, phase: "pause"})
32
+ end
33
+ raise failure
34
+ end
35
+ end.to raise_error { |error| expect(error).to equal(failure) }
36
+ expect(transaction_contents(outer).exist?(outer_id)).to be(true)
37
+ expect(transaction_contents(outer).exist?(inner_id)).to be(false)
38
+ expect(persistence.backend.view.records(Phronomy::WorkflowStorageSchema::STATES).read(workflow_id)).to be_nil
39
+ later_id = transaction_contents(outer).put_text("later-#{workflow_id}")
40
+ :outer_result
41
+ end
42
+
43
+ expect(result).to eq(:outer_result)
44
+ expect(persistence.contents.exist?(outer_id)).to be(true)
45
+ expect(persistence.contents.exist?(later_id)).to be(true)
46
+ expect(persistence.contents.exist?(inner_id)).to be(false)
47
+ expect(persistence.backend.view.records(Phronomy::WorkflowStorageSchema::STATES).read(workflow_id)).to be_nil
48
+ end
49
+
50
+ it "commits both scopes on normal completion and returns their block values" do
51
+ outer_id = inner_id = nil
52
+ result = transaction_service.transaction do |outer|
53
+ outer_id = transaction_contents(outer).put_text("outer-#{SecureRandom.uuid}")
54
+ inner_result = transaction_service.transaction do |inner|
55
+ inner_id = transaction_contents(inner).put_text("inner-#{SecureRandom.uuid}")
56
+ :inner_result
57
+ end
58
+ expect(inner_result).to eq(:inner_result)
59
+ :outer_result
60
+ end
61
+ expect(result).to eq(:outer_result)
62
+ expect(persistence.contents.exist?(outer_id)).to be(true)
63
+ expect(persistence.contents.exist?(inner_id)).to be(true)
64
+ end
65
+
66
+ it "rolls back successful inner writes when the outer scope fails" do
67
+ outer_id = inner_id = nil
68
+ expect do
69
+ transaction_service.transaction do |outer|
70
+ outer_id = transaction_contents(outer).put_text("outer-#{SecureRandom.uuid}")
71
+ result = transaction_service.transaction do |inner|
72
+ inner_id = transaction_contents(inner).put_text("inner-#{SecureRandom.uuid}")
73
+ :inner_result
74
+ end
75
+ expect(result).to eq(:inner_result)
76
+ expect(transaction_contents(outer).exist?(inner_id)).to be(true)
77
+ raise "outer failure"
78
+ end
79
+ end.to raise_error("outer failure")
80
+ expect(persistence.contents.exist?(outer_id)).to be(false)
81
+ expect(persistence.contents.exist?(inner_id)).to be(false)
82
+ end
83
+
84
+ it "rolls back both scopes when an inner error escapes the outer scope" do
85
+ outer_id = inner_id = nil
86
+ expect do
87
+ transaction_service.transaction do |outer|
88
+ outer_id = transaction_contents(outer).put_text("outer-#{SecureRandom.uuid}")
89
+ transaction_service.transaction do |inner|
90
+ inner_id = transaction_contents(inner).put_text("inner-#{SecureRandom.uuid}")
91
+ raise "uncaught inner failure"
92
+ end
93
+ end
94
+ end.to raise_error("uncaught inner failure")
95
+ expect(persistence.contents.exist?(outer_id)).to be(false)
96
+ expect(persistence.contents.exist?(inner_id)).to be(false)
97
+ end
98
+ end
99
+ end
100
+
101
+ it "validates every stream entry before writing any part of a batch" do
102
+ backend = persistence.backend
103
+ schema = Phronomy::Agent::Persistence::StorageSchema
104
+ owner_id = "batch-#{SecureRandom.uuid}"
105
+ record = Phronomy::Storage::DurableRecord.new(record_type: "opaque.transaction", format_version: "0.1", payload: {})
106
+ backend.view.records(schema::ROOTS).insert(key: owner_id, revision: 0, attributes: {}, record: record)
107
+ valid = Phronomy::Storage::Entry::Append.new(id: "first", record: record)
108
+ content_id = nil
109
+ backend.transaction do |view|
110
+ content_id = persistence.contents.put_text("retained-#{owner_id}")
111
+ stream = view.streams(schema::JOURNAL)
112
+ expect { stream.append(stream: owner_id, expected_head: 0, entries: [valid, Object.new]) }
113
+ .to raise_error(Phronomy::Storage::SerializationError)
114
+ expect(stream.head(stream: owner_id)).to eq(0)
115
+ expect(stream.read(stream: owner_id)).to be_empty
116
+ end
117
+ expect(persistence.contents.exist?(content_id)).to be(true)
118
+ entries = backend.view.streams(schema::JOURNAL).append(stream: owner_id, expected_head: 0,
119
+ entries: [valid, Phronomy::Storage::Entry::Append.new(id: "second", record: record)])
120
+ expect(entries.map(&:position)).to eq([1, 2])
121
+ expect(entries.map { |entry| entry.record.payload }).to eq([{}, {}])
122
+ end
123
+ end
@@ -30,6 +30,8 @@ module Phronomy
30
30
  "a Handoff state repository",
31
31
  "a Team repository",
32
32
  "a Team execution repository",
33
+ "neutral storage primitives",
34
+ "storage transaction boundaries",
33
35
  "a Persistence backend"
34
36
  ].freeze
35
37
  end
@@ -41,6 +43,8 @@ require_relative "persistence_contract/an_agent_repository"
41
43
  require_relative "persistence_contract/a_journal_repository"
42
44
  require_relative "persistence_contract/an_execution_repository"
43
45
  require_relative "persistence_contract/a_workflow_state_repository"
46
+ require_relative "persistence_contract/neutral_storage_primitives"
47
+ require_relative "persistence_contract/storage_transaction_boundaries"
44
48
  require_relative "persistence_contract/a_persistence_backend"
45
49
 
46
50
  require_relative "persistence_contract/coordination_repositories"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../common/error"
4
+
5
+ module Phronomy
6
+ class ToolError < Error; end
7
+ end
@@ -51,7 +51,7 @@ module Phronomy
51
51
  # The child Agent owns its own FSMSession/EventLoop lifecycle; this
52
52
  # method only returns its completion handle and performs a short map.
53
53
  klass.define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
54
- persistence = Phronomy::Persistence::InMemory.new
54
+ persistence = Phronomy::Persistence.in_memory
55
55
  agent = agent_class.create(persistence: persistence)
56
56
  task_config = (config || {}).dup
57
57
  if cancellation_token && !task_config[:cancellation_token]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.26.0"
4
+ VERSION = "0.27.0"
5
5
  end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
3
5
  require_relative "workflow_runner"
4
- require_relative "runnable"
6
+ require_relative "../../engine/runnable"
5
7
 
6
8
  module Phronomy
7
9
  # StateChart-style Workflow definition DSL.
8
10
  class Workflow
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
9
13
  include Phronomy::Runnable
10
14
 
11
15
  def self.define(context_class, persistence: nil, &block)
@@ -72,8 +76,6 @@ module Phronomy
72
76
  public
73
77
 
74
78
  class Builder
75
- FINISH = Phronomy::WorkflowRunner::FINISH
76
-
77
79
  def initialize(context_class, persistence: nil)
78
80
  @context_class = context_class
79
81
  @persistence = persistence
@@ -119,7 +121,7 @@ module Phronomy
119
121
  # They may start asynchronous work and register listeners, but returning
120
122
  # Phronomy::TaskResult is an error; completion must arrive as a later event.
121
123
  def transition(from:, to:, guard: nil, on: nil, action: nil)
122
- destination = (to == :__finish__) ? FINISH : to
124
+ destination = (to == :__finish__) ? FSMProtocol::FINISH : to
123
125
  @transitions << {
124
126
  from: from,
125
127
  to: destination,
@@ -184,7 +186,7 @@ module Phronomy
184
186
 
185
187
  undefined_targets = @transitions
186
188
  .map { |transition| transition[:to] }
187
- .reject { |target| target == FINISH } - all_states
189
+ .reject { |target| target == FSMProtocol::FINISH } - all_states
188
190
  unless undefined_targets.empty?
189
191
  raise ArgumentError,
190
192
  "Workflow transition(s) reference undefined state(s): " \
@@ -205,7 +207,7 @@ module Phronomy
205
207
  current = queue.shift
206
208
  @transitions.each do |transition|
207
209
  next unless transition[:from] == current
208
- next if transition[:to] == FINISH
210
+ next if transition[:to] == FSMProtocol::FINISH
209
211
  next if reachable.include?(transition[:to])
210
212
 
211
213
  reachable.add(transition[:to])
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  # Module for defining Workflow context data.
5
7
  #
@@ -12,6 +14,8 @@ module Phronomy
12
14
  # evaluation, return a replacement WorkflowContext, or return +:consume+ to
13
15
  # discard a stale/unrelated event without firing a transition.
14
16
  module WorkflowContext
17
+ include Phronomy::Concurrency::WorkerInputRestricted
18
+
15
19
  def self.included(base)
16
20
  base.extend(ClassMethods)
17
21
  base.instance_variable_set(:@fields, {})
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../common/error"
4
+
5
+ module Phronomy
6
+ class WorkflowContextOwnershipError < Error; end
7
+ end