phronomy 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (557) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +9 -9
  3. data/CHANGELOG.md +92 -0
  4. data/CONTRIBUTING.md +8 -8
  5. data/README.md +13 -3
  6. data/VERIFY.sh +27 -27
  7. data/benchmark/bench_agent_invoke.rb +26 -47
  8. data/benchmark/bench_context_assembler.rb +17 -15
  9. data/benchmark/bench_regression.rb +1 -1
  10. data/benchmark/bench_tool_schema.rb +1 -1
  11. data/docs/architecture/agent-chat-and-state-ownership.md +153 -0
  12. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  13. data/docs/architecture/agent-context.md +4 -2
  14. data/docs/architecture/agent-transition-ownership.md +13 -0
  15. data/docs/architecture/backend-groups.md +152 -0
  16. data/docs/architecture/before-llm-input.md +9 -3
  17. data/docs/architecture/context-management.md +31 -1
  18. data/docs/architecture/context-preparation-steps.md +90 -0
  19. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  20. data/docs/architecture/execution-abstraction-review.md +88 -0
  21. data/docs/architecture/execution-boundaries.md +28 -0
  22. data/docs/architecture/execution-metadata-and-values.md +86 -0
  23. data/docs/architecture/generator-verifier-ownership.md +111 -0
  24. data/docs/architecture/multi-agent-handoff.md +11 -4
  25. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  26. data/docs/architecture/persistence.md +45 -4
  27. data/docs/architecture/r8-unit1.md +104 -0
  28. data/docs/architecture/r8-unit10.md +72 -0
  29. data/docs/architecture/r8-unit11.md +71 -0
  30. data/docs/architecture/r8-unit12.md +67 -0
  31. data/docs/architecture/r8-unit13.md +61 -0
  32. data/docs/architecture/r8-unit14.md +72 -0
  33. data/docs/architecture/r8-unit15.md +65 -0
  34. data/docs/architecture/r8-unit16.md +54 -0
  35. data/docs/architecture/r8-unit17.md +73 -0
  36. data/docs/architecture/r8-unit18.md +92 -0
  37. data/docs/architecture/r8-unit19.md +61 -0
  38. data/docs/architecture/r8-unit2.md +55 -0
  39. data/docs/architecture/r8-unit20.md +51 -0
  40. data/docs/architecture/r8-unit21.md +79 -0
  41. data/docs/architecture/r8-unit22.md +74 -0
  42. data/docs/architecture/r8-unit23.md +81 -0
  43. data/docs/architecture/r8-unit3.md +130 -0
  44. data/docs/architecture/r8-unit4.md +79 -0
  45. data/docs/architecture/r8-unit5.md +115 -0
  46. data/docs/architecture/r8-unit6.md +71 -0
  47. data/docs/architecture/r8-unit7.md +70 -0
  48. data/docs/architecture/r8-unit8.md +78 -0
  49. data/docs/architecture/r8-unit9.md +73 -0
  50. data/docs/architecture/refactoring-closure.md +343 -0
  51. data/docs/architecture/remaining-refactoring-plan.md +390 -0
  52. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  53. data/docs/architecture/storage-execution-boundaries.md +54 -0
  54. data/docs/architecture/tool-schema-recording-gap.md +56 -0
  55. data/docs/architecture/tracing.md +2 -2
  56. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  57. data/docs/architecture.md +222 -0
  58. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  59. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  60. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  61. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  62. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  63. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  64. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +7 -0
  65. data/docs/decisions/031-durable-multi-agent-coordination.md +7 -0
  66. data/docs/decisions/032-storage-backend-composition.md +85 -0
  67. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  68. data/docs/decisions/034-handoff-runner-coordination-ownership.md +78 -0
  69. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  70. data/docs/decisions/036-context-contract-ownership.md +86 -0
  71. data/docs/decisions/037-common-definition-ownership.md +62 -0
  72. data/docs/decisions/038-responsibility-based-source-layout.md +145 -0
  73. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +82 -0
  74. data/docs/decisions/040-configuration-default-composition.md +105 -0
  75. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  76. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  77. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  78. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  79. data/docs/decisions/045-worker-input-restriction-ownership.md +104 -0
  80. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  81. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  82. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  83. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  84. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  85. data/docs/decisions/051-execution-outcome-worker-ownership.md +153 -0
  86. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  87. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  88. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  89. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  90. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  91. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  92. data/docs/decisions/058-neutral-storage-primitives.md +83 -0
  93. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  94. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  95. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  96. data/docs/decisions/062-content-store-service.md +52 -0
  97. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  98. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  99. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  100. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  101. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  102. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  103. data/docs/decisions/README.md +52 -6
  104. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  105. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  106. data/docs/execution-boundaries-preview.md +45 -0
  107. data/docs/features.md +16 -5
  108. data/docs/getting-started.md +11 -8
  109. data/docs/migrations/0.15.md +5 -0
  110. data/docs/migrations/0.22.md +2 -2
  111. data/docs/migrations/durable-semantic-coordination-v2.md +8 -0
  112. data/docs/migrations/handoff-runner-multi-agent.md +37 -0
  113. data/docs/migrations/neutral-storage-spi.md +43 -0
  114. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  115. data/docs/migrations/persistence-failure-contracts.md +67 -0
  116. data/docs/migrations/r8-unit10.md +36 -0
  117. data/docs/migrations/r8-unit11.md +33 -0
  118. data/docs/migrations/r8-unit12.md +31 -0
  119. data/docs/migrations/r8-unit13.md +36 -0
  120. data/docs/migrations/r8-unit14.md +31 -0
  121. data/docs/migrations/r8-unit15.md +31 -0
  122. data/docs/migrations/r8-unit16.md +26 -0
  123. data/docs/migrations/r8-unit17.md +28 -0
  124. data/docs/migrations/r8-unit18.md +29 -0
  125. data/docs/migrations/r8-unit19.md +28 -0
  126. data/docs/migrations/r8-unit20.md +24 -0
  127. data/docs/migrations/r8-unit21.md +28 -0
  128. data/docs/migrations/r8-unit22.md +29 -0
  129. data/docs/migrations/r8-unit23.md +20 -0
  130. data/docs/migrations/r8-unit4.md +67 -0
  131. data/docs/migrations/r8-unit5.md +75 -0
  132. data/docs/migrations/r8-unit6.md +109 -0
  133. data/docs/migrations/r8-unit7.md +31 -0
  134. data/docs/migrations/r8-unit8.md +33 -0
  135. data/docs/migrations/r8-unit9.md +28 -0
  136. data/docs/migrations/shared-state-multi-agent.md +44 -0
  137. data/docs/migrations/storage-async-client.md +103 -0
  138. data/docs/migrations/storage-backend-composition.md +117 -0
  139. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  140. data/docs/migrations/vector-async-clients.md +110 -0
  141. data/docs/persistence-backends.md +190 -215
  142. data/docs/runtime-and-concurrency.md +142 -32
  143. data/lib/phronomy/agent/admission.rb +78 -0
  144. data/lib/phronomy/{agent_already_exists_error.rb → agent/agent_already_exists_error.rb} +2 -0
  145. data/lib/phronomy/{agent_busy_error.rb → agent/agent_busy_error.rb} +2 -0
  146. data/lib/phronomy/{agent_purged_error.rb → agent/agent_purged_error.rb} +2 -0
  147. data/lib/phronomy/agent/api/agent.rb +17 -0
  148. data/lib/phronomy/{execution_rehydration_required_error.rb → agent/api/execution_rehydration_required_error.rb} +2 -0
  149. data/lib/phronomy/agent/async_event_api.rb +38 -33
  150. data/lib/phronomy/agent/base.rb +106 -294
  151. data/lib/phronomy/agent/before_llm_input.rb +64 -0
  152. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  153. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  154. data/lib/phronomy/agent/context_importer.rb +2 -2
  155. data/lib/phronomy/agent/{context_assembler.rb → context_preparation.rb} +157 -288
  156. data/lib/phronomy/agent/control_request.rb +21 -0
  157. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  158. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +32 -63
  159. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  160. data/lib/phronomy/agent/execution/dispatch_preparation.rb +195 -0
  161. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +43 -23
  162. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +8 -5
  163. data/lib/phronomy/agent/execution/execution_coordinator.rb +1744 -0
  164. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  165. data/lib/phronomy/agent/execution/execution_metadata.rb +54 -0
  166. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +288 -0
  167. data/lib/phronomy/agent/execution/execution_registry.rb +463 -0
  168. data/lib/phronomy/agent/execution/execution_session_runner.rb +113 -0
  169. data/lib/phronomy/agent/execution/initial_preparation.rb +323 -0
  170. data/lib/phronomy/agent/execution/invocation_actions.rb +368 -0
  171. data/lib/phronomy/agent/execution/invocation_transitions.rb +124 -0
  172. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  173. data/lib/phronomy/agent/execution_change.rb +161 -0
  174. data/lib/phronomy/agent/execution_environment.rb +36 -0
  175. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  176. data/lib/phronomy/agent/execution_observation.rb +35 -0
  177. data/lib/phronomy/agent/filterable.rb +182 -0
  178. data/lib/phronomy/agent/{handoff_context.rb → handoff/transfer_context.rb} +6 -6
  179. data/lib/phronomy/agent/{handoff_projection.rb → handoff/transfer_projection.rb} +14 -33
  180. data/lib/phronomy/agent/handoff_error.rb +7 -0
  181. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  182. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  183. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  184. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  185. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  186. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  187. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -15
  188. data/lib/phronomy/agent/persistence/agent_repository.rb +69 -0
  189. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  190. data/lib/phronomy/agent/persistence/codec.rb +338 -0
  191. data/lib/phronomy/agent/persistence/execution_repository.rb +124 -0
  192. data/lib/phronomy/agent/persistence/journal_repository.rb +63 -0
  193. data/lib/phronomy/agent/persistence/records.rb +33 -0
  194. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  195. data/lib/phronomy/agent/persistence/storage_schema.rb +29 -0
  196. data/lib/phronomy/agent/persistence/watermark.rb +29 -0
  197. data/lib/phronomy/agent/recovery/invocation_restorer.rb +124 -0
  198. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +28 -51
  199. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +50 -55
  200. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +38 -43
  201. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +26 -23
  202. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  203. data/lib/phronomy/agent/recovery/recovery_support.rb +224 -0
  204. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  205. data/lib/phronomy/agent/retention.rb +17 -0
  206. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  207. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  208. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  209. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  210. data/lib/phronomy/agent/{ruby_llm_materializer.rb → runtime_input.rb} +19 -39
  211. data/lib/phronomy/agent/saved_context_reader.rb +55 -0
  212. data/lib/phronomy/agent/settings.rb +47 -0
  213. data/lib/phronomy/agent/state_writer.rb +166 -0
  214. data/lib/phronomy/agent/store.rb +253 -0
  215. data/lib/phronomy/{stream_callback_error.rb → agent/stream_callback_error.rb} +2 -0
  216. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  217. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  218. data/lib/phronomy/agent/tool_execution/tool_binding.rb +30 -0
  219. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +114 -248
  220. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  221. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  222. data/lib/phronomy/common/configuration_error.rb +7 -0
  223. data/lib/phronomy/common/error.rb +5 -0
  224. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  225. data/lib/phronomy/common/values/serializable.rb +32 -0
  226. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  227. data/lib/phronomy/content_store/storage_schema.rb +12 -0
  228. data/lib/phronomy/content_store/stored_contents.rb +44 -0
  229. data/lib/phronomy/context/assembly.rb +202 -0
  230. data/lib/phronomy/{context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +5 -1
  231. data/lib/phronomy/context/candidate.rb +51 -0
  232. data/lib/phronomy/context/constraint.rb +47 -0
  233. data/lib/phronomy/{agent → context}/context_plan.rb +2 -2
  234. data/lib/phronomy/{agent → context}/context_plan_validator.rb +3 -3
  235. data/lib/phronomy/{agent → context}/context_policy.rb +1 -1
  236. data/lib/phronomy/{agent → context}/context_policy_input.rb +7 -7
  237. data/lib/phronomy/{agent → context}/context_policy_input_builder.rb +1 -1
  238. data/lib/phronomy/context/default_policy.rb +140 -0
  239. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  240. data/lib/phronomy/{agent → context}/llm_input_build_context.rb +2 -2
  241. data/lib/phronomy/{agent → context}/llm_input_manifest.rb +21 -21
  242. data/lib/phronomy/{agent → context}/llm_input_patch.rb +3 -3
  243. data/lib/phronomy/context/manifest_error.rb +10 -0
  244. data/lib/phronomy/context/prompt_template.rb +98 -0
  245. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  246. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  247. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  248. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  249. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  250. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  251. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  252. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  253. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
  254. data/lib/phronomy/embeddings/base.rb +43 -0
  255. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  256. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  257. data/lib/phronomy/engine/concurrency/offload_pool.rb +39 -20
  258. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  259. data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +1 -1
  260. data/lib/phronomy/engine/event_loop.rb +164 -630
  261. data/lib/phronomy/engine/execution_binding.rb +23 -0
  262. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  263. data/lib/phronomy/engine/fsm_protocol.rb +9 -0
  264. data/lib/phronomy/engine/fsm_session.rb +37 -30
  265. data/lib/phronomy/engine/metrics.rb +28 -0
  266. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  267. data/lib/phronomy/engine/runtime.rb +104 -95
  268. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  269. data/lib/phronomy/execution/async_operation.rb +41 -0
  270. data/lib/phronomy/execution/backpressure_error.rb +7 -0
  271. data/lib/phronomy/{blocking.rb → execution/blocking.rb} +2 -2
  272. data/lib/phronomy/execution/cancellation_error.rb +7 -0
  273. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +5 -1
  274. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  275. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  276. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  277. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  278. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  279. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  280. data/lib/phronomy/execution/concurrency/worker_input_restricted.rb +14 -0
  281. data/lib/phronomy/execution/event_loop_reentrancy_error.rb +8 -0
  282. data/lib/phronomy/{execution.rb → execution/execution.rb} +57 -30
  283. data/lib/phronomy/{execution_cancellation_error.rb → execution/execution_cancellation_error.rb} +2 -0
  284. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  285. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  286. data/lib/phronomy/{execution_timeout_error.rb → execution/execution_timeout_error.rb} +2 -0
  287. data/lib/phronomy/{invalid_async_entry_action_error.rb → execution/invalid_async_entry_action_error.rb} +2 -0
  288. data/lib/phronomy/{invalid_async_transition_action_error.rb → execution/invalid_async_transition_action_error.rb} +2 -0
  289. data/lib/phronomy/{invalid_async_workflow_action_error.rb → execution/invalid_async_workflow_action_error.rb} +2 -0
  290. data/lib/phronomy/{invocation_context.rb → execution/invocation_context.rb} +0 -15
  291. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  292. data/lib/phronomy/execution/pool_shutdown_error.rb +7 -0
  293. data/lib/phronomy/execution/recursion_limit_error.rb +7 -0
  294. data/lib/phronomy/execution/runnable.rb +24 -0
  295. data/lib/phronomy/execution/runtime_shutdown_error.rb +7 -0
  296. data/lib/phronomy/execution/runtime_shutdown_reentrancy_error.rb +7 -0
  297. data/lib/phronomy/execution/scheduler_reentrancy_error.rb +9 -0
  298. data/lib/phronomy/{engine → execution}/task_result.rb +7 -3
  299. data/lib/phronomy/execution/timeout_error.rb +7 -0
  300. data/lib/phronomy/execution/wait_policy.rb +24 -0
  301. data/lib/phronomy/filter/filter_block_error.rb +14 -0
  302. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  303. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  304. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  305. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  306. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  307. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  308. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  309. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  310. data/lib/phronomy/llm_adapter/authentication_error.rb +9 -0
  311. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  312. data/lib/phronomy/llm_adapter/base.rb +49 -68
  313. data/lib/phronomy/llm_adapter/context_length_error.rb +9 -0
  314. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  315. data/lib/phronomy/llm_adapter/message.rb +62 -0
  316. data/lib/phronomy/llm_adapter/rate_limit_error.rb +9 -0
  317. data/lib/phronomy/llm_adapter/request.rb +79 -0
  318. data/lib/phronomy/llm_adapter/response.rb +36 -0
  319. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  320. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  321. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  322. data/lib/phronomy/llm_adapter.rb +3 -16
  323. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  324. data/lib/phronomy/multi_agent/admission_registry.rb +15 -2
  325. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  326. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  327. data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
  328. data/lib/phronomy/{agent → multi_agent}/handoff_capability_factory.rb +21 -2
  329. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  330. data/lib/phronomy/{agent → multi_agent}/handoff_policy.rb +1 -1
  331. data/lib/phronomy/multi_agent/handoff_runner.rb +156 -0
  332. data/lib/phronomy/{agent → multi_agent}/handoff_state.rb +2 -2
  333. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  334. data/lib/phronomy/multi_agent/persistence/codec.rb +75 -0
  335. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  336. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  337. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  338. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +124 -0
  339. data/lib/phronomy/multi_agent/persistence/team_repository.rb +69 -0
  340. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  341. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  342. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  343. data/lib/phronomy/multi_agent/store.rb +156 -0
  344. data/lib/phronomy/multi_agent/team_coordinator.rb +87 -75
  345. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  346. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +5 -5
  347. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  348. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  349. data/lib/phronomy/persistence/content_repository.rb +26 -0
  350. data/lib/phronomy/persistence/errors.rb +16 -0
  351. data/lib/phronomy/persistence/migration/initial_format_migration.rb +21 -21
  352. data/lib/phronomy/persistence/persistence.rb +23 -0
  353. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  354. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  355. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  356. data/lib/phronomy/persistence/transaction.rb +86 -0
  357. data/lib/phronomy/persistence_composition/storage_schema.rb +29 -0
  358. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  359. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  360. data/lib/phronomy/runtime_composition/agent_defaults.rb +11 -0
  361. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  362. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  363. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  364. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  365. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  366. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  367. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  368. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  369. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  370. data/lib/phronomy/storage/async/async_client.rb +60 -0
  371. data/lib/phronomy/storage/backend.rb +107 -0
  372. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  373. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  374. data/lib/phronomy/storage/blobs.rb +31 -0
  375. data/lib/phronomy/storage/condition.rb +25 -0
  376. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  377. data/lib/phronomy/storage/conflict_error.rb +9 -0
  378. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  379. data/lib/phronomy/storage/entry.rb +33 -0
  380. data/lib/phronomy/storage/guard_ref.rb +13 -0
  381. data/lib/phronomy/storage/not_found_error.rb +9 -0
  382. data/lib/phronomy/storage/record_codec.rb +177 -0
  383. data/lib/phronomy/storage/records.rb +61 -0
  384. data/lib/phronomy/storage/resource.rb +126 -0
  385. data/lib/phronomy/storage/scope.rb +25 -0
  386. data/lib/phronomy/storage/serialization_error.rb +9 -0
  387. data/lib/phronomy/storage/streams.rb +44 -0
  388. data/lib/phronomy/storage/transaction_error.rb +10 -0
  389. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  390. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  391. data/lib/phronomy/storage/validation.rb +53 -0
  392. data/lib/phronomy/storage/view.rb +131 -0
  393. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  394. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  395. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  396. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  397. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +54 -48
  398. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +1 -1
  399. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +1 -1
  400. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  401. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +52 -46
  402. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  403. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +124 -0
  404. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  405. data/lib/phronomy/tool/authorization.rb +77 -0
  406. data/lib/phronomy/tool/base.rb +549 -9
  407. data/lib/phronomy/tool/call_request.rb +40 -0
  408. data/lib/phronomy/{agent/tool_definition_set.rb → tool/definition_set.rb} +16 -12
  409. data/lib/phronomy/tool/operation.rb +88 -0
  410. data/lib/phronomy/tool/schema.rb +214 -0
  411. data/lib/phronomy/tool/settings.rb +31 -0
  412. data/lib/phronomy/tool/tool_error.rb +7 -0
  413. data/lib/phronomy/{agent → tool}/tool_executor.rb +10 -7
  414. data/lib/phronomy/tools/agent.rb +22 -90
  415. data/lib/phronomy/tools/mcp.rb +21 -14
  416. data/lib/phronomy/tools/vector_search.rb +4 -4
  417. data/lib/phronomy/tracing/automatic.rb +11 -6
  418. data/lib/phronomy/tracing/base.rb +1 -1
  419. data/lib/phronomy/tracing/observation.rb +37 -0
  420. data/lib/phronomy/tracing/settings.rb +36 -0
  421. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  422. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  423. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  424. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  425. data/lib/phronomy/vector_store/base.rb +118 -49
  426. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  427. data/lib/phronomy/vector_store.rb +2 -17
  428. data/lib/phronomy/version.rb +1 -1
  429. data/lib/phronomy/workflow/completion.rb +11 -0
  430. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  431. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  432. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +5 -1
  433. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  434. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
  435. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +186 -0
  436. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +145 -115
  437. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  438. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  439. data/lib/phronomy/workflow/persistence/state_repository.rb +63 -0
  440. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  441. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
  442. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
  443. data/lib/phronomy/workflow/runtime_binding/workflow_terminal_policy.rb +40 -0
  444. data/lib/phronomy.rb +97 -98
  445. data/scripts/api_snapshot.rb +36 -16
  446. data/scripts/storage_spi_snapshot.rb +36 -0
  447. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  448. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  449. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  450. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  451. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  452. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  453. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  454. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  455. data/sig/phronomy/agent/admission.rbs +19 -0
  456. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  457. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  458. data/sig/phronomy/agent/store.rbs +73 -0
  459. data/sig/phronomy/agent.rbs +14 -139
  460. data/sig/phronomy/common.rbs +7 -0
  461. data/sig/phronomy/content_store/contracts.rbs +13 -0
  462. data/sig/phronomy/context.rbs +152 -0
  463. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  464. data/sig/phronomy/embeddings/base.rbs +17 -0
  465. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  466. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  467. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  468. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  469. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  470. data/sig/phronomy/engine/observability.rbs +13 -0
  471. data/sig/phronomy/execution/async_operation.rbs +7 -0
  472. data/sig/phronomy/execution/blocking.rbs +5 -0
  473. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  474. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  475. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  476. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  477. data/sig/phronomy/execution/errors.rbs +7 -0
  478. data/sig/phronomy/execution/execution.rbs +41 -0
  479. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  480. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  481. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  482. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  483. data/sig/phronomy/execution/task_result.rbs +29 -0
  484. data/sig/phronomy/extensions.rbs +0 -21
  485. data/sig/phronomy/handoff.rbs +12 -4
  486. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  487. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  488. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  489. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  490. data/sig/phronomy/llm_context_window.rbs +10 -0
  491. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  492. data/sig/phronomy/multi_agent/store.rbs +54 -0
  493. data/sig/phronomy/multi_agent.rbs +24 -3
  494. data/sig/phronomy/persistence/errors.rbs +18 -0
  495. data/sig/phronomy/persistence/migration.rbs +13 -0
  496. data/sig/phronomy/persistence/transaction.rbs +19 -0
  497. data/sig/phronomy/persistence.rbs +3 -185
  498. data/sig/phronomy/persistence_composition.rbs +19 -0
  499. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  500. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  501. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  502. data/sig/phronomy/storage/contracts.rbs +160 -0
  503. data/sig/phronomy/tool.rbs +58 -5
  504. data/sig/phronomy/tracing/observation.rbs +7 -0
  505. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  506. data/sig/phronomy/vector_store/base.rbs +23 -0
  507. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
  508. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  509. data/sig/phronomy/workflow/store.rbs +8 -0
  510. data/sig/phronomy/workflow.rbs +1 -1
  511. data/sig/phronomy.rbs +0 -49
  512. metadata +462 -130
  513. data/lib/phronomy/agent/agent_invocation_session_builder.rb +0 -584
  514. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  515. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  516. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  517. data/lib/phronomy/agent/context/capability/base.rb +0 -507
  518. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  519. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  520. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  521. data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
  522. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  523. data/lib/phronomy/agent/handoff_request.rb +0 -45
  524. data/lib/phronomy/agent/handoff_runner.rb +0 -178
  525. data/lib/phronomy/agent/phase_machine_builder.rb +0 -152
  526. data/lib/phronomy/agent/provider_call_outcome.rb +0 -106
  527. data/lib/phronomy/agent/recovery_support.rb +0 -504
  528. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  529. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  530. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  531. data/lib/phronomy/agent/tool_call_intercepted.rb +0 -29
  532. data/lib/phronomy/agent/tool_invocation_session_builder.rb +0 -276
  533. data/lib/phronomy/configuration.rb +0 -59
  534. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  535. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  536. data/lib/phronomy/generator_verifier.rb +0 -369
  537. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  538. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  539. data/lib/phronomy/metrics.rb +0 -26
  540. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  541. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  542. data/lib/phronomy/persistence/in_memory.rb +0 -690
  543. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  544. data/lib/phronomy/persistence.rb +0 -276
  545. data/lib/phronomy/recovery.rb +0 -186
  546. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  547. data/lib/phronomy/runnable.rb +0 -54
  548. data/lib/phronomy/token_usage.rb +0 -85
  549. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  550. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  551. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  552. data/lib/phronomy/workflow_recovery.rb +0 -123
  553. data/sig/phronomy/llm_adapter.rbs +0 -11
  554. data/sig/phronomy/runtime.rbs +0 -76
  555. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  556. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  557. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Phronomy
6
+ module MultiAgent
7
+ # Routing and Agent terminal state share the same outer transaction. This
8
+ # participant sees public semantic values only, never Agent records.
9
+ # @api private
10
+ class HandoffParticipant
11
+ def initialize(persistence:, main_agent_id:, expected_revision:)
12
+ @persistence, @main_agent_id, @expected_revision = persistence, main_agent_id, expected_revision
13
+ end
14
+
15
+ def binding
16
+ Phronomy::Agent::ExecutionExtensionState.new(binding_key: "phronomy.handoff", binding_version: 1)
17
+ end
18
+
19
+ def guard_change(change, scope)
20
+ @persistence.participate(scope) { |records| records.handoff_states.load_locked(@main_agent_id) }
21
+ ids = [@main_agent_id, change.agent_id, change.control_request&.target_agent_id].compact
22
+ @persistence.agent_store.guard_agents(scope, agent_ids: ids)
23
+ end
24
+
25
+ def change_evidence(change, scope)
26
+ state = @persistence.participate(scope) { |records| records.handoff_states.load(@main_agent_id) }
27
+ ids = [@main_agent_id, change.agent_id, change.control_request&.target_agent_id].compact.uniq.sort
28
+ [state&.to_h, ids.map { |id| @persistence.agent_store.retained_references(agent_id: id, scope: scope).map(&:to_h) }]
29
+ end
30
+
31
+ def commit(change)
32
+ @persistence.coordinator.atomic do |scope|
33
+ guard_change(change, scope)
34
+ change.prepare_in(scope)
35
+ @persistence.participate(scope) do |records|
36
+ routing = records.handoff_states.load(@main_agent_id)
37
+ unless routing && routing.active_agent_id == change.agent_id
38
+ raise Phronomy::Persistence::StateConflictError, "Handoff no longer owns this execution"
39
+ end
40
+ receipt = nil
41
+ if change.kind == :terminal && change.control_request
42
+ if Array(routing.metadata["cancelled_execution_ids"]).include?(change.execution_id) || change.cancel_requested
43
+ raise Phronomy::CancellationError, "Handoff Source turn was cancelled"
44
+ end
45
+ unless routing.handoff_revision == @expected_revision
46
+ raise Phronomy::Persistence::StateConflictError, "Handoff routing changed before Source transfer"
47
+ end
48
+ request = change.control_request
49
+ context = change.transfer_context(scope)
50
+ ref = records.contents.put_json(context.to_h)
51
+ target_id = "handoff-target-#{Digest::SHA256.hexdigest([change.execution_id, request.target_agent_id].join("\0"))}"
52
+ definition = @persistence.agent_store.definition(agent_id: request.target_agent_id, scope: scope)
53
+ retain(scope, change.agent_id, change.execution_id)
54
+ retain(scope, request.target_agent_id, target_id)
55
+ receipt = {"target_agent_id" => request.target_agent_id, "target_execution_id" => target_id,
56
+ "context_ref" => ref, "owner_key" => "handoff:#{@main_agent_id}"}.freeze
57
+ ids = (Array(routing.metadata["retained_agent_ids"]) + [change.agent_id, request.target_agent_id]).uniq.sort
58
+ transfer = routing.with(active_agent_id: request.target_agent_id, active_handoff_context_ref: ref,
59
+ phase: "target_pending", pending_source_execution_id: change.execution_id, pending_target_execution_id: target_id,
60
+ metadata: routing.metadata.merge("target_definition" => definition.transform_keys(&:to_s),
61
+ "source_agent_id" => change.agent_id, "retained_agent_ids" => ids))
62
+ records.handoff_states.save(@main_agent_id, expected_revision: routing.handoff_revision, state: transfer)
63
+ elsif change.finishing? && routing.phase != "stable" && routing.pending_target_execution_id == change.execution_id
64
+ records.handoff_states.save(@main_agent_id, expected_revision: routing.handoff_revision, state: routing.with(phase: "stable"))
65
+ end
66
+ change.commit_in(scope, transfer_receipt: receipt)
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def retain(scope, id, execution_id)
74
+ @persistence.agent_store.retain(Phronomy::Agent::Retention.new(agent_id: id, execution_id: execution_id,
75
+ owner_key: "handoff:#{@main_agent_id}"), scope: scope)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- module Agent
4
+ module MultiAgent
5
5
  class HandoffPolicy
6
6
  CATEGORIES = %i[current_request history knowledge tool_exchanges].freeze
7
7
  MODES = %i[required forbidden selectable].freeze
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Phronomy
6
+ module MultiAgent
7
+ # Durable turn coordination using Agent's public exact-execution operations.
8
+ # @api public
9
+ class HandoffRunner
10
+ MAX_HANDOFFS = 20
11
+ attr_reader :main_agent, :handoffs
12
+
13
+ def initialize(main_agent:, persistence:, handoffs: [])
14
+ @main_agent, @persistence, @handoffs = main_agent, persistence, Array(handoffs).freeze
15
+ raise ArgumentError, "handoffs must contain MultiAgent::Handoff" unless @handoffs.all? { |edge| edge.is_a?(Handoff) }
16
+ @agents = ([main_agent] + @handoffs.flat_map { |edge| [edge.source_agent, edge.target_agent] }).uniq.to_h { |agent| [agent.agent_id, agent] }.freeze
17
+ unless @agents.values.all? { |agent| agent.persistence.equal?(@persistence.agent_store) }
18
+ raise Phronomy::ConfigurationError, "Handoff graph and coordination store must share the same Agent store"
19
+ end
20
+ if @handoffs.group_by { |edge| [edge.source_agent.agent_id, edge.target_agent.agent_id] }.any? { |_, edges| edges.size > 1 }
21
+ raise ArgumentError, "Duplicate Source/Target Handoff edges"
22
+ end
23
+ @bindings = @handoffs.group_by { |edge| edge.source_agent.agent_id }.transform_values { |edges| edges.map { |edge| HandoffCapabilityFactory.build(edge) }.freeze }.freeze
24
+ @environment = ExecutionEnvironment.current
25
+ @admissions = @environment.admissions
26
+ end
27
+
28
+ def invoke(input, config: {})
29
+ raise Phronomy::EventLoopReentrancyError, "HandoffRunner#invoke cannot block EventLoop" if Phronomy::WaitPolicy.blocking_forbidden?
30
+ raise Phronomy::RuntimeShutdownError, "HandoffRunner belongs to a previous Runtime" unless @environment.current?
31
+ trace = Phronomy::Tracing::Automatic.start("multi_agent.turn", input: input, main_agent_id: main_agent.agent_id)
32
+ error = result = nil
33
+ config = config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new)
34
+ @admissions.admit!(main_agent)
35
+ admitted = true
36
+ state = load_state
37
+ count = 0
38
+ loop do
39
+ active = @agents.fetch(state.active_agent_id) { raise Phronomy::ExecutionRehydrationRequiredError, "Handoff graph lacks active Agent #{state.active_agent_id}" }
40
+ context = state.active_handoff_context_ref && Phronomy::Agent::TransferContext.from_h(@persistence.contents.fetch_json(state.active_handoff_context_ref))
41
+ owner = {"kind" => "handoff", "main_agent_id" => main_agent.agent_id}.freeze
42
+ wiring = config.merge(phronomy_control_bindings: @bindings.fetch(active.agent_id, []), phronomy_transfer_context: context,
43
+ phronomy_execution_participant: HandoffParticipant.new(persistence: @persistence, main_agent_id: main_agent.agent_id, expected_revision: state.handoff_revision),
44
+ phronomy_reservation: owner, phronomy_admission: ReservedChildAdmission.new(persistence: @persistence, owner: owner))
45
+ result = if state.phase == "stable"
46
+ unfinished = @persistence.agent_store.active_executions(agent_id: active.agent_id)
47
+ if unfinished.empty?
48
+ active.invoke(input, config: wiring)
49
+ else
50
+ exact = unfinished.fetch(0)
51
+ unless unfinished.size == 1 && exact.reservation&.correlation == owner
52
+ raise Phronomy::Persistence::StateConflictError, "Active execution belongs to another turn"
53
+ end
54
+ wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(exact.execution_id)
55
+ active.resume_async(exact.execution_id, config: wiring).wait_result
56
+ end
57
+ else
58
+ source = @persistence.agent_store.execution_identity(state.pending_source_execution_id)
59
+ unless @handoffs.any? { |edge| edge.source_agent.agent_id == source.agent_id && edge.target_agent.agent_id == state.active_agent_id }
60
+ raise Phronomy::ExecutionRehydrationRequiredError, "Handoff graph lacks committed Source/Target edge"
61
+ end
62
+ unless active.class.agent_definition == state.metadata.fetch("target_definition").transform_keys(&:to_sym)
63
+ raise Phronomy::ConfigurationError, "Handoff Target definition mismatch"
64
+ end
65
+ wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(state.pending_target_execution_id)
66
+ reservation = Phronomy::Agent::ReservedExecution.new(agent_id: active.agent_id, execution_id: state.pending_target_execution_id, correlation: owner)
67
+ active.start_reserved_async(reservation: reservation, input: context.responsibility, config: wiring).wait_result
68
+ end
69
+ observed = @persistence.agent_store.observe_execution(agent_id: active.agent_id, execution_id: result.fetch(:execution_id))
70
+ if observed.status == :handed_off
71
+ count += 1
72
+ raise Phronomy::HandoffError, "Exceeded maximum Handoffs in one turn" if count > MAX_HANDOFFS
73
+ state = @persistence.handoff_states.load(main_agent.agent_id)
74
+ next
75
+ end
76
+ raise Phronomy::ExecutionRehydrationRequiredError, "Handoff requires approval or recovery" if observed.active?
77
+ observed.value!
78
+ return result.reject { |key, _| key.to_s.start_with?("_phronomy_") || key == :handoff_request }.merge(agent: active)
79
+ end
80
+ rescue Phronomy::CancellationError => caught
81
+ error = caught
82
+ cancel(state.pending_source_execution_id) if state && state.phase != "stable"
83
+ raise
84
+ rescue => caught
85
+ error = caught
86
+ raise
87
+ ensure
88
+ @admissions.release!(main_agent) if admitted
89
+ Phronomy::Tracing::Automatic.finish(trace, output: result && result[:output], error: error) if trace
90
+ end
91
+
92
+ def cancel(execution_id)
93
+ raise Phronomy::EventLoopReentrancyError, "HandoffRunner#cancel cannot block EventLoop" if Phronomy::WaitPolicy.blocking_forbidden?
94
+ leaf = intended = nil
95
+ begin
96
+ @persistence.transaction do |records, scope|
97
+ routing = records.handoff_states.load_locked(main_agent.agent_id)
98
+ # All leaf reads occur after the same guard used by target admission.
99
+ leaf = @persistence.handoff_leaf(execution_id, main_agent_id: main_agent.agent_id, scope: scope)
100
+ next if leaf.terminal?
101
+ unless routing && (leaf.absent? ? routing.pending_target_execution_id == leaf.execution_id : routing.active_agent_id == leaf.agent_id)
102
+ raise Phronomy::Persistence::StateConflictError, "Handoff no longer owns the requested turn"
103
+ end
104
+ @persistence.agent_store.guard_agents(scope, agent_ids: [main_agent.agent_id, leaf.agent_id])
105
+ @persistence.agent_store.request_cancellation(agent_id: leaf.agent_id, execution_id: leaf.execution_id, scope: scope) unless leaf.absent?
106
+ ids = (Array(routing.metadata["cancelled_execution_ids"]) + [leaf.execution_id]).uniq
107
+ intended = routing.with(phase: leaf.absent? ? "stable" : routing.phase, metadata: routing.metadata.merge("cancelled_execution_ids" => ids))
108
+ records.handoff_states.save(main_agent.agent_id, expected_revision: routing.handoff_revision, state: intended)
109
+ end
110
+ rescue => failure
111
+ raise unless intended
112
+ proof = Phronomy::Persistence::SaveOutcome.compare(before: nil, after: [intended.to_h, true], original_error: failure) do
113
+ @persistence.transaction do |records, scope|
114
+ current = records.handoff_states.load_locked(main_agent.agent_id)
115
+ cancelled = leaf.absent? || @persistence.agent_store.cancellation_requested?(agent_id: leaf.agent_id, execution_id: leaf.execution_id, scope: scope)
116
+ [current&.to_h, cancelled]
117
+ end
118
+ end
119
+ raise failure unless proof.disposition == :committed
120
+ end
121
+ return leaf.to_result if leaf.terminal?
122
+ @agents[leaf.agent_id]&.cancel_async(leaf.execution_id)&.wait_result if leaf.active?
123
+ {execution_id: leaf.execution_id, cancellation_requested: true}.freeze
124
+ end
125
+
126
+ def result(execution_id)
127
+ @persistence.handoff_result(execution_id, main_agent_id: main_agent.agent_id)
128
+ end
129
+
130
+ # Explicit history removal releases references before Agent purge is allowed.
131
+ def forget_history!
132
+ @persistence.forget_handoff(main_agent.agent_id)
133
+ end
134
+
135
+ private
136
+
137
+ def load_state
138
+ state = @persistence.handoff_states.load(main_agent.agent_id)
139
+ return state if state
140
+ now = Time.now.utc.iso8601(6)
141
+ initial = HandoffState.new(main_agent_id: main_agent.agent_id, handoff_revision: 1,
142
+ active_agent_id: main_agent.agent_id, active_handoff_context_ref: nil, phase: "stable",
143
+ pending_source_execution_id: nil, pending_target_execution_id: nil,
144
+ created_at: now, updated_at: now, metadata: {"retained_agent_ids" => [main_agent.agent_id]})
145
+ @persistence.transaction do |records, scope|
146
+ @persistence.agent_store.retain(Phronomy::Agent::Retention.new(agent_id: main_agent.agent_id, owner_key: "handoff:#{main_agent.agent_id}"), scope: scope)
147
+ records.handoff_states.save(main_agent.agent_id, expected_revision: nil, state: initial)
148
+ end
149
+ rescue => error
150
+ confirmed = @persistence.handoff_states.load(main_agent.agent_id)
151
+ raise error unless confirmed
152
+ confirmed
153
+ end
154
+ end
155
+ end
156
+ end
@@ -3,7 +3,7 @@
3
3
  require "time"
