phronomy 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +9 -9
  3. data/CHANGELOG.md +52 -0
  4. data/CONTRIBUTING.md +8 -8
  5. data/README.md +9 -3
  6. data/benchmark/bench_agent_invoke.rb +25 -50
  7. data/benchmark/bench_context_assembler.rb +14 -11
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/benchmark/bench_tool_schema.rb +1 -1
  10. data/docs/architecture/agent-chat-and-state-ownership.md +7 -1
  11. data/docs/architecture/agent-transition-ownership.md +13 -89
  12. data/docs/architecture/backend-groups.md +152 -0
  13. data/docs/architecture/before-llm-input.md +3 -3
  14. data/docs/architecture/context-management.md +1 -1
  15. data/docs/architecture/execution-abstraction-review.md +88 -0
  16. data/docs/architecture/execution-boundaries.md +28 -0
  17. data/docs/architecture/execution-metadata-and-values.md +6 -0
  18. data/docs/architecture/multi-agent-handoff.md +6 -5
  19. data/docs/architecture/persistence.md +27 -9
  20. data/docs/architecture/r8-unit1.md +104 -0
  21. data/docs/architecture/r8-unit10.md +72 -0
  22. data/docs/architecture/r8-unit11.md +71 -0
  23. data/docs/architecture/r8-unit12.md +67 -0
  24. data/docs/architecture/r8-unit13.md +61 -0
  25. data/docs/architecture/r8-unit14.md +72 -0
  26. data/docs/architecture/r8-unit15.md +65 -0
  27. data/docs/architecture/r8-unit16.md +54 -0
  28. data/docs/architecture/r8-unit17.md +73 -0
  29. data/docs/architecture/r8-unit18.md +92 -0
  30. data/docs/architecture/r8-unit19.md +61 -0
  31. data/docs/architecture/r8-unit2.md +55 -0
  32. data/docs/architecture/r8-unit20.md +51 -0
  33. data/docs/architecture/r8-unit21.md +79 -0
  34. data/docs/architecture/r8-unit22.md +74 -0
  35. data/docs/architecture/r8-unit23.md +81 -0
  36. data/docs/architecture/r8-unit3.md +130 -0
  37. data/docs/architecture/r8-unit4.md +79 -0
  38. data/docs/architecture/r8-unit5.md +115 -0
  39. data/docs/architecture/r8-unit6.md +71 -0
  40. data/docs/architecture/r8-unit7.md +70 -0
  41. data/docs/architecture/r8-unit8.md +78 -0
  42. data/docs/architecture/r8-unit9.md +73 -0
  43. data/docs/architecture/refactoring-closure.md +10 -1
  44. data/docs/architecture/remaining-refactoring-plan.md +16 -0
  45. data/docs/architecture/storage-execution-boundaries.md +54 -0
  46. data/docs/architecture/tool-schema-recording-gap.md +6 -0
  47. data/docs/architecture/tracing.md +1 -1
  48. data/docs/architecture/workflow-terminal-ownership-design.md +2 -2
  49. data/docs/architecture.md +24 -11
  50. data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
  51. data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
  52. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +2 -0
  53. data/docs/decisions/031-durable-multi-agent-coordination.md +2 -0
  54. data/docs/decisions/032-storage-backend-composition.md +5 -0
  55. data/docs/decisions/034-handoff-runner-coordination-ownership.md +2 -0
  56. data/docs/decisions/035-tool-executor-capability-ownership.md +1 -1
  57. data/docs/decisions/038-responsibility-based-source-layout.md +14 -0
  58. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +8 -0
  59. data/docs/decisions/040-configuration-default-composition.md +12 -1
  60. data/docs/decisions/045-worker-input-restriction-ownership.md +4 -0
  61. data/docs/decisions/051-execution-outcome-worker-ownership.md +2 -0
  62. data/docs/decisions/056-workflow-terminal-policy-ownership.md +1 -1
  63. data/docs/decisions/058-neutral-storage-primitives.md +5 -0
  64. data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
  65. data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
  66. data/docs/decisions/061-persistence-failure-contracts.md +89 -0
  67. data/docs/decisions/062-content-store-service.md +52 -0
  68. data/docs/decisions/063-execution-contracts-and-services.md +88 -0
  69. data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
  70. data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
  71. data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
  72. data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
  73. data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
  74. data/docs/decisions/README.md +18 -1
  75. data/docs/execution-boundaries-preview.md +45 -0
  76. data/docs/features.md +8 -3
  77. data/docs/getting-started.md +9 -6
  78. data/docs/migrations/0.22.md +2 -2
  79. data/docs/migrations/durable-semantic-coordination-v2.md +2 -0
  80. data/docs/migrations/handoff-runner-multi-agent.md +2 -0
  81. data/docs/migrations/persistence-failure-contracts.md +67 -0
  82. data/docs/migrations/r8-unit10.md +36 -0
  83. data/docs/migrations/r8-unit11.md +33 -0
  84. data/docs/migrations/r8-unit12.md +31 -0
  85. data/docs/migrations/r8-unit13.md +36 -0
  86. data/docs/migrations/r8-unit14.md +31 -0
  87. data/docs/migrations/r8-unit15.md +31 -0
  88. data/docs/migrations/r8-unit16.md +26 -0
  89. data/docs/migrations/r8-unit17.md +28 -0
  90. data/docs/migrations/r8-unit18.md +29 -0
  91. data/docs/migrations/r8-unit19.md +28 -0
  92. data/docs/migrations/r8-unit20.md +24 -0
  93. data/docs/migrations/r8-unit21.md +28 -0
  94. data/docs/migrations/r8-unit22.md +29 -0
  95. data/docs/migrations/r8-unit23.md +20 -0
  96. data/docs/migrations/r8-unit4.md +67 -0
  97. data/docs/migrations/r8-unit5.md +75 -0
  98. data/docs/migrations/r8-unit6.md +109 -0
  99. data/docs/migrations/r8-unit7.md +31 -0
  100. data/docs/migrations/r8-unit8.md +33 -0
  101. data/docs/migrations/r8-unit9.md +28 -0
  102. data/docs/migrations/storage-async-client.md +103 -0
  103. data/docs/migrations/vector-async-clients.md +110 -0
  104. data/docs/persistence-backends.md +38 -21
  105. data/docs/runtime-and-concurrency.md +26 -1
  106. data/lib/phronomy/agent/admission.rb +78 -0
  107. data/lib/phronomy/agent/{lifecycle_contract/agent_already_exists_error.rb → agent_already_exists_error.rb} +1 -1
  108. data/lib/phronomy/agent/{lifecycle_contract/agent_busy_error.rb → agent_busy_error.rb} +1 -1
  109. data/lib/phronomy/agent/{lifecycle_contract/agent_purged_error.rb → agent_purged_error.rb} +1 -1
  110. data/lib/phronomy/{recovery → agent/api}/execution_rehydration_required_error.rb +1 -1
  111. data/lib/phronomy/agent/async_event_api.rb +37 -32
  112. data/lib/phronomy/agent/base.rb +74 -72
  113. data/lib/phronomy/agent/before_llm_input.rb +64 -0
  114. data/lib/phronomy/agent/composition/run_once.rb +1 -1
  115. data/lib/phronomy/agent/{context_assembly/context_candidate_resolver.rb → context_candidate_resolver.rb} +2 -2
  116. data/lib/phronomy/agent/{context_assembly/context_assembler.rb → context_preparation.rb} +35 -221
  117. data/lib/phronomy/agent/control_request.rb +21 -0
  118. data/lib/phronomy/agent/execution/agent_execution.rb +1 -1
  119. data/lib/phronomy/agent/execution/agent_invocation.rb +27 -53
  120. data/lib/phronomy/agent/execution/approval_resume_commit.rb +1 -1
  121. data/lib/phronomy/agent/execution/dispatch_preparation.rb +17 -127
  122. data/lib/phronomy/agent/execution/exact_execution.rb +44 -23
  123. data/lib/phronomy/agent/execution/execution_cancellation.rb +8 -4
  124. data/lib/phronomy/agent/execution/execution_coordinator.rb +214 -395
  125. data/lib/phronomy/agent/execution/execution_metadata.rb +1 -0
  126. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +70 -126
  127. data/lib/phronomy/agent/execution/execution_registry.rb +6 -2
  128. data/lib/phronomy/agent/execution/execution_session_runner.rb +22 -27
  129. data/lib/phronomy/agent/execution/initial_preparation.rb +18 -116
  130. data/lib/phronomy/agent/execution/{agent_invocation_session_builder.rb → invocation_actions.rb} +54 -194
  131. data/lib/phronomy/agent/execution/invocation_transitions.rb +39 -1
  132. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +7 -7
  133. data/lib/phronomy/agent/execution_change.rb +161 -0
  134. data/lib/phronomy/agent/execution_environment.rb +36 -0
  135. data/lib/phronomy/agent/execution_extension_state.rb +21 -0
  136. data/lib/phronomy/agent/execution_observation.rb +35 -0
  137. data/lib/phronomy/agent/filterable.rb +182 -0
  138. data/lib/phronomy/agent/handoff/{handoff_context.rb → transfer_context.rb} +4 -4
  139. data/lib/phronomy/agent/handoff/{handoff_projection.rb → transfer_projection.rb} +14 -33
  140. data/lib/phronomy/{llm_contract/transport_error.rb → agent/handoff_error.rb} +1 -1
  141. data/lib/phronomy/agent/journal/journal_projection.rb +1 -1
  142. data/lib/phronomy/agent/knowledge_item.rb +13 -0
  143. data/lib/phronomy/agent/lifecycle/agent_root.rb +1 -1
  144. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +18 -23
  145. data/lib/phronomy/agent/persistence/agent_repository.rb +29 -21
  146. data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
  147. data/lib/phronomy/agent/persistence/codec.rb +1 -21
  148. data/lib/phronomy/agent/persistence/execution_repository.rb +57 -41
  149. data/lib/phronomy/agent/persistence/journal_repository.rb +27 -18
  150. data/lib/phronomy/agent/persistence/records.rb +33 -0
  151. data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
  152. data/lib/phronomy/agent/persistence/storage_schema.rb +7 -2
  153. data/lib/phronomy/agent/persistence/watermark.rb +13 -11
  154. data/lib/phronomy/agent/recovery/invocation_restorer.rb +13 -21
  155. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +10 -9
  156. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +27 -27
  157. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +19 -21
  158. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +23 -16
  159. data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
  160. data/lib/phronomy/agent/recovery/recovery_support.rb +8 -11
  161. data/lib/phronomy/agent/reserved_execution.rb +16 -0
  162. data/lib/phronomy/agent/retention.rb +17 -0
  163. data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
  164. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
  165. data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
  166. data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
  167. data/lib/phronomy/agent/{context_assembly/ruby_llm_materializer.rb → runtime_input.rb} +18 -35
  168. data/lib/phronomy/agent/{context_assembly/saved_context_reader.rb → saved_context_reader.rb} +6 -4
  169. data/lib/phronomy/agent/settings.rb +47 -0
  170. data/lib/phronomy/agent/{context_assembly/state_writer.rb → state_writer.rb} +4 -3
  171. data/lib/phronomy/agent/store.rb +253 -0
  172. data/lib/phronomy/agent/{lifecycle_contract/stream_callback_error.rb → stream_callback_error.rb} +1 -1
  173. data/lib/phronomy/agent/tool_execution/tool_binding.rb +4 -64
  174. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +50 -219
  175. data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
  176. data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
  177. data/lib/phronomy/configuration/runtime_settings.rb +49 -0
  178. data/lib/phronomy/content_store/storage_schema.rb +1 -0
  179. data/lib/phronomy/content_store/stored_contents.rb +2 -1
  180. data/lib/phronomy/context/assembly.rb +202 -0
  181. data/lib/phronomy/{llm_contract/context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +3 -1
  182. data/lib/phronomy/context/candidate.rb +51 -0
  183. data/lib/phronomy/context/constraint.rb +47 -0
  184. data/lib/phronomy/{agent/context_contract → context}/context_plan.rb +1 -1
  185. data/lib/phronomy/{agent/context_contract → context}/context_plan_validator.rb +3 -3
  186. data/lib/phronomy/{agent/context_contract → context}/context_policy.rb +1 -1
  187. data/lib/phronomy/{agent/context_contract → context}/context_policy_input.rb +1 -1
  188. data/lib/phronomy/{agent/context_assembly → context}/context_policy_input_builder.rb +1 -1
  189. data/lib/phronomy/context/default_policy.rb +140 -0
  190. data/lib/phronomy/context/final_budget_validator.rb +33 -0
  191. data/lib/phronomy/{agent/context_contract → context}/llm_input_build_context.rb +1 -1
  192. data/lib/phronomy/{agent/context_contract → context}/llm_input_manifest.rb +19 -19
  193. data/lib/phronomy/{agent/context_contract → context}/llm_input_patch.rb +1 -1
  194. data/lib/phronomy/context/manifest_error.rb +10 -0
  195. data/lib/phronomy/context/prompt_template.rb +98 -0
  196. data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
  197. data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
  198. data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
  199. data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
  200. data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
  201. data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
  202. data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
  203. data/lib/phronomy/embeddings/async/async_client.rb +44 -0
  204. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
  205. data/lib/phronomy/embeddings/base.rb +43 -0
  206. data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
  207. data/lib/phronomy/embeddings/transport_error.rb +11 -0
  208. data/lib/phronomy/engine/concurrency/offload_pool.rb +36 -21
  209. data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
  210. data/lib/phronomy/engine/diagnostics.rb +1 -1
  211. data/lib/phronomy/engine/event_loop.rb +7 -7
  212. data/lib/phronomy/engine/execution_binding.rb +23 -0
  213. data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
  214. data/lib/phronomy/engine/fsm_protocol.rb +1 -6
  215. data/lib/phronomy/engine/fsm_session.rb +3 -3
  216. data/lib/phronomy/engine/metrics.rb +16 -14
  217. data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
  218. data/lib/phronomy/engine/runtime.rb +25 -12
  219. data/lib/phronomy/engine/terminal_decision.rb +7 -0
  220. data/lib/phronomy/execution/async_operation.rb +41 -0
  221. data/lib/phronomy/{engine → execution}/blocking.rb +2 -2
  222. data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +1 -1
  223. data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
  224. data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
  225. data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
  226. data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
  227. data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
  228. data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
  229. data/lib/phronomy/{engine → execution}/execution.rb +61 -27
  230. data/lib/phronomy/execution/execution_receiver.rb +78 -0
  231. data/lib/phronomy/execution/execution_scope_state.rb +35 -0
  232. data/lib/phronomy/{engine → execution}/invocation_context.rb +0 -15
  233. data/lib/phronomy/execution/invocation_controls.rb +73 -0
  234. data/lib/phronomy/execution/runnable.rb +24 -0
  235. data/lib/phronomy/{engine → execution}/task_result.rb +3 -3
  236. data/lib/phronomy/execution/wait_policy.rb +24 -0
  237. data/lib/phronomy/filter/{contract/filter_block_error.rb → filter_block_error.rb} +1 -1
  238. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
  239. data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
  240. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +2 -2
  241. data/lib/phronomy/generation/generator_verifier.rb +7 -7
  242. data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
  243. data/lib/phronomy/{llm_contract → llm_adapter}/authentication_error.rb +3 -1
  244. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
  245. data/lib/phronomy/llm_adapter/base.rb +49 -68
  246. data/lib/phronomy/{llm_contract → llm_adapter}/context_length_error.rb +3 -1
  247. data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
  248. data/lib/phronomy/llm_adapter/message.rb +62 -0
  249. data/lib/phronomy/{llm_contract → llm_adapter}/rate_limit_error.rb +3 -1
  250. data/lib/phronomy/llm_adapter/request.rb +79 -0
  251. data/lib/phronomy/llm_adapter/response.rb +36 -0
  252. data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
  253. data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
  254. data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
  255. data/lib/phronomy/llm_adapter.rb +3 -16
  256. data/lib/phronomy/multi_agent/admission_registry.rb +1 -8
  257. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
  258. data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
  259. data/lib/phronomy/{agent/handoff → multi_agent}/handoff.rb +2 -2
  260. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_capability_factory.rb +21 -2
  261. data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
  262. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_policy.rb +1 -1
  263. data/lib/phronomy/multi_agent/handoff_runner.rb +77 -100
  264. data/lib/phronomy/{agent/handoff → multi_agent}/handoff_state.rb +1 -1
  265. data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
  266. data/lib/phronomy/multi_agent/persistence/codec.rb +20 -0
  267. data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
  268. data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
  269. data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
  270. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +58 -42
  271. data/lib/phronomy/multi_agent/persistence/team_repository.rb +29 -21
  272. data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
  273. data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
  274. data/lib/phronomy/multi_agent/shared_state.rb +2 -2
  275. data/lib/phronomy/multi_agent/store.rb +156 -0
  276. data/lib/phronomy/multi_agent/team_coordinator.rb +83 -74
  277. data/lib/phronomy/multi_agent/team_ownership_registry.rb +2 -12
  278. data/lib/phronomy/output_parser/parse_error.rb +7 -0
  279. data/lib/phronomy/persistence/content_repository.rb +26 -0
  280. data/lib/phronomy/persistence/errors.rb +16 -0
  281. data/lib/phronomy/persistence/migration/initial_format_migration.rb +2 -2
  282. data/lib/phronomy/persistence/persistence.rb +23 -0
  283. data/lib/phronomy/persistence/save_outcome.rb +24 -0
  284. data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
  285. data/lib/phronomy/persistence/storage_boundary.rb +32 -0
  286. data/lib/phronomy/persistence/transaction.rb +86 -0
  287. data/lib/phronomy/persistence_composition/storage_schema.rb +9 -4
  288. data/lib/phronomy/persistence_composition/stores.rb +34 -0
  289. data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
  290. data/lib/phronomy/runtime_composition/agent_defaults.rb +5 -1
  291. data/lib/phronomy/runtime_composition/configuration.rb +146 -0
  292. data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
  293. data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
  294. data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
  295. data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
  296. data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
  297. data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
  298. data/lib/phronomy/storage/async/async_client.rb +60 -0
  299. data/lib/phronomy/storage/backend.rb +8 -2
  300. data/lib/phronomy/testing/eval/metrics.rb +1 -1
  301. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
  302. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +2 -2
  303. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +6 -6
  304. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +55 -52
  305. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +3 -3
  306. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +7 -7
  307. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +11 -11
  308. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +57 -51
  309. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +1 -1
  310. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +23 -22
  311. data/lib/phronomy/tool/authorization.rb +77 -0
  312. data/lib/phronomy/tool/base.rb +549 -9
  313. data/lib/phronomy/tool/call_request.rb +40 -0
  314. data/lib/phronomy/{agent/tool_execution/tool_definition_set.rb → tool/definition_set.rb} +7 -8
  315. data/lib/phronomy/tool/operation.rb +88 -0
  316. data/lib/phronomy/tool/schema.rb +214 -0
  317. data/lib/phronomy/tool/settings.rb +31 -0
  318. data/lib/phronomy/tool/tool_error.rb +7 -0
  319. data/lib/phronomy/tool/tool_executor.rb +58 -0
  320. data/lib/phronomy/tools/agent.rb +22 -90
  321. data/lib/phronomy/tools/mcp.rb +21 -14
  322. data/lib/phronomy/tools/vector_search.rb +4 -4
  323. data/lib/phronomy/tracing/automatic.rb +11 -6
  324. data/lib/phronomy/tracing/base.rb +1 -1
  325. data/lib/phronomy/tracing/observation.rb +37 -0
  326. data/lib/phronomy/tracing/settings.rb +36 -0
  327. data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
  328. data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
  329. data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
  330. data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
  331. data/lib/phronomy/vector_store/base.rb +118 -49
  332. data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
  333. data/lib/phronomy/vector_store.rb +2 -17
  334. data/lib/phronomy/version.rb +1 -1
  335. data/lib/phronomy/workflow/completion.rb +11 -0
  336. data/lib/phronomy/workflow/execution/workflow.rb +7 -7
  337. data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
  338. data/lib/phronomy/workflow/execution/workflow_context.rb +2 -2
  339. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
  340. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +3 -5
  341. data/lib/phronomy/workflow/execution/workflow_runner.rb +58 -79
  342. data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
  343. data/lib/phronomy/workflow/persistence/state_repository.rb +28 -22
  344. data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
  345. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
  346. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
  347. data/lib/phronomy/workflow/{execution → runtime_binding}/workflow_terminal_policy.rb +4 -4
  348. data/lib/phronomy.rb +57 -16
  349. data/scripts/api_snapshot.rb +35 -15
  350. data/sig/_private/phronomy/agent/settings.rbs +19 -0
  351. data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
  352. data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
  353. data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
  354. data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
  355. data/sig/_private/phronomy/tool/settings.rbs +13 -0
  356. data/sig/_private/phronomy/tracing/settings.rbs +15 -0
  357. data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
  358. data/sig/phronomy/agent/admission.rbs +19 -0
  359. data/sig/phronomy/agent/execution_contract.rbs +132 -0
  360. data/sig/phronomy/agent/execution_environment.rbs +20 -0
  361. data/sig/phronomy/agent/store.rbs +73 -0
  362. data/sig/phronomy/agent.rbs +14 -138
  363. data/sig/phronomy/common.rbs +7 -0
  364. data/sig/phronomy/content_store/contracts.rbs +13 -0
  365. data/sig/phronomy/context.rbs +152 -0
  366. data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
  367. data/sig/phronomy/embeddings/base.rbs +17 -0
  368. data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
  369. data/sig/phronomy/engine/control_bindings.rbs +11 -0
  370. data/sig/phronomy/engine/control_protocols.rbs +23 -0
  371. data/sig/phronomy/engine/execution_binding.rbs +6 -0
  372. data/sig/phronomy/engine/execution_receiver.rbs +15 -0
  373. data/sig/phronomy/engine/observability.rbs +13 -0
  374. data/sig/phronomy/execution/async_operation.rbs +7 -0
  375. data/sig/phronomy/execution/blocking.rbs +5 -0
  376. data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
  377. data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
  378. data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
  379. data/sig/phronomy/execution/control_protocols.rbs +18 -0
  380. data/sig/phronomy/execution/errors.rbs +7 -0
  381. data/sig/phronomy/execution/execution.rbs +41 -0
  382. data/sig/phronomy/execution/execution_receiver.rbs +40 -0
  383. data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
  384. data/sig/phronomy/execution/invocation_context.rbs +18 -0
  385. data/sig/phronomy/execution/invocation_controls.rbs +13 -0
  386. data/sig/phronomy/execution/task_result.rbs +29 -0
  387. data/sig/phronomy/extensions.rbs +0 -21
  388. data/sig/phronomy/handoff.rbs +10 -4
  389. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
  390. data/sig/phronomy/llm_adapter/base.rbs +14 -0
  391. data/sig/phronomy/llm_adapter/usage.rbs +14 -0
  392. data/sig/phronomy/llm_adapter/values.rbs +43 -0
  393. data/sig/phronomy/llm_context_window.rbs +10 -0
  394. data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
  395. data/sig/phronomy/multi_agent/store.rbs +54 -0
  396. data/sig/phronomy/multi_agent.rbs +6 -3
  397. data/sig/phronomy/persistence/errors.rbs +18 -0
  398. data/sig/phronomy/persistence/migration.rbs +13 -0
  399. data/sig/phronomy/persistence/transaction.rbs +19 -0
  400. data/sig/phronomy/persistence.rbs +1 -99
  401. data/sig/phronomy/persistence_composition.rbs +19 -0
  402. data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
  403. data/sig/phronomy/storage/async/async_client.rbs +8 -0
  404. data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
  405. data/sig/phronomy/{storage.rbs → storage/contracts.rbs} +2 -16
  406. data/sig/phronomy/tool.rbs +48 -4
  407. data/sig/phronomy/tracing/observation.rbs +7 -0
  408. data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
  409. data/sig/phronomy/vector_store/base.rbs +23 -0
  410. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
  411. data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
  412. data/sig/phronomy/workflow/store.rbs +8 -0
  413. data/sig/phronomy/workflow.rbs +1 -1
  414. data/sig/phronomy.rbs +0 -49
  415. metadata +318 -117
  416. data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
  417. data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
  418. data/lib/phronomy/agent/concerns/filterable.rb +0 -184
  419. data/lib/phronomy/agent/context/capability/base.rb +0 -548
  420. data/lib/phronomy/agent/context/capability/tool_executor.rb +0 -62
  421. data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
  422. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +0 -36
  423. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +0 -22
  424. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
  425. data/lib/phronomy/agent/context_policies/default.rb +0 -142
  426. data/lib/phronomy/agent/execution/phase_machine_builder.rb +0 -101
  427. data/lib/phronomy/agent/execution/provider_call_outcome.rb +0 -106
  428. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +0 -15
  429. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +0 -131
  430. data/lib/phronomy/agent/handoff/handoff_request.rb +0 -45
  431. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +0 -7
  432. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +0 -58
  433. data/lib/phronomy/agent/persistence/queries.rb +0 -61
  434. data/lib/phronomy/agent/selection/candidate.rb +0 -53
  435. data/lib/phronomy/agent/selection/constraint.rb +0 -49
  436. data/lib/phronomy/agent/tool_execution/tool_call_intercepted.rb +0 -29
  437. data/lib/phronomy/agent/tool_execution/tool_invocation_session_builder.rb +0 -276
  438. data/lib/phronomy/configuration/configuration.rb +0 -68
  439. data/lib/phronomy/configuration/global_configuration.rb +0 -26
  440. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
  441. data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
  442. data/lib/phronomy/engine/execution_receiver.rb +0 -65
  443. data/lib/phronomy/engine/runnable.rb +0 -54
  444. data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
  445. data/lib/phronomy/llm_contract/token_usage.rb +0 -85
  446. data/lib/phronomy/multi_agent/persistence/queries.rb +0 -30
  447. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +0 -15
  448. data/lib/phronomy/output_parser/contract/parse_error.rb +0 -7
  449. data/lib/phronomy/persistence/api/persistence.rb +0 -140
  450. data/lib/phronomy/persistence_composition/repositories.rb +0 -77
  451. data/lib/phronomy/recovery/recovery.rb +0 -186
  452. data/lib/phronomy/tool/contract/tool_error.rb +0 -7
  453. data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
  454. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
  455. data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
  456. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +0 -9
  457. data/sig/phronomy/execution_receiver.rbs +0 -34
  458. data/sig/phronomy/llm_adapter.rbs +0 -11
  459. data/sig/phronomy/runtime.rbs +0 -76
  460. /data/lib/phronomy/agent/{context_assembly/context_importer.rb → context_importer.rb} +0 -0
  461. /data/lib/phronomy/{engine → execution}/backpressure_error.rb +0 -0
  462. /data/lib/phronomy/{engine → execution}/cancellation_error.rb +0 -0
  463. /data/lib/phronomy/{engine → execution}/concurrency/worker_input_restricted.rb +0 -0
  464. /data/lib/phronomy/{engine → execution}/event_loop_reentrancy_error.rb +0 -0
  465. /data/lib/phronomy/{engine → execution}/execution_cancellation_error.rb +0 -0
  466. /data/lib/phronomy/{engine → execution}/execution_timeout_error.rb +0 -0
  467. /data/lib/phronomy/{engine → execution}/invalid_async_entry_action_error.rb +0 -0
  468. /data/lib/phronomy/{engine → execution}/invalid_async_transition_action_error.rb +0 -0
  469. /data/lib/phronomy/{engine → execution}/invalid_async_workflow_action_error.rb +0 -0
  470. /data/lib/phronomy/{engine → execution}/pool_shutdown_error.rb +0 -0
  471. /data/lib/phronomy/{engine → execution}/recursion_limit_error.rb +0 -0
  472. /data/lib/phronomy/{engine → execution}/runtime_shutdown_error.rb +0 -0
  473. /data/lib/phronomy/{engine → execution}/runtime_shutdown_reentrancy_error.rb +0 -0
  474. /data/lib/phronomy/{engine → execution}/scheduler_reentrancy_error.rb +0 -0
  475. /data/lib/phronomy/{engine → execution}/timeout_error.rb +0 -0
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Tool
5
+ # Tool operation protocol: execution modes, custom async dispatch, and
6
+ # completion-handle validation. Agent approval/progress are separate owners.
7
+ # @api public
8
+ module Operation
9
+ # Starts one Tool operation. The submitter implements Execution submission
10
+ # for the default offloaded path; custom call_async keeps its public protocol
11
+ # and owns its execution mechanism. Neither receives a concrete Runtime.
12
+ # @return [#on_complete] the original completion handle
13
+ # @api public
14
+ def self.call_async(tool:, args:, cancellation_token: nil, config: {},
15
+ submitter: Phronomy::Execution, on_full: :raise)
16
+ unless %i[cooperative offloaded].include?(tool.class.execution_mode)
17
+ raise Phronomy::ConfigurationError,
18
+ "unknown Tool execution_mode: #{tool.class.execution_mode.inspect}"
19
+ end
20
+ operation = if custom_async?(tool.method(:call_async))
21
+ tool.call_async(args, cancellation_token: cancellation_token, config: config)
22
+ else
23
+ ToolExecutor.call_async(tool: tool, args: args,
24
+ cancellation_token: cancellation_token, config: config,
25
+ submitter: submitter, on_full: on_full)
26
+ end
27
+ unless operation.respond_to?(:on_complete)
28
+ raise Phronomy::ToolError,
29
+ "Tool #{tool.class.name}#call_async must return a completion handle"
30
+ end
31
+ operation
32
+ end
33
+
34
+ # Decorates synchronous Tool results and custom async completions. A custom
35
+ # async operation is mapped through Execution's physical-completion-aware
36
+ # operation contract. The caller owns transformation meaning, ordering and
37
+ # multiplicity: repeated decorations remain distinct stages. Base's internal
38
+ # async-to-sync delegation skips only the synchronous counterpart of these
39
+ # async stages. Explicit application calls retain their own transformations.
40
+ # @return [Class<Phronomy::Tool::Base>] a derived Tool class
41
+ # @api public
42
+ def self.with_result_transform(tool_class, &transform)
43
+ raise ArgumentError, "a result transformation is required" unless transform
44
+ effective_name = tool_class.new.name
45
+ custom_async_call = custom_async?(tool_class.instance_method(:call_async))
46
+ decorated = Class.new(tool_class) do
47
+ tool_name effective_name
48
+ define_method(:call) do |args, **kwargs|
49
+ result = super(args, **kwargs)
50
+ transform.call(result, name, args)
51
+ end
52
+ if custom_async_call
53
+ define_method(:call_async) do |args, **kwargs|
54
+ source = super(args, **kwargs)
55
+ Phronomy::AsyncOperation.map(source, name: "tool-filter-#{name}") do |value|
56
+ transform.call(value, name, args)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ if custom_async_call
62
+ decorated.instance_variable_set(:@phronomy_async_transform_call,
63
+ decorated.instance_method(:call))
64
+ end
65
+ decorated
66
+ end
67
+
68
+ # Base's async bridge uses this bound callable on the original receiver,
69
+ # including after offload. Skip only framework-generated call wrappers whose
70
+ # async counterpart applies the same stage on completion. Stop at application
71
+ # methods and sync-only stages; do not infer application intent or use ambient
72
+ # flags, mutable receiver state, result identity, or filter deduplication.
73
+ # @api private
74
+ def self.synchronous_delegate(tool)
75
+ callable = tool.method(:call)
76
+ while callable.owner.instance_variable_get(:@phronomy_async_transform_call) == callable.unbind
77
+ callable = callable.super_method
78
+ end
79
+ callable
80
+ end
81
+
82
+ def self.custom_async?(method)
83
+ method.owner != Base
84
+ end
85
+ private_class_method :custom_async?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_schemer"
4
+ require "schematist"
5
+ require_relative "../common/values/immutable"
6
+
7
+ module Phronomy
8
+ module Tool
9
+ # One schema defines both the advertised arguments and runtime validation.
10
+ # Schema evaluation is local: remote references and custom vocabularies are
11
+ # not supported. Provider request rendering does not belong to this class.
12
+ # @api public
13
+ class Schema
14
+ KEYWORDS = %w[
15
+ $schema $ref $defs $comment definitions
16
+ type enum const title description default examples deprecated readOnly writeOnly
17
+ properties patternProperties additionalProperties required propertyNames
18
+ minProperties maxProperties dependentRequired dependentSchemas
19
+ items prefixItems contains minContains maxContains
20
+ minItems maxItems uniqueItems minimum maximum exclusiveMinimum exclusiveMaximum multipleOf
21
+ minLength maxLength pattern format allOf anyOf oneOf not if then else
22
+ unevaluatedProperties unevaluatedItems strict
23
+ ].freeze
24
+ FORMATS = %w[date-time date time duration email idn-email hostname idn-hostname ipv4 ipv6 uri uri-reference iri iri-reference uuid uri-template json-pointer relative-json-pointer regex].freeze
25
+ SCHEMA_MAPS = %w[properties patternProperties $defs definitions dependentSchemas].freeze
26
+ SCHEMA_LISTS = %w[allOf anyOf oneOf prefixItems].freeze
27
+ SCHEMA_VALUES = %w[additionalProperties propertyNames items contains not if then else unevaluatedProperties unevaluatedItems].freeze
28
+
29
+ # @api public
30
+ def initialize(schema = nil, &block)
31
+ source = if block
32
+ raise ArgumentError, "pass either a schema or a block" unless schema.nil?
33
+ Schematist::Schema.create(&block).new.to_json_schema
34
+ elsif schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
35
+ schema.new.to_json_schema
36
+ elsif schema.respond_to?(:to_json_schema)
37
+ schema.to_json_schema
38
+ else
39
+ schema
40
+ end
41
+ source = source[:schema] || source["schema"] || source if source.is_a?(Hash)
42
+ @json_schema = self.class.json_value(source)
43
+ unless @json_schema.is_a?(Hash) && @json_schema["type"] == "object"
44
+ raise ArgumentError, "Tool schema must declare an object"
45
+ end
46
+ @references = []
47
+ validate_keywords!(@json_schema)
48
+ options = {ref_resolver: ->(uri) { raise ArgumentError, "external Tool schema reference: #{uri}" }}
49
+ unless JSONSchemer.valid_schema?(@json_schema, **options)
50
+ raise ArgumentError, "invalid Tool JSON Schema"
51
+ end
52
+ @json_schema = Phronomy::Values::Immutable.copy(@json_schema)
53
+ @validator = JSONSchemer.schema(@json_schema, **options)
54
+ @references.each { |reference| @validator.ref(reference) }
55
+ @references = nil
56
+ freeze
57
+ rescue JSONSchemer::InvalidRefPointer, JSONSchemer::InvalidRefResolution => error
58
+ raise ArgumentError, "invalid local Tool schema reference: #{error.message}"
59
+ end
60
+
61
+ # @api public
62
+ attr_reader :json_schema
63
+
64
+ # Return validated keyword arguments without modifying the caller's values.
65
+ # Coercion is limited to explicit property/item types; combinators and
66
+ # references are validated without guessing a branch or resolving a cast.
67
+ # @api public
68
+ def validate(arguments, coerce: false)
69
+ return [nil, "arguments must be an object (Hash)"] unless arguments.is_a?(Hash)
70
+ data = self.class.json_value(arguments)
71
+ data = self.class.json_value(coerce_value(data, @json_schema)) if coerce
72
+ error = @validator.validate(data).first
73
+ return [nil, error_message(error)] if error
74
+ [restore_keys(arguments, data).transform_keys(&:to_sym), nil]
75
+ rescue ArgumentError, TypeError => error
76
+ [nil, error.message]
77
+ end
78
+
79
+ # JSON values, with duplicate normalized keys rejected instead of lost.
80
+ # @api private
81
+ def self.json_value(value)
82
+ case value
83
+ when Hash
84
+ value.each_with_object({}) do |(key, child), result|
85
+ unless key.is_a?(String) || key.is_a?(Symbol)
86
+ raise ArgumentError, "Tool object keys must be Strings or Symbols"
87
+ end
88
+ name = key.to_s
89
+ raise ArgumentError, "duplicate Tool object key: #{name}" if result.key?(name)
90
+ result[name] = json_value(child)
91
+ end
92
+ when Array then value.map { |child| json_value(child) }
93
+ when String, Integer, TrueClass, FalseClass, NilClass then value
94
+ when Float
95
+ raise ArgumentError, "Tool numbers must be finite" unless value.finite?
96
+ value
97
+ else
98
+ raise ArgumentError, "unsupported Tool value: #{value.class}"
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def error_message(error)
105
+ path = error.fetch("data_pointer", "").split("/").drop(1)
106
+ field = path.join(".")
107
+ type = error["type"]
108
+ type = "additionalProperties" if error["schema"] == false && error["schema_pointer"].end_with?("/additionalProperties")
109
+ case type
110
+ when "required"
111
+ key = Array(error.dig("details", "missing_keys")).first
112
+ path.empty? ? "required parameter '#{key}' is missing" : "nested required field '#{field}.#{key}' is missing"
113
+ when "additionalProperties"
114
+ (path.size > 1) ? "nested field '#{path[0...-1].join(".")}' contains undeclared key(s): #{path.last.inspect}" : "unknown parameter(s): #{path.last.inspect}"
115
+ when "enum"
116
+ "parameter '#{field}' must be one of: #{Array(error.dig("schema", "enum")).join(", ")} (got: #{error["data"].inspect})"
117
+ when "string", "integer", "number", "boolean", "object", "array", "null"
118
+ prefix = (path.size > 1) ? "nested field '#{field}'" : "parameter '#{field}'"
119
+ "#{prefix} expected type #{type} (got: #{error["data"].inspect})"
120
+ else
121
+ error.fetch("error", "invalid Tool arguments")
122
+ end
123
+ end
124
+
125
+ def validate_keywords!(schema)
126
+ return if schema == true || schema == false
127
+ raise ArgumentError, "invalid Tool schema node" unless schema.is_a?(Hash)
128
+ unknown = schema.keys - KEYWORDS
129
+ raise ArgumentError, "unsupported Tool schema keywords: #{unknown.join(", ")}" unless unknown.empty?
130
+ if schema.key?("$schema") && schema["$schema"] != "https://json-schema.org/draft/2020-12/schema"
131
+ raise ArgumentError, "Tool schemas require JSON Schema 2020-12"
132
+ end
133
+ if schema.key?("format") && !FORMATS.include?(schema["format"])
134
+ raise ArgumentError, "unsupported Tool schema format: #{schema["format"]}"
135
+ end
136
+ if schema.key?("$ref")
137
+ reference = schema["$ref"]
138
+ if !(reference.is_a?(String) && (reference == "#" || reference.start_with?("#/")))
139
+ raise ArgumentError, "Tool schemas support local JSON Pointer references only"
140
+ end
141
+ @references << reference
142
+ end
143
+ SCHEMA_MAPS.each do |key|
144
+ next unless schema.key?(key)
145
+ raise ArgumentError, "#{key} must be an object" unless schema[key].is_a?(Hash)
146
+ schema[key].each_value { |child| validate_keywords!(child) }
147
+ end
148
+ SCHEMA_LISTS.each do |key|
149
+ next unless schema.key?(key)
150
+ raise ArgumentError, "#{key} must be an array" unless schema[key].is_a?(Array)
151
+ schema[key].each { |child| validate_keywords!(child) }
152
+ end
153
+ SCHEMA_VALUES.each { |key| validate_keywords!(schema[key]) if schema.key?(key) }
154
+ end
155
+
156
+ def coerce_value(value, schema)
157
+ return value unless schema.is_a?(Hash)
158
+ return value if (schema.keys & %w[$ref anyOf oneOf allOf not if]).any?
159
+ type = schema["type"]
160
+ if type.is_a?(Array)
161
+ alternatives = type - ["null"]
162
+ type = alternatives.first if alternatives.length == 1
163
+ end
164
+ case type
165
+ when "object"
166
+ return value unless value.is_a?(Hash)
167
+ value.to_h { |key, child| [key, coerce_value(child, schema.fetch("properties", {}).fetch(key, {}))] }
168
+ when "array"
169
+ return value unless value.is_a?(Array)
170
+ value.map { |child| coerce_value(child, schema.fetch("items", {})) }
171
+ when "string" then value.nil? ? value : value.to_s
172
+ when "integer"
173
+ return value if value.nil? || value.is_a?(Integer)
174
+ raise ArgumentError, "fractional Tool value cannot be coerced to integer" if value.is_a?(Numeric) && value != value.to_i
175
+ begin
176
+ Integer(value)
177
+ rescue ArgumentError, TypeError
178
+ raise ArgumentError, "parameter cannot be coerced to integer: #{value.inspect}"
179
+ end
180
+ when "number"
181
+ begin
182
+ value.nil? ? value : Float(value)
183
+ rescue ArgumentError, TypeError
184
+ raise ArgumentError, "parameter cannot be coerced to number: #{value.inspect}"
185
+ end
186
+ when "boolean"
187
+ case value
188
+ when true, false, nil then value
189
+ else
190
+ case value.to_s.downcase
191
+ when "true" then true
192
+ when "false" then false
193
+ else raise ArgumentError, "parameter cannot be coerced to boolean: #{value.inspect}"
194
+ end
195
+ end
196
+ else value
197
+ end
198
+ end
199
+
200
+ def restore_keys(original, value)
201
+ case value
202
+ when Hash
203
+ value.to_h do |key, child|
204
+ old_key = (original.is_a?(Hash) && original.key?(key.to_sym)) ? key.to_sym : key
205
+ [old_key, restore_keys(original.is_a?(Hash) ? original[old_key] : nil, child)]
206
+ end
207
+ when Array
208
+ value.each_with_index.map { |child, index| restore_keys(original.is_a?(Array) ? original[index] : nil, child) }
209
+ else value
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/configuration_error"
4
+
5
+ module Phronomy
6
+ module Tool
7
+ # Tool-owned defaults. Composition supplies values; result processing reads
8
+ # them at its existing boundary, after execution or asynchronous completion.
9
+ # @api private
10
+ class Settings
11
+ def self.install_provider(&provider)
12
+ raise ArgumentError, "Tool settings provider requires a block" unless provider
13
+
14
+ @provider = provider
15
+ nil
16
+ end
17
+
18
+ # Resolve the current value container without caching or notifications.
19
+ def self.current
20
+ settings = @provider&.call
21
+ unless settings.is_a?(Settings)
22
+ raise ConfigurationError, "Tool settings provider must return Tool::Settings"
23
+ end
24
+
25
+ settings
26
+ end
27
+
28
+ attr_accessor :max_result_size
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ class ToolError < Error; end
7
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Tool
5
+ # Routes Tool work according to the Tool execution contract.
6
+ #
7
+ # Both execution modes return {Phronomy::TaskResult}; only the execution mechanism
8
+ # differs.
9
+ #
10
+ # :cooperative Tool calls execute inline and must return quickly. call_async
11
+ # wraps their result in an already-settled TaskResult and never consumes an
12
+ # OffloadPool worker.
13
+ #
14
+ # :offloaded Tool calls route synchronous work through OffloadPool, whose
15
+ # caller-facing completion handle is also a TaskResult. Phronomy does not distinguish
16
+ # whether the reason is blocking I/O, CPU-bound work, or another long
17
+ # synchronous operation.
18
+ #
19
+ # @api private
20
+ module ToolExecutor
21
+ # @api private
22
+ def self.call_async(
23
+ tool:,
24
+ args:,
25
+ cancellation_token: nil,
26
+ config: {},
27
+ submitter: Phronomy::Execution,
28
+ on_full: :raise,
29
+ synchronous_call: tool.method(:call)
30
+ )
31
+ mode = tool.class.execution_mode
32
+
33
+ case mode
34
+ when :cooperative
35
+ task = Phronomy::TaskResult.deferred(name: "tool-#{tool.name}")
36
+ begin
37
+ task.complete(
38
+ synchronous_call.call(args, cancellation_token: cancellation_token)
39
+ )
40
+ rescue => error
41
+ task.fail(error)
42
+ end
43
+ task
44
+ when :offloaded
45
+ submitter.submit(
46
+ cancellation_token: cancellation_token,
47
+ on_full: on_full
48
+ ) do
49
+ synchronous_call.call(args, cancellation_token: cancellation_token)
50
+ end
51
+ else
52
+ raise Phronomy::ConfigurationError,
53
+ "unknown Tool execution_mode: #{mode.inspect}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -9,7 +9,7 @@ module Phronomy
9
9
  # returns to EventLoop immediately; the Tool completion handle settles when the
10
10
  # child Agent FSMSession finishes. An OffloadPool worker is therefore never
11
11
  # consumed merely to wait for another Agent.
12
- class Agent < Phronomy::Agent::Context::Capability::Base
12
+ class Agent < Phronomy::Tool::Base
13
13
  execution_mode :cooperative
14
14
  description "Wraps a Phronomy::Agent as a Tool"
15
15
  param :input, type: :string, desc: "The input to forward to the wrapped Agent"
@@ -39,11 +39,16 @@ module Phronomy
39
39
  if cancellation_token
40
40
  invoke_options[:config] = {cancellation_token: cancellation_token}
41
41
  end
42
- result = Phronomy::Agent.run_once(
43
- definition: agent_class,
44
- input: input,
45
- **invoke_options
42
+ # Use the same supplied store factory as the asynchronous path.
43
+ # Preserve the creation arguments previously supplied by run_once.
44
+ persistence = Phronomy::Agent::DefaultPersistence.build
45
+ agent = agent_class.create(
46
+ context: nil,
47
+ knowledge: [],
48
+ persistence: persistence,
49
+ on_event: nil
46
50
  )
51
+ result = agent.invoke(input, **invoke_options)
47
52
  result[:output].to_s
48
53
  end
49
54
 
@@ -51,7 +56,7 @@ module Phronomy
51
56
  # The child Agent owns its own FSMSession/EventLoop lifecycle; this
52
57
  # method only returns its completion handle and performs a short map.
53
58
  klass.define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
54
- persistence = Phronomy::Persistence.in_memory
59
+ persistence = Phronomy::Agent::DefaultPersistence.build
55
60
  agent = agent_class.create(persistence: persistence)
56
61
  task_config = (config || {}).dup
57
62
  if cancellation_token && !task_config[:cancellation_token]
@@ -83,106 +88,33 @@ module Phronomy
83
88
 
84
89
  # Agent-backed Tools have an asynchronous implementation that does not use
85
90
  # ToolExecutor/OffloadPool. Validation and Tool error policy still match
86
- # Capability::Base#call.
91
+ # Tool::Base#call.
87
92
  def call_async(
88
93
  args,
89
94
  cancellation_token: nil,
90
95
  config: {}
91
96
  )
92
- cancellation_token&.raise_if_cancelled!
93
- validated_args, schema_error = send(:validate_and_coerce, args)
94
-
95
- if schema_error
96
- return schema_error_task(schema_error)
97
- end
98
-
99
- source = execute_async(
100
- **(validated_args || {}),
101
- cancellation_token: cancellation_token,
102
- config: config || {}
103
- )
104
- unless source.respond_to?(:on_complete)
105
- raise Phronomy::ToolError,
106
- "#{self.class.name} asynchronous execution must return a completion handle"
97
+ call_async_operation(args, cancellation_token: cancellation_token,
98
+ operation_name: "agent-tool-#{name}") do |validated_args|
99
+ execute_async(**validated_args, cancellation_token: cancellation_token, config: config || {})
107
100
  end
108
-
109
- result_task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}")
110
- source.on_complete do |result, error|
111
- if error
112
- settle_async_error(result_task, error)
113
- next
114
- end
115
-
116
- begin
117
- result_task.complete(send(:truncate_result_if_needed, result))
118
- rescue => result_error
119
- settle_async_error(result_task, result_error)
120
- end
121
- end
122
- result_task
123
- rescue Phronomy::ToolError, Phronomy::CancellationError => error
124
- failed_task(error)
125
- rescue => error
126
- result_task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}")
127
- settle_async_error(result_task, error)
128
- result_task
129
101
  end
