phronomy 0.27.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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +9 -9
  3. data/CHANGELOG.md +52 -0
  4. data/CONTRIBUTING.md +8 -8
  5. data/README.md +9 -3
  6. data/benchmark/bench_agent_invoke.rb +25 -50
  7. data/benchmark/bench_context_assembler.rb +14 -11
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/benchmark/bench_tool_schema.rb +1 -1
  10. data/docs/architecture/agent-chat-and-state-ownership.md +7 -1
  11. data/docs/architecture/agent-transition-ownership.md +13 -89
  12. data/docs/architecture/backend-groups.md +152 -0
  13. data/docs/architecture/before-llm-input.md +3 -3
  14. data/docs/architecture/context-management.md +1 -1
  15. data/docs/architecture/execution-abstraction-review.md +88 -0
  16. data/docs/architecture/execution-boundaries.md +28 -0
  17. data/docs/architecture/execution-metadata-and-values.md +6 -0
  18. data/docs/architecture/multi-agent-handoff.md +6 -5
  19. data/docs/architecture/persistence.md +27 -9
  20. data/docs/architecture/r8-unit1.md +104 -0
  21. data/docs/architecture/r8-unit10.md +72 -0
  22. data/docs/architecture/r8-unit11.md +71 -0
  23. data/docs/architecture/r8-unit12.md +67 -0
  24. data/docs/architecture/r8-unit13.md +61 -0
  25. data/docs/architecture/r8-unit14.md +72 -0
  26. data/docs/architecture/r8-unit15.md +65 -0
  27. data/docs/architecture/r8-unit16.md +54 -0
  28. data/docs/architecture/r8-unit17.md +73 -0
  29. data/docs/architecture/r8-unit18.md +92 -0
  30. data/docs/architecture/r8-unit19.md +61 -0
  31. data/docs/architecture/r8-unit2.md +55 -0
  32. data/docs/architecture/r8-unit20.md +51 -0
  33. data/docs/architecture/r8-unit21.md +79 -0
  34. data/docs/architecture/r8-unit22.md +74 -0
  35. data/docs/architecture/r8-unit23.md +81 -0
  36. data/docs/architecture/r8-unit3.md +130 -0
  37. data/docs/architecture/r8-unit4.md +79 -0
  38. data/docs/architecture/r8-unit5.md +115 -0
  39. data/docs/architecture/r8-unit6.md +71 -0
  40. data/docs/architecture/r8-unit7.md +70 -0
  41. data/docs/architecture/r8-unit8.md +78 -0
  42. data/docs/architecture/r8-unit9.md +73 -0
  43. data/docs/architecture/refactoring-closure.md +10 -1
  44. data/docs/architecture/remaining-refactoring-plan.md +16 -0
  45. data/docs/architecture/storage-execution-boundaries.md +54 -0
  46. data/docs/architecture/tool-schema-recording-gap.md +6 -0
  47. data/docs/architecture/tracing.md +1 -1
  48. data/docs/architecture/workflow-terminal-ownership-design.md +2 -2
  49. data/docs/architecture.md +24 -11
  50. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  51. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  52. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +2 -0
  53. data/docs/decisions/031-durable-multi-agent-coordination.md +2 -0
  54. data/docs/decisions/032-storage-backend-composition.md +5 -0
  55. data/docs/decisions/034-handoff-runner-coordination-ownership.md +2 -0
  56. data/docs/decisions/035-tool-executor-capability-ownership.md +1 -1
  57. data/docs/decisions/038-responsibility-based-source-layout.md +14 -0
  58. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +8 -0
  59. data/docs/decisions/040-configuration-default-composition.md +12 -1
  60. data/docs/decisions/045-worker-input-restriction-ownership.md +4 -0
  61. data/docs/decisions/051-execution-outcome-worker-ownership.md +2 -0
  62. data/docs/decisions/056-workflow-terminal-policy-ownership.md +1 -1
  63. data/docs/decisions/058-neutral-storage-primitives.md +5 -0
  64. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  65. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  66. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  67. data/docs/decisions/062-content-store-service.md +52 -0
  68. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  69. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  70. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  71. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  72. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  73. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  74. data/docs/decisions/README.md +18 -1
  75. data/docs/execution-boundaries-preview.md +45 -0
  76. data/docs/features.md +8 -3
  77. data/docs/getting-started.md +9 -6
  78. data/docs/migrations/0.22.md +2 -2
  79. data/docs/migrations/durable-semantic-coordination-v2.md +2 -0
  80. data/docs/migrations/handoff-runner-multi-agent.md +2 -0
  81. data/docs/migrations/persistence-failure-contracts.md +67 -0
  82. data/docs/migrations/r8-unit10.md +36 -0
  83. data/docs/migrations/r8-unit11.md +33 -0
  84. data/docs/migrations/r8-unit12.md +31 -0
  85. data/docs/migrations/r8-unit13.md +36 -0
  86. data/docs/migrations/r8-unit14.md +31 -0
  87. data/docs/migrations/r8-unit15.md +31 -0
  88. data/docs/migrations/r8-unit16.md +26 -0
  89. data/docs/migrations/r8-unit17.md +28 -0
  90. data/docs/migrations/r8-unit18.md +29 -0
  91. data/docs/migrations/r8-unit19.md +28 -0
  92. data/docs/migrations/r8-unit20.md +24 -0
  93. data/docs/migrations/r8-unit21.md +28 -0
  94. data/docs/migrations/r8-unit22.md +29 -0
  95. data/docs/migrations/r8-unit23.md +20 -0
  96. data/docs/migrations/r8-unit4.md +67 -0
  97. data/docs/migrations/r8-unit5.md +75 -0
  98. data/docs/migrations/r8-unit6.md +109 -0
  99. data/docs/migrations/r8-unit7.md +31 -0
  100. data/docs/migrations/r8-unit8.md +33 -0
  101. data/docs/migrations/r8-unit9.md +28 -0
  102. data/docs/migrations/storage-async-client.md +103 -0
  103. data/docs/migrations/vector-async-clients.md +110 -0
  104. data/docs/persistence-backends.md +38 -21
  105. data/docs/runtime-and-concurrency.md +26 -1
  106. data/lib/phronomy/agent/admission.rb +78 -0
  107. data/lib/phronomy/agent/{lifecycle_contract/agent_already_exists_error.rb → agent_already_exists_error.rb} +1 -1
  108. data/lib/phronomy/agent/{lifecycle_contract/agent_busy_error.rb → agent_busy_error.rb} +1 -1
  109. data/lib/phronomy/agent/{lifecycle_contract/agent_purged_error.rb → agent_purged_error.rb} +1 -1
  110. data/lib/phronomy/{recovery → agent/api}/execution_rehydration_required_error.rb +1 -1
  111. data/lib/phronomy/agent/async_event_api.rb +37 -32
  112. data/lib/phronomy/agent/base.rb +74 -72
  113. data/lib/phronomy/agent/before_llm_input.rb +64 -0
  114. data/lib/phronomy/agent/composition/run_once.rb +1 -1
  115. data/lib/phronomy/agent/{context_assembly/context_candidate_resolver.rb → context_candidate_resolver.rb} +2 -2
  116. data/lib/phronomy/agent/{context_assembly/context_assembler.rb → context_preparation.rb} +35 -221
  117. data/lib/phronomy/agent/control_request.rb +21 -0
  118. data/lib/phronomy/agent/execution/agent_execution.rb +1 -1
  119. data/lib/phronomy/agent/execution/agent_invocation.rb +27 -53
  120. data/lib/phronomy/agent/execution/approval_resume_commit.rb +1 -1
  121. data/lib/phronomy/agent/execution/dispatch_preparation.rb +17 -127
  122. data/lib/phronomy/agent/execution/exact_execution.rb +44 -23
  123. data/lib/phronomy/agent/execution/execution_cancellation.rb +8 -4
  124. data/lib/phronomy/agent/execution/execution_coordinator.rb +214 -395
  125. data/lib/phronomy/agent/execution/execution_metadata.rb +1 -0
  126. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +70 -126
  127. data/lib/phronomy/agent/execution/execution_registry.rb +6 -2
  128. data/lib/phronomy/agent/execution/execution_session_runner.rb +22 -27
  129. data/lib/phronomy/agent/execution/initial_preparation.rb +18 -116
  130. data/lib/phronomy/agent/execution/{agent_invocation_session_builder.rb → invocation_actions.rb} +54 -194
  131. data/lib/phronomy/agent/execution/invocation_transitions.rb +39 -1
  132. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +7 -7
  133. data/lib/phronomy/agent/execution_change.rb +161 -0
  134. data/lib/phronomy/agent/execution_environment.rb +36 -0
  135. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  136. data/lib/phronomy/agent/execution_observation.rb +35 -0
  137. data/lib/phronomy/agent/filterable.rb +182 -0
  138. data/lib/phronomy/agent/handoff/{handoff_context.rb → transfer_context.rb} +4 -4
  139. data/lib/phronomy/agent/handoff/{handoff_projection.rb → transfer_projection.rb} +14 -33
  140. data/lib/phronomy/{llm_contract/transport_error.rb → agent/handoff_error.rb} +1 -1
  141. data/lib/phronomy/agent/journal/journal_projection.rb +1 -1
  142. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  143. data/lib/phronomy/agent/lifecycle/agent_root.rb +1 -1
  144. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +18 -23
  145. data/lib/phronomy/agent/persistence/agent_repository.rb +29 -21
  146. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  147. data/lib/phronomy/agent/persistence/codec.rb +1 -21
  148. data/lib/phronomy/agent/persistence/execution_repository.rb +57 -41
  149. data/lib/phronomy/agent/persistence/journal_repository.rb +27 -18
  150. data/lib/phronomy/agent/persistence/records.rb +33 -0
  151. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  152. data/lib/phronomy/agent/persistence/storage_schema.rb +7 -2
  153. data/lib/phronomy/agent/persistence/watermark.rb +13 -11
  154. data/lib/phronomy/agent/recovery/invocation_restorer.rb +13 -21
  155. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +10 -9
  156. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +27 -27
  157. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +19 -21
  158. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +23 -16
  159. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  160. data/lib/phronomy/agent/recovery/recovery_support.rb +8 -11
  161. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  162. data/lib/phronomy/agent/retention.rb +17 -0
  163. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  164. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  165. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  166. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  167. data/lib/phronomy/agent/{context_assembly/ruby_llm_materializer.rb → runtime_input.rb} +18 -35
  168. data/lib/phronomy/agent/{context_assembly/saved_context_reader.rb → saved_context_reader.rb} +6 -4
  169. data/lib/phronomy/agent/settings.rb +47 -0
  170. data/lib/phronomy/agent/{context_assembly/state_writer.rb → state_writer.rb} +4 -3
  171. data/lib/phronomy/agent/store.rb +253 -0
  172. data/lib/phronomy/agent/{lifecycle_contract/stream_callback_error.rb → stream_callback_error.rb} +1 -1
  173. data/lib/phronomy/agent/tool_execution/tool_binding.rb +4 -64
  174. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +50 -219
  175. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  176. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  177. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  178. data/lib/phronomy/content_store/storage_schema.rb +1 -0
  179. data/lib/phronomy/content_store/stored_contents.rb +2 -1
  180. data/lib/phronomy/context/assembly.rb +202 -0
  181. data/lib/phronomy/{llm_contract/context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +3 -1
  182. data/lib/phronomy/context/candidate.rb +51 -0
  183. data/lib/phronomy/context/constraint.rb +47 -0
  184. data/lib/phronomy/{agent/context_contract → context}/context_plan.rb +1 -1
  185. data/lib/phronomy/{agent/context_contract → context}/context_plan_validator.rb +3 -3
  186. data/lib/phronomy/{agent/context_contract → context}/context_policy.rb +1 -1
  187. data/lib/phronomy/{agent/context_contract → context}/context_policy_input.rb +1 -1
  188. data/lib/phronomy/{agent/context_assembly → context}/context_policy_input_builder.rb +1 -1
  189. data/lib/phronomy/context/default_policy.rb +140 -0
  190. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  191. data/lib/phronomy/{agent/context_contract → context}/llm_input_build_context.rb +1 -1
  192. data/lib/phronomy/{agent/context_contract → context}/llm_input_manifest.rb +19 -19
  193. data/lib/phronomy/{agent/context_contract → context}/llm_input_patch.rb +1 -1
  194. data/lib/phronomy/context/manifest_error.rb +10 -0
  195. data/lib/phronomy/context/prompt_template.rb +98 -0
  196. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  197. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  198. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  199. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  200. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  201. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  202. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  203. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  204. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
  205. data/lib/phronomy/embeddings/base.rb +43 -0
  206. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  207. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  208. data/lib/phronomy/engine/concurrency/offload_pool.rb +36 -21
  209. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  210. data/lib/phronomy/engine/diagnostics.rb +1 -1
  211. data/lib/phronomy/engine/event_loop.rb +7 -7
  212. data/lib/phronomy/engine/execution_binding.rb +23 -0
  213. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  214. data/lib/phronomy/engine/fsm_protocol.rb +1 -6
  215. data/lib/phronomy/engine/fsm_session.rb +3 -3
  216. data/lib/phronomy/engine/metrics.rb +16 -14
  217. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  218. data/lib/phronomy/engine/runtime.rb +25 -12
  219. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  220. data/lib/phronomy/execution/async_operation.rb +41 -0
  221. data/lib/phronomy/{engine → execution}/blocking.rb +2 -2
  222. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +1 -1
  223. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  224. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  225. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  226. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  227. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  228. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  229. data/lib/phronomy/{engine → execution}/execution.rb +61 -27
  230. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  231. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  232. data/lib/phronomy/{engine → execution}/invocation_context.rb +0 -15
  233. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  234. data/lib/phronomy/execution/runnable.rb +24 -0
  235. data/lib/phronomy/{engine → execution}/task_result.rb +3 -3
  236. data/lib/phronomy/execution/wait_policy.rb +24 -0
  237. data/lib/phronomy/filter/{contract/filter_block_error.rb → filter_block_error.rb} +1 -1
  238. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  239. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  240. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +2 -2
  241. data/lib/phronomy/generation/generator_verifier.rb +7 -7
  242. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  243. data/lib/phronomy/{llm_contract → llm_adapter}/authentication_error.rb +3 -1
  244. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  245. data/lib/phronomy/llm_adapter/base.rb +49 -68
  246. data/lib/phronomy/{llm_contract → llm_adapter}/context_length_error.rb +3 -1
  247. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  248. data/lib/phronomy/llm_adapter/message.rb +62 -0
  249. data/lib/phronomy/{llm_contract → llm_adapter}/rate_limit_error.rb +3 -1
  250. data/lib/phronomy/llm_adapter/request.rb +79 -0
  251. data/lib/phronomy/llm_adapter/response.rb +36 -0
  252. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  253. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  254. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  255. data/lib/phronomy/llm_adapter.rb +3 -16
  256. data/lib/phronomy/multi_agent/admission_registry.rb +1 -8
  257. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  258. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  259. data/lib/phronomy/{agent/handoff → multi_agent}/handoff.rb +2 -2
  260. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_capability_factory.rb +21 -2
  261. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  262. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_policy.rb +1 -1
  263. data/lib/phronomy/multi_agent/handoff_runner.rb +77 -100
  264. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_state.rb +1 -1
  265. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  266. data/lib/phronomy/multi_agent/persistence/codec.rb +20 -0
  267. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  268. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  269. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  270. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +58 -42
  271. data/lib/phronomy/multi_agent/persistence/team_repository.rb +29 -21
  272. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  273. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  274. data/lib/phronomy/multi_agent/shared_state.rb +2 -2
  275. data/lib/phronomy/multi_agent/store.rb +156 -0
  276. data/lib/phronomy/multi_agent/team_coordinator.rb +83 -74
  277. data/lib/phronomy/multi_agent/team_ownership_registry.rb +2 -12
  278. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  279. data/lib/phronomy/persistence/content_repository.rb +26 -0
  280. data/lib/phronomy/persistence/errors.rb +16 -0
  281. data/lib/phronomy/persistence/migration/initial_format_migration.rb +2 -2
  282. data/lib/phronomy/persistence/persistence.rb +23 -0
  283. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  284. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  285. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  286. data/lib/phronomy/persistence/transaction.rb +86 -0
  287. data/lib/phronomy/persistence_composition/storage_schema.rb +9 -4
  288. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  289. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  290. data/lib/phronomy/runtime_composition/agent_defaults.rb +5 -1
  291. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  292. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  293. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  294. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  295. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  296. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  297. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  298. data/lib/phronomy/storage/async/async_client.rb +60 -0
  299. data/lib/phronomy/storage/backend.rb +8 -2
  300. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  301. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  302. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +2 -2
  303. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +6 -6
  304. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +55 -52
  305. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +3 -3
  306. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +7 -7
  307. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +11 -11
  308. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +57 -51
  309. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +1 -1
  310. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +23 -22
  311. data/lib/phronomy/tool/authorization.rb +77 -0
  312. data/lib/phronomy/tool/base.rb +549 -9
  313. data/lib/phronomy/tool/call_request.rb +40 -0
  314. data/lib/phronomy/{agent/tool_execution/tool_definition_set.rb → tool/definition_set.rb} +7 -8
  315. data/lib/phronomy/tool/operation.rb +88 -0
  316. data/lib/phronomy/tool/schema.rb +214 -0
  317. data/lib/phronomy/tool/settings.rb +31 -0
  318. data/lib/phronomy/tool/tool_error.rb +7 -0
  319. data/lib/phronomy/tool/tool_executor.rb +58 -0
  320. data/lib/phronomy/tools/agent.rb +22 -90
  321. data/lib/phronomy/tools/mcp.rb +21 -14
  322. data/lib/phronomy/tools/vector_search.rb +4 -4
  323. data/lib/phronomy/tracing/automatic.rb +11 -6
  324. data/lib/phronomy/tracing/base.rb +1 -1
  325. data/lib/phronomy/tracing/observation.rb +37 -0
  326. data/lib/phronomy/tracing/settings.rb +36 -0
  327. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  328. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  329. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  330. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  331. data/lib/phronomy/vector_store/base.rb +118 -49
  332. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  333. data/lib/phronomy/vector_store.rb +2 -17
  334. data/lib/phronomy/version.rb +1 -1
  335. data/lib/phronomy/workflow/completion.rb +11 -0
  336. data/lib/phronomy/workflow/execution/workflow.rb +7 -7
  337. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  338. data/lib/phronomy/workflow/execution/workflow_context.rb +2 -2
  339. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
  340. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +3 -5
  341. data/lib/phronomy/workflow/execution/workflow_runner.rb +58 -79
  342. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  343. data/lib/phronomy/workflow/persistence/state_repository.rb +28 -22
  344. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  345. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
  346. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
  347. data/lib/phronomy/workflow/{execution → runtime_binding}/workflow_terminal_policy.rb +4 -4
  348. data/lib/phronomy.rb +57 -16
  349. data/scripts/api_snapshot.rb +35 -15
  350. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  351. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  352. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  353. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  354. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  355. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  356. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  357. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  358. data/sig/phronomy/agent/admission.rbs +19 -0
  359. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  360. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  361. data/sig/phronomy/agent/store.rbs +73 -0
  362. data/sig/phronomy/agent.rbs +14 -138
  363. data/sig/phronomy/common.rbs +7 -0
  364. data/sig/phronomy/content_store/contracts.rbs +13 -0
  365. data/sig/phronomy/context.rbs +152 -0
  366. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  367. data/sig/phronomy/embeddings/base.rbs +17 -0
  368. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  369. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  370. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  371. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  372. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  373. data/sig/phronomy/engine/observability.rbs +13 -0
  374. data/sig/phronomy/execution/async_operation.rbs +7 -0
  375. data/sig/phronomy/execution/blocking.rbs +5 -0
  376. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  377. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  378. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  379. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  380. data/sig/phronomy/execution/errors.rbs +7 -0
  381. data/sig/phronomy/execution/execution.rbs +41 -0
  382. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  383. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  384. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  385. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  386. data/sig/phronomy/execution/task_result.rbs +29 -0
  387. data/sig/phronomy/extensions.rbs +0 -21
  388. data/sig/phronomy/handoff.rbs +10 -4
  389. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  390. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  391. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  392. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  393. data/sig/phronomy/llm_context_window.rbs +10 -0
  394. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  395. data/sig/phronomy/multi_agent/store.rbs +54 -0
  396. data/sig/phronomy/multi_agent.rbs +6 -3
  397. data/sig/phronomy/persistence/errors.rbs +18 -0
  398. data/sig/phronomy/persistence/migration.rbs +13 -0
  399. data/sig/phronomy/persistence/transaction.rbs +19 -0
  400. data/sig/phronomy/persistence.rbs +1 -99
  401. data/sig/phronomy/persistence_composition.rbs +19 -0
  402. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  403. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  404. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  405. data/sig/phronomy/{storage.rbs → storage/contracts.rbs} +2 -16
  406. data/sig/phronomy/tool.rbs +48 -4
  407. data/sig/phronomy/tracing/observation.rbs +7 -0
  408. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  409. data/sig/phronomy/vector_store/base.rbs +23 -0
  410. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
  411. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  412. data/sig/phronomy/workflow/store.rbs +8 -0
  413. data/sig/phronomy/workflow.rbs +1 -1
  414. data/sig/phronomy.rbs +0 -49
  415. metadata +318 -117
  416. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  417. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  418. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  419. data/lib/phronomy/agent/context/capability/base.rb +0 -548
  420. data/lib/phronomy/agent/context/capability/tool_executor.rb +0 -62
  421. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  422. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +0 -36
  423. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +0 -22
  424. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  425. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  426. data/lib/phronomy/agent/execution/phase_machine_builder.rb +0 -101
  427. data/lib/phronomy/agent/execution/provider_call_outcome.rb +0 -106
  428. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +0 -15
  429. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +0 -131
  430. data/lib/phronomy/agent/handoff/handoff_request.rb +0 -45
  431. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +0 -7
  432. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +0 -58
  433. data/lib/phronomy/agent/persistence/queries.rb +0 -61
  434. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  435. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  436. data/lib/phronomy/agent/tool_execution/tool_call_intercepted.rb +0 -29
  437. data/lib/phronomy/agent/tool_execution/tool_invocation_session_builder.rb +0 -276
  438. data/lib/phronomy/configuration/configuration.rb +0 -68
  439. data/lib/phronomy/configuration/global_configuration.rb +0 -26
  440. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  441. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  442. data/lib/phronomy/engine/execution_receiver.rb +0 -65
  443. data/lib/phronomy/engine/runnable.rb +0 -54
  444. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  445. data/lib/phronomy/llm_contract/token_usage.rb +0 -85
  446. data/lib/phronomy/multi_agent/persistence/queries.rb +0 -30
  447. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +0 -15
  448. data/lib/phronomy/output_parser/contract/parse_error.rb +0 -7
  449. data/lib/phronomy/persistence/api/persistence.rb +0 -140
  450. data/lib/phronomy/persistence_composition/repositories.rb +0 -77
  451. data/lib/phronomy/recovery/recovery.rb +0 -186
  452. data/lib/phronomy/tool/contract/tool_error.rb +0 -7
  453. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  454. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  455. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  456. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +0 -9
  457. data/sig/phronomy/execution_receiver.rbs +0 -34
  458. data/sig/phronomy/llm_adapter.rbs +0 -11
  459. data/sig/phronomy/runtime.rbs +0 -76
  460. /data/lib/phronomy/agent/{context_assembly/context_importer.rb → context_importer.rb} +0 -0
  461. /data/lib/phronomy/{engine → execution}/backpressure_error.rb +0 -0
  462. /data/lib/phronomy/{engine → execution}/cancellation_error.rb +0 -0
  463. /data/lib/phronomy/{engine → execution}/concurrency/worker_input_restricted.rb +0 -0
  464. /data/lib/phronomy/{engine → execution}/event_loop_reentrancy_error.rb +0 -0
  465. /data/lib/phronomy/{engine → execution}/execution_cancellation_error.rb +0 -0
  466. /data/lib/phronomy/{engine → execution}/execution_timeout_error.rb +0 -0
  467. /data/lib/phronomy/{engine → execution}/invalid_async_entry_action_error.rb +0 -0
  468. /data/lib/phronomy/{engine → execution}/invalid_async_transition_action_error.rb +0 -0
  469. /data/lib/phronomy/{engine → execution}/invalid_async_workflow_action_error.rb +0 -0
  470. /data/lib/phronomy/{engine → execution}/pool_shutdown_error.rb +0 -0
  471. /data/lib/phronomy/{engine → execution}/recursion_limit_error.rb +0 -0
  472. /data/lib/phronomy/{engine → execution}/runtime_shutdown_error.rb +0 -0
  473. /data/lib/phronomy/{engine → execution}/runtime_shutdown_reentrancy_error.rb +0 -0
  474. /data/lib/phronomy/{engine → execution}/scheduler_reentrancy_error.rb +0 -0
  475. /data/lib/phronomy/{engine → execution}/timeout_error.rb +0 -0
@@ -4,171 +4,148 @@ require "time"
4
4
 
5
5
  module Phronomy
6
6
  module MultiAgent
7
- # Coordinates durable handoffs across Agents using one Persistence instance.
7
+ # Durable turn coordination using Agent's public exact-execution operations.
8
8
  # @api public
9
9
  class HandoffRunner
10
10
  MAX_HANDOFFS = 20
11
-
12
- # @api public
13
11
  attr_reader :main_agent, :handoffs
14
12
 
15
- # @api public
16
- def initialize(main_agent:, handoffs: [])
17
- @main_agent, @handoffs = main_agent, Array(handoffs).freeze
18
- @persistence = main_agent.persistence
19
- raise ArgumentError, "handoffs must contain Agent::Handoff" unless @handoffs.all? { |h| h.is_a?(Phronomy::Agent::Handoff) }
20
- @agents = ([main_agent] + @handoffs.flat_map { |edge| [edge.source_agent, edge.target_agent] }).uniq.to_h { |a| [a.agent_id, a] }.freeze
21
- unless @agents.values.all? { |a| a.persistence.equal?(@persistence) }
22
- raise Phronomy::ConfigurationError, "Durable Handoff graph requires one Persistence instance"
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"
23
19
  end
24
- if @handoffs.group_by { |h| [h.source_agent.agent_id, h.target_agent.agent_id] }.any? { |_, edges| edges.size > 1 }
20
+ if @handoffs.group_by { |edge| [edge.source_agent.agent_id, edge.target_agent.agent_id] }.any? { |_, edges| edges.size > 1 }
25
21
  raise ArgumentError, "Duplicate Source/Target Handoff edges"
26
22
  end
27
- @bindings = @handoffs.group_by { |h| h.source_agent.agent_id }.transform_values { |edges| edges.map { |h| Phronomy::Agent::HandoffCapabilityFactory.build(h) }.freeze }.freeze
28
- @runtime = Phronomy::Runtime.instance
29
- @admissions = Phronomy::MultiAgent::AdmissionRegistry.for(@runtime)
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
30
26
  end
31
27
 
32
- # @api public
33
28
  def invoke(input, config: {})
34
- trace_handle = Phronomy::Tracing::Automatic.start("multi_agent.turn",
35
- input: input, main_agent_id: main_agent.agent_id,
36
- **main_agent.send(:_build_caller_meta, config))
37
- operation_error = result = nil
38
- if Phronomy::Runtime.in_event_loop_context?
39
- raise Phronomy::EventLoopReentrancyError, "HandoffRunner#invoke cannot block EventLoop"
40
- end
41
- unless Phronomy::Runtime.instance.equal?(@runtime)
42
- raise Phronomy::RuntimeShutdownError, "HandoffRunner belongs to a previous Runtime"
43
- end
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
44
33
  config = config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new)