4
4
 
5
5
  module Phronomy
6
- module Agent
6
+ module MultiAgent
7
7
  # Immutable current-format semantic record; contains no Runtime handles.
8
8
  # @api private
9
9
  class HandoffState
@@ -14,7 +14,7 @@ module Phronomy
14
14
  source = values.transform_keys(&:to_s)
15
15
  raise ArgumentError, "HandoffState schema mismatch" unless source.keys.sort == ATTRIBUTES.sort
16
16
  canonical = Phronomy::CanonicalJSON.load(Phronomy::CanonicalJSON.dump(source))
17
- ATTRIBUTES.each { |key| instance_variable_set("@#{key}", Phronomy::Agent::Immutable.copy(canonical.fetch(key))) }
17
+ ATTRIBUTES.each { |key| instance_variable_set("@#{key}", Phronomy::Values::Immutable.copy(canonical.fetch(key))) }
18
18
  raise ArgumentError, "missing main_agent_id" if main_agent_id.to_s.empty?
19
19
  raise ArgumentError, "invalid handoff_revision" unless handoff_revision.is_a?(Integer) && handoff_revision >= 0
20
20
  raise ArgumentError, "invalid metadata" unless metadata.is_a?(Hash)
@@ -9,32 +9,42 @@ module Phronomy
9
9
  ParallelChild = Data.define(:index, :agent, :input, :config)