130
102
 
131
103
  private
132
104
 
133
- # Subclasses created by .from_agent and Orchestrator override this method.
105
+ # Subclasses created by .from_agent override this method.
134
106
  # It deliberately remains private so it is not part of the public Tool API.
135
107
  def execute_async(input:, cancellation_token: nil, config: {})
136
- task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}-fallback")
137
- begin
138
- task.complete(execute(input: input, cancellation_token: cancellation_token))
139
- rescue => error
140
- task.fail(error)
108
+ Phronomy::AsyncOperation.capture(name: "agent-tool-#{name}-fallback") do
109
+ execute(input: input, cancellation_token: cancellation_token)
141
110
  end
142
- task
143
111
  end
144
112
 
145
- def schema_error_task(schema_error)
146
- task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}-schema")
147
- if self.class.on_schema_error == :raise
148
- task.fail(Phronomy::ToolError.new(
149
- "#{self.class.name} schema error: #{schema_error}"
150
- ))
151
- else
152
- task.complete("Schema validation failed: #{schema_error}")
153
- end
154
- task
155
- end
156
-
157
- def failed_task(error)
158
- Phronomy::TaskResult.deferred(name: "agent-tool-#{name}-failed").tap do |task|
159
- task.fail(error)
160
- end
161
- end
113
+ # Agent recovery is an Agent operation outcome, not a generic Tool error.
114
+ def async_error_value(error)
115
+ raise error if error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
162
116
 