45
34
  @admissions.admit!(main_agent)
46
35
  admitted = true
47
36
  state = load_state
48
37
  count = 0
49
38
  loop do
50
- active = @agents.fetch(state.active_agent_id) do
51
- raise Phronomy::ExecutionRehydrationRequiredError, "Handoff graph lacks active Agent #{state.active_agent_id}"
52
- end
53
- context = state.active_handoff_context_ref && Phronomy::Agent::HandoffContext.from_h(@persistence.contents.fetch_json(state.active_handoff_context_ref))
54
- wiring = config.merge(phronomy_handoff_bindings: @bindings.fetch(active.agent_id, []),
55
- phronomy_handoff_context: context,
56
- phronomy_coordination: {"kind" => "handoff", "main_agent_id" => main_agent.agent_id, "handoff_revision" => state.handoff_revision}).freeze
57
- active.instance_variable_set(:@_phronomy_coordination_config, wiring)
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))
58
45
  result = if state.phase == "stable"
59
- unfinished = @persistence.executions.list_active(active.agent_id)
46
+ unfinished = @persistence.agent_store.active_executions(agent_id: active.agent_id)
60
47
  if unfinished.empty?
61
48
  active.invoke(input, config: wiring)
62
49
  else
63
50
  exact = unfinished.fetch(0)