10
10
  private_constant :ParallelChild
11
11
 
12
- # Own one live cancellation token shared with this invocation's static
13
- # children, including when Recovery reconstructs the invocation.
14
- # @api private
15
- def __invocation_config(config)
16
- config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new)
12
+ attr_reader :coordination_store
13
+
14
+ def self.create(coordination_store: nil, **options, &block)
15
+ new(coordination_store: coordination_store, **options, &block)
17
16
  end
18
17
 
19
- # Resumes retained static child coordination with current class wiring.
20
- # @api public
21
- def resume(execution_id, config: {})
22
- if Phronomy::Runtime.in_event_loop_context?
23
- raise Phronomy::EventLoopReentrancyError, "Orchestrator#resume cannot block EventLoop"
18
+ def self.load(agent_id, persistence:, coordination_store:, **options, &block)
19
+ participant = DurableSubagentCoordinator.new(agent_class: self, persistence: coordination_store)
20
+ super(agent_id, persistence: persistence, execution_wiring: {phronomy_execution_participant: participant}, **options, &block)
21
+ end
22
+
23
+ def initialize(coordination_store: nil, execution_wiring: {}, **options, &block)
24
+ @coordination_store = coordination_store || execution_wiring[:phronomy_execution_participant]&.persistence || Phronomy::MultiAgent::Store.configured
25
+ super(execution_wiring: execution_wiring, **options, &block)
26
+ if @coordination_store && !@coordination_store.agent_store.equal?(persistence)
27
+ raise Phronomy::ConfigurationError, "Orchestrator stores must share the same Agent store"
24
28
  end
