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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class GeneratorVerifier
5
+ # Connects one reused Agent incarnation through its public operation and
6
+ # notification contracts. Terminal results belong to each returned TaskResult;
7
+ # only approval suspension needs the incarnation listener while that Task is
8
+ # still pending. Generation owns the request-to-Workflow correlation.
9
+ # @api private
10
+ class AgentOperation
11
+ def initialize(agent_class)
12
+ @mutex = Mutex.new
13
+ @pending = []
14
+ @agent = agent_class.new(on_event: method(:receive_event))
15
+ end
16
+
17
+ def start(input, listener:)
18
+ request = Object.new
19
+ # Workflow entry calls are serial. Keep submitted requests until their
20
+ # own result settles: Agent admission itself may run after an earlier
21
+ # request completes, so a pending submission is not necessarily busy.
22
+ @mutex.synchronize { @pending << [request, listener] }
23
+ task = @agent.invoke_async(input)
24
+ task.on_complete do |result, error|
25
+ release(request)
26
+ # Handoff was not a generation completion in the event-based path.
27
+ next if !error && result.is_a?(Hash) && result[:handoff_request]
28
+
29
+ event = if error
30
+ type = if error.is_a?(Phronomy::TimeoutError)
31
+ :timeout
32
+ elsif error.is_a?(Phronomy::CancellationError)
33
+ :cancelled
34
+ else
35
+ :error
36
+ end
37
+ Phronomy::Agent::StreamEvent.new(type: type, payload: {error: error})
38
+ else
39
+ Phronomy::Agent::StreamEvent.new(type: :done, payload: result)
40
+ end
41
+ listener.call(event)
42
+ end
43
+ task
44
+ rescue
45
+ release(request)
46
+ raise
47
+ end
48
+
49
+ private
50
+
51
+ def receive_event(event)
52
+ return unless event.type == :approval_required
53
+
54
+ listener = @mutex.synchronize { @pending.first&.last }
55
+ listener&.call(event)
56
+ end
57
+
58
+ def release(request)
59
+ @mutex.synchronize do
60
+ @pending.delete_if { |entry| entry.first.equal?(request) }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class GeneratorVerifier
5
+ # Boot supplies the default concrete parser. The generation algorithm owns
6
+ # its output interpretation and fallback, not the concrete parser selection.
7
+ # @api private
8
+ module DefaultParser
9
+ FACTORIES = {}
10
+ private_constant :FACTORIES
11
+
12
+ # Private one-time composition binding; not an application registry.
13
+ # @api private
14
+ def self.install_factory(factory)
15
+ raise ArgumentError, "factory must respond to call" unless factory.respond_to?(:call)
16
+
17
+ FACTORIES.replace(parser: factory).freeze
18
+ nil
19
+ end
20
+
21
+ # @api private
22
+ def self.build
23
+ FACTORIES.fetch(:parser).call
24
+ end
25
+ end
26
+ end
27
+ end
@@ -71,7 +71,7 @@ module Phronomy
71
71
  workflow_instance_id: next_state.workflow_instance_id,
72
72
  request_id: request_id
73
73
  )
74
- @draft_agent.send(:__invoke_async_with_event_sink, prompt, on_event: listener)
74
+ @draft_agent.start(prompt, listener: listener)
75
75
  next_state
76
76
  end
77
77
 
@@ -86,7 +86,7 @@ module Phronomy
86
86
  workflow_instance_id: next_state.workflow_instance_id,
87
87
  request_id: request_id
88
88
  )
89
- @review_agent.send(:__invoke_async_with_event_sink, prompt, on_event: listener)
89
+ @review_agent.start(prompt, listener: listener)
90
90
  next_state
91
91
  end
92
92
 
@@ -20,7 +20,7 @@ module Phronomy
20
20
  alias_method :trusted?, :trusted
21
21
  end
22
22
 