64
- unless unfinished.size == 1 && exact.metadata.dig("coordination", "main_agent_id") == main_agent.agent_id
65
- raise Phronomy::Storage::ConflictError, "Active Agent execution belongs to another coordination turn"
51
+ unless unfinished.size == 1 && exact.reservation&.correlation == owner
52
+ raise Phronomy::Persistence::StateConflictError, "Active execution belongs to another turn"
66
53
  end
67
54
  wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(exact.execution_id)
68
- stored_input = @persistence.contents.fetch_text(exact.metadata.fetch("current_input_ref"))
69
- Phronomy::Agent::ExactExecution.start(agent: active, execution_id: exact.execution_id, input: stored_input, config: wiring).wait_result
55
+ active.resume_async(exact.execution_id, config: wiring).wait_result
70
56
  end
71
57
  else
72
- source = @persistence.executions.load(state.pending_source_execution_id)
58
+ source = @persistence.agent_store.execution_identity(state.pending_source_execution_id)
73
59
  unless @handoffs.any? { |edge| edge.source_agent.agent_id == source.agent_id && edge.target_agent.agent_id == state.active_agent_id }
74
60
  raise Phronomy::ExecutionRehydrationRequiredError, "Handoff graph lacks committed Source/Target edge"
75
61
  end
76
- definition = state.metadata.fetch("target_definition")
77
- unless active.class.agent_definition == definition.transform_keys(&:to_sym)
62
+ unless active.class.agent_definition == state.metadata.fetch("target_definition").transform_keys(&:to_sym)
78
63
  raise Phronomy::ConfigurationError, "Handoff Target definition mismatch"
