phronomy 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +14 -7
  5. data/VERIFY.sh +27 -27
  6. data/benchmark/bench_agent_invoke.rb +26 -22
  7. data/benchmark/bench_context_assembler.rb +4 -5
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  10. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  11. data/docs/architecture/agent-context.md +5 -3
  12. data/docs/architecture/agent-transition-ownership.md +89 -0
  13. data/docs/architecture/before-llm-input.md +6 -0
  14. data/docs/architecture/context-management.md +30 -0
  15. data/docs/architecture/context-preparation-steps.md +90 -0
  16. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  17. data/docs/architecture/execution-metadata-and-values.md +80 -0
  18. data/docs/architecture/generator-verifier-ownership.md +111 -0
  19. data/docs/architecture/multi-agent-handoff.md +8 -2
  20. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  21. data/docs/architecture/persistence.md +29 -6
  22. data/docs/architecture/refactoring-closure.md +334 -0
  23. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  24. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  25. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  26. data/docs/architecture/tracing.md +4 -4
  27. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  28. data/docs/architecture.md +209 -0
  29. data/docs/async-composition.md +205 -0
  30. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  31. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  32. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  33. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  34. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  35. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  36. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  37. data/docs/decisions/032-storage-backend-composition.md +80 -0
  38. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  39. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  40. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  41. data/docs/decisions/036-context-contract-ownership.md +86 -0
  42. data/docs/decisions/037-common-definition-ownership.md +62 -0
  43. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  44. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  45. data/docs/decisions/040-configuration-default-composition.md +94 -0
  46. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  47. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  48. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  49. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  50. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  51. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  52. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  53. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  54. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  55. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  56. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  57. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  58. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  59. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  60. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  61. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  62. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  63. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  64. data/docs/decisions/README.md +35 -6
  65. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  66. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  67. data/docs/features.md +21 -12
  68. data/docs/getting-started.md +10 -10
  69. data/docs/migrations/0.15.md +5 -0
  70. data/docs/migrations/durable-semantic-coordination-v2.md +7 -1
  71. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  72. data/docs/migrations/neutral-storage-spi.md +43 -0
  73. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  74. data/docs/migrations/shared-state-multi-agent.md +44 -0
  75. data/docs/migrations/storage-backend-composition.md +117 -0
  76. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  77. data/docs/persistence-backends.md +162 -204
  78. data/docs/runtime-and-concurrency.md +205 -60
  79. data/lib/phronomy/agent/api/agent.rb +17 -0
  80. data/lib/phronomy/agent/async_event_api.rb +2 -2
  81. data/lib/phronomy/agent/base.rb +55 -251
  82. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  83. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  84. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  85. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  86. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  87. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  88. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  89. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  90. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  91. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  92. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  93. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  94. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  95. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  96. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  97. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  98. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  99. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +20 -96
  100. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  101. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  102. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +12 -13
  103. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  104. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  105. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  106. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  107. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  108. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  109. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  110. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  111. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  112. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +22 -73
  113. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  114. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  115. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  116. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  117. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  118. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  119. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  120. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  121. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  122. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  123. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  124. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  125. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  126. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  127. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  128. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  129. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  130. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  131. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  132. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  133. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  134. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  135. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  136. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  137. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  138. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  139. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  140. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +36 -41
  141. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  142. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +9 -13
  143. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  144. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  145. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  146. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  147. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  148. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  149. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  150. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  151. data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +2 -2
  152. data/lib/phronomy/common/configuration_error.rb +7 -0
  153. data/lib/phronomy/common/error.rb +5 -0
  154. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  155. data/lib/phronomy/common/values/serializable.rb +32 -0
  156. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  157. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  158. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  159. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  160. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  161. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +15 -8
  162. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  163. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  164. data/lib/phronomy/engine/concurrency/offload_pool.rb +29 -16
  165. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  166. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  167. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  168. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  169. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  170. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  171. data/lib/phronomy/engine/event_loop.rb +160 -626
  172. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  173. data/lib/phronomy/engine/execution.rb +229 -0
  174. data/lib/phronomy/engine/execution_cancellation_error.rb +14 -0
  175. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  176. data/lib/phronomy/engine/execution_timeout_error.rb +14 -0
  177. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  178. data/lib/phronomy/engine/fsm_session.rb +38 -31
  179. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +3 -1
  180. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +3 -1
  181. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +3 -1
  182. data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +13 -1
  183. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  184. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  185. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  186. data/lib/phronomy/engine/runtime.rb +80 -84
  187. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  188. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  189. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  190. data/lib/phronomy/engine/{task.rb → task_result.rb} +101 -42
  191. data/lib/phronomy/engine/timeout_error.rb +7 -0
  192. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  193. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  194. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  195. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  196. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  197. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  198. data/lib/phronomy/llm_adapter/base.rb +2 -2
  199. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  200. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  201. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  202. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  203. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  204. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  205. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  206. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  207. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +8 -8
  208. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  209. data/lib/phronomy/multi_agent/orchestrator.rb +36 -68
  210. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  211. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  212. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  213. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  214. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  215. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  216. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  217. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  218. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  219. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  220. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  221. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  222. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  223. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  224. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  225. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  226. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  227. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  228. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  229. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  230. data/lib/phronomy/storage/backend.rb +101 -0
  231. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  232. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  233. data/lib/phronomy/storage/blobs.rb +31 -0
  234. data/lib/phronomy/storage/condition.rb +25 -0
  235. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  236. data/lib/phronomy/storage/conflict_error.rb +9 -0
  237. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  238. data/lib/phronomy/storage/entry.rb +33 -0
  239. data/lib/phronomy/storage/guard_ref.rb +13 -0
  240. data/lib/phronomy/storage/not_found_error.rb +9 -0
  241. data/lib/phronomy/storage/record_codec.rb +177 -0
  242. data/lib/phronomy/storage/records.rb +61 -0
  243. data/lib/phronomy/storage/resource.rb +126 -0
  244. data/lib/phronomy/storage/scope.rb +25 -0
  245. data/lib/phronomy/storage/serialization_error.rb +9 -0
  246. data/lib/phronomy/storage/streams.rb +44 -0
  247. data/lib/phronomy/storage/transaction_error.rb +10 -0
  248. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  249. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  250. data/lib/phronomy/storage/validation.rb +53 -0
  251. data/lib/phronomy/storage/view.rb +131 -0
  252. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  253. data/lib/phronomy/testing/fake_clock.rb +13 -9
  254. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  255. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  256. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  257. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  258. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  259. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  260. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  261. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  262. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  263. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  264. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  265. data/lib/phronomy/tools/agent.rb +6 -6
  266. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  267. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  268. data/lib/phronomy/version.rb +1 -1
  269. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  270. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  271. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  272. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  273. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +126 -75
  274. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  275. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  276. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  277. data/lib/phronomy/workflow/phase_machine_builder.rb +8 -8
  278. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  279. data/lib/phronomy.rb +52 -94
  280. data/scripts/api_snapshot.rb +4 -2
  281. data/scripts/storage_spi_snapshot.rb +36 -0
  282. data/sig/phronomy/agent.rbs +4 -5
  283. data/sig/phronomy/execution_receiver.rbs +34 -0
  284. data/sig/phronomy/extensions.rbs +5 -5
  285. data/sig/phronomy/handoff.rbs +4 -2
  286. data/sig/phronomy/multi_agent.rbs +19 -3
  287. data/sig/phronomy/persistence.rbs +7 -91
  288. data/sig/phronomy/runtime.rbs +34 -7
  289. data/sig/phronomy/storage.rbs +174 -0
  290. data/sig/phronomy/tool.rbs +11 -2
  291. data/sig/phronomy/workflow.rbs +1 -1
  292. metadata +236 -99
  293. data/lib/phronomy/agent/execution_coordinator.rb +0 -3138
  294. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  295. data/lib/phronomy/agent/recovery_support.rb +0 -504
  296. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  297. data/lib/phronomy/agent/tool_executor.rb +0 -55
  298. data/lib/phronomy/generator_verifier.rb +0 -369
  299. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  300. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  301. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
  302. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  303. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  304. data/lib/phronomy/persistence/in_memory.rb +0 -690
  305. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  306. data/lib/phronomy/persistence.rb +0 -276
  307. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  308. data/lib/phronomy/workflow_recovery.rb +0 -123
  309. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  310. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  311. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  312. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  313. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  314. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  315. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  316. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  317. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  318. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  319. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  320. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  321. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  322. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  323. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "concurrency/worker_input_restricted"