23
- private_constant :PipelineState, :WorkflowBuilder, :AgentResultReceiver
23
+ private_constant :PipelineState, :WorkflowBuilder, :AgentResultReceiver, :AgentOperation
24
24
 
25
25
  def initialize(
26
26
  draft_agent:,
@@ -80,8 +80,8 @@ module Phronomy
80
80
 
81
81
  def build_workflow
82
82
  WorkflowBuilder.new(
83
- draft_agent: @draft_agent_class.new,
84
- review_agent: @review_agent_class.new,
83
+ draft_agent: AgentOperation.new(@draft_agent_class),
84
+ review_agent: AgentOperation.new(@review_agent_class),
85
85
  draft_prompt_builder: @draft_prompt_builder,
86
86
  review_prompt_builder: @review_prompt_builder,
87
87
  draft_result_parser: @draft_result_parser,
@@ -92,7 +92,7 @@ module Phronomy
92
92
  end
93
93
 
94
94
  def default_parse_draft(text)
95
- json_parser.parse(text)
95
+ default_parser.parse(text)
96
96
  rescue Phronomy::ParseError
97
97
  {
98
98
  answer: text.to_s,
@@ -102,7 +102,7 @@ module Phronomy
102
102
  end
103
103
 
104
104
  def default_parse_review(text)
105
- json_parser.parse(text)
105
+ default_parser.parse(text)
106
106
  rescue Phronomy::ParseError
107
107
  {
108
108
  approved: false,
@@ -111,8 +111,8 @@ module Phronomy
111
111
  }
112
112
  end
113
113
 
114
- def json_parser
115
- @json_parser ||= Phronomy::OutputParser::JsonParser.new
114
+ def default_parser
115
+ @default_parser ||= DefaultParser.build
116
116
  end
117
117
  end
118
118
  end
@@ -6,9 +6,9 @@ module Phronomy
6
6
  class LowConfidenceError < Error
7
7
  attr_reader :result
8
8
 
9
- def initialize(result)
9
+ def initialize(result, code: nil)
10
10
  @result = result
11
- super("Answer confidence #{result.confidence} is below the required threshold")
11
+ super("Answer confidence #{result.confidence} is below the required threshold", code: code)
12
12
  end
13
13
  end
14
14
  end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ module Integrations
7
+ # Optional transport helper. Payload meaning and destinations belong to the app.
8
+ # @api public
9
+ class OrderedEventDelivery
10
+ class ClosedError < Phronomy::Error; end
11
+ class OverflowError < Phronomy::Error; end
12
+
13
+ # Close and drain after the block, preserving its result or original failure.
14
+ # A flush timeout limits waiting, not an in-flight network request.
15
+ # @api public
16
+ def self.open(deliver:, capacity: 256, batch_size: 32, flush_timeout: 30, prepare_batch: nil)
17
+ raise ArgumentError, "block required" unless block_given?
18
+ reject_event_loop!
19
+ validate_timeout!(flush_timeout)
20
+ raise ArgumentError, "deliver must be callable" unless deliver.respond_to?(:call)
21
+ delivery = new(capacity: capacity, batch_size: batch_size, prepare_batch: prepare_batch, &deliver)
22
+ begin
23
+ yield delivery
24
+ ensure
25
+ primary = $!
26
+ begin
27
+ delivery.close_and_wait(timeout: flush_timeout)
28
+ rescue => secondary
29
+ if primary
30
+ report_secondary(secondary)
31
+ else
32
+ raise
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.reject_event_loop!
39
+ if Phronomy::Runtime.in_event_loop_context?
40
+ raise Phronomy::EventLoopReentrancyError, "delivery flush cannot block EventLoop"
41
+ end
42
+ end
43
+
44
+ def self.validate_timeout!(timeout)
45
+ valid = timeout.nil? || (timeout.is_a?(Numeric) && timeout.finite? && timeout >= 0)
46
+ raise ArgumentError, "flush timeout must be a finite nonnegative number or nil" unless valid
47
+ end
48
+
49
+ def self.report_secondary(error)
50
+ Phronomy.configuration.logger&.error("[OrderedEventDelivery] secondary delivery failure: #{error.class}: #{error.message}")
51
+ rescue
52
+ # Diagnostic reporting must not replace the primary application error.
53
+ nil
54
+ end
55
+ private_class_method :reject_event_loop!, :validate_timeout!, :report_secondary
56
+
57
+ # Construction does not start a worker or create a pool.
58
+ # @api public
59
+ def initialize(capacity: 256, batch_size: 32, prepare_batch: nil, &deliver)
60
+ raise ArgumentError, "deliver block required" unless deliver
61
+ unless capacity.is_a?(Integer) && capacity.positive? && batch_size.is_a?(Integer) && batch_size.positive?
62
+ raise ArgumentError, "capacity and batch_size must be positive Integers"
63
+ end
64
+ raise ArgumentError, "prepare_batch must be callable" if prepare_batch && !prepare_batch.respond_to?(:call)
65
+ @capacity, @batch_size = capacity, batch_size
66
+ @prepare_batch, @deliver = prepare_batch, deliver
67
+ @mutex = Mutex.new
68
+ @condition = ConditionVariable.new
69
+ @queue = []
70
+ @accepting = true
71
+ @worker = nil
72
+ @failure = nil
73
+ end
74
+
75
+ # Snapshot plain data; admission order is the queue insertion order.
76
+ # Capacity counts queued payloads, excluding the active batch.
77
+ # @api public
78
+ def publish(payload)
79
+ value = immutable_copy(payload)
80
+ ticket = @mutex.synchronize do
81
+ raise @failure if @failure
82
+ raise ClosedError.new("event delivery closed", code: "delivery.closed") unless @accepting
83
+ raise OverflowError.new("event delivery queue full", code: "delivery.capacity_exceeded") if @queue.length >= @capacity
84
+ @queue << value
85
+ next nil if @worker
86
+ @worker = {started: false}
87
+ end
88
+ start_drain(ticket) if ticket
89
+ error = @mutex.synchronize { @failure }
90
+ raise error if error
91
+ self
92
+ rescue => error
93
+ # Listener dispatch can log an exception and continue. Retain the failure
94
+ # for close while continuing to drain already accepted work if possible.
95
+ @mutex.synchronize do
96
+ @failure ||= error
97
+ @accepting = false
98
+ end
99
+ raise
100
+ end
101
+
102
+ # External callers only. Concurrent close calls observe the same drain.
103
+ # Timeout does not abort a network operation or settle the worker's result.
104
+ # @api public
105
+ def close_and_wait(timeout: nil)
106
+ self.class.send(:reject_event_loop!)
107
+ self.class.send(:validate_timeout!, timeout)
108
+ deadline = timeout && monotonic_now + timeout
109
+ @mutex.synchronize do
110
+ @accepting = false
111
+ while @worker
112
+ remaining = deadline && deadline - monotonic_now
113
+ raise Phronomy::TimeoutError, "event delivery flush timed out" if remaining && remaining <= 0
114
+ @condition.wait(@mutex, remaining)
115
+ end
116
+ raise @failure if @failure
117
+ end
118
+ self
119
+ end
120
+
121
+ private
122
+
123
+ def start_drain(ticket)
124
+ Phronomy::Blocking.call_async do
125
+ started = @mutex.synchronize do
126
+ ticket[:started] = true if @worker.equal?(ticket)
127
+ end
128
+ if started
129
+ begin
130
+ drain(ticket)
131
+ rescue => error
132
+ fail_worker(ticket, error)
133
+ ensure
134
+ finish_worker(ticket)
135
+ end
136
+ end
137
+ end.on_complete do |_value, error|
138
+ fail_worker(ticket, error) if error
139
+ end
140
+ rescue => error
141
+ fail_worker(ticket, error)
142
+ end
143
+
144
+ def drain(ticket)
145
+ loop do
146
+ batch = @mutex.synchronize do
147
+ if @queue.empty?
148
+ # Publish must observe queue emptiness and ownership release together.
149
+ release_worker(ticket)
150
+ nil
151
+ else
152
+ @queue.shift(@batch_size)
153
+ end
154
+ end
155
+ return unless batch
156
+ prepared = @prepare_batch ? @prepare_batch.call(batch.freeze) : batch
157
+ raise ArgumentError, "prepare_batch must return an Array" unless prepared.is_a?(Array)
158
+ immutable_copy(prepared).each { |payload| @deliver.call(payload) }
159
+ end
160
+ end
161
+
162
+ def fail_worker(ticket, error)
163
+ @mutex.synchronize do
164
+ @failure ||= error
165
+ @accepting = false
166
+ @queue.clear
167
+ # Logical cancellation can precede physical worker exit. Keep waiting
168
+ # for a started delivery; never mistake settled TaskResult for exit.
169
+ release_worker(ticket) unless ticket[:started]
170
+ end
171
+ end
172
+
173
+ def finish_worker(ticket)
174
+ @mutex.synchronize { release_worker(ticket) }
175
+ end
176
+
177
+ def release_worker(ticket)
178
+ return unless @worker.equal?(ticket)
179
+ @worker = nil
180
+ @condition.broadcast
181
+ end
182
+
183
+ def immutable_copy(value, ancestors = {})
184
+ case value
185
+ when Hash, Array
186
+ raise ArgumentError, "event payload must contain acyclic plain data" if ancestors[value.object_id]
187
+ path = ancestors.merge(value.object_id => true)
188
+ if value.is_a?(Hash)
189
+ value.to_h { |key, child| [immutable_copy(key, path), immutable_copy(child, path)] }.freeze
190
+ else
191
+ value.map { |child| immutable_copy(child, path) }.freeze
192
+ end
193
+ when String then value.dup.freeze
194
+ when Integer, Symbol, TrueClass, FalseClass, NilClass then value
195
+ when Float
196
+ raise ArgumentError, "event payload numbers must be finite" unless value.finite?
197
+ value
198
+ else
199
+ raise ArgumentError, "event payload must contain plain data: #{value.class}"
200
+ end
201
+ end
202
+
203
+ def monotonic_now
204
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module LLMAdapter
5
+ # Offloads the entire operation, including SDK initialization and decoding.
6
+ # Construction does not start a Runtime.
7
+ # @api private
8
+ class AsyncClient
9
+ # @api private
10
+ def initialize(adapter:, pool: nil, submitter: Phronomy::Execution)
11
+ @adapter = adapter
12
+ @pool = pool
13
+ @submitter = submitter
14
+ end
15
+
16
+ # @api private
17
+ def complete_async(request, cancellation_token: nil, pool: @pool)
18
+ submit(pool, cancellation_token) do
19
+ @adapter.complete(request, cancellation_token: cancellation_token)
20
+ end
21
+ end
22
+
23
+ # The block is an internal event sink, never an Application callback.
24
+ # @api private
25
+ def stream_async(request, cancellation_token: nil, pool: @pool, &block)
26
+ raise ArgumentError, "stream_async requires a block" unless block
27
+ submit(pool, cancellation_token) do
28
+ @adapter.stream(request, cancellation_token: cancellation_token) do |chunk|
29
+ cancellation_token&.raise_if_cancelled!
30
+ block.call(chunk)
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def submit(pool, cancellation_token, &operation)
38
+ if pool
39
+ Phronomy::Execution.submit(pool: pool, cancellation_token: cancellation_token,
40
+ on_full: :raise, &operation)
41
+ else
42
+ @submitter.submit(cancellation_token: cancellation_token, on_full: :raise, &operation)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,5 +3,7 @@
3
3
  require_relative "transport_error"
4
4
 
5
5
  module Phronomy
6
- class AuthenticationError < TransportError; end
6
+ module LLMAdapter
7
+ class AuthenticationError < TransportError; end
8
+ end
7
9
  end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Phronomy
6
+ module LLMAdapter
7
+ # SDK objects are operation-local. Only Phronomy request/result values cross
8
+ # the backend boundary; a Tool definition never contains executable work.
9
+ # @api public
10
+ class RubyLLM < Base
11
+ ToolsRequested = Class.new(StandardError) do
12
+ attr_reader :message_value
13
+ def initialize(message_value)
14
+ @message_value = message_value
15
+ super("assistant requested Tools")
16
+ end
17
+ end
18
+ private_constant :ToolsRequested
19
+
20
+ # @api public
21
+ def identity
22
+ super.merge("sdk_version" => ::RubyLLM::VERSION).freeze
23
+ end
24
+
25
+ # @api public
26
+ def input_budget(model_config)
27
+ config = model_config.to_h.transform_keys(&:to_s)
28
+ return nil unless config["model"]
29
+ model = ::RubyLLM.models.find(config["model"], provider: config["provider"])
30
+ limit = model&.context_window
31
+ return nil unless limit.is_a?(Integer) && limit.positive?
32
+ Phronomy::LlmContextWindow::TokenBudget.new(max_input_tokens: limit)
33
+ rescue ::RubyLLM::ModelNotFoundError
34
+ nil
35
+ rescue => error
36
+ raise_provider_error(error)
37
+ end
38
+
39
+ protected
40
+
41
+ # @api public
42
+ def perform_complete(request, cancellation_token:)
43
+ execute_request(request, cancellation_token)
44
+ end
45
+
46
+ # @api public
47
+ def perform_stream(request, cancellation_token:, &block)
48
+ execute_request(request, cancellation_token, &block)
49
+ end
50
+
51
+ private
52
+
53
+ def execute_request(request, token)
54
+ callback_error = nil
55
+ chat = build_chat(request)
56
+ token&.raise_if_cancelled!
57
+ chat.after_message do |message|
58
+ raise ToolsRequested.new(message) if message.tool_call?
59
+ end
60
+ response = if block_given?
61
+ sink = proc do |chunk|
62
+ token&.raise_if_cancelled!
63
+ value = stream_value(chunk)
64
+ begin
65
+ yield value
66
+ rescue => error
67
+ callback_error = error
68
+ raise
69
+ end
70
+ end
71
+ request.message ? chat.ask(request.message, &sink) : chat.complete(&sink)
72
+ else
73
+ request.message ? chat.ask(request.message) : chat.complete
74
+ end
75
+ response_value(response)
76
+ rescue ToolsRequested => requested
77
+ response_value(requested.message_value)
78
+ rescue => error
79
+ raise error if error.equal?(callback_error)
80
+ raise_provider_error(error)
81
+ end
82
+
83
+ def build_chat(request)
84
+ config = request.model_config
85
+ options = {}
86
+ options[:model] = config["model"] if config["model"]
87
+ if config["provider"]
88
+ options[:provider] = config["provider"].to_sym
89
+ options[:assume_model_exists] = config.fetch("assume_model_exists", true)
90
+ elsif config.key?("assume_model_exists")
91
+ options[:assume_model_exists] = config["assume_model_exists"]
92
+ end
93
+ chat = ::RubyLLM.chat(**options)
94
+ chat.with_temperature(config["temperature"]) if config["temperature"]
95
+ chat.with_max_output_tokens(config["max_output_tokens"]) if config["max_output_tokens"]
96
+ if request.system
97
+ chat.with_instructions(request.system, cache_until_here: config["cache_instructions"])
98
+ end
99
+ request.tools.each { |definition| chat.with_tools(tool_declaration(definition)) }
100
+ request.messages.each { |value| chat.messages << sdk_message(value) }
101
+ chat
102
+ end
103
+
104
+ def tool_declaration(definition)
105
+ name = definition.fetch("name")
106
+ klass = Class.new(::RubyLLM::Tool) do
107
+ define_singleton_method(:tool_name) { name }
108
+ define_method(:execute) do |**_arguments|
109
+ raise InvalidResultError, "SDK attempted to execute a declaration-only Tool"
110
+ end
111
+ end
112
+ klass.description(definition.fetch("description"))
113
+ klass.parameters(definition.fetch("parameters_schema"))
114
+ klass.provider_options(definition.fetch("provider_options", {}))
115
+ klass
116
+ end
117
+
118
+ def sdk_message(value)
119
+ calls = value.tool_calls.to_h do |call|
120
+ [call.id, ::RubyLLM::ToolCall.new(id: call.id, name: call.name,
121
+ arguments: call.arguments, thought_signature: call.metadata["thought_signature"])]
122
+ end
123
+ content = value.content
124
+ content = JSON.generate(content) if content.is_a?(Hash) || content.is_a?(Array)
125
+ content = "" if value.role == :assistant && content.nil? && !calls.empty?
126
+ ::RubyLLM::Message.new(role: value.role, content: content,
127
+ tool_calls: calls.empty? ? nil : calls, tool_call_id: value.tool_call_id,
128
+ model: value.metadata["model_id"])
129
+ end
130
+
131
+ def response_value(message)
132
+ unless message.respond_to?(:content) && message.respond_to?(:role) && message.role.to_sym == :assistant
133
+ raise InvalidResultError, "provider returned an invalid assistant message"
134
+ end
135
+ raw_calls = message.tool_calls if message.respond_to?(:tool_calls)
136
+ calls = raw_calls.is_a?(Hash) ? raw_calls.values : (raw_calls || [])
137
+ raise InvalidResultError, "provider returned invalid Tool calls" unless calls.is_a?(Array)
138
+ calls = calls.map do |call|
139
+ metadata = {}
140
+ if call.respond_to?(:thought_signature) && call.thought_signature
141
+ metadata["thought_signature"] = call.thought_signature
142
+ end
143
+ Phronomy::Tool::CallRequest.new(id: call.id, name: call.name,
144
+ arguments: call.arguments, metadata: metadata)
145
+ end
146
+ tokens = message.tokens if message.respond_to?(:tokens)
147
+ usage = Phronomy::LLMAdapter::TokenUsage.new(
148
+ input: tokens&.input, output: tokens&.output,
149
+ cached: tokens&.cache_read, cache_creation: tokens&.cache_write
150
+ )
151
+ metadata = {}
152
+ metadata["model_id"] = message.model if message.respond_to?(:model) && message.model
153
+ Response.new(content: message.content, tool_calls: calls, usage: usage, metadata: metadata)
154
+ rescue ArgumentError, TypeError, NoMethodError => error
155
+ raise InvalidResultError, "malformed provider response: #{error.message}"
156
+ end
157
+
158
+ def stream_value(chunk)
159
+ raise InvalidResultError, "provider returned an invalid stream chunk" unless chunk.respond_to?(:content)
160
+ StreamChunk.new(content: chunk.content)
161
+ rescue ArgumentError => error
162
+ raise InvalidResultError, "malformed provider chunk: #{error.message}"
163
+ end
164
+
165
+ # Translate SDK errors only. Cancellation and callback failures retain
166
+ # identity; raising here preserves the original provider exception cause.
167
+ def raise_provider_error(error)
168
+ case error
169
+ when ::RubyLLM::RateLimitError
170
+ raise Phronomy::LLMAdapter::RateLimitError, error.message
171
+ when ::RubyLLM::UnauthorizedError, ::RubyLLM::ForbiddenError
172
+ raise Phronomy::LLMAdapter::AuthenticationError, error.message
173
+ when ::RubyLLM::ContextLengthExceededError
174
+ raise Phronomy::LLMAdapter::ContextLengthError, error.message
175
+ when ::RubyLLM::Error
176
+ raise Phronomy::LLMAdapter::TransportError, error.message
177
+ else
178
+ raise error
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end