79
64
  end
80
65
  wiring[:cancellation_token].cancel! if Array(state.metadata["cancelled_execution_ids"]).include?(state.pending_target_execution_id)
81
- Phronomy::Agent::ExactExecution.start(agent: active, execution_id: state.pending_target_execution_id,
82
- input: context.responsibility, config: wiring).wait_result
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
83
68
  end
84
- execution = @persistence.executions.load(result.fetch(:execution_id))
85
- if execution.status == :handed_off
69
+ observed = @persistence.agent_store.observe_execution(agent_id: active.agent_id, execution_id: result.fetch(:execution_id))
70
+ if observed.status == :handed_off
86
71
  count += 1
87
72
  raise Phronomy::HandoffError, "Exceeded maximum Handoffs in one turn" if count > MAX_HANDOFFS
88
73
  state = @persistence.handoff_states.load(main_agent.agent_id)
89
74
  next
90
75
  end
91
- if execution.active?
92
- raise Phronomy::ExecutionRehydrationRequiredError, "Handoff execution requires approval or recovery"
93
- end
94
- raise Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]) if result[:error]
76
+ raise Phronomy::ExecutionRehydrationRequiredError, "Handoff requires approval or recovery" if observed.active?
77
+ observed.value!
95
78
  return result.reject { |key, _| key.to_s.start_with?("_phronomy_") || key == :handoff_request }.merge(agent: active)