4
+
3
5
  require_relative "runtime/timer_queue"
4
6
  require_relative "runtime/shutdown_result"
5
7
  require_relative "runtime/timer_service"
6
- require_relative "runtime/agent_ownership_registry"
7
- require_relative "runtime/team_ownership_registry"
8
8
 
9
9
  module Phronomy
10
10
  class Runtime
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
11
13
  @instance_mutex = Mutex.new
12
14
 
13
15
  class << self
@@ -64,9 +66,8 @@ module Phronomy
64
66
  @pool_registry = Phronomy::Concurrency::PoolRegistry.new(
65
67
  timer_queue_provider: -> { timer_queue }
66
68
  )
67
- @multi_agent_admissions = Phronomy::MultiAgent::AdmissionRegistry.new
68
- @agent_ownership_registry = AgentOwnershipRegistry.new(runtime: self)
69
- @team_ownership_registry = TeamOwnershipRegistry.new
69
+ @shutdown_participants = {}
70
+ @shutdown_participant_error = nil
70
71
  @lifecycle_mutex = Mutex.new
71
72
  @shutdown_mutex = Mutex.new
72
73
  @state = :running
@@ -103,83 +104,40 @@ module Phronomy
103
104
  @timer_service.timer_queue
104
105
  end
105
106
 
