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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Persistence
6
+ # Owns Agent root encoding and validates returned storage metadata.
7
+ # @api private
8
+ class AgentRepository
9
+ def initialize(view) = @view = view
10
+
11
+ def create(root)
12
+ record = Codec.encode_agent_root(root)
13
+ @view.atomic do |bound|
14
+ entry = bound.records(StorageSchema::ROOTS).insert(key: root.agent_id.to_s,
15
+ revision: Integer(root.agent_revision), attributes: {}, record: record)
16
+ decode(entry, root.agent_id, revision: root.agent_revision)
17
+ end
18
+ end
19
+
20
+ def load(agent_id)
21
+ value = @view.atomic do |bound|
22
+ entry = bound.records(StorageSchema::ROOTS).read(agent_id.to_s)
23
+ entry && decode(entry, agent_id)
24
+ end
25
+ value || raise(Phronomy::Storage::NotFoundError, "Agent not found: #{agent_id}")
26
+ end
27
+
28
+ def save(agent_id, expected_revision:, root:)
29
+ expected = Integer(expected_revision)
30
+ revision = Integer(root.agent_revision)
31
+ raise Phronomy::Storage::ConflictError, "root save must advance revision exactly once" unless revision == expected + 1
32
+ unless root.agent_id.to_s == agent_id.to_s
33
+ raise Phronomy::Storage::SerializationError, "Agent root identity mismatch"
34
+ end
35
+ record = Codec.encode_agent_root(root)
36
+ @view.atomic do |bound|
37
+ entry = bound.records(StorageSchema::ROOTS).replace(key: agent_id.to_s,
38
+ expected_revision: expected, next_revision: revision, attributes: {}, record: record)
39
+ decode(entry, agent_id, revision: revision)
40
+ end
41
+ end
42
+
43
+ def delete(agent_id)
44
+ @view.records(StorageSchema::ROOTS).delete(key: agent_id.to_s)
45
+ end
46
+
47
+ private
48
+
49
+ def decode(entry, agent_id, revision: nil)
50
+ root = Codec.decode_agent_root(entry.record)
51
+ valid = entry.key == agent_id.to_s && root.agent_id == entry.key &&
52
+ root.agent_revision == entry.revision && (!revision || revision == entry.revision)
53
+ unless valid
54
+ raise Phronomy::Storage::SerializationError, "backend returned another Agent identity/revision"
55
+ end
56
+ root
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Persistence
6
+ # Current-format record schemas and conversion for this domain.
7
+ # @api private
8
+ module Codec
9
+ extend Phronomy::Storage::RecordCodec
10
+
11
+ AGENT_ROOT_RECORD_TYPE = "phronomy.agent_root"
12
+ AGENT_ROOT_FORMAT_VERSION = "0.1"
13
+ AGENT_EXECUTION_RECORD_TYPE = "phronomy.agent_execution"
14
+ AGENT_EXECUTION_FORMAT_VERSION = "0.1"
15
+ JOURNAL_RECORD_TYPE = "phronomy.journal_record"
16
+ JOURNAL_FORMAT_VERSION = "0.1"
17
+ AGENT_ROOT_KEYS = %w[
18
+ agent_id agent_definition_id agent_definition_version agent_revision
19
+ context_revision journal_position lifecycle_status transcript_generation
20
+ created_at updated_at metadata
21
+ ].freeze
22
+
23
+ AGENT_EXECUTION_KEYS = %w[
24
+ execution_id agent_id execution_revision status phase
25
+ base_agent_revision base_context_revision base_journal_position
26
+ working_records llm_calls approval_request result_ref error_ref
27
+ created_at updated_at terminal_reason metadata
28
+ ].freeze
29
+
30
+ JOURNAL_RECORD_KEYS = %w[
31
+ record_id agent_id sequence execution_id llm_call_id kind channel role
32
+ content_ref parent_id causation_id visibility context_generation
33
+ context_candidate occurred_at metadata
34
+ ].freeze
35
+
36
+ LLM_CALL_RECORD_KEYS = %w[
37
+ llm_call_id execution_id sequence status manifest_ref output_ref
38
+ error_ref usage_ref started_at completed_at metadata
39
+ ].freeze
40
+
41
+ APPROVAL_REQUEST_KEYS = %w[id execution_id items created_at].freeze
42
+
43
+ APPROVAL_REQUEST_OPTIONAL_KEYS = %w[approved].freeze
44
+
45
+ APPROVAL_ITEM_KEYS = %w[
46
+ tool_invocation_id tool_call_id tool_name arguments facts reason origin metadata
47
+ ].freeze
48
+
49
+ module_function
50
+
51
+ def encode_handoff_state(value)
52
+ payload = value.to_h
53
+ Phronomy::Agent::HandoffState.from_h(payload)
54
+ build_record("phronomy.handoff_state", "0.1", payload)
55
+ rescue Phronomy::Storage::SerializationError
56
+ raise
57
+ rescue => error
58
+ serialization_error("cannot encode HandoffState", error)
59
+ end
60
+
61
+ def decode_handoff_state(record)
62
+ payload = current_payload!(record, record_type: "phronomy.handoff_state",
63
+ format_version: "0.1", keys: Phronomy::Agent::HandoffState::ATTRIBUTES, label: "HandoffState")
64
+ Phronomy::Agent::HandoffState.from_h(payload)
65
+ rescue Phronomy::Storage::SerializationError
66
+ raise
67
+ rescue => error
68
+ serialization_error("cannot decode HandoffState", error)
69
+ end
70
+
71
+ def encode_agent_root(root)
72
+ payload = top_level_string_keys(root.to_h, label: "AgentRoot payload")
73
+ payload["lifecycle_status"] = root.lifecycle_status.to_s
74
+ validate_agent_root_payload!(payload)
75
+ build_record(AGENT_ROOT_RECORD_TYPE, AGENT_ROOT_FORMAT_VERSION, payload)
76
+ rescue Phronomy::Storage::SerializationError
77
+ raise
78
+ rescue => error
79
+ serialization_error("cannot encode AgentRoot", error)
80
+ end
81
+
82
+ def decode_agent_root(record)
83
+ payload = current_payload!(
84
+ record,
85
+ record_type: AGENT_ROOT_RECORD_TYPE,
86
+ format_version: AGENT_ROOT_FORMAT_VERSION,
87
+ keys: AGENT_ROOT_KEYS,
88
+ label: "AgentRoot payload"
89
+ )
90
+ validate_agent_root_payload!(payload)
91
+ Phronomy::Agent::AgentRoot.from_h(payload)
92
+ rescue Phronomy::Storage::SerializationError
93
+ raise
94
+ rescue => error
95
+ serialization_error("cannot decode AgentRoot", error)
96
+ end
97
+
98
+ def encode_agent_execution(execution)
99
+ payload = top_level_string_keys(execution.to_h, label: "AgentExecution payload")
100
+ payload["status"] = execution.status.to_s
101
+ payload["phase"] = execution.phase.to_s
102
+ payload["working_records"] = execution.working_records.map do |record|
103
+ journal_payload(record, require_sequence: false)
104
+ end
105
+ payload["llm_calls"] = execution.llm_calls.map do |call|
106
+ llm_call_payload(call)
107
+ end
108
+ validate_agent_execution_payload!(payload)
109
+ build_record(
110
+ AGENT_EXECUTION_RECORD_TYPE,
111
+ AGENT_EXECUTION_FORMAT_VERSION,
112
+ payload
113
+ )
114
+ rescue Phronomy::Storage::SerializationError
115
+ raise
116
+ rescue => error
117
+ serialization_error("cannot encode AgentExecution", error)
118
+ end
119
+
120
+ def decode_agent_execution(record)
121
+ payload = current_payload!(
122
+ record,
123
+ record_type: AGENT_EXECUTION_RECORD_TYPE,
124
+ format_version: AGENT_EXECUTION_FORMAT_VERSION,
125
+ keys: AGENT_EXECUTION_KEYS,
126
+ label: "AgentExecution payload"
127
+ )
128
+ validate_agent_execution_payload!(payload)
129
+ Phronomy::Agent::AgentExecution.from_h(payload)
130
+ rescue Phronomy::Storage::SerializationError
131
+ raise
132
+ rescue => error
133
+ serialization_error("cannot decode AgentExecution", error)
134
+ end
135
+
136
+ def encode_journal_record(journal_record)
137
+ payload = journal_payload(journal_record, require_sequence: true)
138
+ build_record(JOURNAL_RECORD_TYPE, JOURNAL_FORMAT_VERSION, payload)
139
+ rescue Phronomy::Storage::SerializationError
140
+ raise
141
+ rescue => error
142
+ serialization_error("cannot encode JournalRecord", error)
143
+ end
144
+
145
+ def decode_journal_record(record)
146
+ payload = current_payload!(
147
+ record,
148
+ record_type: JOURNAL_RECORD_TYPE,
149
+ format_version: JOURNAL_FORMAT_VERSION,
150
+ keys: JOURNAL_RECORD_KEYS,
151
+ label: "JournalRecord payload"
152
+ )
153
+ validate_journal_payload!(payload, label: "JournalRecord payload", require_sequence: true)
154
+ Phronomy::Agent::JournalRecord.from_h(payload)
155
+ rescue Phronomy::Storage::SerializationError
156
+ raise
157
+ rescue => error
158
+ serialization_error("cannot decode JournalRecord", error)
159
+ end
160
+
161
+ def validate_agent_root_payload!(payload)
162
+ validate_exact_keys!(payload, AGENT_ROOT_KEYS, label: "AgentRoot payload")
163
+ require_nonempty_string!(payload, "agent_id", label: "AgentRoot payload")
164
+ require_nonempty_string!(payload, "agent_definition_id", label: "AgentRoot payload")
165
+ require_integer!(payload, "agent_definition_version", label: "AgentRoot payload")
166
+ require_nonnegative_integer!(payload, "agent_revision", label: "AgentRoot payload")
167
+ require_nonnegative_integer!(payload, "context_revision", label: "AgentRoot payload")
168
+ require_nonnegative_integer!(payload, "journal_position", label: "AgentRoot payload")
169
+ require_enum_string!(
170
+ payload,
171
+ "lifecycle_status",
172
+ Phronomy::Agent::AgentRoot::LIFECYCLE_STATUSES.map(&:to_s),
173
+ label: "AgentRoot payload"
174
+ )
175
+ require_nonnegative_integer!(payload, "transcript_generation", label: "AgentRoot payload")
176
+ require_nonempty_string!(payload, "created_at", label: "AgentRoot payload")
177
+ require_nonempty_string!(payload, "updated_at", label: "AgentRoot payload")
178
+ require_canonical_hash!(payload, "metadata", label: "AgentRoot payload")
179
+ payload
180
+ end
181
+
182
+ def validate_agent_execution_payload!(payload)
183
+ validate_exact_keys!(payload, AGENT_EXECUTION_KEYS, label: "AgentExecution payload")
184
+ require_nonempty_string!(payload, "execution_id", label: "AgentExecution payload")
185
+ require_nonempty_string!(payload, "agent_id", label: "AgentExecution payload")
186
+ require_nonnegative_integer!(payload, "execution_revision", label: "AgentExecution payload")
187
+ require_enum_string!(
188
+ payload,
189
+ "status",
190
+ Phronomy::Agent::AgentExecution::TRANSITIONS.keys.map(&:to_s),
191
+ label: "AgentExecution payload"
192
+ )
193
+ require_nonempty_string!(payload, "phase", label: "AgentExecution payload")
194
+ require_nonnegative_integer!(payload, "base_agent_revision", label: "AgentExecution payload")
195
+ require_nonnegative_integer!(payload, "base_context_revision", label: "AgentExecution payload")
196
+ require_nonnegative_integer!(payload, "base_journal_position", label: "AgentExecution payload")
197
+ require_optional_string!(payload, "result_ref", label: "AgentExecution payload")
198
+ require_optional_string!(payload, "error_ref", label: "AgentExecution payload")
199
+ require_nonempty_string!(payload, "created_at", label: "AgentExecution payload")
200
+ require_nonempty_string!(payload, "updated_at", label: "AgentExecution payload")
201
+ require_optional_string!(payload, "terminal_reason", label: "AgentExecution payload")
202
+ require_canonical_hash!(payload, "metadata", label: "AgentExecution payload")
203
+
204
+ working_records = payload.fetch("working_records")
205
+ unless working_records.is_a?(Array)
206
+ raise Phronomy::Storage::SerializationError,
207
+ "AgentExecution payload working_records must be an Array"
208
+ end
209
+ working_records.each_with_index do |record, index|
210
+ validate_journal_payload!(
211
+ record,
212
+ label: "AgentExecution working_records[#{index}]",
213
+ require_sequence: false
214
+ )
215
+ record_agent_id = record.fetch("agent_id")
216
+ unless record_agent_id == payload.fetch("agent_id")
217
+ raise Phronomy::Storage::SerializationError,
218
+ "AgentExecution working_records[#{index}] agent_id mismatch"
219
+ end
220
+ record_execution_id = record.fetch("execution_id")
221
+ if record_execution_id && record_execution_id != payload.fetch("execution_id")
222
+ raise Phronomy::Storage::SerializationError,
223
+ "AgentExecution working_records[#{index}] execution_id mismatch"
224
+ end
225
+ end
226
+
227
+ llm_calls = payload.fetch("llm_calls")
228
+ unless llm_calls.is_a?(Array)
229
+ raise Phronomy::Storage::SerializationError,
230
+ "AgentExecution payload llm_calls must be an Array"
231
+ end
232
+ llm_calls.each_with_index do |call, index|
233
+ validate_llm_call_payload!(call, label: "AgentExecution llm_calls[#{index}]")
234
+ unless call.fetch("execution_id") == payload.fetch("execution_id")
235
+ raise Phronomy::Storage::SerializationError,
236
+ "AgentExecution llm_calls[#{index}] execution_id mismatch"
237
+ end
238
+ end
239
+
240
+ validate_approval_request!(
241
+ payload.fetch("approval_request"),
242
+ execution_id: payload.fetch("execution_id")
243
+ )
244
+ payload
245
+ end
246
+
247
+ def validate_journal_payload!(payload, label:, require_sequence:)
248
+ validate_exact_keys!(payload, JOURNAL_RECORD_KEYS, label: label)
249
+ require_nonempty_string!(payload, "record_id", label: label)
250
+ require_nonempty_string!(payload, "agent_id", label: label)
251
+ sequence = payload.fetch("sequence")
252
+ if require_sequence
253
+ unless sequence.is_a?(Integer) && sequence.positive?
254
+ raise Phronomy::Storage::SerializationError,
255
+ "#{label} sequence must be a positive Integer"
256
+ end
257
+ elsif !(sequence.nil? || (sequence.is_a?(Integer) && sequence.positive?))
258
+ raise Phronomy::Storage::SerializationError,
259
+ "#{label} sequence must be nil or a positive Integer"
260
+ end
261
+ require_optional_string!(payload, "execution_id", label: label)
262
+ require_optional_string!(payload, "llm_call_id", label: label)
263
+ require_nonempty_string!(payload, "kind", label: label)
264
+ require_nonempty_string!(payload, "channel", label: label)
265
+ require_optional_string!(payload, "role", label: label)
266
+ require_optional_string!(payload, "content_ref", label: label)
267
+ require_optional_string!(payload, "parent_id", label: label)
268
+ require_optional_string!(payload, "causation_id", label: label)
269
+ require_nonempty_string!(payload, "visibility", label: label)
270
+ require_nonnegative_integer!(payload, "context_generation", label: label)
271
+ require_boolean!(payload, "context_candidate", label: label)
272
+ require_nonempty_string!(payload, "occurred_at", label: label)
273
+ require_canonical_hash!(payload, "metadata", label: label)
274
+ payload
275
+ end
276
+
277
+ def validate_llm_call_payload!(payload, label:)
278
+ validate_exact_keys!(payload, LLM_CALL_RECORD_KEYS, label: label)
279
+ require_nonempty_string!(payload, "llm_call_id", label: label)
280
+ require_nonempty_string!(payload, "execution_id", label: label)
281
+ require_positive_integer!(payload, "sequence", label: label)
282
+ require_enum_string!(
283
+ payload,
284
+ "status",
285
+ Phronomy::Agent::LLMCallRecord::STATUSES.map(&:to_s),
286
+ label: label
287
+ )
288
+ require_nonempty_string!(payload, "manifest_ref", label: label)
289
+ require_optional_string!(payload, "output_ref", label: label)
290
+ require_optional_string!(payload, "error_ref", label: label)
291
+ require_optional_string!(payload, "usage_ref", label: label)
292
+ require_nonempty_string!(payload, "started_at", label: label)
293
+ require_optional_string!(payload, "completed_at", label: label)
294
+ require_canonical_hash!(payload, "metadata", label: label)
295
+ payload
296
+ end
297
+
298
+ def validate_approval_request!(request, execution_id:)
299
+ return if request.nil?
300
+
301
+ validate_allowed_keys!(
302
+ request,
303
+ required_keys: APPROVAL_REQUEST_KEYS,
304
+ optional_keys: APPROVAL_REQUEST_OPTIONAL_KEYS,
305
+ label: "approval_request"
306
+ )
307
+ require_nonempty_string!(request, "id", label: "approval_request")
308
+ require_nonempty_string!(request, "execution_id", label: "approval_request")
309
+ unless request.fetch("execution_id") == execution_id
310
+ raise Phronomy::Storage::SerializationError,
311
+ "approval_request execution_id mismatch"
312
+ end
313
+ require_nonempty_string!(request, "created_at", label: "approval_request")
314
+ if request.key?("approved") && !boolean?(request.fetch("approved"))
315
+ raise Phronomy::Storage::SerializationError,
316
+ "approval_request approved must be true or false"
317
+ end
318
+
319
+ items = request.fetch("items")
320
+ unless items.is_a?(Array) && !items.empty?
321
+ raise Phronomy::Storage::SerializationError,
322
+ "approval_request items must be a non-empty Array"
323
+ end
324
+ items.each_with_index do |item, index|
325
+ item_label = "approval_request items[#{index}]"
326
+ validate_exact_keys!(item, APPROVAL_ITEM_KEYS, label: item_label)
327
+ require_nonempty_string!(item, "tool_invocation_id", label: item_label)
328
+ require_optional_string!(item, "tool_call_id", label: item_label)
329
+ require_nonempty_string!(item, "tool_name", label: item_label)
330
+ require_canonical_hash!(item, "arguments", label: item_label)
331
+ require_canonical_hash!(item, "facts", label: item_label)
332
+ require_optional_string!(item, "reason", label: item_label)
333
+ require_nonempty_string!(item, "origin", label: item_label)
334
+ require_canonical_hash!(item, "metadata", label: item_label)
335
+ end
336
+ request
337
+ end
338
+
339
+ def journal_payload(record, require_sequence:)
340
+ payload = top_level_string_keys(record.to_h, label: "JournalRecord payload")
341
+ %w[kind channel role visibility].each do |key|
342
+ value = payload[key]
343
+ payload[key] = value.to_s if value
344
+ end
345
+ validate_journal_payload!(payload, label: "JournalRecord payload", require_sequence: require_sequence)
346
+ payload
347
+ end
348
+
349
+ def llm_call_payload(call)
350
+ payload = top_level_string_keys(call.to_h, label: "LLMCallRecord payload")
351
+ payload["status"] = call.status.to_s
352
+ validate_llm_call_payload!(payload, label: "LLMCallRecord payload")
353
+ payload
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Persistence
6
+ # Owns execution activity, ownership, admission and durable record encoding.
7
+ # @api private
8
+ class ExecutionRepository
9
+ def initialize(view) = @view = view
10
+
11
+ def create_active(execution)
12
+ raise Phronomy::Storage::SerializationError, "create_active requires an active AgentExecution" unless execution.active?
13
+ record = Codec.encode_agent_execution(execution)
14
+ admission do
15
+ @view.atomic do |bound|
16
+ entry = bound.records(StorageSchema::EXECUTIONS).insert(key: execution.execution_id.to_s,
17
+ revision: Integer(execution.execution_revision), attributes: attributes(execution), record: record)
18
+ decode(entry, execution.execution_id, owner: execution.agent_id,
19
+ revision: execution.execution_revision, active: true)
20
+ end
21
+ end
22
+ end
23
+
24
+ def load(execution_id)
25
+ value = @view.atomic do |bound|
26
+ entry = bound.records(StorageSchema::EXECUTIONS).read(execution_id.to_s)
27
+ entry && decode(entry, execution_id)
28
+ end
29
+ value || raise(Phronomy::Storage::NotFoundError, "Execution not found: #{execution_id}")
30
+ end
31
+
32
+ def save(execution_id, expected_revision:, execution:)
33
+ expected = Integer(expected_revision)
34
+ revision = Integer(execution.execution_revision)
35
+ raise Phronomy::Storage::ConflictError, "execution save must advance revision exactly once" unless revision == expected + 1
36
+ unless execution.execution_id.to_s == execution_id.to_s
37
+ raise Phronomy::Storage::SerializationError, "Execution identity mismatch"
38
+ end
39
+ record = Codec.encode_agent_execution(execution)
40
+ admission do
41
+ @view.atomic do |bound|
42
+ entry = bound.records(StorageSchema::EXECUTIONS).replace(key: execution_id.to_s,
43
+ expected_revision: expected, next_revision: revision, attributes: attributes(execution),
44
+ expected_attributes: {}, record: record)
45
+ decode(entry, execution_id, owner: execution.agent_id, revision: revision, active: execution.active?)
46
+ end
47
+ end
48
+ end
49
+
50
+ def list_active(agent_id)
51
+ @view.atomic do |bound|
52
+ bound.records(StorageSchema::EXECUTIONS).scan(index: :owner_active,
53
+ equals: {owner: agent_id.to_s, active: true}).map { |entry| decode(entry, entry.key, owner: agent_id, active: true) }.freeze
54
+ end
55
+ end
56
+
57
+ def list(agent_id, after: nil, limit: 100)
58
+ raise ArgumentError, "limit must be a positive Integer" unless limit.is_a?(Integer) && limit.positive?
59
+ @view.atomic do |bound|
60
+ bound.records(StorageSchema::EXECUTIONS).scan(index: :owner, equals: {owner: agent_id.to_s},
61
+ after: after&.to_s, limit: limit).map { |entry| decode(entry, entry.key, owner: agent_id) }.freeze
62
+ end
63
+ end
64
+
65
+ def delete(execution_id)
66
+ @view.records(StorageSchema::EXECUTIONS).delete(key: execution_id.to_s)
67
+ end
68
+
69
+ def delete_for_agent(agent_id)
70
+ @view.records(StorageSchema::EXECUTIONS).delete_matching(index: :owner, equals: {owner: agent_id.to_s})
71
+ end
72
+
73
+ def assert_idle!(agent_id)
74
+ condition = Phronomy::Storage::Condition::NoRows.new(resource: StorageSchema::EXECUTIONS,
75
+ index: :owner_active, equals: {owner: agent_id.to_s, active: true})
76
+ @view.check!(guards: [Phronomy::Storage::GuardRef.new(resource: StorageSchema::ROOTS, key: agent_id.to_s)],
77
+ conditions: [condition])
78
+ rescue Phronomy::Storage::ConditionFailedError => error
79
+ raise unless error.condition.equal?(condition)
80
+ raise Phronomy::AgentBusyError, error.message
81
+ end
82
+
83
+ private
84
+
85
+ def attributes(execution) = {owner: execution.agent_id.to_s, active: execution.active?}
86
+
87
+ def admission
88
+ yield
89
+ rescue Phronomy::Storage::UniqueConstraintError => error
90
+ raise unless error.resource == StorageSchema::EXECUTIONS.id && error.constraint == :one_active_owner
91
+ raise Phronomy::AgentBusyError, error.message
92
+ end
93
+
94
+ def decode(entry, execution_id, owner: nil, revision: nil, active: nil)
95
+ execution = Codec.decode_agent_execution(entry.record)
96
+ valid = entry.key == execution_id.to_s && execution.execution_id == entry.key &&
97
+ execution.execution_revision == entry.revision && attributes(execution) == entry.attributes &&
98
+ (!owner || execution.agent_id == owner.to_s) && (!revision || execution.execution_revision == revision) &&
99
+ (active.nil? || execution.active? == active)
100
+ unless valid
101
+ raise Phronomy::Storage::SerializationError, "backend returned another Agent Execution identity/metadata"
102
+ end
103
+ execution
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Persistence
6
+ # Owns Handoff state encoding and the initial revision.
7
+ # @api private
8
+ class HandoffStateRepository
9
+ def initialize(view) = @view = view
10
+
11
+ def load(main_agent_id)
12
+ @view.atomic do |bound|
13
+ entry = bound.records(StorageSchema::HANDOFF_STATES).read(main_agent_id.to_s)
14
+ entry && decode(entry, main_agent_id)
15
+ end
16
+ end
17
+
18
+ def save(main_agent_id, expected_revision:, state:)
19
+ expected = expected_revision.nil? ? nil : Integer(expected_revision)
20
+ revision = expected.nil? ? 1 : expected + 1
21
+ unless state.main_agent_id == main_agent_id.to_s && state.handoff_revision == revision
22
+ raise Phronomy::Storage::SerializationError, "Handoff identity/revision mismatch"
23
+ end
24
+ record = Codec.encode_handoff_state(state)
25
+ @view.atomic do |bound|
26
+ records = bound.records(StorageSchema::HANDOFF_STATES)
27
+ values = {key: main_agent_id.to_s, attributes: {active_agent_id: state.active_agent_id}, record: record}
28
+ entry = if expected.nil?
29
+ records.insert(**values, revision: revision)
30
+ else
31
+ records.replace(**values, expected_revision: expected, next_revision: revision)
32
+ end
33
+ decode(entry, main_agent_id, revision: revision)
34
+ end
35
+ rescue Phronomy::Storage::NotFoundError => error
36
+ raise Phronomy::Storage::ConflictError, error.message
37
+ end
38
+
39
+ def delete(main_agent_id, expected_revision:)
40
+ @view.records(StorageSchema::HANDOFF_STATES).delete(key: main_agent_id.to_s, expected_revision: Integer(expected_revision))
41
+ end
42
+
43
+ private
44
+
45
+ def decode(entry, main_agent_id, revision: nil)
46
+ state = Codec.decode_handoff_state(entry.record)
47
+ valid = entry.key == main_agent_id.to_s && state.main_agent_id == entry.key &&
48
+ state.handoff_revision == entry.revision && (!revision || revision == entry.revision) &&
49
+ entry.attributes == {active_agent_id: state.active_agent_id}
50
+ unless valid
51
+ raise Phronomy::Storage::SerializationError, "backend returned another Handoff identity/revision"
52
+ end
53
+ state
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Persistence
6
+ # Assigns domain sequence numbers and validates opaque stream entries.
7
+ # @api private
8
+ class JournalRepository
9
+ def initialize(view) = @view = view
10
+
11
+ def append(agent_id, expected_position:, records:)
12
+ expected = Integer(expected_position)
13
+ entries = Array(records).each_with_index.map do |record, index|
14
+ unless record.agent_id.to_s == agent_id.to_s
15
+ raise Phronomy::Storage::SerializationError, "Journal record Agent mismatch"
16
+ end
17
+ sequenced = record.with_sequence(expected + index + 1)
18
+ Phronomy::Storage::Entry::Append.new(id: sequenced.record_id.to_s, record: Codec.encode_journal_record(sequenced))
19
+ end
20
+ @view.atomic do |bound|
21
+ stored = bound.streams(StorageSchema::JOURNAL).append(stream: agent_id.to_s,
22
+ expected_head: expected, entries: entries)
23
+ decode(stored, agent_id, after: expected)
24
+ end
25
+ end
26
+
27
+ def read(agent_id, after: nil, limit: nil)
28
+ position = after.nil? ? 0 : Integer(after)
29
+ count = limit.nil? ? nil : Integer(limit)
30
+ @view.atomic do |bound|
31
+ stored = (count == 0) ? [] : bound.streams(StorageSchema::JOURNAL).read(stream: agent_id.to_s, after: position, limit: count)
32
+ decode(stored, agent_id, after: position)
33
+ end
34
+ end
35
+
36
+ def head(agent_id) = @view.streams(StorageSchema::JOURNAL).head(stream: agent_id.to_s)
37
+ def delete(agent_id) = @view.streams(StorageSchema::JOURNAL).delete(stream: agent_id.to_s)
38
+
39
+ private
40
+
41
+ def decode(entries, agent_id, after:)
42
+ entries.each_with_index.map do |entry, index|
43
+ record = Codec.decode_journal_record(entry.record)
44
+ unless record.agent_id == agent_id.to_s && record.record_id == entry.id &&
45
+ record.sequence == entry.position && entry.position == after + index + 1
46
+ raise Phronomy::Storage::SerializationError, "backend returned another Journal identity/sequence"
47
+ end
48
+ record
49
+ end.freeze
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end