25
- execution = persistence.executions.load(execution_id)
26
- raise Phronomy::Persistence::ConflictError, "Parent execution owner mismatch" unless execution.agent_id == agent_id
27
- input = persistence.contents.fetch_text(execution.metadata.fetch("current_input_ref"))
28
- result = Phronomy::Agent::ExactExecution.start(agent: self, execution_id: execution_id, input: input, config: config).wait_result
29
- raise Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]) if result[:error]
30
- result
29
+ @participant = @coordination_store && DurableSubagentCoordinator.new(agent_class: self.class, persistence: @coordination_store)
30
+ end
31
+
32
+ # Current composition, never serialized in an execution record.
33
+ def __invocation_config(config)
34
+ if !self.class.registered_subagents.empty? && !@participant
35
+ raise Phronomy::ConfigurationError, "Durable subagents require coordination_store:"
36
+ end
37
+ config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new,
38
+ phronomy_execution_participant: @participant).compact
31
39
  end
32
40
 
33
41
  def self.subagent(name, agent_class, on_error: :raise, inherit_knowledge: true)
34
42
  # A subagent Tool is logically asynchronous: ToolInvocation starts the
35
43
  # child Agent and resumes when its completion TaskResult settles. It must not
36
44
  # occupy an OffloadPool worker while waiting for the child.