96
79
  end
97
- rescue Phronomy::CancellationError => error
98
- operation_error = error
80
+ rescue Phronomy::CancellationError => caught
81
+ error = caught
99
82
  cancel(state.pending_source_execution_id) if state && state.phase != "stable"
100
83
  raise
101
- rescue => error
102
- operation_error = error
84
+ rescue => caught
85
+ error = caught
103
86
  raise
104
87
  ensure
105
88
  @admissions.release!(main_agent) if admitted
106
- Phronomy::Tracing::Automatic.finish(trace_handle, output: result && result[:output], error: operation_error)
89
+ Phronomy::Tracing::Automatic.finish(trace, output: result && result[:output], error: error) if trace
107
90
  end
108
91
 
109
- # Durably scopes cancellation to an exact turn, including a transferred
110
- # Target reservation. A completed older turn never cancels a later turn.
111
- # @api public
112
92
  def cancel(execution_id)
113
- if Phronomy::Runtime.in_event_loop_context?
114
- raise Phronomy::EventLoopReentrancyError, "HandoffRunner#cancel cannot block EventLoop"
115
- end
116
- intended = leaf = leaf_id = nil
93
+ raise Phronomy::EventLoopReentrancyError, "HandoffRunner#cancel cannot block EventLoop" if Phronomy::WaitPolicy.blocking_forbidden?
94
+ leaf = intended = nil
117
95
  begin