106
- # Returns only an immutable process-local routing/ownership view. Mutable
107
- # execution state remains inside EventLoop and is never exposed to callers.
108
- # @api private
109
- def __agent_execution_owner(execution_id)
110
- loop_instance = @lifecycle_mutex.synchronize { @event_loop }
111
- loop_instance&.agent_execution_owner(execution_id)
112
- end
113
-
114
- # @api private
115
- def __team_owner(id, klass:, create:, persistence:, &block)
116
- @team_ownership_registry.fetch(id, klass: klass, create: create, persistence: persistence, &block)
117
- end
118
-
119
- # @api private
120
- def __get_team(id, klass:)
121
- @team_ownership_registry.get(id.to_s, klass: klass)
122
- end
123
-
124
- # @api private
125
- def __create_agent(agent_id, expected_class:, &block)
126
- @agent_ownership_registry.create(agent_id, expected_class: expected_class, &block)
127
- end
128
-
129
- # @api private
130
- def __load_agent(agent_id, expected_class:, &block)
131
- @agent_ownership_registry.load(agent_id, expected_class: expected_class, &block)
132
- end
133
-
134
- # @api private
135
- def __get_agent(agent_id, expected_class:)
136
- @agent_ownership_registry.get(agent_id, expected_class: expected_class)
137
- end
138
-
139
- # @api private
140
- def __agent_owned?(agent)
141
- @agent_ownership_registry.owned?(agent)
142
- end
143
-
144
- # @api private
145
- def __begin_agent_purge(agent)
146
- @agent_ownership_registry.begin_purge(agent)
147
- end
148
-
149
- # @api private
150
- def __complete_agent_purge(agent, token)
151
- @agent_ownership_registry.complete_purge(agent, token)
152
- end
153
-
154
- # @api private
155
- def __abort_agent_purge(agent, token)
156
- @agent_ownership_registry.abort_purge(agent, token)
157
- end
158
-
107
+ # Lookup only; inspection must not create an EventLoop during shutdown.
159
108
  # @api private
160
- def __leave_agent_purge_uncertain(agent, token)
161
- @agent_ownership_registry.leave_purge_uncertain(agent, token)
109
+ def __event_loop_if_initialized
110
+ @lifecycle_mutex.synchronize { @event_loop }
162
111
  end
163
112
 
113
+ # Shares one internal participant per key for this Runtime's lifetime.
114
+ # begin_draining must be idempotent, nonblocking and must not call Runtime:
115
+ # admission closes under the same lifecycle lock as registration.
116
+ # wait_until_idle receives an absolute monotonic deadline and returns a Boolean.
117
+ # Optional after_runtime_shutdown runs outside the lifecycle lock only after
118
+ # all waits succeed, EventLoop stops, and pools/timer finish shutdown. It must
119
+ # be idempotent, short, and perform no I/O. Exceptions mark cleanup incomplete.
120
+ # This is a shutdown contract, not an application extension point.
164
121
  # @api private
165
- def __agent_execution_admitted?(agent_id)
166
- loop_instance = @lifecycle_mutex.synchronize { @event_loop }
167
- loop_instance&.agent_execution_admitted?(agent_id) || false
168
- end
122
+ def __register_shutdown_participant(key:, participant:)
123
+ unless participant.respond_to?(:begin_draining) && participant.respond_to?(:wait_until_idle)
124
+ raise ArgumentError, "shutdown participant must implement begin_draining and wait_until_idle"
125
+ end
169
126
 