37
- tool_class = Class.new(Phronomy::Tools::Agent) do
45
+ tool_class = Class.new(Phronomy::Tool::Base) do
46
+ execution_mode :cooperative
47
+ param :input, type: :string, desc: "The input to forward to the wrapped Agent"
38
48
  def self.__framework_owned_operation? = true
39
49
  tool_name "dispatch_to_#{name}"
40
50
  description "Dispatch work to the #{name} subagent (#{agent_class.name})"
@@ -53,14 +63,17 @@ module Phronomy
53
63
  # cancelled: an already admitted child must be settled, not forgotten.
54
64
  define_method(:call_async) do |args, cancellation_token: nil, config: {}|
55
65
  if @_orchestrator_context&.fetch(:parent, nil) && config[:phronomy_tool_invocation_id]
56
- validated, schema_error = send(:validate_and_coerce, args)
66
+ validated, schema_error = validate_arguments(args)
57
67
  raise Phronomy::ToolError, schema_error if schema_error
58
68
  execute_async(**validated, cancellation_token: cancellation_token, config: config)
59
69
  else
60
- super(args, cancellation_token: cancellation_token, config: config)
70
+ call_async_operation(args, cancellation_token: cancellation_token,
71
+ operation_name: "agent-tool-#{self.name}") do |validated_args|
72
+ execute_async(**validated_args, cancellation_token: cancellation_token, config: config || {})
73
+ end
61
74
  end