118
- @persistence.transaction do |tx|
119
- first = tx.executions.load(execution_id)
120
- unless first.metadata.dig("coordination", "main_agent_id") == main_agent.agent_id
121
- raise Phronomy::Storage::ConflictError, "Execution does not belong to this Handoff anchor"
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"
122
103
  end
123
- leaf = first
124
- leaf_id = first.execution_id
125
- seen = {}
126
- while leaf&.status == :handed_off
127
- raise Phronomy::Storage::SerializationError, "Cyclic Handoff chain" if seen[leaf_id]
128
- seen[leaf_id] = true
129
- leaf_id = leaf.metadata.fetch("handoff_target_execution_id")
130
- begin
131
- leaf = tx.executions.load(leaf_id)
132
- rescue Phronomy::Storage::NotFoundError
133
- leaf = nil
134
- end
135
- end
136
- next if leaf&.terminal?
137
- routing = tx.handoff_states.load(main_agent.agent_id)
138
- unless routing && (leaf ? routing.active_agent_id == leaf.agent_id : routing.pending_target_execution_id == leaf_id)
139
- raise Phronomy::Storage::ConflictError, "Handoff routing no longer owns the requested turn"
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]
140
117
  end
141
- ids = (Array(routing.metadata["cancelled_execution_ids"]) + [leaf_id]).uniq
142
- intended = routing.with(phase: leaf ? routing.phase : "stable",
143
- metadata: routing.metadata.merge("cancelled_execution_ids" => ids))
144
- tx.handoff_states.save(main_agent.agent_id, expected_revision: routing.handoff_revision, state: intended)
145
118
  end