163
- def settle_async_error(task, error)
164
- if error.is_a?(Phronomy::ToolError) || error.is_a?(Phronomy::CancellationError) || error.is_a?(Phronomy::ExecutionRehydrationRequiredError)
165
- task.fail(error)
166
- return task
167
- end
168
-
169
- if self.class.on_error == :suppress
170
- message = "[Phronomy] Tool #{self.class.name} suppressed error: " \
171
- "#{error.class}: #{error.message}"
172
- if Phronomy.configuration.logger
173
- Phronomy.configuration.logger.warn(message)
174
- else
175
- warn message
176
- end
177
- task.complete("Tool error suppressed: #{error.message}")
178
- else
179
- wrapped = Phronomy::ToolError.new(
180
- "#{self.class.name} execution failed: #{error.message}"
181
- )
182
- wrapped.set_backtrace(error.backtrace)
183
- task.fail(wrapped)
184
- end
185
- task
117
+ super
186
118
  end
187
119
  end
188
120
  end
@@ -7,7 +7,7 @@ require "uri"
7
7
 
8
8
  module Phronomy
9
9
  module Tools
10
- # A Phronomy::Agent::Context::Capability::Base subclass that wraps a tool exposed by an external
10
+ # A Phronomy::Tool::Base subclass that wraps a tool exposed by an external
11
11
  # MCP (Model Context Protocol) server.
