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
@@ -8,7 +8,7 @@ module Phronomy
8
8
  #
9
9
  # Requires:
10
10
  # - The +redis+ gem (add to your Gemfile)
11
- # - A Redis server with the RediSearch (RedisSearch) module enabled
11
+ # - A RESP2 Redis client and a server with the RediSearch module enabled
12
12
  # (or Redis Stack which bundles RediSearch)
13
13
  #
14
14
  # Vectors are stored as FLOAT32 binary blobs in Redis Hash fields and
@@ -46,17 +46,17 @@ module Phronomy
46
46
  @mutex = Mutex.new
47
47
  end
48
48
 
49
+ protected
50
+
49
51
  # @param id [String]
50
52
  # @param embedding [Array<Float>]
51
53
  # @param metadata [Hash]
52
54
  # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
53
55
  # @api public
54
- def add(id:, embedding:, metadata: {}, cancellation_token: nil)
55
- cancellation_token&.raise_if_cancelled!
56
+ def perform_add(id:, embedding:, metadata: {}, cancellation_token: nil)
56
57
  # Establish expected dimension on first add (not race-free for concurrent
57
- # first adds), then validate, then create/reuse the index.
58
+ # first adds), then create/reuse the index. Base has validated the input.
58
59
  @dimension ||= embedding.size
59
- validate_embedding_dimension!(embedding, @dimension)
60
60
  ensure_index!(@dimension)