170
- # @api private
171
- def __admit_multi_agent(coordinator)
172
- current_state = @lifecycle_mutex.synchronize { @state }
173
- unless current_state == :running
174
- raise Phronomy::RuntimeShutdownError,
175
- "Runtime is #{current_state}; new Multi-Agent turns are not accepted"
127
+ @lifecycle_mutex.synchronize do
128
+ unless @state == :running
129
+ raise Phronomy::RuntimeShutdownError,
130
+ "Runtime is #{@state}; shutdown participants cannot be registered"
131
+ end
132
+ @shutdown_participants[key] ||= participant
176
133
  end
177
- @multi_agent_admissions.admit!(coordinator)
178
134
  end
179
135
 
136
+ # Lookup only: never registers a participant or reopens its admission gate.
137
+ # Retained participants remain available during and after shutdown.
180
138
  # @api private
181
- def __release_multi_agent(coordinator)
182
- @multi_agent_admissions.release!(coordinator)
139
+ def __shutdown_participant(key:)
140
+ @lifecycle_mutex.synchronize { @shutdown_participants[key] }
183
141
  end
184
142
 
185
143
  def event_loop
@@ -214,6 +172,7 @@ module Phronomy
214
172
 
215
173
  @failure ||= error
216
174
  @state = :failed
175
+ begin_draining_participants
217
176
  end
218
177
  end
219
178
 
@@ -232,17 +191,14 @@ module Phronomy
232
191
  return @shutdown_result if @shutdown_result
233
192
 
234
193
  drain_deadline = monotonic_now + timeout
235
- loop_instance = @lifecycle_mutex.synchronize do
194
+ loop_instance, participants = @lifecycle_mutex.synchronize do
236
195
  @state = :draining unless @state == :failed
237
- @event_loop
196
+ begin_draining_participants
197
+ [@event_loop, @shutdown_participants.values]
238
198
  end
239
199
  loop_instance&.begin_draining
240
- @agent_ownership_registry.begin_draining
241
- @team_ownership_registry.begin_draining
242
200
 
243
- admission_idle = @multi_agent_admissions.wait_until_idle(drain_deadline)
244
- agent_ownership_stable = @agent_ownership_registry.wait_until_stable(drain_deadline)
245
- team_ownership_stable = @team_ownership_registry.wait_until_stable(drain_deadline)
201
+ participants_idle = wait_for_participants(participants, drain_deadline)
246
202
  loop_idle = !loop_instance || loop_instance.wait_until_idle(drain_deadline)
247
203
 
248
204
  @lifecycle_mutex.synchronize do
@@ -257,11 +213,15 @@ module Phronomy
257
213
  end
258
214
 
259
215
  subsystem_error = shutdown_pools_and_timer
260
- cleanup_complete = admission_idle && agent_ownership_stable && team_ownership_stable && loop_idle &&
216
+ participant_error = @lifecycle_mutex.synchronize { @shutdown_participant_error }
217
+ cleanup_complete = participants_idle && participant_error.nil? &&
218
+ loop_idle &&
261
219
  (!loop_instance || !loop_instance.thread_alive?) &&
262
220
  event_loop_status != :cancel_timeout &&
221
+ (!loop_instance || loop_instance.__receiver_cleanup_complete?) &&
263
222
  subsystem_error.nil?
264
223
 
224
+ cleanup_complete = finalize_participants(participants) if cleanup_complete
265
225
  failure = @lifecycle_mutex.synchronize { @failure } || subsystem_error
266
226
  runtime_outcome = if failure || event_loop_status == :failed
267
227
  :failed
@@ -269,9 +229,6 @@ module Phronomy
269
229
  :terminated
270
230
  end
271
231
 