62
75
  rescue => error
63
- Phronomy::TaskResult.deferred(name: "subagent-dispatch-failed").tap { |task| task.fail(error) }
76
+ Phronomy::TaskResult.failed(error, name: "subagent-dispatch-failed")
64
77
  end
65
78
 
66
79
  define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
@@ -86,29 +99,25 @@ module Phronomy
86
99
  agent = agent_class.new
87
100
  if inherit_knowledge
88
101
  Array(ctx[:knowledge]).each do |entry|
89
- agent.add_knowledge(entry.fetch(:content), metadata: entry.fetch(:metadata, {}))
102
+ agent.add_knowledge(entry.content, metadata: entry.metadata)
90
103
  end
91
104
  end
92
- source = agent.invoke_async(input, config: task_config)
93
- result_task = Phronomy::TaskResult.deferred(
94
- name: "subagent-tool-#{name}"
95
- )
96
- source.on_complete do |result, error|
97
- if error
98
- (on_error == :raise) ? result_task.fail(error) : result_task.complete(nil)
99
- else
100
- result_task.complete(result[:output])
101
- end
105
+ Phronomy::AsyncOperation.call(name: "subagent-tool-#{name}",
106
+ on_error: ->(error) { raise error if on_error == :raise },
107
+ transform: ->(result) { result[:output] }) do
108
+ agent.invoke_async(input, config: task_config)
102
109
  end
