phronomy 0.27.0 → 0.29.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 (517) 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 +28 -7
  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/application-recipes.md +229 -0
  11. data/docs/architecture/agent-chat-and-state-ownership.md +7 -1
  12. data/docs/architecture/agent-transition-ownership.md +13 -89
  13. data/docs/architecture/backend-groups.md +152 -0
  14. data/docs/architecture/before-llm-input.md +3 -3
  15. data/docs/architecture/context-management.md +1 -1
  16. data/docs/architecture/dx05-agent-scope.md +119 -0
  17. data/docs/architecture/dx05-workflow-task-state.md +134 -0
  18. data/docs/architecture/error-api-review.md +245 -0
  19. data/docs/architecture/execution-abstraction-review.md +88 -0
  20. data/docs/architecture/execution-boundaries.md +28 -0
  21. data/docs/architecture/execution-metadata-and-values.md +6 -0
  22. data/docs/architecture/multi-agent-handoff.md +6 -5
  23. data/docs/architecture/persistence.md +27 -9
  24. data/docs/architecture/r8-unit1.md +104 -0
  25. data/docs/architecture/r8-unit10.md +72 -0
  26. data/docs/architecture/r8-unit11.md +71 -0
  27. data/docs/architecture/r8-unit12.md +67 -0
  28. data/docs/architecture/r8-unit13.md +61 -0
  29. data/docs/architecture/r8-unit14.md +72 -0
  30. data/docs/architecture/r8-unit15.md +65 -0
  31. data/docs/architecture/r8-unit16.md +54 -0
  32. data/docs/architecture/r8-unit17.md +73 -0
  33. data/docs/architecture/r8-unit18.md +92 -0
  34. data/docs/architecture/r8-unit19.md +61 -0
  35. data/docs/architecture/r8-unit2.md +55 -0
  36. data/docs/architecture/r8-unit20.md +51 -0
  37. data/docs/architecture/r8-unit21.md +79 -0
  38. data/docs/architecture/r8-unit22.md +74 -0
  39. data/docs/architecture/r8-unit23.md +81 -0
  40. data/docs/architecture/r8-unit3.md +130 -0
  41. data/docs/architecture/r8-unit4.md +79 -0
  42. data/docs/architecture/r8-unit5.md +115 -0
  43. data/docs/architecture/r8-unit6.md +71 -0
  44. data/docs/architecture/r8-unit7.md +70 -0
  45. data/docs/architecture/r8-unit8.md +78 -0
  46. data/docs/architecture/r8-unit9.md +73 -0
  47. data/docs/architecture/refactoring-closure.md +10 -1
  48. data/docs/architecture/remaining-refactoring-plan.md +16 -0
  49. data/docs/architecture/storage-execution-boundaries.md +54 -0
  50. data/docs/architecture/tool-schema-recording-gap.md +6 -0
  51. data/docs/architecture/tracing.md +1 -1
  52. data/docs/architecture/workflow-terminal-ownership-design.md +2 -2
  53. data/docs/architecture.md +24 -11
  54. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  55. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  56. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +2 -0
  57. data/docs/decisions/031-durable-multi-agent-coordination.md +2 -0
  58. data/docs/decisions/032-storage-backend-composition.md +5 -0
  59. data/docs/decisions/034-handoff-runner-coordination-ownership.md +2 -0
  60. data/docs/decisions/035-tool-executor-capability-ownership.md +1 -1
  61. data/docs/decisions/038-responsibility-based-source-layout.md +14 -0
  62. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +8 -0
  63. data/docs/decisions/040-configuration-default-composition.md +12 -1
  64. data/docs/decisions/045-worker-input-restriction-ownership.md +4 -0
  65. data/docs/decisions/051-execution-outcome-worker-ownership.md +2 -0
  66. data/docs/decisions/056-workflow-terminal-policy-ownership.md +1 -1
  67. data/docs/decisions/058-neutral-storage-primitives.md +5 -0
  68. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  69. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  70. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  71. data/docs/decisions/062-content-store-service.md +52 -0
  72. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  73. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  74. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  75. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  76. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  77. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  78. data/docs/decisions/README.md +18 -1
  79. data/docs/execution-boundaries-preview.md +45 -0
  80. data/docs/failure-reason-codes.md +45 -0
  81. data/docs/features.md +12 -4
  82. data/docs/getting-started.md +52 -48
  83. data/docs/migrations/0.22.md +2 -2
  84. data/docs/migrations/durable-semantic-coordination-v2.md +2 -0
  85. data/docs/migrations/handoff-runner-multi-agent.md +2 -0
  86. data/docs/migrations/persistence-failure-contracts.md +67 -0
  87. data/docs/migrations/r8-unit10.md +36 -0
  88. data/docs/migrations/r8-unit11.md +33 -0
  89. data/docs/migrations/r8-unit12.md +31 -0
  90. data/docs/migrations/r8-unit13.md +36 -0
  91. data/docs/migrations/r8-unit14.md +31 -0
  92. data/docs/migrations/r8-unit15.md +31 -0
  93. data/docs/migrations/r8-unit16.md +26 -0
  94. data/docs/migrations/r8-unit17.md +28 -0
  95. data/docs/migrations/r8-unit18.md +29 -0
  96. data/docs/migrations/r8-unit19.md +28 -0
  97. data/docs/migrations/r8-unit20.md +24 -0
  98. data/docs/migrations/r8-unit21.md +28 -0
  99. data/docs/migrations/r8-unit22.md +29 -0
  100. data/docs/migrations/r8-unit23.md +20 -0
  101. data/docs/migrations/r8-unit4.md +67 -0
  102. data/docs/migrations/r8-unit5.md +75 -0
  103. data/docs/migrations/r8-unit6.md +109 -0
  104. data/docs/migrations/r8-unit7.md +31 -0
  105. data/docs/migrations/r8-unit8.md +33 -0
  106. data/docs/migrations/r8-unit9.md +28 -0
  107. data/docs/migrations/storage-async-client.md +103 -0
  108. data/docs/migrations/vector-async-clients.md +110 -0
  109. data/docs/ordered-event-delivery.md +54 -0
  110. data/docs/persistence-backends.md +38 -21
  111. data/docs/runtime-and-concurrency.md +26 -1
  112. data/docs/workflow-completion.md +92 -0
  113. data/gemfiles/http_minimum.gemfile +5 -0
  114. data/lib/phronomy/agent/admission.rb +78 -0
  115. data/lib/phronomy/agent/{lifecycle_contract/agent_already_exists_error.rb → agent_already_exists_error.rb} +1 -1
  116. data/lib/phronomy/agent/{lifecycle_contract/agent_busy_error.rb → agent_busy_error.rb} +1 -1
  117. data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
  118. data/lib/phronomy/agent/{lifecycle_contract/agent_purged_error.rb → agent_purged_error.rb} +1 -1
  119. data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
  120. data/lib/phronomy/agent/api/agent.rb +13 -0
  121. data/lib/phronomy/{recovery → agent/api}/execution_rehydration_required_error.rb +1 -1
  122. data/lib/phronomy/agent/async_event_api.rb +132 -103
  123. data/lib/phronomy/agent/base.rb +146 -113
  124. data/lib/phronomy/agent/before_llm_input.rb +70 -0
  125. data/lib/phronomy/agent/composition/run_once.rb +1 -1
  126. data/lib/phronomy/agent/{context_assembly/context_candidate_resolver.rb → context_candidate_resolver.rb} +2 -2
  127. data/lib/phronomy/agent/{context_assembly/context_assembler.rb → context_preparation.rb} +35 -221
  128. data/lib/phronomy/agent/control_request.rb +21 -0
  129. data/lib/phronomy/agent/execution/agent_execution.rb +1 -1
  130. data/lib/phronomy/agent/execution/agent_invocation.rb +27 -53
  131. data/lib/phronomy/agent/execution/approval_resume_commit.rb +1 -1
  132. data/lib/phronomy/agent/execution/dispatch_preparation.rb +17 -127
  133. data/lib/phronomy/agent/execution/exact_execution.rb +55 -32
  134. data/lib/phronomy/agent/execution/execution_cancellation.rb +8 -4
  135. data/lib/phronomy/agent/execution/execution_coordinator.rb +229 -407
  136. data/lib/phronomy/agent/execution/execution_metadata.rb +1 -0
  137. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +71 -127
  138. data/lib/phronomy/agent/execution/execution_registry.rb +6 -2
  139. data/lib/phronomy/agent/execution/execution_session_runner.rb +22 -27
  140. data/lib/phronomy/agent/execution/initial_preparation.rb +19 -120
  141. data/lib/phronomy/agent/execution/{agent_invocation_session_builder.rb → invocation_actions.rb} +54 -194
  142. data/lib/phronomy/agent/execution/invocation_transitions.rb +39 -1
  143. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +8 -11
  144. data/lib/phronomy/agent/execution_change.rb +161 -0
  145. data/lib/phronomy/agent/execution_environment.rb +36 -0
  146. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  147. data/lib/phronomy/agent/execution_observation.rb +35 -0
  148. data/lib/phronomy/agent/filterable.rb +188 -0
  149. data/lib/phronomy/agent/handoff/{handoff_context.rb → transfer_context.rb} +4 -4
  150. data/lib/phronomy/agent/handoff/{handoff_projection.rb → transfer_projection.rb} +14 -33
  151. data/lib/phronomy/{llm_contract/transport_error.rb → agent/handoff_error.rb} +1 -1
  152. data/lib/phronomy/agent/journal/journal_projection.rb +1 -1
  153. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  154. data/lib/phronomy/agent/lifecycle/agent_root.rb +1 -1
  155. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
  156. data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
  157. data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
  158. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +117 -31
  159. data/lib/phronomy/agent/lifecycle/scope.rb +198 -0
  160. data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
  161. data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
  162. data/lib/phronomy/agent/persistence/agent_repository.rb +29 -21
  163. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  164. data/lib/phronomy/agent/persistence/codec.rb +1 -21
  165. data/lib/phronomy/agent/persistence/execution_repository.rb +57 -41
  166. data/lib/phronomy/agent/persistence/journal_repository.rb +27 -18
  167. data/lib/phronomy/agent/persistence/records.rb +33 -0
  168. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  169. data/lib/phronomy/agent/persistence/storage_schema.rb +7 -2
  170. data/lib/phronomy/agent/persistence/watermark.rb +13 -11
  171. data/lib/phronomy/agent/recovery/invocation_restorer.rb +13 -21
  172. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +13 -12
  173. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +34 -34
  174. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +33 -39
  175. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +28 -25
  176. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  177. data/lib/phronomy/agent/recovery/recovery_support.rb +10 -19
  178. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  179. data/lib/phronomy/agent/retention.rb +17 -0
  180. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  181. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  182. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  183. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  184. data/lib/phronomy/agent/{context_assembly/ruby_llm_materializer.rb → runtime_input.rb} +18 -35
  185. data/lib/phronomy/agent/{context_assembly/saved_context_reader.rb → saved_context_reader.rb} +6 -4
  186. data/lib/phronomy/agent/settings.rb +47 -0
  187. data/lib/phronomy/agent/{context_assembly/state_writer.rb → state_writer.rb} +4 -3
  188. data/lib/phronomy/agent/store.rb +253 -0
  189. data/lib/phronomy/agent/{lifecycle_contract/stream_callback_error.rb → stream_callback_error.rb} +3 -3
  190. data/lib/phronomy/agent/tool_execution/tool_binding.rb +4 -64
  191. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +50 -219
  192. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  193. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  194. data/lib/phronomy/common/error.rb +65 -1
  195. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  196. data/lib/phronomy/content_store/base.rb +7 -2
  197. data/lib/phronomy/content_store/storage_schema.rb +1 -0
  198. data/lib/phronomy/content_store/stored_contents.rb +2 -1
  199. data/lib/phronomy/context/assembly.rb +195 -0
  200. data/lib/phronomy/{llm_contract/context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +3 -1
  201. data/lib/phronomy/context/candidate.rb +51 -0
  202. data/lib/phronomy/context/constraint.rb +47 -0
  203. data/lib/phronomy/{agent/context_contract → context}/context_plan.rb +1 -1
  204. data/lib/phronomy/{agent/context_contract → context}/context_plan_validator.rb +3 -3
  205. data/lib/phronomy/{agent/context_contract → context}/context_policy.rb +1 -1
  206. data/lib/phronomy/{agent/context_contract → context}/context_policy_input.rb +1 -1
  207. data/lib/phronomy/{agent/context_assembly → context}/context_policy_input_builder.rb +1 -1
  208. data/lib/phronomy/context/default_policy.rb +140 -0
  209. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  210. data/lib/phronomy/{agent/context_contract → context}/llm_input_build_context.rb +1 -1
  211. data/lib/phronomy/{agent/context_contract → context}/llm_input_manifest.rb +19 -19
  212. data/lib/phronomy/{agent/context_contract → context}/llm_input_patch.rb +1 -1
  213. data/lib/phronomy/context/manifest_error.rb +10 -0
  214. data/lib/phronomy/context/prompt_template.rb +98 -0
  215. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  216. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  217. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  218. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  219. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  220. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  221. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  222. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  223. data/lib/phronomy/embeddings/authentication_error.rb +11 -0
  224. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +52 -0
  225. data/lib/phronomy/embeddings/base.rb +43 -0
  226. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  227. data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
  228. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  229. data/lib/phronomy/engine/concurrency/offload_pool.rb +36 -21
  230. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  231. data/lib/phronomy/engine/diagnostics.rb +1 -1
  232. data/lib/phronomy/engine/event_loop.rb +7 -7
  233. data/lib/phronomy/engine/execution_binding.rb +23 -0
  234. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  235. data/lib/phronomy/engine/fsm_protocol.rb +1 -6
  236. data/lib/phronomy/engine/fsm_session.rb +3 -3
  237. data/lib/phronomy/engine/metrics.rb +16 -14
  238. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  239. data/lib/phronomy/engine/runtime.rb +25 -12
  240. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  241. data/lib/phronomy/execution/async_operation.rb +41 -0
  242. data/lib/phronomy/{engine → execution}/blocking.rb +2 -2
  243. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +1 -1
  244. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  245. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  246. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  247. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  248. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  249. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  250. data/lib/phronomy/{engine → execution}/execution.rb +61 -27
  251. data/lib/phronomy/{engine → execution}/execution_cancellation_error.rb +2 -2
  252. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  253. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  254. data/lib/phronomy/{engine → execution}/execution_timeout_error.rb +2 -2
  255. data/lib/phronomy/{engine → execution}/invocation_context.rb +0 -15
  256. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  257. data/lib/phronomy/execution/runnable.rb +24 -0
  258. data/lib/phronomy/{engine → execution}/task_result.rb +9 -4
  259. data/lib/phronomy/execution/wait_policy.rb +24 -0
  260. data/lib/phronomy/filter/{contract/filter_block_error.rb → filter_block_error.rb} +3 -3
  261. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  262. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  263. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +2 -2
  264. data/lib/phronomy/generation/generator_verifier.rb +7 -7
  265. data/lib/phronomy/generation/low_confidence_error.rb +2 -2
  266. data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
  267. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  268. data/lib/phronomy/{llm_contract → llm_adapter}/authentication_error.rb +3 -1
  269. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  270. data/lib/phronomy/llm_adapter/base.rb +49 -68
  271. data/lib/phronomy/{llm_contract → llm_adapter}/context_length_error.rb +3 -1
  272. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  273. data/lib/phronomy/llm_adapter/message.rb +62 -0
  274. data/lib/phronomy/{llm_contract → llm_adapter}/rate_limit_error.rb +3 -1
  275. data/lib/phronomy/llm_adapter/request.rb +79 -0
  276. data/lib/phronomy/llm_adapter/response.rb +36 -0
  277. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  278. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  279. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  280. data/lib/phronomy/llm_adapter.rb +3 -16
  281. data/lib/phronomy/multi_agent/admission_registry.rb +1 -8
  282. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  283. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  284. data/lib/phronomy/{agent/handoff → multi_agent}/handoff.rb +2 -2
  285. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_capability_factory.rb +21 -2
  286. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  287. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_policy.rb +1 -1
  288. data/lib/phronomy/multi_agent/handoff_runner.rb +77 -100
  289. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_state.rb +1 -1
  290. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  291. data/lib/phronomy/multi_agent/persistence/codec.rb +20 -0
  292. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  293. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  294. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  295. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +60 -44
  296. data/lib/phronomy/multi_agent/persistence/team_repository.rb +29 -21
  297. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  298. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  299. data/lib/phronomy/multi_agent/shared_state.rb +2 -2
  300. data/lib/phronomy/multi_agent/store.rb +156 -0
  301. data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
  302. data/lib/phronomy/multi_agent/team_coordinator.rb +99 -77
  303. data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
  304. data/lib/phronomy/multi_agent/team_ownership_registry.rb +2 -12
  305. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  306. data/lib/phronomy/persistence/content_repository.rb +26 -0
  307. data/lib/phronomy/persistence/errors.rb +16 -0
  308. data/lib/phronomy/persistence/migration/initial_format_migration.rb +2 -2
  309. data/lib/phronomy/persistence/persistence.rb +23 -0
  310. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  311. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  312. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  313. data/lib/phronomy/persistence/transaction.rb +86 -0
  314. data/lib/phronomy/persistence_composition/storage_schema.rb +9 -4
  315. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  316. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  317. data/lib/phronomy/runtime_composition/agent_defaults.rb +5 -1
  318. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  319. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  320. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  321. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  322. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  323. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  324. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  325. data/lib/phronomy/storage/async/async_client.rb +60 -0
  326. data/lib/phronomy/storage/backend.rb +8 -2
  327. data/lib/phronomy/storage/condition_failed_error.rb +2 -2
  328. data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
  329. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  330. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  331. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +2 -2
  332. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +6 -6
  333. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +55 -52
  334. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +3 -3
  335. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +7 -7
  336. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +11 -11
  337. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +57 -51
  338. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +1 -1
  339. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +23 -22
  340. data/lib/phronomy/tool/authorization.rb +77 -0
  341. data/lib/phronomy/tool/base.rb +551 -9
  342. data/lib/phronomy/tool/call_request.rb +40 -0
  343. data/lib/phronomy/{agent/tool_execution/tool_definition_set.rb → tool/definition_set.rb} +7 -8
  344. data/lib/phronomy/tool/operation.rb +88 -0
  345. data/lib/phronomy/tool/schema.rb +214 -0
  346. data/lib/phronomy/tool/settings.rb +31 -0
  347. data/lib/phronomy/tool/tool_error.rb +7 -0
  348. data/lib/phronomy/tool/tool_executor.rb +58 -0
  349. data/lib/phronomy/tools/agent.rb +22 -90
  350. data/lib/phronomy/tools/mcp.rb +21 -14
  351. data/lib/phronomy/tools/vector_search.rb +4 -4
  352. data/lib/phronomy/tracing/automatic.rb +11 -6
  353. data/lib/phronomy/tracing/base.rb +36 -7
  354. data/lib/phronomy/tracing/observation.rb +37 -0
  355. data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
  356. data/lib/phronomy/tracing/settings.rb +36 -0
  357. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  358. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  359. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  360. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  361. data/lib/phronomy/vector_store/base.rb +118 -49
  362. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  363. data/lib/phronomy/vector_store.rb +2 -17
  364. data/lib/phronomy/version.rb +1 -1
  365. data/lib/phronomy/workflow/completion.rb +11 -0
  366. data/lib/phronomy/workflow/execution/workflow.rb +37 -9
  367. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  368. data/lib/phronomy/workflow/execution/workflow_context.rb +2 -2
  369. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +28 -0
  370. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +77 -8
  371. data/lib/phronomy/workflow/execution/workflow_runner.rb +105 -92
  372. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  373. data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
  374. data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
  375. data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
  376. data/lib/phronomy/workflow/persistence/state_repository.rb +28 -22
  377. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  378. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +61 -0
  379. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +158 -0
  380. data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
  381. data/lib/phronomy/workflow/{execution → runtime_binding}/workflow_terminal_policy.rb +4 -4
  382. data/lib/phronomy.rb +63 -16
  383. data/scripts/api_snapshot.rb +47 -15
  384. data/scripts/check_readme_ruby.rb +24 -29
  385. data/scripts/check_readme_runnable.rb +92 -81
  386. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  387. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  388. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  389. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  390. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  391. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  392. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  393. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  394. data/sig/phronomy/agent/admission.rbs +19 -0
  395. data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
  396. data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
  397. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  398. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  399. data/sig/phronomy/agent/store.rbs +73 -0
  400. data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
  401. data/sig/phronomy/agent.rbs +29 -135
  402. data/sig/phronomy/common.rbs +7 -0
  403. data/sig/phronomy/content_store/contracts.rbs +13 -0
  404. data/sig/phronomy/context.rbs +152 -0
  405. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  406. data/sig/phronomy/embeddings/base.rbs +23 -0
  407. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  408. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  409. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  410. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  411. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  412. data/sig/phronomy/engine/observability.rbs +13 -0
  413. data/sig/phronomy/execution/async_operation.rbs +7 -0
  414. data/sig/phronomy/execution/blocking.rbs +5 -0
  415. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  416. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  417. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  418. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  419. data/sig/phronomy/execution/errors.rbs +7 -0
  420. data/sig/phronomy/execution/execution.rbs +41 -0
  421. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  422. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  423. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  424. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  425. data/sig/phronomy/execution/task_result.rbs +29 -0
  426. data/sig/phronomy/extensions.rbs +1 -21
  427. data/sig/phronomy/filter/filter_block_error.rbs +6 -0
  428. data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
  429. data/sig/phronomy/handoff.rbs +10 -4
  430. data/sig/phronomy/integrations.rbs +14 -0
  431. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  432. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  433. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  434. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  435. data/sig/phronomy/llm_context_window.rbs +10 -0
  436. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  437. data/sig/phronomy/multi_agent/store.rbs +54 -0
  438. data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
  439. data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
  440. data/sig/phronomy/multi_agent.rbs +6 -3
  441. data/sig/phronomy/persistence/errors.rbs +18 -0
  442. data/sig/phronomy/persistence/migration.rbs +13 -0
  443. data/sig/phronomy/persistence/transaction.rbs +19 -0
  444. data/sig/phronomy/persistence.rbs +1 -99
  445. data/sig/phronomy/persistence_composition.rbs +19 -0
  446. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  447. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  448. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  449. data/sig/phronomy/{storage.rbs → storage/contracts.rbs} +4 -18
  450. data/sig/phronomy/tool.rbs +48 -4
  451. data/sig/phronomy/tracing/observation.rbs +7 -0
  452. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  453. data/sig/phronomy/vector_store/base.rbs +23 -0
  454. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +15 -0
  455. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  456. data/sig/phronomy/workflow/store.rbs +8 -0
  457. data/sig/phronomy/workflow.rbs +5 -1
  458. data/sig/phronomy.rbs +4 -49
  459. metadata +365 -117
  460. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  461. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  462. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  463. data/lib/phronomy/agent/context/capability/base.rb +0 -548
  464. data/lib/phronomy/agent/context/capability/tool_executor.rb +0 -62
  465. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  466. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +0 -36
  467. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +0 -22
  468. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  469. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  470. data/lib/phronomy/agent/execution/phase_machine_builder.rb +0 -101
  471. data/lib/phronomy/agent/execution/provider_call_outcome.rb +0 -106
  472. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +0 -15
  473. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +0 -131
  474. data/lib/phronomy/agent/handoff/handoff_request.rb +0 -45
  475. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +0 -7
  476. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +0 -58
  477. data/lib/phronomy/agent/persistence/queries.rb +0 -61
  478. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  479. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  480. data/lib/phronomy/agent/tool_execution/tool_call_intercepted.rb +0 -29
  481. data/lib/phronomy/agent/tool_execution/tool_invocation_session_builder.rb +0 -276
  482. data/lib/phronomy/configuration/configuration.rb +0 -68
  483. data/lib/phronomy/configuration/global_configuration.rb +0 -26
  484. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  485. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  486. data/lib/phronomy/engine/execution_receiver.rb +0 -65
  487. data/lib/phronomy/engine/runnable.rb +0 -54
  488. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  489. data/lib/phronomy/llm_contract/token_usage.rb +0 -85
  490. data/lib/phronomy/multi_agent/persistence/queries.rb +0 -30
  491. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +0 -15
  492. data/lib/phronomy/output_parser/contract/parse_error.rb +0 -7
  493. data/lib/phronomy/persistence/api/persistence.rb +0 -140
  494. data/lib/phronomy/persistence_composition/repositories.rb +0 -77
  495. data/lib/phronomy/recovery/recovery.rb +0 -186
  496. data/lib/phronomy/tool/contract/tool_error.rb +0 -7
  497. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  498. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  499. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  500. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +0 -9
  501. data/sig/phronomy/execution_receiver.rbs +0 -34
  502. data/sig/phronomy/llm_adapter.rbs +0 -11
  503. data/sig/phronomy/runtime.rbs +0 -76
  504. /data/lib/phronomy/agent/{context_assembly/context_importer.rb → context_importer.rb} +0 -0
  505. /data/lib/phronomy/{engine → execution}/backpressure_error.rb +0 -0
  506. /data/lib/phronomy/{engine → execution}/cancellation_error.rb +0 -0
  507. /data/lib/phronomy/{engine → execution}/concurrency/worker_input_restricted.rb +0 -0
  508. /data/lib/phronomy/{engine → execution}/event_loop_reentrancy_error.rb +0 -0
  509. /data/lib/phronomy/{engine → execution}/invalid_async_entry_action_error.rb +0 -0
  510. /data/lib/phronomy/{engine → execution}/invalid_async_transition_action_error.rb +0 -0
  511. /data/lib/phronomy/{engine → execution}/invalid_async_workflow_action_error.rb +0 -0
  512. /data/lib/phronomy/{engine → execution}/pool_shutdown_error.rb +0 -0
  513. /data/lib/phronomy/{engine → execution}/recursion_limit_error.rb +0 -0
  514. /data/lib/phronomy/{engine → execution}/runtime_shutdown_error.rb +0 -0
  515. /data/lib/phronomy/{engine → execution}/runtime_shutdown_reentrancy_error.rb +0 -0
  516. /data/lib/phronomy/{engine → execution}/scheduler_reentrancy_error.rb +0 -0
  517. /data/lib/phronomy/{engine → execution}/timeout_error.rb +0 -0
@@ -0,0 +1,31 @@
1
+ # Migrating to r8 unit 7
2
+
3
+ Apply after core b337fc77dae530269bc34d895365032873d544ad (r8 unit 6).
4
+ Public Agent invocation, streaming, approval, cancellation and recovery signatures
5
+ and persistence record schemas are unchanged. The current unit 6 examples need
6
+ no source changes. Run them against the candidate using PHRONOMY_PATH and the
7
+ correct BUNDLE_GEMFILE for each example directory.
8
+
9
+ There are no compatibility aliases for removed internal APIs:
10
+
11
+ | Internal use | Replacement |
12
+ | --- | --- |
13
+ | AgentInvocationSessionBuilder | AgentInvocation plus the owning ExecutionEnvironment.build_agent_session |
14
+ | AgentInvocationSessionBuilder entry/prepared actions | InvocationActions |
15
+ | ExecutionSessionRunner.new(runtime: ...) | ExecutionSessionRunner.new(environment: ...) |
16
+ | OwnershipRegistry.for/existing_for(Runtime) | Pass an ExecutionEnvironment; EngineEnvironment adapts Runtime |
17
+ | ExecutionReceiver subclasses initialize(event_loop:) | initialize(channel:), preserving super |
18
+ | AgentInvocation.begin_llm_call! implicit Runtime lookup | Explicit llm_call_id from durable prepared execution |
19
+
20
+ ExecutionEnvironment is a private framework connection, not an application
21
+ configuration feature. Do not serialize environments, channels, sessions or live
22
+ registries. The default composition is installed by requiring phronomy. State
23
+ policy loading alone does not install a runtime. Existing persisted Agent/Tool
24
+ identities, revisions, authorization snapshots and recovery classifications are
25
+ retained. This change does not add support for exchanging in-flight unit 5 or
26
+ older execution snapshots with unit 6/7 workers.
27
+
28
+ Stop/drain old processes using the existing deployment procedure before replacing
29
+ code. Deploy all internal call-site changes together. The application package
30
+ contains a strict-base dry-run/applier, expected-tree verification, review patch,
31
+ validation logs and rollback guidance. Run --verify before and after commit.
@@ -0,0 +1,33 @@
1
+ # Migrating to r8 unit 8
2
+
3
+ Apply after core a601a0654ace42640f0251b129359ca1f326e3ad (unit 7).
4
+ This is a core-only change. The unit 6 examples at
5
+ ed455c52018338f7c5a4c2948ae0d7db131aa090 require no source changes.
6
+
7
+ Public Agent invocation/stream/approval/cancellation/recovery signatures and
8
+ Tool#call_async(args, cancellation_token:, config:) are unchanged. Tool::Operation
9
+ adds an explicit C06 operation entry and result transformation API; application
10
+ Tools do not need to adopt it. The supplied submitter applies to the default
11
+ offloaded operation only. Custom async implementations retain their existing
12
+ execution mechanism and configuration.
13
+
14
+ | Internal use | Replacement |
15
+ | --- | --- |
16
+ | Agent::ToolInvocationSessionBuilder.build / build_for_resume | The owning ExecutionEnvironment.build_tool_session, with resume_event/resume_phase when needed |
17
+ | ToolInvocationSessionBuilder transition constants | Agent::ToolInvocationTransitions |
18
+ | ToolInvocationSessionBuilder entry operations | Agent::ToolInvocationActions |
19
+ | ToolInvocation.start_authorization/runtime: and start_execution/runtime: | Pass environment: explicitly; never reconstruct it from a new global Runtime |
20
+ | Tool::ToolExecutor.call_async(runtime:) | Tool::Operation.call_async(submitter:) for contract consumers; default executor takes submitter: internally |
21
+ | Agent ToolBinding's custom-async detection and mapping mechanism | Tool::Operation.with_result_transform; Agent retains filter configuration |
22
+
23
+ Removed internal names have no aliases. Deploy all internal call sites together.
24
+ Do not persist execution environments, submitters, FSM sessions or parent sinks.
25
+ Durable IDs, record schemas, revision checks and saved approval evidence are
26
+ unchanged. The change does not certify new in-flight cross-version compatibility.
27
+ Use the existing stop/drain procedure before replacing running processes.
28
+
29
+ The application ZIP contains strict-base --check/--apply/--verify commands,
30
+ complete replacement files and deletions, expected-tree verification, a review
31
+ patch, validation evidence and rollback instructions. Run --verify before and
32
+ after commit. Verify examples with PHRONOMY_PATH pointing at the candidate and
33
+ unset a stale BUNDLE_GEMFILE before entering each Bundler context.
@@ -0,0 +1,28 @@
1
+ # R8 unit 9 migration
2
+
3
+ Apply to core unit 8, commit e9f6cc16da19be82a0b7777486926ba46d070def.
4
+ Examples source remains at unit 6, ed455c52018338f7c5a4c2948ae0d7db131aa090.
5
+
6
+ No public Tool or Agent signature, stored schema, migration CLI or example source
7
+ change is required. Existing configured filter order and repeated registrations
8
+ are preserved. The correction removes only the extra transformation caused by
9
+ Base's standard async-to-sync delegation under a custom async result decorator.
10
+
11
+ A custom call_async using super now passes its raw delegated result through its
12
+ own processing before the async decorator applies the configured transform.
13
+ Code accidentally relying on the duplicate/intermediate transformation should
14
+ express the required stages explicitly in application filter configuration.
15
+
16
+ A custom call_async explicitly calling public call retains that call's filters;
17
+ the enclosing async result decorator also retains its stage. Do not substitute
18
+ call for super when the intent is to delegate to Base's normal async execution.
19
+ The framework does not suppress application-authored calls or infer their intent.
20
+
21
+ Private changes: Base uses Tool::Operation.synchronous_delegate; ToolExecutor
22
+ accepts a bound synchronous_call keyword. Applications should not use these
23
+ private bridge helpers. Custom async implementations retain their existing
24
+ execution mechanism and keyword protocol.
25
+
26
+ Use the normal process stop/drain and restart procedure. This unit does not add
27
+ cross-version in-flight compatibility or rollback of external Tool side effects.
28
+ See docs/architecture/r8-unit9.md for operation-boundary examples.
@@ -0,0 +1,103 @@
1
+ # Storage asynchronous execution and ContentStore ownership
2
+
3
+ P4 adds the public Beta `Phronomy::Storage::AsyncClient`. Synchronous storage
4
+ backends and `Persistence#transaction` keep their existing interfaces. Backend
5
+ authors implement no scheduler or async methods.
6
+
7
+ ## Public transaction API
8
+
9
+ Construct `Storage::AsyncClient.new(backend: raw_backend, pool: nil)` around a
10
+ Storage SPI 2 backend. For an existing Persistence facade, use
11
+ `persistence.backend`. The transaction block receives a **raw Storage::View**,
12
+ not the domain repositories yielded by `Persistence#transaction`.
13
+
14
+ `transaction_async(cancellation_token: nil, timeout: nil) { |view| ... }`
15
+ returns the pool's original TaskResult. The complete transaction runs on one
16
+ worker, using the backend's normal connection and scope. Do not split a
17
+ transaction across workers or wait for another TaskResult inside its block.
18
+ Explicit synchronous `backend.transaction` calls within the block retain nested
19
+ savepoint behavior.
20
+
21
+ ### Complete offline example
22
+
23
+ This requires a checkout containing P4; the previously released 0.27.0 gem does
24
+ not contain the new client. It uses only the public raw storage contract.
25
+
26
+ ```ruby runnable
27
+ require "phronomy"
28
+ require "json"
29
+
30
+ resource = Phronomy::Storage::Resource.new(id: "example.notes", kind: :records)
31
+ backend = Phronomy::Storage::Backends::InMemory.new(resources: [resource])
32
+ client = Phronomy::Storage::AsyncClient.new(backend: backend)
33
+
34
+ begin
35
+ record = Phronomy::Storage::DurableRecord.new(
36
+ record_type: "example.note", format_version: "0.1", payload: {"text" => "saved"}
37
+ )
38
+ task = client.transaction_async do |view|
39
+ rows = view.records(resource)
40
+ rows.insert(key: "note-1", revision: 1, attributes: {}, record: record)
41
+ rows.fetch("note-1").record.payload
42
+ end
43
+ puts JSON.generate(task.wait_result)
44
+ ensure
45
+ Phronomy.reset_runtime!
46
+ end
47
+ ```
48
+
49
+ The output is `{"text":"saved"}`. Return a value from the transaction. A bound
50
+ view or resource handle becomes invalid after the block finishes, even if it
51
+ was returned through TaskResult. Scope also rejects access from another thread.
52
+ Exceptions and invalid non-local block exits roll back through the existing
53
+ backend rules; no new transaction implementation is introduced.
54
+
55
+ ## Admission, cancellation and ownership
56
+
57
+ Construction starts no Runtime. Each call resolves the current default pool,
58
+ unless an explicit caller-owned `pool:` was supplied. Runtime reset does not
59
+ transfer ownership of an injected pool.
60
+
61
+ Admission uses `on_full: :raise`: a full or stopped pool raises immediately.
62
+ Queue wait counts toward `timeout`. Cancellation/timeout before worker pickup
63
+ skips the transaction. After work starts, TaskResult may settle while the
64
+ physical transaction continues and commits. A timeout or cancellation is **not
65
+ a rollback guarantee**. The client does not kill a thread, add retries, or claim
66
+ that an uncertain commit succeeded or failed. Applications needing a durable
67
+ outcome must reconcile through their domain's identities and stored state.
68
+
69
+ ## Existing Agent, Workflow and coordination operations
70
+
71
+ Internal `Storage::AsyncClient.submit(pool:)` accepts an existing synchronous
72
+ storage unit. This is private framework API, not an application extension API.
73
+ It submits exactly once and adds no transaction, token, or timeout. This lets
74
+ domain operations preserve their own transaction and reconciliation boundaries.
75
+ See the [operation inventory](../architecture/storage-execution-boundaries.md).
76
+
77
+ In particular, a Workflow runner may have only an injected repository rather
78
+ than access to its raw backend. Internal submit therefore accepts a callable
79
+ and the owner's pool; it does not require a dummy backend or a Persistence
80
+ facade. Public transaction_async requires a real SPI 2 backend.
81
+
82
+ Durable commit/readback policy, admission release, callback delivery and the
83
+ application of results to live state remain with Agent/Workflow/MultiAgent.
84
+ No cancellation condition is added to existing terminal saves. Tool execution,
85
+ authorization, tracing and application approval listeners retain their own
86
+ execution boundaries.
87
+
88
+ ## ContentStore path and diagram
89
+
90
+ P4 originally moved `ContentStore::StoredContents` into
91
+ `content_store/backends/stored_contents.rb`. [ADR-062](../decisions/062-content-store-service.md)
92
+ supersedes that split: the implementation now lives at
93
+ `content_store/stored_contents.rb`, beside its shared API and owned schema.
94
+ The obsolete backend directory and collapse entry are deleted without a shim.
95
+ Its constant, synchronous operations, digest rules and exceptions are unchanged.
96
+ Use normal `require "phronomy"`; internal file paths are not a public load API.
97
+ Zeitwerk still collapses `storage/async/`. `Storage::Backends` remains a namespace.
98
+ ContentStore receives no artificial async API.
99
+
100
+ M10 is G57 Content Service in B3, outside Backend Contracts/Implementations;
101
+ M73 is retired. M69 Storage AsyncClient stays in B4. Text panels are transparent.
102
+ Arrows targeting M33/M41/M44 are hidden by display settings; all measured
103
+ dependencies, source evidence and matrix cells remain available.
@@ -0,0 +1,110 @@
1
+ # VectorStore and Embeddings: asynchronous client migration
2
+
3
+ This unreleased P3 change moves the Beta asynchronous API to explicit clients.
4
+ It changes the receiver used by callers. Backend authors continue to implement
5
+ only synchronous methods; method arguments and results remain unchanged.
6
+
7
+ | Previous receiver | New receiver |
8
+ |---|---|
9
+ | `store.add_async(...)` | `async_store.add_async(...)` |
10
+ | `store.search_async(...)` | `async_store.search_async(...)` |
11
+ | `store.remove_async(...)` | `async_store.remove_async(...)` |
12
+ | `store.clear_async(...)` | `async_store.clear_async(...)` |
13
+ | `embeddings.embed_async(text, token, timeout: seconds)` | `async_embeddings.embed_async(text, token, timeout: seconds)` |
14
+
15
+ Construct the clients with `VectorStore::AsyncClient.new(backend: store)` and
16
+ `VectorStore::Embeddings::AsyncClient.new(adapter: embeddings)`. The embedding
17
+ cancellation token stays positional. Synchronous `add`, `search`, `remove`,
18
+ `clear`, `size` and `embed` stay on the existing backend objects.
19
+
20
+ `VectorStore::AsyncBackend` and inherited async methods are removed. There is
21
+ no include/prepend, backend `.async` factory, or automatic compatibility shim.
22
+ Update the receiver wherever an application previously called an async method.
23
+ The clients are public Beta API; their `async/` directories do not introduce
24
+ `Phronomy::Async` or feature-specific `Async` namespaces.
25
+
26
+ ## Complete offline example
27
+
28
+ This example uses an intentionally simple local embedding adapter to demonstrate
29
+ the API. It needs no provider key, network request, or database server. The
30
+ implementation supplies only `embed`; Phronomy supplies asynchronous execution.
31
+ Run it against a checkout containing P3, not the previously released 0.27.0 gem.
32
+
33
+ ```ruby runnable
34
+ require "phronomy"
35
+ require "json"
36
+
37
+ class KeywordEmbeddings < Phronomy::Embeddings::Base
38
+ protected
39
+
40
+ def perform_embed(text, cancellation_token = nil)
41
+ cancellation_token&.raise_if_cancelled!
42
+ words = text.downcase
43
+ [words.include?("ruby") ? 1.0 : 0.0,
44
+ words.include?("python") ? 1.0 : 0.0]
45
+ end
46
+ end
47
+
48
+ begin
49
+ store = Phronomy::VectorStore::InMemory.new(dimension: 2)
50
+ embeddings = KeywordEmbeddings.new
51
+ async_store = Phronomy::VectorStore::AsyncClient.new(backend: store)
52
+ async_embeddings = Phronomy::Embeddings::AsyncClient.new(adapter: embeddings)
53
+
54
+ {"ruby" => "Ruby guide", "python" => "Python guide"}.each do |id, title|
55
+ async_embeddings.embed_async(title).flat_map do |vector|
56
+ async_store.add_async(id: id, embedding: vector, metadata: {title: title})
57
+ end.wait_result
58
+ end
59
+
60
+ matches = async_embeddings.embed_async("Ruby").flat_map do |vector|
61
+ async_store.search_async(query_embedding: vector, k: 1)
62
+ end.wait_result
63
+ puts JSON.generate(matches)
64
+ ensure
65
+ Phronomy.reset_runtime!
66
+ end
67
+ ```
68
+
69
+ The result contains the `ruby` document. The application waits only outside
70
+ worker callbacks; `flat_map` returns the next TaskResult without blocking a
71
+ worker on another operation. `Tools::VectorSearch` continues to call synchronous
72
+ `embed` and `search` inside its existing execution boundary, so this migration
73
+ does not add nested offload work to that Tool.
74
+
75
+ ## Execution and cancellation
76
+
77
+ Both clients accept optional `pool:` injection. Usually omit it. Construction
78
+ starts no Runtime, and each operation resolves the current default pool. An
79
+ explicit pool remains caller-owned across Runtime resets; it must provide the
80
+ existing OffloadPool submit contract, including its timer service for timeouts.
81
+
82
+ Queue admission uses `on_full: :raise`. A full or stopped pool raises immediately;
83
+ an accepted operation returns the original TaskResult. Timeout includes queue
84
+ wait. Cancellation or timeout before worker pickup skips the operation; after
85
+ execution starts it settles the result without forcibly stopping I/O or undoing
86
+ backend effects. These are the existing OffloadPool semantics.
87
+
88
+ Synchronous RBS signatures accept the structural `_CancellationSignal`
89
+ interface, which requires only `raise_if_cancelled!`. Existing
90
+ `Concurrency::CancellationToken` objects still satisfy it. Async RBS signatures
91
+ live in `sig/phronomy/vector_store/async/async_client.rbs` and
92
+ `sig/phronomy/embeddings/async/async_client.rbs` and retain the full CancellationToken
93
+ and TaskResult contract needed by the execution pool. Do not substitute a
94
+ minimal synchronous signal for an async operation's full token.
95
+
96
+ ## Source ownership
97
+
98
+ The public class names below are unchanged. Their implementation files move:
99
+
100
+ | Public class | New path under `lib/phronomy/` |
101
+ |---|---|
102
+ | `VectorStore::InMemory` | `vector_store/backends/in_memory.rb` |
103
+ | `VectorStore::Pgvector` | `vector_store/backends/pgvector.rb` |
104
+ | `VectorStore::RedisSearch` | `vector_store/backends/redis_search.rb` |
105
+ | `VectorStore::Embeddings::RubyLLMEmbeddings` | `embeddings/backends/ruby_llm_embeddings.rb` |
106
+
107
+ Use `require "phronomy"` and the public constants. Direct partial-load paths are
108
+ not the public loading contract; no require shim is kept in the contract
109
+ directories. Backend Contracts have no execution or implementation dependency.
110
+ P4 Storage/ContentStore work remains pending.
@@ -0,0 +1,54 @@
1
+ # Ordered event delivery
2
+
3
+ Load the application integration explicitly:
4
+
5
+ ```ruby
6
+ require "phronomy"
7
+ require "phronomy/integrations/ordered_event_delivery"
8
+
9
+ Phronomy::Integrations::OrderedEventDelivery.open(
10
+ capacity: 256, batch_size: 32, flush_timeout: 30,
11
+ deliver: ->(payload) { send_to_application_transport(payload) }
12
+ ) do |delivery|
13
+ agent = MyAgent.new(on_event: ->(event) {
14
+ payload = application_payload(event)
15
+ delivery.publish(payload) if payload
16
+ })
17
+ agent.stream("Hello")
18
+ end
19
+ ```
20
+
21
+ The block runs on the external caller, and its result is returned after drain.
22
+ `open` rejects EventLoop callers before executing their block. Publication only
23
+ snapshots and queues plain data; it does not wait for capacity or send network
24
+ traffic. The existing Blocking API supplies a worker only while draining. The
25
+ integration creates no scheduler and never shuts down the shared Runtime/pool.
26
+
27
+ The app owns authentication, destination, authorization, Rails executor wrapping,
28
+ event-to-payload conversion, and any token coalescing. `prepare_batch:` optionally
29
+ receives an immutable Array of snapshots on the drain worker and returns an Array
30
+ of payloads to send in order. Its default is no transformation. Example 18 passes
31
+ its own TokenEventBatch, preserving adjacent-token coalescing with equal metadata.
32
+
33
+ Capacity limits the waiting queue, excluding an already extracted batch. Multiple
34
+ publishers are ordered by queue admission, not by thread start time. Hashes,
35
+ Arrays, Strings and scalar plain values are copied into immutable snapshots;
36
+ cycles, non-finite numbers and arbitrary objects fail publication.
37
+
38
+ On overflow/invalid publication, acceptance closes and the failure remains visible
39
+ to final drain even if an Agent listener merely logs its exception. Previously
40
+ accepted work drains if transport is healthy. A failed batch conversion, delivery
41
+ or worker admission also closes acceptance; remaining work may not be delivered.
42
+ There is no silent drop-as-success, automatic retry or durable delivery guarantee.
43
+
44
+ `close_and_wait` can be called by multiple external threads. It waits for physical
45
+ drain completion, not just TaskResult settlement. A timeout ends only the wait;
46
+ a network operation can still complete afterwards. Configure transport timeouts
47
+ in the transport itself. Network failure does not prove that nothing was sent.
48
+
49
+ If application execution fails, its original exception, message and backtrace
50
+ remain primary after cleanup. A simultaneous delivery failure is reported through
51
+ the configured Phronomy logger. Without a logger there is no durable diagnostic
52
+ guarantee; logger failure falls back to stderr without replacing the primary error.
53
+ If only delivery fails, that failure is raised. The small final-answer-only Job
54
+ needs no queue and is unchanged.
@@ -2,8 +2,11 @@
2
2
 
3
3
  [ADR-058](decisions/058-neutral-storage-primitives.md) defines the current
4
4
  extension contract. This is an intentional breaking replacement of the old
5
- fixed-repository Backend SPI. Application code keeps `Persistence.new(backend:)`,
6
- `Persistence.in_memory`, its eight domain repositories and result queries.
5
+ fixed-repository Backend SPI. Application construction now uses
6
+ `PersistenceComposition.build(backend:)` or `.in_memory`; pass its `agent`, `team`,
7
+ or `workflow` component to that domain. `Persistence.new(backend:)` provides only
8
+ the neutral coordinator. The old eight-repository facade and its queries were
9
+ removed; see [r8 unit 3](architecture/r8-unit3.md).
7
10
 
8
11
  ## Ownership and composition
9
12
 
@@ -13,9 +16,10 @@ record types, active-execution policy, content digest algorithm or watermark API
13
16
 
14
17
  Agent, Team, Workflow and ContentStore declare their own resource schemas.
15
18
  `PersistenceComposition::StorageSchema` gathers those declarations and
16
- `PersistenceComposition::Repositories` assembles domain wrappers over a View.
17
- The Team and Workflow schema files live beside their features in nested loader
18
- roots; loading metadata does not load their runtime implementations.
19
+ `PersistenceComposition` binds domain record adapters to one coordinator.
20
+ Team and Workflow schemas live in their existing `persistence/` implementation
21
+ directories. An isolated Agent uses `PersistenceComposition.agent` and does not
22
+ construct Team or Workflow stores.
19
23
  `ContentStore::StoredContents` owns SHA-256 identity and digest verification.
20
24
  The Agent-owned `Watermark` composes guarded revision and stream-head conditions.
21
25
 
@@ -41,11 +45,10 @@ view.check!(guards: guards, conditions: conditions)
41
45
 
42
46
  The backend declares `spi_version: 2` and true values for `atomic_resources`,
43
47
  `record_cas`, `stream_cas`, `conditional_unique`, `guarded_checks` and
44
- `nested_savepoints`. Persistence validates these capabilities and the required
45
- resource declarations before exposing repositories. Old duck-typed backends are
46
- rejected with `UnsupportedBackendError`; there is no eight-slot compatibility
47
- adapter. Public Persistence capabilities retain `atomic_all`, `atomic_admission`
48
- and `optimistic_revision`, derived by composition from these primitives.
48
+ `nested_savepoints`. Persistence validates backend capabilities; composition
49
+ validates the required domain resource declarations before binding adapters.
50
+ Unsupported backends raise `Persistence::UnsupportedBackendError`. There is no
51
+ eight-slot compatibility adapter or duplicated facade capability API.
49
52
 
50
53
  A root handle routes to the current transaction on the same backend/thread.
51
54
  A bound view and all its handles expire on commit or rollback and reject use from
@@ -139,12 +142,24 @@ stale revisions and ordinary constraint conflicts remain `ConflictError`.
139
142
  meanings; `TransactionError` identifies invalid scope use. The removed
140
143
  `ActiveExecutionConflictError` and `Storage::Repositories` have no aliases.
141
144
 
142
- The following sections describe the retained **domain Persistence** surface.
145
+ The following sections describe the **domain-owned record protocols**, used by
146
+ domain implementations and backend conformance tests. They are not a shared
147
+ application facade. Unqualified
148
+ ConflictError, NotFoundError, SerializationError and UnsupportedBackendError in
149
+ these domain sections refer to `Phronomy::Persistence` errors. Repository exits
150
+ translate the four raw Storage categories, preserving `cause`, message and trace.
151
+ Backend implementation code continues to raise `Phronomy::Storage` errors as
152
+ specified above. Unknown/transport errors and Storage::TransactionError are not
153
+ translated. See [ADR-061](decisions/061-persistence-failure-contracts.md) and the
154
+ [application migration](migrations/persistence-failure-contracts.md).
143
155
 
144
156
  ## Contents repository
145
157
 
146
- `ContentStore::StoredContents < ContentStore::Base` supplies this domain surface
147
- over neutral Blobs, with text/JSON helpers and canonical content-ID calculation.
158
+ `ContentStore::StoredContents < ContentStore::Base` supplies text/JSON helpers
159
+ and canonical content-ID calculation over neutral Blobs. The private
160
+ Persistence::ContentRepository adapts its primitive methods to the upper failure
161
+ contract when accessed through `persistence.contents`. Direct StoredContents
162
+ callers retain the raw Storage error surface.
148
163
 
149
164
  Required primitive surface:
150
165
 
@@ -159,7 +174,7 @@ Required semantics:
159
174
  - content is immutable and content-addressed;
160
175
  - writing identical bytes is idempotent and returns the same content ID;
161
176
  - `fetch` returns a binary `String` isolated from caller mutation;
162
- - a missing content ID raises `Storage::NotFoundError`;
177
+ - a missing content ID through `persistence.contents` raises `Persistence::NotFoundError`;
163
178
  - one content ID must never resolve to different bytes; a digest-integrity
164
179
  violation raises `ContentStore::IntegrityError`.
165
180
 
@@ -361,8 +376,9 @@ Array of supported values
361
376
  Hash with String/Symbol keys and supported values
362
377
  ```
363
378
 
364
- If a value cannot be represented, raise `Storage::SerializationError` rather
365
- than silently converting it into a lossy form. JSON backends may return String
379
+ If a value cannot be represented, the backend/codec raises
380
+ `Storage::SerializationError`; the domain repository exposes
381
+ `Persistence::SerializationError`. Do not silently convert it into a lossy form. JSON backends may return String
366
382
  keys after decoding; `WorkflowRunner` deliberately accepts String and Symbol keys
367
383
  and normalizes them when comparing durable snapshots.
368
384
 
@@ -391,16 +407,17 @@ differs, raise `ConflictError`. On success return `true`.
391
407
  The operation must not return a replacement AgentRoot or Journal. A mismatch is a
392
408
  conflict, not a request to reload/merge mutable state.
393
409
 
394
- When used inside `Persistence#transaction`, a SQL backend should perform the
410
+ When used inside `Agent::Store#transaction`, a SQL backend should perform the
395
411
  watermark check in the same database transaction as the subsequent durable
396
412
  write.
397
413
 
398
414
  ## Transaction contract
399
415
 
400
- A transaction block may combine operations across all repositories:
416
+ A domain transaction combines its own record operations. Multiple domains join
417
+ one coordinator scope explicitly; each receives only its own record protocol:
401
418
 
402
419
  ```ruby
403
- persistence.transaction do |tx|
420
+ stores.agent.transaction do |tx|
404
421
  tx.assert_agent_watermark!(...)
405
422
  content_ref = tx.contents.put_text("...")
406
423
  tx.journals.append(...)
@@ -493,7 +510,7 @@ require "phronomy"
493
510
  require "phronomy/testing/persistence_contract"
494
511
 
495
512
  RSpec.describe MyPersistenceBackend do
496
- let(:persistence) { Phronomy::Persistence.new(backend: described_class.new(...)) }
513
+ let(:stores) { Phronomy::PersistenceComposition.build(backend: described_class.new(...)) }
497
514
 
498
515
  it_behaves_like "a persistence content store"
499
516
  it_behaves_like "an Agent repository"
@@ -507,7 +524,7 @@ RSpec.describe MyPersistenceBackend do
507
524
  end
508
525
  ```
509
526
 
510
- `Persistence.in_memory` is run through the same shipped contract source in
527
+ `PersistenceComposition.in_memory` is run through the same shipped contract source in
511
528
  Phronomy CI. The files under `spec/support/shared_examples/` are compatibility
512
529
  require wrappers only; the authoritative shared-example implementations live
513
530
  under `lib/phronomy/testing/persistence_contract/` so the core suite and external
@@ -292,7 +292,7 @@ remain durable conflict defense rather than distributed ownership.
292
292
  ## Tool execution modes
293
293
 
294
294
  The default dispatch helper is the private
295
- `Phronomy::Agent::Context::Capability::ToolExecutor`, colocated with Capability
295
+ `Phronomy::Tool::ToolExecutor`, colocated with Capability
296
296
  Base. Agent ToolInvocation supplies Runtime and admission policy for the
297
297
  standard path, and owns authorization and logical result handling. Custom
298
298
  `call_async` implementations keep the public Tool protocol. This ownership is
@@ -577,10 +577,35 @@ TaskResult reports only caller-facing settlement.
577
577
  ## EventLoop metrics
578
578
 
579
579
  `Phronomy::Metrics.snapshot` also reports EventLoop queue depth and lag values.
580
+
581
+ Metrics and Diagnostics only observe resources already owned by the default
582
+ Runtime. Calling `Metrics.snapshot`, `Diagnostics.snapshot` or `Diagnostics.dump`
583
+ does not initialize a Runtime, pool, EventLoop or timer. Missing resources report
584
+ zero values, including `offload_pool_size: 0` when no default pool exists. Once a
585
+ pool exists, this field remains its configured capacity, not a live-worker count.
586
+ Named pools are not aggregated into the default-pool metrics.
587
+
588
+ Retained resources remain observable during and after shutdown, including
589
+ incomplete cleanup. Observation does not reopen admission or change a shutdown
590
+ result. After the default Runtime is reset, missing resources again report zero;
591
+ the next snapshot uses any newly initialized default Runtime. Counters are sampled
592
+ independently and do not constitute an atomic cross-resource snapshot.
580
593
  Use these to distinguish worker saturation from EventLoop backlog/latency.
581
594
 
582
595
  ## Shutdown
583
596
 
597
+ Pool creation and registration close atomically before the shutdown resource set
598
+ is captured. Accepted continuations can still acquire resources during Runtime's
599
+ draining phase. Once resource shutdown begins, all pools close admission before
600
+ any pool waits for its workers.
601
+
602
+ The stop-phase monotonic deadline is shared by EventLoop and every pool join.
603
+ A join timeout does not stop a synchronous worker. Runtime reports incomplete
604
+ cleanup while any worker remains, even if that worker's TaskResult has already
605
+ been cancelled or timed out. It does not forcefully interrupt worker threads.
606
+ See [R8 unit19](architecture/r8-unit19.md) and its
607
+ [migration notes](migrations/r8-unit19.md) for the shutdown budget and result rules.
608
+
584
609
  `Runtime#shutdown` is terminal for that Runtime. It drains/terminates the
585
610
  Runtime-owned EventLoop, then closes pools and timers according to the Runtime
586
611
  shutdown contract.
@@ -0,0 +1,92 @@
1
+ # A bounded Workflow completion recipe
2
+
3
+ This is a **legacy manual-signal recipe**, retained for compatibility and its
4
+ bounded-lifetime tests. New code should use
5
+ [`task_state`](architecture/dx05-workflow-task-state.md), which owns completion
6
+ correlation and failure propagation for operations started by a Workflow state.
7
+ The helper below is application code and is not a Phronomy integration API.
8
+ Its limitations do not describe the new task-state API.
9
+
10
+ Use the application-local function below only for **one live waiting stage, with
11
+ no restart, reentry or replacement while its operation is pending**. It separates
12
+ result mapping (application logic) from the repetitive notification plumbing.
13
+ For repeated/resumed stages, the app must put an operation ID in every success
14
+ AND error payload and check it with a receiver-side transition `guard:` before
15
+ applying the payload. Signal admission alone is not that check. The example
16
+ below intentionally does not claim that wider guarantee.
17
+
18
+ ```ruby runnable
19
+ def forward_completion(task, workflow:, workflow_instance_id:, event:, on_delivery_error:)
20
+ task.on_complete do |payload, operation_error|
21
+ delivery_error = nil
22
+ begin
23
+ accepted = workflow.signal(
24
+ workflow_instance_id: workflow_instance_id, event: event,
25
+ payload: operation_error ? {error: operation_error} : payload
26
+ )
27
+ delivery_error = Phronomy::Error.new("Workflow did not accept #{event}") unless accepted
28
+ rescue => error
29
+ delivery_error = error
30
+ end
31
+ on_delivery_error.call(delivery_error) if delivery_error
32
+ end
33
+ nil
34
+ end
35
+
36
+ class RecipeAnswerAgent < Phronomy::Agent::Base
37
+ agent_definition id: "bounded-completion-recipe", version: 1
38
+ model "gpt-4o-mini"
39
+ instructions "Answer briefly."
40
+ end
41
+ class CompletionState
42
+ include Phronomy::WorkflowContext
43
+ field :question, type: :replace
44
+ field :answer, type: :replace
45
+ field :error_message, type: :replace
46
+ end
47
+
48
+ agent = RecipeAnswerAgent.new
49
+ errors = Queue.new
50
+ workflow = nil
51
+ workflow = Phronomy::Workflow.define(CompletionState) do
52
+ initial :asking
53
+ state :asking
54
+ state :done
55
+ entry :asking, ->(state) {
56
+ task = agent.invoke_async(state.question).map { |result| {answer: result.fetch(:output)} }
57
+ forward_completion(task, workflow: workflow,
58
+ workflow_instance_id: state.workflow_instance_id, event: :answer_ready,
59
+ on_delivery_error: ->(error) { errors << error })
60
+ state
61
+ }
62
+ transition from: :asking, on: :answer_ready, to: :done,
63
+ action: ->(state, event) {
64
+ error = event.payload[:error]
65
+ error ? state.merge(error_message: error.message) : state.merge(answer: event.payload.fetch(:answer))
66
+ }
67
+ transition from: :done, to: :__finish__
68
+ end
69
+ final = workflow.invoke({question: "Explain completion handles"},
70
+ config: {workflow_instance_id: "bounded-recipe"})
71
+ puts final.error_message || final.answer
72
+ raise errors.pop unless errors.empty?
73
+ ```
74
+
75
+ `forward_completion` returns nil after registering one callback. Already-completed
76
+ results are notified immediately. Two registrations are two subscriptions; the
77
+ application avoids duplicates. No task state is changed, no retry is attempted,
78
+ and no callback waits for execution, storage or network delivery.
79
+
80
+ Operation failures and cancellation notify the Workflow with `{error: error}`.
81
+ A false signal result and a raised signal exception each notify the required
82
+ `on_delivery_error` callback once. A failure of that callback follows TaskResult's
83
+ existing logger policy; it is not retried and does not rewrite the task result.
84
+ The application observes delivery errors and chooses cancellation/recovery.
85
+
86
+ Signal acceptance does not guarantee transition completion or a durable save.
87
+ Incomplete tasks retain their subscriptions until settlement under the existing
88
+ TaskResult lifecycle. The caller still owns operation cancellation and lifetime;
89
+ this recipe installs no Runtime shutdown hook or global subscription registry.
90
+
91
+ See [the full application recipe](application-recipes.md#workflow-completion-and-failures)
92
+ for the same failure behavior without extracting this application-local function.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ eval_gemfile "mcp_1_0.gemfile"
4
+ gem "faraday", "= 2.1.0"
5
+ gem "webmock", require: false