146
- rescue => error
147
- confirmed = @persistence.handoff_states.load(main_agent.agent_id)
148
- raise error unless intended && confirmed && Array(confirmed.metadata["cancelled_execution_ids"]).include?(leaf_id)
119
+ raise failure unless proof.disposition == :committed
149
120
  end
150
- return @persistence.execution_result(leaf_id) if leaf&.terminal?
151
- Phronomy::Agent::ExecutionCancellation.signal(leaf_id, leaf.agent_id) if leaf&.active?
152
- {execution_id: leaf_id, cancellation_requested: !leaf&.terminal?}.freeze
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
153
124
  end
154
125
 
155
- # Reads a specified source/Target chain without current graph continuation.
156
- # @api public
157
126
  def result(execution_id)
158
127
  @persistence.handoff_result(execution_id, main_agent_id: main_agent.agent_id)
159
128
  end
160
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
+
161
135
  private
162
136
 
163
137
  def load_state
164
138
  state = @persistence.handoff_states.load(main_agent.agent_id)
165
139
  return state if state
166
140
  now = Time.now.utc.iso8601(6)
167
- initial = Phronomy::Agent::HandoffState.new(main_agent_id: main_agent.agent_id, handoff_revision: 1,
168
- active_agent_id: main_agent.agent_id, active_handoff_context_ref: nil,
169
- phase: "stable", pending_source_execution_id: nil, pending_target_execution_id: nil,
170
- created_at: now, updated_at: now, metadata: {})
171
- @persistence.transaction { |tx| tx.handoff_states.save(main_agent.agent_id, expected_revision: nil, state: initial) }
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
172
149
  rescue => error