103
- result_task
104
110
  rescue => error
105
- result_task ||= Phronomy::TaskResult.deferred(
106
- name: "subagent-tool-#{name}"
107
- )
108
- (on_error == :raise) ? result_task.fail(error) : result_task.complete(nil)
109
- result_task
111
+ Phronomy::AsyncOperation.capture(name: "subagent-tool-#{name}",
112
+ on_error: ->(failure) { raise failure if on_error == :raise }) { raise error }
110
113
  end
111
- private :execute_async
114
+ # Recovery requirements retain their Agent meaning across delegation.
115
+ def async_error_value(error)
116
+ raise error if error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
117
+
118
+ super
119
+ end
120
+ private :execute_async, :async_error_value
112
121
  end
113
122
 
114
123
  @_subagent_tool_classes = (@_subagent_tool_classes || []) + [tool_class]
@@ -138,7 +147,7 @@ module Phronomy
138
147
  invocation_context: nil,
139
148
  inherit_knowledge: true
140
149
  )
141
- if Phronomy::Runtime.in_event_loop_context?
150
+ if Phronomy::WaitPolicy.blocking_forbidden?
142
151
  raise Phronomy::EventLoopReentrancyError,
143
152
  "dispatch_parallel cannot block the EventLoop; use dispatch_parallel_async"
144
153
  end
@@ -169,25 +178,17 @@ module Phronomy
169
178
  invocation_context: nil,
170
179
  inherit_knowledge: inherit_knowledge
171
180
  )
172
- Phronomy::Execution.send(:__run_async, children,
181
+ Phronomy::Execution.run_async(children,
173
182
  timeout: timeout, cancellation_token: cancellation_token,
174
- invocation_context: invocation_context, concurrency_limit: max_concurrency) do |child, execution|
175
- # Preserve a child-specific context as well as this dispatch's controls.
176
- # Agent admission adds that context's constraints to the supplied token.
177
- binding = Phronomy::Execution.__operation_binding(
178
- invocation_context: execution.invocation_context,
179
- cancellation_token: child.config[:cancellation_token]
180
- )
181
- child_context = child.config[:invocation_context] ||
182
- execution.invocation_context.merge(parent_task_id: invocation_context&.task_id)
183
- begin
184
- source = child.agent.invoke_async(child.input,
185
- config: child.config.merge(cancellation_token: binding.token),
183
+ invocation_context: invocation_context, max_concurrency: max_concurrency) do |child, execution|
184
+ execution.start_child(
185
+ invocation_context: child.config[:invocation_context],
186
+ cancellation_token: child.config[:cancellation_token],
187
+ parent_task_id: invocation_context&.task_id
188
+ ) do |child_context, token|
189
+ child.agent.invoke_async(child.input,
190
+ config: child.config.merge(cancellation_token: token),
186
191
  invocation_context: child_context)
187
- binding.track(source)
188
- rescue
189
- binding.close
190
- raise
191
192
  end
192
193
  end.map do |outcomes|
193
194
  failure = outcomes.find { |outcome| outcome.status != :completed }
@@ -203,7 +204,7 @@ module Phronomy
203
204
  config: nil,
204
205
  inherit_knowledge: true
205
206
  )
206
- if Phronomy::Runtime.in_event_loop_context?
207
+ if Phronomy::WaitPolicy.blocking_forbidden?
207
208
  raise Phronomy::EventLoopReentrancyError,
208
209
  "subagent cannot block the EventLoop; use the async Agent API"
209
210
  end
@@ -221,12 +222,6 @@ module Phronomy
221
222
  self.class.registered_subagents.keys.any? { |key| "dispatch_to_#{key}" == name.to_s }
222
223
  end
223
224
 
224
- # Reserve child identity/input/knowledge in the parent's existing Agent transaction.
225
- # @api private
226
- def __prepare_coordination_record(execution, tx:)
227
- DurableSubagentCoordinator.prepare(self, execution, tx: tx)
228
- end
229
-
230
225
  private
231
226
 
232
227
  def prepare_tool_class(tool_class, invocation: nil)
@@ -263,16 +258,7 @@ module Phronomy
263
258
  end
264
259
  end
265
260
 