12
12
  #
13
13
  # Uses the official MCP Ruby SDK v1.x for transport handling, which provides
@@ -27,7 +27,7 @@ module Phronomy
27
27
  # tool_name: "search_web"
28
28
  # )
29
29
  # agent_class.tools(web_search)
30
- class Mcp < Phronomy::Agent::Context::Capability::Base
30
+ class Mcp < Phronomy::Tool::Base
31
31
  SUPPORTED_SCHEMA_DIALECTS = [
32
32
  "https://json-schema.org/draft/2020-12/schema",
33
33
  "https://json-schema.org/draft/2020-12/schema#"
@@ -273,8 +273,9 @@ module Phronomy
273
273
  end
274
274
 
275
275
  def warn_mcp(message)
276
- if Phronomy.configuration.logger
277
- Phronomy.configuration.logger.warn(message)
276
+ logger = Phronomy::RuntimeSettings.current.logger
277
+ if logger
278
+ logger.warn(message)
278
279
  else
279
280
  Kernel.warn(message)
280
281
  end
@@ -344,7 +345,8 @@ module Phronomy
344
345
  private
345
346
 
346
347
  def perform_mcp_call(cancellation_token:, args:)
347
- mcp_cancellation = build_mcp_cancellation(cancellation_token)
348
+ subscriptions = Phronomy::Concurrency::ResultSubscriptions.new
349
+ mcp_cancellation = build_mcp_cancellation(cancellation_token, subscriptions: subscriptions)
348
350
  response = begin
349
351
  @mcp_client.call_tool(
350
352
  name: self.class.tool_name,
@@ -376,13 +378,17 @@ module Phronomy
376
378
 
377
379
  result = validate_call_tool_response!(response)
378
380
  format_tool_result(result)
381
+ ensure
382
+ # A caller-facing timeout can settle before this SDK call returns.
383
+ # Keep its bridge alive until this invocation actually leaves the SDK.
384
+ subscriptions&.close
379
385
  end
380
386
 
381
- def build_mcp_cancellation(cancellation_token)
387
+ def build_mcp_cancellation(cancellation_token, subscriptions:)
382
388
  return nil unless cancellation_token
383
389
 
384
390
  mcp_cancellation = MCP::Cancellation.new
385
- cancellation_token.on_cancel do
391
+ subscriptions.explicit_cancellation(cancellation_token) do
386
392
  mcp_cancellation.cancel(reason: "phronomy_cancelled")
387
393
  end
388
394
  mcp_cancellation
@@ -478,17 +484,18 @@ module Phronomy
478
484
  end
479
485
 
480
486
  def schedule_transport_cleanup(transport)
481
- cleanup_pool = Phronomy::Runtime.instance.pool(
482
- :mcp_cleanup,
487
+ Phronomy::Execution.submit(
488
+ pool_name: :mcp_cleanup,
483
489
  size: MCP_CLEANUP_POOL_SIZE,
484
- queue_size: MCP_CLEANUP_QUEUE_SIZE
485
- )
486
- cleanup_pool.submit(on_full: :raise) do
490
+ queue_size: MCP_CLEANUP_QUEUE_SIZE,
491
+ on_full: :raise
492
+ ) do
487
493
  self.class.send(:close_transport_safely, transport)
488
494
  end
489
- rescue Phronomy::BackpressureError, Phronomy::PoolShutdownError
495
+ rescue Phronomy::BackpressureError, Phronomy::PoolShutdownError, Phronomy::RuntimeShutdownError
490
496
  # During shutdown or an exceptional cleanup burst, prefer a bounded
491
- # synchronous fallback over leaking the child process/socket.
497
+ # synchronous fallback over leaking the child process/socket. Runtime
498
+ # can reject acquisition before the cleanup pool itself is consulted.
492
499
  self.class.send(:close_transport_safely, transport)
493
500
  end
494
501
  end
@@ -3,7 +3,7 @@
3
3
  module Phronomy
4
4
  module Tools
5
5
  # A Capability::Base subclass that wraps a {Phronomy::VectorStore::Base} and
6
- # a {Phronomy::VectorStore::Embeddings::Base} adapter so that an agent can
6
+ # a {Phronomy::Embeddings::Base} adapter so that an agent can
7
7
  # perform semantic search as a tool call.
8
8
  #
9
9
  # Do not instantiate this class directly. Use the factory method
@@ -11,7 +11,7 @@ module Phronomy
11
11
  #
12
12
  # @example
13
13
  # store = Phronomy::VectorStore::InMemory.new
14
- # emb = Phronomy::VectorStore::Embeddings::RubyLLMEmbeddings.new(model: "...")
14
+ # emb = Phronomy::Embeddings::RubyLLMEmbeddings.new(model: "...")
15
15
  # tool = Phronomy::Tools::VectorSearch.from_store(store, embeddings: emb,
16
16
  # k: 3, tool_name: "search_docs",
17
17
  # description: "Search the company knowledge base.")
@@ -19,7 +19,7 @@ module Phronomy
19
19
  # agent.tools tool
20
20
  #
21
21
  # @api public
22
- class VectorSearch < Phronomy::Agent::Context::Capability::Base
22
+ class VectorSearch < Phronomy::Tool::Base
23
23
  description "Search for relevant documents using semantic similarity."
24
24
  param :query, type: :string, desc: "The natural-language search query"
25
25
 
@@ -27,7 +27,7 @@ module Phronomy
27
27
  # Build a VectorSearch tool backed by the given store and embeddings adapter.
28
28
  #
29
29
  # @param store [Phronomy::VectorStore::Base]
30
- # @param embeddings [Phronomy::VectorStore::Embeddings::Base]
30
+ # @param embeddings [Phronomy::Embeddings::Base]
31
31
  # @param k [Integer] number of results to return (default 5)
32
32
  # @param tool_name [String] name exposed to the LLM
33
33
  # @param description [String, nil] optional description override