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
@@ -20,7 +20,7 @@ module Phronomy
20
20
  alias_method :trusted?, :trusted
21
21
  end
22
22
 
23
- private_constant :PipelineState, :WorkflowBuilder, :AgentResultReceiver
23
+ private_constant :PipelineState, :WorkflowBuilder, :AgentResultReceiver, :AgentOperation
24
24
 
25
25
  def initialize(
26
26
  draft_agent:,
@@ -80,8 +80,8 @@ module Phronomy
80
80
 
81
81
  def build_workflow
82
82
  WorkflowBuilder.new(
83
- draft_agent: @draft_agent_class.new,
84
- review_agent: @review_agent_class.new,
83
+ draft_agent: AgentOperation.new(@draft_agent_class),
84
+ review_agent: AgentOperation.new(@review_agent_class),
85
85
  draft_prompt_builder: @draft_prompt_builder,
86
86
  review_prompt_builder: @review_prompt_builder,
87
87
  draft_result_parser: @draft_result_parser,
@@ -92,7 +92,7 @@ module Phronomy
92
92
  end
93
93
 
94
94
  def default_parse_draft(text)
95
- json_parser.parse(text)
95
+ default_parser.parse(text)
96
96
  rescue Phronomy::ParseError
97
97
  {
98
98
  answer: text.to_s,
@@ -102,7 +102,7 @@ module Phronomy
102
102
  end
103
103
 
104
104
  def default_parse_review(text)
105
- json_parser.parse(text)
105
+ default_parser.parse(text)
106
106
  rescue Phronomy::ParseError
107
107
  {
108
108
  approved: false,
@@ -111,8 +111,8 @@ module Phronomy
111
111
  }
112
112
  end
113
113
 
114
- def json_parser
115
- @json_parser ||= Phronomy::OutputParser::JsonParser.new
114
+ def default_parser
115
+ @default_parser ||= DefaultParser.build
116
116
  end
117
117
  end
118
118
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module LLMAdapter
5
+ # Offloads the entire operation, including SDK initialization and decoding.
6
+ # Construction does not start a Runtime.
7
+ # @api private
8
+ class AsyncClient
9
+ # @api private
10
+ def initialize(adapter:, pool: nil, submitter: Phronomy::Execution)
11
+ @adapter = adapter
12
+ @pool = pool
13
+ @submitter = submitter
14
+ end
15
+
16
+ # @api private
17
+ def complete_async(request, cancellation_token: nil, pool: @pool)
18
+ submit(pool, cancellation_token) do
19
+ @adapter.complete(request, cancellation_token: cancellation_token)
20
+ end
21
+ end
22
+
23
+ # The block is an internal event sink, never an Application callback.
24
+ # @api private
25
+ def stream_async(request, cancellation_token: nil, pool: @pool, &block)
26
+ raise ArgumentError, "stream_async requires a block" unless block
27
+ submit(pool, cancellation_token) do
28
+ @adapter.stream(request, cancellation_token: cancellation_token) do |chunk|
29
+ cancellation_token&.raise_if_cancelled!
30
+ block.call(chunk)
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def submit(pool, cancellation_token, &operation)
38
+ if pool
39
+ Phronomy::Execution.submit(pool: pool, cancellation_token: cancellation_token,
40
+ on_full: :raise, &operation)
41
+ else
42
+ @submitter.submit(cancellation_token: cancellation_token, on_full: :raise, &operation)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,5 +3,7 @@
3
3
  require_relative "transport_error"
4
4
 
5
5
  module Phronomy
6
- class AuthenticationError < TransportError; end
6
+ module LLMAdapter
7
+ class AuthenticationError < TransportError; end
8
+ end
7
9
  end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Phronomy
6
+ module LLMAdapter
7
+ # SDK objects are operation-local. Only Phronomy request/result values cross
8
+ # the backend boundary; a Tool definition never contains executable work.
9
+ # @api public
10
+ class RubyLLM < Base
11
+ ToolsRequested = Class.new(StandardError) do
12
+ attr_reader :message_value
13
+ def initialize(message_value)
14
+ @message_value = message_value
15
+ super("assistant requested Tools")
16
+ end
17
+ end
18
+ private_constant :ToolsRequested
19
+
20
+ # @api public
21
+ def identity
22
+ super.merge("sdk_version" => ::RubyLLM::VERSION).freeze
23
+ end
24
+
25
+ # @api public
26
+ def input_budget(model_config)
27
+ config = model_config.to_h.transform_keys(&:to_s)
28
+ return nil unless config["model"]
29
+ model = ::RubyLLM.models.find(config["model"], provider: config["provider"])
30
+ limit = model&.context_window
31
+ return nil unless limit.is_a?(Integer) && limit.positive?
32
+ Phronomy::LlmContextWindow::TokenBudget.new(max_input_tokens: limit)
33
+ rescue ::RubyLLM::ModelNotFoundError
34
+ nil
35
+ rescue => error
36
+ raise_provider_error(error)
37
+ end
38
+
39
+ protected
40
+
41
+ # @api public
42
+ def perform_complete(request, cancellation_token:)
43
+ execute_request(request, cancellation_token)
44
+ end
45
+
46
+ # @api public
47
+ def perform_stream(request, cancellation_token:, &block)
48
+ execute_request(request, cancellation_token, &block)
49
+ end
50
+
51
+ private
52
+
53
+ def execute_request(request, token)
54
+ callback_error = nil
55
+ chat = build_chat(request)
56
+ token&.raise_if_cancelled!
57
+ chat.after_message do |message|
58
+ raise ToolsRequested.new(message) if message.tool_call?
59
+ end
60
+ response = if block_given?
61
+ sink = proc do |chunk|
62
+ token&.raise_if_cancelled!
63
+ value = stream_value(chunk)
64
+ begin
65
+ yield value
66
+ rescue => error
67
+ callback_error = error
68
+ raise
69
+ end
70
+ end
71
+ request.message ? chat.ask(request.message, &sink) : chat.complete(&sink)
72
+ else
73
+ request.message ? chat.ask(request.message) : chat.complete
74
+ end
75
+ response_value(response)
76
+ rescue ToolsRequested => requested
77
+ response_value(requested.message_value)
78
+ rescue => error
79
+ raise error if error.equal?(callback_error)
80
+ raise_provider_error(error)
81
+ end
82
+
83
+ def build_chat(request)
84
+ config = request.model_config
85
+ options = {}
86
+ options[:model] = config["model"] if config["model"]
87
+ if config["provider"]
88
+ options[:provider] = config["provider"].to_sym
89
+ options[:assume_model_exists] = config.fetch("assume_model_exists", true)
90
+ elsif config.key?("assume_model_exists")
91
+ options[:assume_model_exists] = config["assume_model_exists"]
92
+ end
93
+ chat = ::RubyLLM.chat(**options)
94
+ chat.with_temperature(config["temperature"]) if config["temperature"]
95
+ chat.with_max_output_tokens(config["max_output_tokens"]) if config["max_output_tokens"]
96
+ if request.system
97
+ chat.with_instructions(request.system, cache_until_here: config["cache_instructions"])
98
+ end
99
+ request.tools.each { |definition| chat.with_tools(tool_declaration(definition)) }
100
+ request.messages.each { |value| chat.messages << sdk_message(value) }
101
+ chat
102
+ end
103
+
104
+ def tool_declaration(definition)
105
+ name = definition.fetch("name")
106
+ klass = Class.new(::RubyLLM::Tool) do
107
+ define_singleton_method(:tool_name) { name }
108
+ define_method(:execute) do |**_arguments|
109
+ raise InvalidResultError, "SDK attempted to execute a declaration-only Tool"
110
+ end
111
+ end
112
+ klass.description(definition.fetch("description"))
113
+ klass.parameters(definition.fetch("parameters_schema"))
114
+ klass.provider_options(definition.fetch("provider_options", {}))
115
+ klass
116
+ end
117
+
118
+ def sdk_message(value)
119
+ calls = value.tool_calls.to_h do |call|
120
+ [call.id, ::RubyLLM::ToolCall.new(id: call.id, name: call.name,
121
+ arguments: call.arguments, thought_signature: call.metadata["thought_signature"])]
122
+ end
123
+ content = value.content
124
+ content = JSON.generate(content) if content.is_a?(Hash) || content.is_a?(Array)
125
+ content = "" if value.role == :assistant && content.nil? && !calls.empty?
126
+ ::RubyLLM::Message.new(role: value.role, content: content,
127
+ tool_calls: calls.empty? ? nil : calls, tool_call_id: value.tool_call_id,
128
+ model: value.metadata["model_id"])
129
+ end
130
+
131
+ def response_value(message)
132
+ unless message.respond_to?(:content) && message.respond_to?(:role) && message.role.to_sym == :assistant
133
+ raise InvalidResultError, "provider returned an invalid assistant message"
134
+ end
135
+ raw_calls = message.tool_calls if message.respond_to?(:tool_calls)
136
+ calls = raw_calls.is_a?(Hash) ? raw_calls.values : (raw_calls || [])
137
+ raise InvalidResultError, "provider returned invalid Tool calls" unless calls.is_a?(Array)
138
+ calls = calls.map do |call|
139
+ metadata = {}
140
+ if call.respond_to?(:thought_signature) && call.thought_signature
141
+ metadata["thought_signature"] = call.thought_signature
142
+ end
143
+ Phronomy::Tool::CallRequest.new(id: call.id, name: call.name,
144
+ arguments: call.arguments, metadata: metadata)
145
+ end
146
+ tokens = message.tokens if message.respond_to?(:tokens)
147
+ usage = Phronomy::LLMAdapter::TokenUsage.new(
148
+ input: tokens&.input, output: tokens&.output,
149
+ cached: tokens&.cache_read, cache_creation: tokens&.cache_write
150
+ )
151
+ metadata = {}
152
+ metadata["model_id"] = message.model if message.respond_to?(:model) && message.model
153
+ Response.new(content: message.content, tool_calls: calls, usage: usage, metadata: metadata)
154
+ rescue ArgumentError, TypeError, NoMethodError => error
155
+ raise InvalidResultError, "malformed provider response: #{error.message}"
156
+ end
157
+
158
+ def stream_value(chunk)
159
+ raise InvalidResultError, "provider returned an invalid stream chunk" unless chunk.respond_to?(:content)
160
+ StreamChunk.new(content: chunk.content)
161
+ rescue ArgumentError => error
162
+ raise InvalidResultError, "malformed provider chunk: #{error.message}"
163
+ end
164
+
165
+ # Translate SDK errors only. Cancellation and callback failures retain
166
+ # identity; raising here preserves the original provider exception cause.
167
+ def raise_provider_error(error)
168
+ case error
169
+ when ::RubyLLM::RateLimitError
170
+ raise Phronomy::LLMAdapter::RateLimitError, error.message
171
+ when ::RubyLLM::UnauthorizedError, ::RubyLLM::ForbiddenError
172
+ raise Phronomy::LLMAdapter::AuthenticationError, error.message
173
+ when ::RubyLLM::ContextLengthExceededError
174
+ raise Phronomy::LLMAdapter::ContextLengthError, error.message
175
+ when ::RubyLLM::Error
176
+ raise Phronomy::LLMAdapter::TransportError, error.message
177
+ else
178
+ raise error
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -1,91 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "request"
4
+ require_relative "response"
5
+ require_relative "stream_chunk"
6
+ require_relative "invalid_result_error"
7
+
3
8
  module Phronomy
4
9
  module LLMAdapter
5
- # Beta extension SPI for LLM call adapters.
6
- #
7
- # External adapters implement {#complete} and {#stream}. The adapter or the
8
- # underlying provider client owns transport timeout, retry, backoff, and
9
- # rate-limit behavior. Phronomy owns cooperative cancellation and isolates
10
- # synchronous provider calls in {OffloadPool}.
11
- #
12
- # The Agent pipeline calls the framework-owned {#complete_async} and
13
- # {#stream_async} wrappers. Adapter implementers do not need to depend on
14
- # EventLoop, FSMSession, OffloadPool, AgentInvocation, or ExecutionCoordinator.
15
- #
16
- # The current input to this SPI is the configured/materialized chat runtime
17
- # object. Formalizing this SPI therefore does not imply a provider-neutral
18
- # replacement for RubyLLMMaterializer.
19
- #
10
+ # Owns one-turn operations. Backends implement protected synchronous hooks.
11
+ # SDK construction and translation happen entirely inside those hooks.
20
12
  # @api public
21
13
  class Base
22
- # Performs a blocking (non-streaming) LLM completion.
23
- #
24
- # Implementors call the configured chat/runtime client and return its
25
- # response object. Transport/retry policy remains adapter-owned.
26
- #
27
- # @param chat [Object] the configured/materialized chat runtime object
28
- # @param message [String, nil] user message, or nil to continue without adding a new user turn
29
- # @param config [Hash] invocation config (e.g. +:cancellation_token+)
30
- # @return [Object] LLM response object
31
- # @raise [NotImplementedError]
32
14
  # @api public
33
- def complete(chat, message, config: {})
34
- raise NotImplementedError, "#{self.class}#complete is not implemented"
15
+ def identity
16
+ {"adapter_name" => self.class.name}.freeze
17
+ end
18
+
19
+ # Local model metadata only. Missing capacity is permitted.
20
+ # @api public
21
+ def input_budget(model_config)
22
+ nil
35
23
  end
36
24
 
37
- # Performs a blocking streaming LLM completion.
38
- #
39
- # @param chat [Object] the configured/materialized chat runtime object
40
- # @param message [String, nil] user message, or nil to continue without adding a new user turn
41
- # @param config [Hash] invocation config
42
- # @yield [chunk] streaming chunk from the LLM
43
- # @return [Object] LLM response object
44
- # @raise [NotImplementedError]
45
25
  # @api public
46
- def stream(chat, message, config: {}, &block)
47
- raise NotImplementedError, "#{self.class}#stream is not implemented"
26
+ def complete(request, cancellation_token: nil)
27
+ validate_request!(request, cancellation_token)
28
+ response = perform_complete(request, cancellation_token: cancellation_token)
29
+ cancellation_token&.raise_if_cancelled!
30
+ validate_response!(response)
48
31
  end
49
32
 
50
- # Submits a non-streaming LLM call to {OffloadPool}.
51
- #
52
- # Transport timeout and retry remain the responsibility of the adapter or
53
- # provider client; Phronomy does not attach an additional operation timeout.
54
- #
55
- # @return [Phronomy::TaskResult] caller-facing completion handle
56
- # @api private
57
- def complete_async(chat, message, config: {}, pool: default_pool)
58
- token = config[:cancellation_token]
59
- pool.submit(cancellation_token: token, on_full: :raise) do
60
- complete(chat, message, config: config)
33
+ # Notifications run on the caller thread. AsyncClient supplies an internal
34
+ # event sink; application callbacks remain with their owning domain.
35
+ # @api public
36
+ def stream(request, cancellation_token: nil)
37
+ raise ArgumentError, "stream requires a block" unless block_given?
38
+ validate_request!(request, cancellation_token)
39
+ response = perform_stream(request, cancellation_token: cancellation_token) do |chunk|
40
+ cancellation_token&.raise_if_cancelled!
41
+ raise InvalidResultError, "stream must yield StreamChunk values" unless chunk.is_a?(StreamChunk)
42
+ yield chunk
61
43
  end
44
+ cancellation_token&.raise_if_cancelled!
45
+ validate_response!(response)
62
46
  end
63
47
 
64
- # Submits a streaming LLM call to {OffloadPool}.
65
- #
66
- # The block is invoked on an OffloadPool worker thread. Agent code must pass
67
- # only a lightweight internal sink that posts a value to EventLoop;
68
- # application callbacks must never be passed directly to this method.
69
- #
70
- # @yield [chunk] streaming chunk on the worker thread
71
- # @return [Phronomy::TaskResult] caller-facing completion handle
72
- # @api private
73
- def stream_async(chat, message, config: {}, pool: default_pool, &block)
74
- raise ArgumentError, "stream_async requires a block" unless block
48
+ protected
75
49
 
76
- token = config[:cancellation_token]
77
- pool.submit(cancellation_token: token, on_full: :raise) do
78
- stream(chat, message, config: config) do |chunk|
79
- token&.raise_if_cancelled!("invocation cancelled during streaming")
80
- block.call(chunk)
81
- end
82
- end
50
+ # @api public
51
+ def perform_complete(request, cancellation_token:)
52
+ raise NotImplementedError, "#{self.class}#perform_complete is not implemented"
53
+ end
54
+
55
+ # @api public
56
+ def perform_stream(request, cancellation_token:, &block)
57
+ raise NotImplementedError, "#{self.class}#perform_stream is not implemented"
83
58
  end
84
59
 
85
60
  private
86
61
 
87
- def default_pool
88
- Phronomy::Runtime.instance.offload
62
+ def validate_request!(request, token)
63
+ token&.raise_if_cancelled!
64
+ raise ArgumentError, "LLM operation requires a Request" unless request.is_a?(Request)
65
+ end
66
+
67
+ def validate_response!(response)
68
+ raise InvalidResultError, "LLM operation must return a Response" unless response.is_a?(Response)
69
+ response
89
70
  end
90
71
  end
91
72
  end
@@ -3,5 +3,7 @@
3
3
  require_relative "../common/error"
4
4
 
5
5
  module Phronomy
6
- class ContextLengthError < Error; end
6
+ module LLMAdapter
7
+ class ContextLengthError < Phronomy::Error; end
8
+ end
7
9
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/error"
4
+
5
+ module Phronomy
6
+ module LLMAdapter
7
+ # @api public
8
+ class InvalidResultError < Phronomy::Error; end
9
+ end
10
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/values/immutable"
4
+ require_relative "../tool/schema"
5
+ require_relative "../tool/call_request"
6
+
7
+ module Phronomy
8
+ module LLMAdapter
9
+ # Provider-independent conversation value, with no executable capabilities.
10
+ # @api public
11
+ class Message
12
+ attr_reader :role, :content, :tool_calls, :tool_call_id, :metadata
13
+
14
+ # @api public
15
+ def initialize(role:, content: nil, tool_calls: [], tool_call_id: nil, metadata: {})
16
+ unless role.respond_to?(:to_sym) && %i[system user assistant tool].include?(role.to_sym)
17
+ raise ArgumentError, "invalid LLM message role"
18
+ end
19
+ unless content.nil? || content.is_a?(String) || content.is_a?(Array) || content.is_a?(Hash)
20
+ raise ArgumentError, "LLM content must be text, structured JSON or nil"
21
+ end
22
+ unless tool_calls.is_a?(Array) && tool_calls.all? { |call| call.is_a?(Phronomy::Tool::CallRequest) }
23
+ raise ArgumentError, "LLM Tool calls must be Tool::CallRequest values"
24
+ end
25
+ ids = tool_calls.map(&:id)
26
+ raise ArgumentError, "duplicate LLM Tool call id" unless ids.uniq == ids
27
+ raise ArgumentError, "only assistant messages can request Tools" if !tool_calls.empty? && role.to_sym != :assistant
28
+ if role.to_sym == :tool && (!tool_call_id.is_a?(String) || tool_call_id.empty?)
29
+ raise ArgumentError, "Tool message requires tool_call_id"
30
+ end
31
+ unless tool_call_id.nil? || tool_call_id.is_a?(String)
32
+ raise ArgumentError, "tool_call_id must be a String"
33
+ end
34
+ raise ArgumentError, "LLM metadata must be a Hash" unless metadata.is_a?(Hash)
35
+ Phronomy::Values::Immutable.validate_canonical_json!(content, label: "LLM content")
36
+ Phronomy::Values::Immutable.validate_canonical_json!(metadata, label: "LLM metadata")
37
+ @role = role.to_sym
38
+ @content = Phronomy::Values::Immutable.copy(content)
39
+ @tool_calls = tool_calls.dup.freeze
40
+ @tool_call_id = tool_call_id&.dup&.freeze
41
+ @metadata = Phronomy::Values::Immutable.copy(metadata)
42
+ freeze
43
+ end
44
+
45
+ # @api public
46
+ def tool_call?
47
+ !tool_calls.empty?
48
+ end
49
+
50
+ # @api public
51
+ def content_present?
52
+ !content.nil? && !content.empty?
53
+ end
54
+
55
+ # @api public
56
+ def to_h
57
+ {"role" => role.to_s, "content" => content, "tool_calls" => tool_calls.map(&:to_h),
58
+ "tool_call_id" => tool_call_id, "metadata" => metadata}.compact
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,5 +3,7 @@
3
3
  require_relative "transport_error"
4
4
 
5
5
  module Phronomy
6
- class RateLimitError < TransportError; end
6
+ module LLMAdapter
7
+ class RateLimitError < TransportError; end
8
+ end
7
9
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "message"
4
+
5
+ module Phronomy
6
+ module LLMAdapter
7
+ # Immutable input for one model turn, including inert Tool definitions.
8
+ # @api public
9
+ class Request
10
+ attr_reader :model_config, :system, :messages, :tools, :message
11
+
12
+ # @api public
13
+ def initialize(model_config: {}, system: nil, messages: [], tools: [], message: nil)
14
+ if !(model_config.is_a?(Hash) && (system.nil? || system.is_a?(String)) && (message.nil? || message.is_a?(String)))
15
+ raise ArgumentError, "invalid LLM request configuration or text"
16
+ end
17
+ unless messages.is_a?(Array) && messages.all? { |item| item.is_a?(Message) }
18
+ raise ArgumentError, "LLM request messages must be Message values"
19
+ end
20
+ unless tools.is_a?(Array) && tools.all? { |tool| tool.is_a?(Hash) }
21
+ raise ArgumentError, "LLM request tools must be definition Hashes"
22
+ end
23
+ normalized_tools = tools.map do |definition|
24
+ definition = normalize_keys(definition)
25
+ name = definition["name"]
26
+ unless name.is_a?(String) && !name.empty? && definition["description"].is_a?(String)
27
+ raise ArgumentError, "Tool definition requires name and description"
28
+ end
29
+ if definition.key?("provider_options") && !definition["provider_options"].is_a?(Hash)
30
+ raise ArgumentError, "Tool provider_options must be a Hash"
31
+ end
32
+ Phronomy::Tool::Schema.new(definition.fetch("parameters_schema"))
33
+ Phronomy::Values::Immutable.validate_canonical_json!(definition, label: "Tool definition")
34
+ definition
35
+ end
36
+ names = normalized_tools.map { |tool| tool.fetch("name") }
37
+ raise ArgumentError, "duplicate Tool definition name" unless names.uniq == names
38
+ config = normalize_keys(model_config)
39
+ Phronomy::Values::Immutable.validate_canonical_json!(config, label: "LLM model configuration")
40
+ max = config["max_output_tokens"]
41
+ if !max.nil? && (!max.is_a?(Integer) || max <= 0)
42
+ raise ArgumentError, "max_output_tokens must be a positive Integer"
43
+ end
44
+ %w[model provider].each do |key|
45
+ value = config[key]
46
+ if !(value.nil? || (value.is_a?(String) && !value.empty?))
47
+ raise ArgumentError, "#{key} must be a non-empty String or nil"
48
+ end
49
+ end
50
+ %w[assume_model_exists cache_instructions].each do |key|
51
+ raise ArgumentError, "#{key} must be boolean or nil" unless [true, false, nil].include?(config[key])
52
+ end
53
+ temperature = config["temperature"]
54
+ if !(temperature.nil? || (temperature.is_a?(Numeric) && temperature.finite?))
55
+ raise ArgumentError, "temperature must be a finite number or nil"
56
+ end
57
+ @model_config = Phronomy::Values::Immutable.copy(config)
58
+ @system = system&.dup&.freeze
59
+ @message = message&.dup&.freeze
60
+ @messages = messages.dup.freeze
61
+ @tools = Phronomy::Values::Immutable.copy(normalized_tools)
62
+ freeze
63
+ end
64
+
65
+ private
66
+
67
+ def normalize_keys(value)
68
+ value.each_with_object({}) do |(key, child), result|
69
+ unless key.is_a?(String) || key.is_a?(Symbol)
70
+ raise ArgumentError, "LLM configuration/definition keys must be Strings or Symbols"
71
+ end
72
+ name = key.to_s
73
+ raise ArgumentError, "duplicate LLM configuration/definition key: #{name}" if result.key?(name)
74
+ result[name] = child
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "message"
4
+ require_relative "token_usage"
5
+
6
+ module Phronomy
7
+ module LLMAdapter
8
+ # Completed assistant turn. Tool requests do not authorize execution.
9
+ # @api public
10
+ class Response < Message
11
+ attr_reader :usage
12
+
13
+ # @api public
14
+ def initialize(content: nil, tool_calls: [], usage: Phronomy::LLMAdapter::TokenUsage.new, metadata: {}, role: :assistant)
15
+ raise ArgumentError, "LLM response must be an assistant message" unless role == :assistant || role == "assistant"
16
+ raise ArgumentError, "LLM usage must be LLMAdapter::TokenUsage" unless usage.is_a?(Phronomy::LLMAdapter::TokenUsage)
17
+ @usage = usage
18
+ super(role: :assistant, content: content, tool_calls: tool_calls, metadata: metadata)
19
+ end
20
+
21
+ # @api public
22
+ def self.from_h(value)
23
+ source = value.transform_keys(&:to_s)
24
+ new(role: source.fetch("role", "assistant"), content: source["content"],
25
+ tool_calls: source.fetch("tool_calls", []).map { |call| Phronomy::Tool::CallRequest.from_h(call) },
26
+ usage: Phronomy::LLMAdapter::TokenUsage.new(**source.fetch("usage", {}).transform_keys(&:to_sym)),
27
+ metadata: source.fetch("metadata", {}))
28
+ end
29
+
30
+ # @api public
31
+ def to_h
32
+ super.merge("usage" => usage.to_h.transform_keys(&:to_s))
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module LLMAdapter
5
+ # Intermediate text notification; the final Response is authoritative.
6
+ # @api public
7
+ class StreamChunk
8
+ attr_reader :content
9
+
10
+ # @api public
11
+ def initialize(content:)
12
+ raise ArgumentError, "stream content must be a String or nil" unless content.nil? || content.is_a?(String)
13
+ @content = content&.dup&.freeze
14
+ freeze
15
+ end
16
+ end
17
+ end
18
+ end