61
61
  @redis.call(
62
62
  "HSET", "#{DOC_PREFIX}#{id}",
@@ -71,20 +71,17 @@ module Phronomy
71
71
  # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
72
72
  # @return [Array<Hash>] sorted by descending similarity score
73
73
  # @api public
74
- def search(query_embedding:, k: 5, cancellation_token: nil)
75
- cancellation_token&.raise_if_cancelled!
74
+ def perform_search(query_embedding:, k: 5, cancellation_token: nil)
76
75
  # search never establishes dimension. If dimension is unknown and the
77
76
  # index has not been created yet, there are no documents to return.
78
77
  return [] if @dimension.nil? && !@index_created
79
78
 
80
- validate_embedding_dimension!(query_embedding, @dimension)
81
79
  ensure_index!(@dimension)
82
- k_safe = validate_k!(k)
83
80
  blob = pack_vector(query_embedding)
84
81
 
85
82
  raw = @redis.call(
86
83
  "FT.SEARCH", @index_name,
87
- "*=>[KNN #{k_safe} @embedding $BLOB AS score]",
84
+ "*=>[KNN #{k} @embedding $BLOB AS score]",
88
85
  "PARAMS", 2, "BLOB", blob,
89
86
  "SORTBY", "score",
90
87
  "RETURN", 2, "score", "metadata",
@@ -94,26 +91,26 @@ module Phronomy
94
91
  parse_results(raw)
95
92
  end
96
93
 
97
- def remove(id:)
94
+ def perform_remove(id:)
98
95
  @redis.call("DEL", "#{DOC_PREFIX}#{id}")
99
96
  self
100
97
  end
101
98
 
102
99
  # Returns the number of documents indexed.
103
100
  # Queries FT.INFO when the index has been created; returns 0 otherwise.
104
- def size
101
+ def perform_size
105
102
  return 0 unless @index_created
106
103
 
107
- raw = @redis.call("FT.INFO", @index_name)
108
- return 0 unless raw.is_a?(Array)
109
-
110
- idx = raw.index("num_docs")
111
- idx ? raw[idx + 1].to_i : 0
112
- rescue
113
- 0
104
+ fields = parse_fields(@redis.call("FT.INFO", @index_name), "FT.INFO")
105
+ count = fields["num_docs"]
106
+ count = Integer(count, 10) if count.is_a?(String) && count.match?(/\A\d+\z/)
107
+ unless count.is_a?(Integer) && count >= 0
108
+ raise InvalidResultError, "Redis FT.INFO requires a non-negative integer num_docs"
109
+ end
110
+ count
114
111
  end
115
112
 
116
- def clear
113
+ def perform_clear
117
114
  @mutex.synchronize do
118
115
  begin
119
116
  @redis.call("FT.DROPINDEX", @index_name, "DD")
@@ -125,6 +122,11 @@ module Phronomy
125
122
  self
126
123
  end
127
124
 
125
+ # @api public
126
+ def embedding_dimension
127
+ @dimension
128
+ end
129
+
128
130
  private
129
131
 
130
132
  def ensure_index!(dim)
@@ -160,33 +162,68 @@ module Phronomy
160
162
  #
161
163
  # Redis FT.SEARCH returns: [count, key1, [field, value, ...], key2, ...]
162
164
  def parse_results(raw)
163
- return [] if raw.nil? || !raw.is_a?(Array) || raw.size < 2
165
+ unless raw.is_a?(Array) && raw.size.odd? && raw.first.is_a?(Integer) &&
166
+ raw.first >= (raw.size - 1) / 2 && raw.first.zero? == (raw.size == 1)
167
+ raise InvalidResultError, "Redis FT.SEARCH requires a count and document/field pairs"
168
+ end
164
169
 
165
170
  results = []
166
- i = 1
167
- while i < raw.size
168
- key = raw[i]
169
- fields = raw[i + 1]
170
- i += 2
171
-
172
- next unless fields.is_a?(Array)
173
-
174
- field_hash = fields.each_slice(2).to_h
175
- score_str = field_hash["score"]
176
- metadata_str = field_hash["metadata"]
177
-
178
- next if score_str.nil?
171
+ raw.drop(1).each_slice(2) do |key, fields|
172
+ unless key.is_a?(String) && key.start_with?(DOC_PREFIX)
173
+ raise InvalidResultError, "Redis FT.SEARCH returned an invalid document key"
174
+ end
175
+ # Redis documents this null content when a key expires/is updated
176
+ # during the search. The key remains in the total count.
177
+ next if fields.nil?
179
178
 
180
- id = key.to_s.delete_prefix(DOC_PREFIX)
179
+ field_hash = parse_fields(fields, "FT.SEARCH")
180
+ id = key.delete_prefix(DOC_PREFIX)
181
181
  # RediSearch returns cosine distance (0=identical, 2=opposite);
182
182
  # convert to cosine similarity for consistency with other backends.
183
- score = 1.0 - score_str.to_f
184
- metadata = metadata_str ? JSON.parse(metadata_str, symbolize_names: true) : {}
183
+ score = 1.0 - parse_distance(field_hash["score"])
184
+ metadata = parse_metadata(field_hash["metadata"])
185
185
 
186
186
  results << {id: id, score: score, metadata: metadata}
187
187
  end
188
188
  results
189
189
  end
190
+
191
+ # RESP2 field/value decoding belongs to this backend, not the Contract.
192
+ def parse_fields(raw, command)
193
+ unless raw.is_a?(Array) && raw.size.even?
194
+ raise InvalidResultError, "Redis #{command} requires field/value pairs"
195
+ end
196
+ raw.each_slice(2).each_with_object({}) do |(key, value), fields|
197
+ unless key.is_a?(String) && !fields.key?(key)
198
+ raise InvalidResultError, "Redis #{command} returned an invalid or duplicate field"
199
+ end
200
+ fields[key] = value
201
+ end
202
+ end
203
+
204
+ def parse_distance(raw)
205
+ valid = (raw.is_a?(String) && raw.match?(/\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/)) ||
206
+ (raw.is_a?(Numeric) && raw.real?)
207
+ value = Float(raw, exception: false) if valid
208
+ unless value&.finite?
209
+ raise InvalidResultError, "Redis FT.SEARCH requires a finite numeric score"
210
+ end
211
+ value
212
+ end
213
+
214
+ def parse_metadata(raw)
215
+ return {} if raw.nil?
216
+ unless raw.is_a?(String)
217
+ raise InvalidResultError, "Redis metadata must contain a JSON object"
218
+ end
219
+ parsed = JSON.parse(raw, symbolize_names: true)
220
+ unless parsed.is_a?(Hash)
221
+ raise InvalidResultError, "Redis metadata must contain a JSON object"
222
+ end
223
+ parsed
224
+ rescue JSON::ParserError
225
+ raise InvalidResultError, "Redis metadata contains invalid JSON"
226
+ end
190
227
  end
191
228
  end
192
229
  end
@@ -1,87 +1,156 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "invalid_result_error"
4
+
3
5
  module Phronomy
4
6
  module VectorStore
5
- # Public extension SPI for vector stores.
6
- #
7
- # Backends implement the synchronous add/search/remove/clear/size contract.
8
- # Phronomy supplies async convenience methods through {AsyncBackend}; blocking
9
- # backend work is offloaded through the framework-owned bounded OffloadPool.
10
- #
7
+ # Owns synchronous operations, input validation and the public result shape.
8
+ # Backends implement protected perform_* operations and embedding_dimension.
9
+ # No execution client, SDK or embedding provider is selected by this class.
11
10
  # @api public
12
11
  class Base
13
- include AsyncBackend
14
-
15
- # Add a document with its vector embedding.
16
- #
17
- # @param id [String] unique document identifier
18
- # @param embedding [Array<Float>] vector embedding
19
- # @param metadata [Hash] arbitrary metadata
20
- # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
12
+ # Validate before invoking the backend. Cancellation is checked before the
13
+ # write, never reported after a successful write. No operation is retried.
14
+ # @param id [String]
15
+ # @param embedding [Array<Numeric>]
16
+ # @param metadata [Hash]
17
+ # @param cancellation_token [#raise_if_cancelled!, nil]
18
+ # @return [self]
21
19
  # @api public
22
20
  def add(id:, embedding:, metadata: {}, cancellation_token: nil)
23
21
  cancellation_token&.raise_if_cancelled!
24
- raise NotImplementedError, "#{self.class}#add is not implemented"
22
+ validate_id!(id)
23
+ raise ArgumentError, "metadata must be a Hash" unless metadata.is_a?(Hash)
24
+ vector = validate_vector!(embedding)
25
+ perform_add(id: id, embedding: vector, metadata: metadata, cancellation_token: cancellation_token)
26
+ self
25
27
  end
26
28
 
27
- # Return the k most similar documents to the query embedding.
28
- #
29
- # @param query_embedding [Array<Float>]
30
- # @param k [Integer] number of results
31
- # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
32
- # @return [Array<Hash>] each element: { id:, score:, metadata: }
29
+ # Return at most k results ordered by descending similarity. Malformed
30
+ # backend output raises InvalidResultError; transport errors propagate.
31
+ # @param query_embedding [Array<Numeric>]
32
+ # @param k [Integer, String] positive integer or its decimal string
33
+ # @param cancellation_token [#raise_if_cancelled!, nil]
34
+ # @return [Array<Hash>]
33
35
  # @api public
34
36
  def search(query_embedding:, k: 5, cancellation_token: nil)
35
37
  cancellation_token&.raise_if_cancelled!
36
- raise NotImplementedError, "#{self.class}#search is not implemented"
38
+ limit = validate_k!(k)
39
+ vector = validate_vector!(query_embedding)
40
+ results = perform_search(query_embedding: vector, k: limit, cancellation_token: cancellation_token)
41
+ validate_results!(results, limit)
42
+ results
37
43
  end
38
44
 
39
- # Remove a single document by id.
40
- #
41
- # @param id [String] document identifier
45
+ # Remove a document; an absent id is a successful no-op.
46
+ # @param id [String]
47
+ # @return [self]
42
48
  # @api public
43
49
  def remove(id:)
44
- raise NotImplementedError, "#{self.class}#remove is not implemented"
50
+ validate_id!(id)
51
+ perform_remove(id: id)
52
+ self
45
53
  end
46
54
 
47
- # Remove all documents.
55
+ # Clear documents without changing the configured dimension.
56
+ # @return [self]
48
57
  # @api public
49
58
  def clear
50
- raise NotImplementedError, "#{self.class}#clear is not implemented"
59
+ perform_clear
60
+ self
51
61
  end
52
62
 
53
- # Return the number of documents stored.
54
- #
55
- # @return [Integer]
63
+ # @return [Integer] non-negative document count
56
64
  # @api public
57
65
  def size
58
- raise NotImplementedError, "#{self.class}#size is not implemented"
66
+ count = perform_size
67
+ unless count.is_a?(Integer) && count >= 0
68
+ raise InvalidResultError, "size must return a non-negative Integer"
69
+ end
70
+ count
71
+ end
72
+
73
+ protected
74
+
75
+ # @return [Integer, nil] nil delegates dimension enforcement to the backend
76
+ # @api public
77
+ def embedding_dimension
78
+ nil
79
+ end
80
+
81
+ # Storage extension point; inputs have passed common validation.
82
+ # @api public
83
+ def perform_add(id:, embedding:, metadata:, cancellation_token:)
84
+ raise NotImplementedError, "#{self.class}#perform_add is not implemented"
85
+ end
86
+
87
+ # Search extension point; return the documented result hashes in order.
88
+ # @api public
89
+ def perform_search(query_embedding:, k:, cancellation_token:)
90
+ raise NotImplementedError, "#{self.class}#perform_search is not implemented"
91
+ end
92
+
93
+ # @api public
94
+ def perform_remove(id:)
95
+ raise NotImplementedError, "#{self.class}#perform_remove is not implemented"
96
+ end
97
+
98
+ # @api public
99
+ def perform_clear
100
+ raise NotImplementedError, "#{self.class}#perform_clear is not implemented"
101
+ end
102
+
103
+ # @api public
104
+ def perform_size
105
+ raise NotImplementedError, "#{self.class}#perform_size is not implemented"
59
106
  end
60
107
 
61
108
  private
62
109
 
63
- # Validates that embedding has the expected dimension.
64
- # Raises ArgumentError if sizes differ.
65
- # A nil expected_dimension is a no-op (dimension not yet established).
66
- def validate_embedding_dimension!(embedding, expected_dimension)
67
- return unless expected_dimension
110
+ def validate_id!(id)
111
+ raise ArgumentError, "id must be a String" unless id.is_a?(String)
112
+ end
68
113
 
69
- actual = embedding.size
70
- return if actual == expected_dimension
114
+ def finite_real?(value)
115
+ value.is_a?(Numeric) && value.real? && value.finite? && value.to_f.finite?
116
+ end
71
117
 
72
- raise ArgumentError,
73
- "Embedding dimension mismatch: expected #{expected_dimension}, got #{actual}"
118
+ def validate_vector!(embedding)
119
+ unless embedding.is_a?(Array) && embedding.all? { |value| finite_real?(value) }
120
+ raise ArgumentError, "embedding must be an Array of finite real numbers"
121
+ end
122
+ dimension = embedding_dimension
123
+ if dimension && embedding.size != dimension
124
+ raise ArgumentError, "Embedding dimension mismatch: expected #{dimension}, got #{embedding.size}"
125
+ end
126
+ embedding.map(&:to_f)
74
127
  end
75
128
 
76
- # Validates that k is a positive integer.
77
- # Accepts any value accepted by Integer() (e.g. "5"), but raises
78
- # ArgumentError for non-integer strings, zero, and negative values.
79
129
  def validate_k!(k)
80
- int_k = Integer(k)
81
- raise ArgumentError, "k must be a positive integer, got #{int_k}" unless int_k >= 1
82
- int_k
83
- rescue ArgumentError => e
84
- raise ArgumentError, "k must be a positive integer: #{e.message}"
130
+ valid = k.is_a?(Integer) || (k.is_a?(String) && k.match?(/\A\+?\d+\z/))
131
+ unless valid
132
+ raise ArgumentError, "k must be a positive integer"
133
+ end
134
+ value = k.is_a?(String) ? Integer(k, 10) : k
135
+ raise ArgumentError, "k must be a positive integer" unless value >= 1
136
+ value
137
+ end
138
+
139
+ def validate_results!(results, limit)
140
+ unless results.is_a?(Array) && results.size <= limit
141
+ raise InvalidResultError, "search must return an Array with at most k results"
142
+ end
143
+ previous = Float::INFINITY
144
+ results.each do |result|
145
+ unless result.is_a?(Hash) && result[:id].is_a?(String) &&
146
+ result[:metadata].is_a?(Hash) && finite_real?(result[:score])
147
+ raise InvalidResultError, "search results require a String id, finite score and Hash metadata"
148
+ end
149
+ if result[:score] > previous
150
+ raise InvalidResultError, "search results must be ordered by descending score"
151
+ end
152
+ previous = result[:score]
153
+ end
85
154
  end
86
155
  end
87
156
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ module VectorStore
7
+ # A backend could not produce a valid synchronous result. This includes
8
+ # malformed external responses that must not be replaced with success values.
9
+ # @api public
10
+ class InvalidResultError < Phronomy::Error; end
11
+ end
12
+ end
@@ -1,23 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- # Vector store infrastructure: backends, embeddings adapters, document loaders,
5
- # and text splitters.
6
- #
7
- # Sub-namespaces are auto-loaded by Zeitwerk:
8
- # Phronomy::VectorStore::Base
9
- # Phronomy::VectorStore::InMemory
10
- # Phronomy::VectorStore::Pgvector
11
- # Phronomy::VectorStore::RedisSearch
12
- # Phronomy::VectorStore::Embeddings::Base
13
- # Phronomy::VectorStore::Embeddings::RubyLLMEmbeddings
14
- # Phronomy::VectorStore::Loader::Base
15
- # Phronomy::VectorStore::Loader::PlainTextLoader
16
- # Phronomy::VectorStore::Loader::MarkdownLoader
17
- # Phronomy::VectorStore::Loader::CsvLoader
18
- # Phronomy::VectorStore::Splitter::Base
19
- # Phronomy::VectorStore::Splitter::FixedSizeSplitter
20
- # Phronomy::VectorStore::Splitter::RecursiveSplitter
4
+ # Synchronous vector search/storage contract, its backends and execution client.
5
+ # Embeddings and Documents are independent Phronomy responsibilities.
21
6
  module VectorStore
22
7
  end
23
8
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.27.0"
4
+ VERSION = "0.29.0"
5
5
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Workflow
5
+ # Workflow definition contract shared with its execution runner.
6
+ # @api private
7
+ module Completion
8
+ FINISH = :__finish__
9
+ end
10
+ end
11
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../engine/concurrency/worker_input_restricted"
3
+ require_relative "../../execution/concurrency/worker_input_restricted"
4
4
 
5
5
  require_relative "workflow_runner"
6
- require_relative "../../engine/runnable"
6
+ require_relative "../../execution/runnable"
7
7
 
8
8
  module Phronomy
9
9
  # StateChart-style Workflow definition DSL.
@@ -67,7 +67,7 @@ module Phronomy
67
67
  def _apply_invocation_context(config, invocation_context)
68
68
  effective = config.merge(invocation_context: invocation_context)
69
69
  if effective[:cancellation_token].nil?
70
- token = invocation_context.effective_timeout_token
70
+ token = Phronomy::InvocationControls.effective_timeout_token(invocation_context)
71
71
  effective = effective.merge(cancellation_token: token) if token
72
72
  end
73
73
  effective
@@ -85,6 +85,7 @@ module Phronomy
85
85
  @exit_actions = {}
86
86
  @transitions = []
87
87
  @wait_state_names = []
88
+ @task_states = {}
88
89
  end
89
90
 
90
91
  def initial(state_name) # rubocop:disable Style/TrivialAccessors
@@ -94,9 +95,10 @@ module Phronomy
94
95
  # Declares a Workflow state.
95
96
  #
96
97
  # Entry actions are synchronous Run-to-Completion callbacks. To start
97
- # asynchronous work, register its listener/callback inside the action and
98
- # return the context or nil. Returning Phronomy::TaskResult is an error.
98
+ # asynchronous work owned by this state, use task_state. Ordinary actions
99
+ # return the context or nil; returning Phronomy::TaskResult is an error.
99
100
  def state(name, action: nil)
101
+ reject_task_state_overlap!(name)
100
102
  @declared_states << name
101
103
  entry(name, action) if action
102
104
  end
@@ -110,9 +112,30 @@ module Phronomy
110
112
  end
111
113
 
112
114
  def wait_state(name)
115
+ reject_task_state_overlap!(name)
113
116
  @wait_state_names << name
114
117
  end
115
118
 
119
+ # Starts one asynchronous operation on state entry and advances only
120
+ # after its successful result has been applied on the execution owner.
121
+ # Task completion is correlated to this state entry, not a durable ID.
122
+ # @param name [Symbol] state name
123
+ # @param start [#call] short callback accepting a context snapshot and returning TaskResult
124
+ # @param apply [#call] short callback accepting current context and value; returns context or nil
125
+ # @return [void]
126
+ # @api public
127
+ def task_state(name, start:, apply:)
128
+ unless name.is_a?(Symbol) && start.respond_to?(:call) && apply.respond_to?(:call)
129
+ raise ArgumentError, "task_state requires a Symbol name and callable start/apply"
130
+ end
131
+ if @declared_states.include?(name) || @wait_state_names.include?(name)
132
+ raise ArgumentError, "Workflow state #{name.inspect} is already declared"
133
+ end
134
+ @declared_states << name
135
+ @task_states[name] = {start: start, apply: apply}.freeze
136
+ nil
137
+ end
138
+
116
139
  # Declares a transition.
117
140
  #
118
141
  # Guards and actions may accept either (context) or (context, event).
@@ -121,7 +144,7 @@ module Phronomy
121
144
  # They may start asynchronous work and register listeners, but returning
122
145
  # Phronomy::TaskResult is an error; completion must arrive as a later event.
123
146
  def transition(from:, to:, guard: nil, on: nil, action: nil)
124
- destination = (to == :__finish__) ? FSMProtocol::FINISH : to
147
+ destination = to
125
148
  @transitions << {
126
149
  from: from,
127
150
  to: destination,
@@ -167,6 +190,7 @@ module Phronomy
167
190
  external_events: external_events,
168
191
  entry_point: @initial || @declared_states.first,
169
192
  wait_state_names: @wait_state_names.dup,
193
+ task_states: @task_states.dup.freeze,
170
194
  persistence: @persistence
171
195
  )
172
196
  Workflow.new(runner)
@@ -174,6 +198,10 @@ module Phronomy
174
198
 
175
199
  private
176
200
 
201
+ def reject_task_state_overlap!(name)
202
+ raise ArgumentError, "Workflow task_state #{name.inspect} is already declared" if @task_states.key?(name)
203
+ end
204
+
177
205
  def validate_graph!
178
206
  all_states = (@declared_states + @wait_state_names).uniq
179
207
  entry_point = @initial || @declared_states.first
@@ -186,7 +214,7 @@ module Phronomy
186
214
 
187
215
  undefined_targets = @transitions
188
216
  .map { |transition| transition[:to] }
189
- .reject { |target| target == FSMProtocol::FINISH } - all_states
217
+ .reject { |target| target == Completion::FINISH } - all_states
190
218
  unless undefined_targets.empty?
191
219
  raise ArgumentError,
192
220
  "Workflow transition(s) reference undefined state(s): " \
@@ -207,7 +235,7 @@ module Phronomy
207
235
  current = queue.shift
208
236
  @transitions.each do |transition|
209
237
  next unless transition[:from] == current
210
- next if transition[:to] == FSMProtocol::FINISH
238
+ next if transition[:to] == Completion::FINISH
211
239
  next if reachable.include?(transition[:to])
212
240
 
213
241
  reachable.add(transition[:to])
@@ -222,7 +250,7 @@ module Phronomy
222
250
  "[Phronomy] Workflow has unreachable state(s): " \
223
251
  "#{unreachable.sort.inspect}. These states can never be entered " \
224
252
  "from the initial state #{entry_point.inspect}."
225
- logger = Phronomy.configuration.logger
253
+ logger = Phronomy::RuntimeSettings.current.logger
226
254
  logger ? logger.warn(message) : Kernel.warn(message)
227
255
  end
228
256
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Workflow's synchronous callback rules, independent of FSM representation.
5
+ # Guards and transition actions may receive the triggering application event;
6
+ # actions may initiate async work but must not return a TaskResult.
7
+ # @api private
8
+ module WorkflowActionRules
9
+ module_function
10
+
11
+ def allowed?(guard, context, event)
12
+ guard.nil? || call_with_optional_event(guard, context, event)
13
+ end
14
+
15
+ def transition(callable, context, event, metadata:)
16
+ return context if callable.nil?
17
+
18
+ result = call_with_optional_event(callable, context, event)
19
+ if result.is_a?(Phronomy::TaskResult)
20
+ raise Phronomy::InvalidAsyncTransitionActionError,
21
+ "Transition action " \
22
+ "#{metadata[:from].inspect} --#{metadata[:event].inspect}--> " \
23
+ "#{metadata[:to].inspect} returned Phronomy::TaskResult. " \
24
+ "Start the asynchronous operation, register its callback/listener, " \
25
+ "and return the WorkflowContext or nil."
26
+ end
27
+ workflow_context_result?(result) ? result : context
28
+ end
29
+
30
+ def entry(callable, context, state_name)
31
+ result = callable.call(context)
32
+ if result.is_a?(Phronomy::TaskResult)
33
+ raise Phronomy::InvalidAsyncEntryActionError,
34
+ "Entry action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
35
+ "Start the asynchronous operation, register its callback/listener, " \
36
+ "and return the WorkflowContext or nil."
37
+ end
38
+ result
39
+ end
40
+
41
+ def apply_entry(callable, context, state_name)
42
+ result = entry(callable, context, state_name)
43
+ workflow_context_result?(result) ? result : context
44
+ end
45
+
46
+ def exit(callable, context, state_name)
47
+ result = callable.call(context)
48
+ if result.is_a?(Phronomy::TaskResult)
49
+ raise Phronomy::InvalidAsyncEntryActionError,
50
+ "Exit action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
51
+ "Exit actions are synchronous Run-to-Completion callbacks."
52
+ end
53
+ nil
54
+ end
55
+
56
+ def call_with_optional_event(callable, context, event)
57
+ parameters = callable.respond_to?(:parameters) ? callable.parameters : callable.method(:call).parameters
58
+ (parameters.length >= 2) ? callable.call(context, event) : callable.call(context)
59
+ end
60
+ private_class_method :call_with_optional_event
61
+
62
+ def workflow_context_result?(result)
63
+ result.respond_to?(:set_graph_metadata)
64
+ end
65
+ private_class_method :workflow_context_result?
66
+ end
67
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../engine/concurrency/worker_input_restricted"
3
+ require_relative "../../execution/concurrency/worker_input_restricted"
4
4
 
5
5
  module Phronomy
6
6
  # Module for defining Workflow context data.
@@ -138,7 +138,7 @@ module Phronomy
138
138
  # dispatch thread. All Workflow execution APIs now use that same path; there
139
139
  # is no caller-thread synchronous exception.
140
140
  def _assert_write_permitted!
141
- return if Phronomy::Runtime.in_event_loop_context?
141
+ return if Phronomy::WaitPolicy.blocking_forbidden?
142
142
 
143
143
  raise Phronomy::WorkflowContextOwnershipError,
144
144
  "WorkflowContext fields may only be mutated from the EventLoop dispatch " \