173
150
  confirmed = @persistence.handoff_states.load(main_agent.agent_id)
174
151
  raise error unless confirmed
@@ -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
@@ -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::Storage::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::Concurrency::OperationBinding.new(
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
@@ -10,6 +10,26 @@ module Phronomy
10
10
 
11
11
  module_function
12
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
+
13
33
  def encode_team_root(value)
14
34
  payload = value.to_h
15
35
  Phronomy::MultiAgent::TeamRoot.from_h(payload)
@@ -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
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Domain-owned Team metadata, independent of coordination runtime loading.
5
+ # @api private
6
+ module MultiAgent
7
+ module Persistence
8
+ module StorageSchema
9
+ HANDOFF_STATES = Phronomy::Storage::Resource.new(id: "handoff.states", kind: :records,
10
+ attributes: {active_agent_id: :string})
11
+ ROOTS = Phronomy::Storage::Resource.new(id: "team.roots", kind: :records,
12
+ guard: {resource: "team.roots", via: :key})
13
+ EXECUTIONS = Phronomy::Storage::Resource.new(id: "team.executions", kind: :records,
14
+ attributes: {owner: :string, active: :boolean}, immutable_attributes: [:owner],
15
+ indexes: {owner: [:owner], owner_active: [:owner, :active]},
16
+ unique: [{name: :one_active_owner, fields: [:owner], where: {active: true}}],
17
+ guard: {resource: ROOTS.id, via: :owner})
18
+ end
19
+ end
20
+ end
21
+ end