272
- @agent_ownership_registry.shutdown! if cleanup_complete
273
- @team_ownership_registry.shutdown! if cleanup_complete
274
-
275
232
  result = ShutdownResult.new(
276
233
  runtime_outcome: runtime_outcome,
277
234
  cleanup_status: cleanup_complete ? :complete : :incomplete,
@@ -290,6 +247,45 @@ module Phronomy
290
247
 
291
248
  private
292
249
 
250
+ # Called with the lifecycle lock held; participants only close their gates.
251
+ def begin_draining_participants
252
+ @shutdown_participants.each_value do |participant|
253
+ participant.begin_draining
254
+ rescue => error
255
+ record_participant_failure(error)
256
+ end
257
+ end
258
+
259
+ # All gates are already closed. Wait without holding the lifecycle lock and
260
+ # give every participant the same deadline, even if an earlier wait fails.
261
+ def wait_for_participants(participants, deadline)
262
+ participants.map do |participant|
263
+ participant.wait_until_idle(deadline) == true
264
+ rescue => error
265
+ @lifecycle_mutex.synchronize { record_participant_failure(error) }
266
+ false
267
+ end.all?
268
+ end
269
+
270
+ # Finalize every participant even if one fails. Earlier releases are not
271
+ # rolled back, and a failure prevents replacement of the default Runtime.
272
+ def finalize_participants(participants)
273
+ participants.map do |participant|
274
+ participant.after_runtime_shutdown if participant.respond_to?(:after_runtime_shutdown)
275
+ true
276
+ rescue => error
277
+ @lifecycle_mutex.synchronize { record_participant_failure(error) }
278
+ false
279
+ end.all?
280
+ end
281
+
282
+ # Called with the lifecycle lock held. Failed hooks make cleanup uncertain.
283
+ def record_participant_failure(error)
284
+ @shutdown_participant_error ||= error
285
+ @failure ||= error
286
+ @state = :failed
287
+ end
288
+
293
289
  def ensure_accepting_work!
294
290
  current_state = @lifecycle_mutex.synchronize { @state }
295
291
  return if %i[running draining].include?(current_state)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class RuntimeShutdownError < Error; end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "runtime_shutdown_error"
4
+
5
+ module Phronomy
6
+ class RuntimeShutdownReentrancyError < RuntimeShutdownError; end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "event_loop_reentrancy_error"
4
+
5
+ module Phronomy
6
+ # Backward-compatible error class name for callers that still rescue the old
7
+ # scheduler-oriented exception. New code should use EventLoopReentrancyError.
8
+ class SchedulerReentrancyError < EventLoopReentrancyError; end
9
+ end
@@ -1,22 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "concurrency/worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  # A thread-free asynchronous completion handle.
5
7
  #
6
- # Task does not execute work. Execution belongs to EventLoop/FSMSession or
7
- # OffloadPool. Task represents completion, failure, cancellation, callbacks,
8
+ # TaskResult does not execute work. Execution belongs to EventLoop/FSMSession or
9
+ # OffloadPool. TaskResult represents completion, failure, cancellation, callbacks,
8
10
  # and a blocking wait for callers outside EventLoop.
9
11
  #
10
- # Framework components own Task settlement. Application code should observe a
11
- # Task through {#wait_result}, {#on_complete}, {#map}, and state readers rather
12
+ # Framework components own TaskResult settlement. Application code should observe a
13
+ # TaskResult through {#wait_result}, {#on_complete}, {#map}, and state readers rather
12
14
  # than calling {#complete}, {#fail}, or {#cancel!}. Operation-wide cancellation
13
15
  # is supplied through the CancellationToken accepted by the API that created
14
- # the Task.
15
- class Task
16
+ # the TaskResult.
17
+ class TaskResult
18
+ include Phronomy::Concurrency::WorkerInputRestricted
19
+
16
20
  STATES = %i[pending completed failed cancelled].freeze
17
21
  TERMINAL_STATES = %i[completed failed cancelled].freeze
18
22
  private_constant :TERMINAL_STATES
19
23
 
24
+ # A shallow immutable input-position record. Application values are retained
25
+ # by reference; they are neither copied nor frozen by this record.
26
+ # @api public
27
+ class Outcome < Struct.new(:index, :status, :value, :error)
28
+ def initialize(**fields)
29
+ super
30
+ freeze
31
+ end
32
+ end
33
+
34
+ # Observe already-created results without taking ownership of their work.
35
+ # Invalid inputs are rejected before any completion subscription is added.
36
+ # @param results [Array<TaskResult>]
37
+ # @return [TaskResult<Array<Outcome>>]
38
+ # @raise [TypeError] for a non-Array list or non-TaskResult element
39
+ # @api public
40
+ def self.all_settled(results)
41
+ raise TypeError, "results must be an Array" unless results.is_a?(Array)
42
+
43
+ inputs = results.dup
44
+ inputs.each_with_index do |result, index|
45
+ unless result.is_a?(Phronomy::TaskResult)
46
+ raise TypeError, "results[#{index}] must be a Phronomy::TaskResult"
47
+ end
48
+ end
49
+ combined = Phronomy::TaskResult.deferred(name: "all-settled")
50
+ collector = Concurrency::ResultCollector.new(inputs.length) do |_, outcomes|
51
+ combined.complete(outcomes)
52
+ end
53
+ inputs.each_with_index { |result, index| collector.watch(index, result) }
54
+ collector.finish(:completed)
55
+ combined
56
+ end
57
+
20
58
  # Creates an unsettled completion handle for framework-owned execution.
21
59
  # @api private
22
60
  def self.deferred(name: nil, parent: nil)
@@ -24,26 +62,26 @@ module Phronomy
24
62
  end
25
63
 
26
64
  # Public factories for already-settled values. No execution is started.
27
- # Always create a base Task: there is no physical worker to supervise.
65
+ # Always create a base TaskResult: there is no physical worker to supervise.
28
66
  # @param value [Object] already available result
29
67
  # @param name [String, nil] optional diagnostic name
30
- # @return [Phronomy::Task] a completed base Task
68
+ # @return [Phronomy::TaskResult] a completed base TaskResult
31
69
  # @api public
32
70
  def self.completed(value = nil, name: nil)
33
- Phronomy::Task.deferred(name: name).tap { |task| task.complete(value) }
71
+ Phronomy::TaskResult.deferred(name: name).tap { |task| task.complete(value) }
34
72
  end
35
73
 
36
74
  # Represents an already known failure without raising the stored error.
37
75
  # Observation through wait_result raises it; on_complete receives it.
38
76
  # @param error [Exception] original failure object
39
77
  # @param name [String, nil] optional diagnostic name
40
- # @return [Phronomy::Task] a failed base Task
78
+ # @return [Phronomy::TaskResult] a failed base TaskResult
41
79
  # @raise [ArgumentError] if error is not an Exception
42
80
  # @api public
43
81
  def self.failed(error, name: nil)
44
82
  raise ArgumentError, "error must be an Exception" unless error.is_a?(Exception)
45
83
 
46
- Phronomy::Task.deferred(name: name).tap { |task| task.fail(error) }
84
+ Phronomy::TaskResult.deferred(name: name).tap { |task| task.fail(error) }
47
85
  end
48
86
 
49
87
  attr_reader :name, :parent
@@ -68,13 +106,13 @@ module Phronomy
68
106
  @mutex.synchronize { @status }
69
107
  end
70
108
 
71
- # @return [Boolean] whether the Task has reached a terminal state
109
+ # @return [Boolean] whether the TaskResult has reached a terminal state
72
110
  # @api public
73
111
  def done?
74
112
  @mutex.synchronize { TERMINAL_STATES.include?(@status) }
75
113
  end
76
114
 
77
- # @return [Boolean] whether the Task has not yet reached a terminal state
115
+ # @return [Boolean] whether the TaskResult has not yet reached a terminal state
78
116
  # @api public
79
117
  def alive?
80
118
  !done?
@@ -82,19 +120,19 @@ module Phronomy
82
120
 
83
121
  # Blocks the calling thread until settlement.
84
122
  #
85
- # EventLoop is never allowed to wait for a Task; framework continuation must
123
+ # EventLoop is never allowed to wait for a TaskResult; framework continuation must
86
124
  # proceed through explicit events. The optional timeout is waiter-local: it
87
- # does not settle or cancel the Task.
125
+ # does not settle or cancel the TaskResult.
88
126
  #
89
127
  # @param timeout [Numeric, nil] maximum seconds this caller will block
90
128
  # @return [Object] the completed value
91
129
  # @raise [Phronomy::TimeoutError] when the waiter-local timeout expires
92
- # @raise [Exception] the error that settled the Task
130
+ # @raise [Exception] the error that settled the TaskResult
93
131
  # @api public
94
132
  def wait_result(timeout: nil)
95
133
  if Phronomy::Runtime.in_event_loop_context? && !done?
96
134
  raise Phronomy::EventLoopReentrancyError,
97
- "Task#wait_result cannot block the EventLoop thread; continue via an event"
135
+ "TaskResult#wait_result cannot block the EventLoop thread; continue via an event"
98
136
  end
99
137
 
100
138
  deadline = timeout && monotonic_now + timeout.to_f
@@ -104,7 +142,7 @@ module Phronomy
104
142
  remaining = deadline - monotonic_now
105
143
  if remaining <= 0
106
144
  raise Phronomy::TimeoutError,
107
- "timed out waiting for Task #{@name || "(unnamed)"}"
145
+ "timed out waiting for TaskResult #{@name || "(unnamed)"}"
108
146
  end
109
147
  @cond.wait(@mutex, remaining)
110
148
  else
@@ -118,13 +156,13 @@ module Phronomy
118
156
  value
119
157
  end
120
158
 
121
- # Compatibility wait that does not re-raise the Task error.
159
+ # Compatibility wait that does not re-raise the TaskResult error.
122
160
  # Returns self when settled, nil on timeout.
123
161
  # @api private
124
162
  def join(limit = nil)
125
163
  if Phronomy::Runtime.in_event_loop_context? && !done?
126
164
  raise Phronomy::EventLoopReentrancyError,
127
- "Task#join cannot block the EventLoop thread; continue via an event"
165
+ "TaskResult#join cannot block the EventLoop thread; continue via an event"
128
166
  end
129
167
 
130
168
  deadline = limit && monotonic_now + limit.to_f
@@ -148,7 +186,7 @@ module Phronomy
148
186
  # registers after settlement, an OffloadPool worker, or a framework control
149
187
  # thread. Callbacks must therefore be thread-safe and should complete quickly.
150
188
  # A callback failure is logged and does not suppress delivery to other
151
- # completion callbacks or change the Task's already-settled result.
189
+ # completion callbacks or change the TaskResult's already-settled result.
152
190
  #
153
191
  # @yield [value, error]
154
192
  # @return [self]
@@ -168,53 +206,74 @@ module Phronomy
168
206
  self
169
207
  end
170
208
 
171
- # Settles this Task successfully. Framework-owned settlement API.
209
+ # Settles this TaskResult successfully. Framework-owned settlement API.
172
210
  # @api private
173
211
  def complete(value = nil)
174
212
  settle!(:completed, value: value)
175
213
  end
176
214
 
177
- # Settles this Task with a failure. Framework-owned settlement API.
215
+ # Settles this TaskResult with a failure. Framework-owned settlement API.
178
216
  # @api private
179
217
  def fail(error)
180
218
  raise ArgumentError, "error is required" unless error
181
219
  settle!(:failed, error: error)
182
220
  end
183
221
 
184
- # Settles this Task as cancelled. Framework-owned settlement API.
222
+ # Settles this TaskResult as cancelled. Framework-owned settlement API.
185
223
  #
186
224
  # This method does not propagate backwards into a CancellationToken that may
187
- # have been used to create the Task. Tokens can be shared across operations;
225
+ # have been used to create the TaskResult. Tokens can be shared across operations;
188
226
  # operation-wide cancellation is owned by the creating API.
189
227
  # @api private
190
- def cancel!(error = Phronomy::CancellationError.new("Task cancelled"))
228
+ def cancel!(error = Phronomy::CancellationError.new("TaskResult cancelled"))
191
229
  changed = settle!(:cancelled, error: error)
192
230
  if changed
193
231
  children = @mutex.synchronize { @children.dup }
194
- children.each(&:cancel!)
232
+ children.each { |child| child.cancel!(error) }
195
233
  end
196
234
  self
197
235
  end
198
236
 
199
- # Creates a derived Task by transforming this Task's successful result.
237
+ # Creates a derived TaskResult by transforming this TaskResult's successful result.
200
238
  # @api public
201
239
  def map(&block)
202
240
  raise ArgumentError, "map requires a block" unless block
203
241
 
204
- mapped = self.class.deferred(name: "#{@name}-mapped", parent: @parent)
205
- on_complete do |value, error|
206
- if error
207
- mapped.fail(error)
208
- next
209
- end
242
+ Concurrency::ResultComposition.new(self, flatten: false, &block).start
243
+ end
210
244
 
211
- begin
212
- mapped.complete(block.call(value))
213
- rescue => mapped_error
214
- mapped.fail(mapped_error)
215
- end
216
- end
217
- mapped
245
+ # Transform a successful value into the result of another asynchronous step.
246
+ # The block must return a TaskResult. No implicit wrapping or nested value
247
+ # flattening is performed by map.
248
+ # @return [TaskResult]
249
+ # @api public
250
+ def flat_map(&block)
251
+ raise ArgumentError, "flat_map requires a block" unless block
252
+
253
+ Concurrency::ResultComposition.new(self, flatten: true, &block).start
254
+ end
255
+
256
+ # Atomic state observation and removable framework subscriptions.
257
+ # @api private
258
+ def __snapshot
259
+ @mutex.synchronize { [@status, @value, @error] }
260
+ end
261
+
262
+ # @api private
263
+ def __unsubscribe(callback)
264
+ @mutex.synchronize { @on_complete_callbacks.delete(callback) }
265
+ end
266
+
267
+ # Bound at admission, before application code can attach continuations.
268
+ # @api private
269
+ def __bind_execution(execution)
270
+ @execution_scope = execution
271
+ self
272
+ end
273
+
274
+ # @api private
275
+ def __execution_scope
276
+ @execution_scope
218
277
  end
219
278
 
220
279
  protected
@@ -250,7 +309,7 @@ module Phronomy
250
309
  callback.call(value, error)
251
310
  rescue => callback_error
252
311
  Phronomy.configuration.logger&.error do
253
- "[Task] on_complete callback raised #{callback_error.class}: #{callback_error.message}"
312
+ "[TaskResult] on_complete callback raised #{callback_error.class}: #{callback_error.message}"
254
313
  end
255
314
  end
256
315
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class TimeoutError < Error; end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../common/error"
4
+
5
+ module Phronomy
6
+ class FilterBlockError < Error
7
+ attr_reader :filter
8
+
9
+ def initialize(message, filter: nil)
10
+ super(message)
11
+ @filter = filter
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class GeneratorVerifier
5
+ # Converts Agent events to correlated Workflow events. PipelineState owns
6
+ # stale-result rejection and all Workflow state mutation.
7
+ class AgentResultReceiver
8
+ def initialize(draft_result_parser:, review_result_parser:)
9
+ @draft_result_parser = draft_result_parser
10
+ @review_result_parser = review_result_parser
11
+ end
12
+
13
+ def draft_listener(workflow:, workflow_instance_id:, request_id:)
14
+ agent_listener(:draft, workflow, workflow_instance_id, request_id) do |output|
15
+ draft_payload(output)
16
+ end
17
+ end
18
+
19
+ def review_listener(workflow:, workflow_instance_id:, request_id:)
20
+ agent_listener(:review, workflow, workflow_instance_id, request_id) do |output|
21
+ review_payload(output)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def draft_payload(output)
28
+ parsed = @draft_result_parser.call(output)
29
+ {
30
+ draft: parsed[:answer].to_s,
31
+ self_score: clamp(parsed[:confidence]),
32
+ citations: normalize_citations(parsed[:citations])
33
+ }
34
+ end
35
+
36
+ def review_payload(output)
37
+ parsed = @review_result_parser.call(output)
38
+ {
39
+ review_score: clamp(parsed[:score]),
40
+ approved: parsed[:approved] == true,
41
+ feedback: parsed[:feedback].to_s
42
+ }
43
+ end
44
+
45
+ def agent_listener(phase, workflow, workflow_instance_id, request_id, &completion)
46
+ ->(agent_event) {
47
+ case agent_event.type
48
+ when :done
49
+ begin
50
+ payload = completion.call(agent_event.payload[:output])
51
+ workflow.signal(
52
+ workflow_instance_id: workflow_instance_id,
53
+ event: :"#{phase}_completed",
54
+ payload: {request_id: request_id, **payload}
55
+ )
56
+ rescue => error
57
+ signal_failure(workflow, workflow_instance_id, request_id, phase, error)
58
+ end
59
+ when :error, :timeout, :cancelled
60
+ error = agent_event.payload[:error] ||
61
+ Phronomy::Error.new("#{phase.to_s.capitalize} Agent ended with #{agent_event.type}")
62
+ signal_failure(workflow, workflow_instance_id, request_id, phase, error)
63
+ when :approval_required
64
+ error = Phronomy::Error.new("GeneratorVerifier #{phase} Agent suspended for approval")
65
+ signal_failure(workflow, workflow_instance_id, request_id, phase, error)
66
+ end
67
+ }
68
+ end
69
+
70
+ def signal_failure(workflow, workflow_instance_id, request_id, phase, error)
71
+ workflow.signal(
72
+ workflow_instance_id: workflow_instance_id,
73
+ event: :"#{phase}_failed",
74
+ payload: {request_id: request_id, error: error}
75
+ )
76
+ end
77
+
78
+ def clamp(value)
79
+ value.to_f.clamp(0.0, 1.0)
80
+ end
81
+
82
+ def normalize_citations(raw)
83
+ Array(raw).filter_map do |citation|
84
+ citation.is_a?(Hash) ? citation.transform_keys(&:to_sym) : nil
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end