266
- def active_knowledge_snapshot
267
- journal_projection.context_records.filter_map do |record|
268
- next unless record.kind == :knowledge
269
-
270
- {
271
- content: persistence.contents.fetch_text(record.content_ref),
272
- metadata: (record.metadata || {}).dup.freeze
273
- }.freeze
274
- end.freeze
275
- end
261
+ def active_knowledge_snapshot = knowledge_snapshot
276
262
 
277
263
  def build_subagent(agent_class, inherit_knowledge: true, knowledge_snapshot: nil)
278
264
  agent = agent_class.new
@@ -281,8 +267,8 @@ module Phronomy
281
267
  snapshot = knowledge_snapshot || active_knowledge_snapshot
282
268
  snapshot.each do |entry|
283
269
  agent.add_knowledge(
284
- entry.fetch(:content),
285
- metadata: entry.fetch(:metadata, {})
270
+ entry.content,
271
+ metadata: entry.metadata
286
272
  )
287
273
  end
288
274
  agent
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module MultiAgent
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
+ module_function
12
+
13
+ def encode_handoff_state(value)
14
+ payload = value.to_h
15
+ Phronomy::MultiAgent::HandoffState.from_h(payload)
16
+ build_record("phronomy.handoff_state", "0.2", payload)
17
+ rescue Phronomy::Storage::SerializationError
18
+ raise
19
+ rescue => error
20
+ serialization_error("cannot encode HandoffState", error)
21
+ end
22
+
23
+ def decode_handoff_state(record)
24
+ payload = current_payload!(record, record_type: "phronomy.handoff_state",
25
+ format_version: "0.2", keys: Phronomy::MultiAgent::HandoffState::ATTRIBUTES, label: "HandoffState")
26
+ Phronomy::MultiAgent::HandoffState.from_h(payload)
27
+ rescue Phronomy::Storage::SerializationError
28
+ raise
29
+ rescue => error
30
+ serialization_error("cannot decode HandoffState", error)
31
+ end
32
+
33
+ def encode_team_root(value)
34
+ payload = value.to_h
35
+ Phronomy::MultiAgent::TeamRoot.from_h(payload)
36
+ build_record("phronomy.team_root", "0.1", payload)
37
+ rescue Phronomy::Storage::SerializationError
38
+ raise
39
+ rescue => error
40
+ serialization_error("cannot encode TeamRoot", error)
41
+ end
42
+
43
+ def decode_team_root(record)
44
+ payload = current_payload!(record, record_type: "phronomy.team_root",
45
+ format_version: "0.1", keys: Phronomy::MultiAgent::TeamRoot::ATTRIBUTES, label: "TeamRoot")
46
+ Phronomy::MultiAgent::TeamRoot.from_h(payload)
47
+ rescue Phronomy::Storage::SerializationError
48
+ raise
49
+ rescue => error
50
+ serialization_error("cannot decode TeamRoot", error)
51
+ end
52
+
53
+ def encode_team_execution(value)
54
+ payload = value.to_h
55
+ Phronomy::MultiAgent::TeamExecution.from_h(payload)
56
+ build_record("phronomy.team_execution", "0.1", payload)
57
+ rescue Phronomy::Storage::SerializationError
58
+ raise
59
+ rescue => error
60
+ serialization_error("cannot encode TeamExecution", error)
61
+ end
62
+
63
+ def decode_team_execution(record)
64
+ payload = current_payload!(record, record_type: "phronomy.team_execution",
65
+ format_version: "0.1", keys: Phronomy::MultiAgent::TeamExecution::ATTRIBUTES, label: "TeamExecution")
66
+ Phronomy::MultiAgent::TeamExecution.from_h(payload)
67
+ rescue Phronomy::Storage::SerializationError
68
+ raise
69
+ rescue => error
70
+ serialization_error("cannot decode TeamExecution", error)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module MultiAgent
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
+ Phronomy::Persistence::StorageBoundary.call do
13
+ @view.atomic do |bound|
14
+ entry = bound.records(StorageSchema::HANDOFF_STATES).read(main_agent_id.to_s)
15
+ entry && decode(entry, main_agent_id)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Lock before inspecting child existence so cancellation cannot decide
21
+ # that a child is absent using a snapshot from before its admission.
22
+ def load_locked(main_agent_id)
23
+ Phronomy::Persistence::StorageBoundary.call do
24
+ @view.atomic do |bound|
25
+ bound.check!(guards: [Phronomy::Storage::GuardRef.new(
26
+ resource: StorageSchema::HANDOFF_STATES, key: main_agent_id.to_s
27
+ )], conditions: [])
28
+ self.class.new(bound).load(main_agent_id)
29
+ end
30
+ end
31
+ end
32
+
33
+ def save(main_agent_id, expected_revision:, state:)
34
+ Phronomy::Persistence::StorageBoundary.call do
35
+ expected = expected_revision.nil? ? nil : Integer(expected_revision)
36
+ revision = expected.nil? ? 1 : expected + 1
37
+ unless state.main_agent_id == main_agent_id.to_s && state.handoff_revision == revision
38
+ raise Phronomy::Storage::SerializationError, "Handoff identity/revision mismatch"
39
+ end
40
+ record = Codec.encode_handoff_state(state)
41
+ @view.atomic do |bound|
42
+ records = bound.records(StorageSchema::HANDOFF_STATES)
43
+ values = {key: main_agent_id.to_s, attributes: {active_agent_id: state.active_agent_id}, record: record}
44
+ entry = if expected.nil?
45
+ records.insert(**values, revision: revision)
46
+ else
47
+ records.replace(**values, expected_revision: expected, next_revision: revision)
48
+ end
49
+ decode(entry, main_agent_id, revision: revision)
50
+ end
51
+ rescue Phronomy::Storage::NotFoundError => error
52
+ raise Phronomy::Storage::ConflictError, error.message
53
+ end
54
+ end
55
+
56
+ def delete(main_agent_id, expected_revision:)
57
+ Phronomy::Persistence::StorageBoundary.call do
58
+ @view.records(StorageSchema::HANDOFF_STATES).delete(key: main_agent_id.to_s, expected_revision: Integer(expected_revision))
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def decode(entry, main_agent_id, revision: nil)
65
+ state = Codec.decode_handoff_state(entry.record)
66
+ valid = entry.key == main_agent_id.to_s && state.main_agent_id == entry.key &&
67
+ state.handoff_revision == entry.revision && (!revision || revision == entry.revision) &&
68
+ entry.attributes == {active_agent_id: state.active_agent_id}
69
+ unless valid
70
+ raise Phronomy::Storage::SerializationError, "backend returned another Handoff identity/revision"
71
+ end
72
+ state
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module MultiAgent
5
+ module Persistence
6
+ # Storage implementation of Team's own record operations.
7
+ # @api private
8
+ class Records
9
+ attr_reader :contents, :teams, :team_executions, :handoff_states
10
+
11
+ def initialize(view)
12
+ @view = view
13
+ @contents = Phronomy::Persistence::ContentRepository.new(Phronomy::ContentStore::StoredContents.new(view))
14
+ @teams = TeamRepository.new(view)
15
+ @team_executions = TeamExecutionRepository.new(view)
16
+ @handoff_states = HandoffStateRepository.new(view)
17
+ end
18
+
19
+ def guard_team!(team_id)
20
+ Phronomy::Persistence::StorageBoundary.call do
21
+ @view.check!(guards: [Phronomy::Storage::GuardRef.new(resource: Phronomy::MultiAgent::Persistence::StorageSchema::ROOTS, key: team_id)], conditions: [])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end