phronomy 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +14 -7
  5. data/VERIFY.sh +27 -27
  6. data/benchmark/bench_agent_invoke.rb +26 -22
  7. data/benchmark/bench_context_assembler.rb +4 -5
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  10. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  11. data/docs/architecture/agent-context.md +5 -3
  12. data/docs/architecture/agent-transition-ownership.md +89 -0
  13. data/docs/architecture/before-llm-input.md +6 -0
  14. data/docs/architecture/context-management.md +30 -0
  15. data/docs/architecture/context-preparation-steps.md +90 -0
  16. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  17. data/docs/architecture/execution-metadata-and-values.md +80 -0
  18. data/docs/architecture/generator-verifier-ownership.md +111 -0
  19. data/docs/architecture/multi-agent-handoff.md +8 -2
  20. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  21. data/docs/architecture/persistence.md +29 -6
  22. data/docs/architecture/refactoring-closure.md +334 -0
  23. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  24. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  25. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  26. data/docs/architecture/tracing.md +4 -4
  27. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  28. data/docs/architecture.md +209 -0
  29. data/docs/async-composition.md +205 -0
  30. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  31. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  32. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  33. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  34. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  35. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  36. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  37. data/docs/decisions/032-storage-backend-composition.md +80 -0
  38. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  39. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  40. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  41. data/docs/decisions/036-context-contract-ownership.md +86 -0
  42. data/docs/decisions/037-common-definition-ownership.md +62 -0
  43. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  44. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  45. data/docs/decisions/040-configuration-default-composition.md +94 -0
  46. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  47. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  48. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  49. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  50. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  51. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  52. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  53. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  54. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  55. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  56. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  57. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  58. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  59. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  60. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  61. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  62. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  63. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  64. data/docs/decisions/README.md +35 -6
  65. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  66. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  67. data/docs/features.md +21 -12
  68. data/docs/getting-started.md +10 -10
  69. data/docs/migrations/0.15.md +5 -0
  70. data/docs/migrations/durable-semantic-coordination-v2.md +7 -1
  71. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  72. data/docs/migrations/neutral-storage-spi.md +43 -0
  73. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  74. data/docs/migrations/shared-state-multi-agent.md +44 -0
  75. data/docs/migrations/storage-backend-composition.md +117 -0
  76. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  77. data/docs/persistence-backends.md +162 -204
  78. data/docs/runtime-and-concurrency.md +205 -60
  79. data/lib/phronomy/agent/api/agent.rb +17 -0
  80. data/lib/phronomy/agent/async_event_api.rb +2 -2
  81. data/lib/phronomy/agent/base.rb +55 -251
  82. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  83. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  84. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  85. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  86. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  87. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  88. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  89. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  90. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  91. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  92. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  93. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  94. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  95. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  96. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  97. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  98. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  99. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +20 -96
  100. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  101. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  102. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +12 -13
  103. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  104. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  105. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  106. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  107. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  108. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  109. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  110. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  111. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  112. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +22 -73
  113. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  114. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  115. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  116. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  117. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  118. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  119. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  120. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  121. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  122. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  123. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  124. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  125. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  126. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  127. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  128. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  129. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  130. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  131. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  132. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  133. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  134. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  135. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  136. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  137. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  138. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  139. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  140. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +36 -41
  141. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  142. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +9 -13
  143. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  144. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  145. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  146. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  147. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  148. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  149. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  150. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  151. data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +2 -2
  152. data/lib/phronomy/common/configuration_error.rb +7 -0
  153. data/lib/phronomy/common/error.rb +5 -0
  154. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  155. data/lib/phronomy/common/values/serializable.rb +32 -0
  156. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  157. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  158. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  159. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  160. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  161. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +15 -8
  162. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  163. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  164. data/lib/phronomy/engine/concurrency/offload_pool.rb +29 -16
  165. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  166. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  167. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  168. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  169. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  170. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  171. data/lib/phronomy/engine/event_loop.rb +160 -626
  172. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  173. data/lib/phronomy/engine/execution.rb +229 -0
  174. data/lib/phronomy/engine/execution_cancellation_error.rb +14 -0
  175. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  176. data/lib/phronomy/engine/execution_timeout_error.rb +14 -0
  177. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  178. data/lib/phronomy/engine/fsm_session.rb +38 -31
  179. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +3 -1
  180. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +3 -1
  181. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +3 -1
  182. data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +13 -1
  183. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  184. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  185. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  186. data/lib/phronomy/engine/runtime.rb +80 -84
  187. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  188. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  189. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  190. data/lib/phronomy/engine/{task.rb → task_result.rb} +101 -42
  191. data/lib/phronomy/engine/timeout_error.rb +7 -0
  192. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  193. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  194. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  195. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  196. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  197. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  198. data/lib/phronomy/llm_adapter/base.rb +2 -2
  199. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  200. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  201. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  202. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  203. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  204. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  205. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  206. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  207. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +8 -8
  208. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  209. data/lib/phronomy/multi_agent/orchestrator.rb +36 -68
  210. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  211. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  212. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  213. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  214. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  215. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  216. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  217. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  218. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  219. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  220. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  221. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  222. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  223. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  224. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  225. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  226. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  227. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  228. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  229. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  230. data/lib/phronomy/storage/backend.rb +101 -0
  231. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  232. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  233. data/lib/phronomy/storage/blobs.rb +31 -0
  234. data/lib/phronomy/storage/condition.rb +25 -0
  235. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  236. data/lib/phronomy/storage/conflict_error.rb +9 -0
  237. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  238. data/lib/phronomy/storage/entry.rb +33 -0
  239. data/lib/phronomy/storage/guard_ref.rb +13 -0
  240. data/lib/phronomy/storage/not_found_error.rb +9 -0
  241. data/lib/phronomy/storage/record_codec.rb +177 -0
  242. data/lib/phronomy/storage/records.rb +61 -0
  243. data/lib/phronomy/storage/resource.rb +126 -0
  244. data/lib/phronomy/storage/scope.rb +25 -0
  245. data/lib/phronomy/storage/serialization_error.rb +9 -0
  246. data/lib/phronomy/storage/streams.rb +44 -0
  247. data/lib/phronomy/storage/transaction_error.rb +10 -0
  248. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  249. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  250. data/lib/phronomy/storage/validation.rb +53 -0
  251. data/lib/phronomy/storage/view.rb +131 -0
  252. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  253. data/lib/phronomy/testing/fake_clock.rb +13 -9
  254. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  255. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  256. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  257. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  258. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  259. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  260. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  261. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  262. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  263. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  264. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  265. data/lib/phronomy/tools/agent.rb +6 -6
  266. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  267. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  268. data/lib/phronomy/version.rb +1 -1
  269. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  270. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  271. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  272. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  273. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +126 -75
  274. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  275. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  276. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  277. data/lib/phronomy/workflow/phase_machine_builder.rb +8 -8
  278. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  279. data/lib/phronomy.rb +52 -94
  280. data/scripts/api_snapshot.rb +4 -2
  281. data/scripts/storage_spi_snapshot.rb +36 -0
  282. data/sig/phronomy/agent.rbs +4 -5
  283. data/sig/phronomy/execution_receiver.rbs +34 -0
  284. data/sig/phronomy/extensions.rbs +5 -5
  285. data/sig/phronomy/handoff.rbs +4 -2
  286. data/sig/phronomy/multi_agent.rbs +19 -3
  287. data/sig/phronomy/persistence.rbs +7 -91
  288. data/sig/phronomy/runtime.rbs +34 -7
  289. data/sig/phronomy/storage.rbs +174 -0
  290. data/sig/phronomy/tool.rbs +11 -2
  291. data/sig/phronomy/workflow.rbs +1 -1
  292. metadata +236 -99
  293. data/lib/phronomy/agent/execution_coordinator.rb +0 -3138
  294. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  295. data/lib/phronomy/agent/recovery_support.rb +0 -504
  296. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  297. data/lib/phronomy/agent/tool_executor.rb +0 -55
  298. data/lib/phronomy/generator_verifier.rb +0 -369
  299. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  300. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  301. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
  302. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  303. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  304. data/lib/phronomy/persistence/in_memory.rb +0 -690
  305. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  306. data/lib/phronomy/persistence.rb +0 -276
  307. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  308. data/lib/phronomy/workflow_recovery.rb +0 -123
  309. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  310. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  311. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  312. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  313. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  314. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  315. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  316. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  317. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  318. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  319. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  320. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  321. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  322. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  323. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../engine/concurrency/worker_input_restricted"
4
+
3
5
  require "securerandom"
4
6
  require_relative "concerns/filterable"
5
7
  require_relative "concerns/before_llm_input"
@@ -31,6 +33,8 @@ module Phronomy
31
33
  # max_iterations 15
32
34
  # end
33
35
  class Base
36
+ include Phronomy::Concurrency::WorkerInputRestricted
37
+
34
38
  include Phronomy::Runnable
35
39
  include Concerns::Filterable
36
40
  include Concerns::BeforeLLMInput
@@ -119,15 +123,9 @@ module Phronomy
119
123
  if val.nil?
120
124
  @max_output_tokens
121
125
  else
122
- @max_output_tokens = val.to_i
123
- end
124
- end
125
-
126
- def context_window(val = nil)
127
- if val.nil?
128
- @context_window
129
- else
130
- @context_window = val.to_i
126
+ value = Integer(val)
127
+ raise ArgumentError, "max_output_tokens must be positive" unless value.positive?
128
+ @max_output_tokens = value
131
129
  end
132
130
  end
133
131
 
@@ -204,7 +202,7 @@ module Phronomy
204
202
  runtime = Phronomy::Runtime.instance
205
203
  materialized = false
206
204
 
207
- agent = runtime.__load_agent(key, expected_class: self) do |owner_runtime|
205
+ agent = OwnershipRegistry.for(runtime).load(key, expected_class: self) do |owner_runtime|
208
206
  materialized = true
209
207
  instance = __construct_owned_agent(
210
208
  owner_runtime,
@@ -235,14 +233,17 @@ module Phronomy
235
233
  # Returns only the process-local live Agent owner. Does not access
236
234
  # Persistence and returns nil when this Runtime has no live owner.
237
235
  def get(agent_id)
238
- Phronomy::Runtime.instance.__get_agent(agent_id, expected_class: self)
236
+ key = agent_id.to_s
237
+ raise ArgumentError, "agent_id must not be empty" if key.empty?
238
+
239
+ OwnershipRegistry.existing_for(Phronomy::Runtime.instance)&.get(key, expected_class: self)
239
240
  end
240
241
 
241
242
  # Resolves the live Agent instance that currently owns execution_id in
242
243
  # this process. The Runtime returns only a read-only ownership view;
243
244
  # mutable Agent execution state remains EventLoop-owned.
244
245
  def live_for_execution(execution_id)
245
- owner = Phronomy::Runtime.instance.__agent_execution_owner(execution_id)
246
+ owner = Phronomy::Agent::ExecutionRegistry.existing_for(Phronomy::Runtime.instance)&.agent_execution_owner(execution_id)
246
247
  unless owner
247
248
  raise Phronomy::ExecutionRehydrationRequiredError,
248
249
  "no live execution owner for #{execution_id}; durable rehydration is required"
@@ -338,7 +339,7 @@ module Phronomy
338
339
 
339
340
  runtime = Phronomy::Runtime.instance
340
341
  begin
341
- runtime.__create_agent(effective_agent_id, expected_class: self.class) do |owner_runtime|
342
+ OwnershipRegistry.for(runtime).create(effective_agent_id, expected_class: self.class) do |owner_runtime|
342
343
  __prepare_runtime_owner!(owner_runtime, effective_agent_id)
343
344
  initialize_owned_state(
344
345
  agent_id: effective_agent_id,
@@ -350,7 +351,7 @@ module Phronomy
350
351
  )
351
352
  self
352
353
  end
353
- rescue Phronomy::Persistence::ConflictError => error
354
+ rescue Phronomy::Storage::ConflictError => error
354
355
  raise Phronomy::AgentAlreadyExistsError,
355
356
  "Agent #{effective_agent_id.inspect} already exists durably: #{error.message}"
356
357
  end
@@ -395,34 +396,8 @@ module Phronomy
395
396
  def add_knowledge(content, metadata: {})
396
397
  __assert_live_agent!
397
398
  current = agent_root
398
- next_root = nil
399
- appended = nil
400
- persistence.transaction do |tx|
401
- tx.executions.assert_idle!(agent_id)
402
- record = build_knowledge_record(
403
- tx: tx,
404
- root: current,
405
- content: content,
406
- metadata: metadata
407
- )
408
- appended = tx.journals.append(
409
- agent_id,
410
- expected_position: current.journal_position,
411
- records: [record]
412
- )
413
- next_root = current.with(
414
- agent_revision: current.agent_revision + 1,
415
- context_revision: current.context_revision + 1,
416
- journal_position: current.journal_position + appended.length
417
- )
418
- tx.agents.save(
419
- agent_id,
420
- expected_revision: current.agent_revision,
421
- root: next_root
422
- )
423
- end
424
- _append_journal_records(appended)
425
- @root = next_root
399
+ update = state_writer.add_knowledge(root: current, content: content, metadata: metadata)
400
+ apply_state_update(update)
426
401
  self
427
402
  end
428
403
 
@@ -450,9 +425,10 @@ module Phronomy
450
425
 
451
426
  __assert_live_agent!
452
427
  runtime = @_phronomy_runtime_owner
453
- token = runtime.__begin_agent_purge(self)
454
- if runtime.__agent_execution_admitted?(agent_id)
455
- runtime.__abort_agent_purge(self, token)
428
+ registry = OwnershipRegistry.for(runtime)
429
+ token = registry.begin_purge(self)
430
+ if Phronomy::Agent::ExecutionRegistry.existing_for(runtime)&.agent_execution_admitted?(agent_id)
431
+ registry.abort_purge(self, token)
456
432
  raise Phronomy::AgentBusyError,
457
433
  "Agent #{agent_id.inspect} has a nonterminal top-level execution"
458
434
  end
@@ -471,20 +447,20 @@ module Phronomy
471
447
  tx.agents.delete(agent_id)
472
448
  end
473
449
  rescue Phronomy::AgentBusyError,
474
- Phronomy::Persistence::ConflictError,
475
- Phronomy::Persistence::NotFoundError,
476
- Phronomy::Persistence::SerializationError,
450
+ Phronomy::Storage::ConflictError,
451
+ Phronomy::Storage::NotFoundError,
452
+ Phronomy::Storage::SerializationError,
477
453
  ArgumentError
478
- runtime.__abort_agent_purge(self, token)
454
+ registry.abort_purge(self, token)
479
455
  raise
480
456
  rescue
481
457
  # Without F1 reconciliation support we cannot infer that a failed
482
458
  # durable delete did not commit. Keep the identity fail-closed.
483
- runtime.__leave_agent_purge_uncertain(self, token)
459
+ registry.leave_purge_uncertain(self, token)
484
460
  raise
485
461
  end
486
462
 
487
- runtime.__complete_agent_purge(self, token)
463
+ registry.complete_purge(self, token)
488
464
  true
489
465
  end
490
466
 
@@ -585,7 +561,7 @@ module Phronomy
585
561
  )
586
562
  @persistence = persistence ||
587
563
  Phronomy.configuration.persistence ||
588
- Phronomy::Persistence::InMemory.new
564
+ DefaultPersistence.build
589
565
  @agent_id = agent_id.to_s.freeze
590
566
 
591
567
  if load_existing
@@ -630,117 +606,27 @@ module Phronomy
630
606
 
631
607
  def create_agent_root!(context:, knowledge:, metadata:)
632
608
  definition = self.class.agent_definition
633
- root = Agent::AgentRoot.create(
634
- agent_id: agent_id,
635
- agent_definition_id: definition.fetch(:id),
636
- agent_definition_version: definition.fetch(:version),
637
- metadata: metadata
638
- )
639
- persistence.transaction do |tx|
640
- tx.agents.create(root)
641
- records = initial_context_records(tx: tx, root: root, context: context)
642
- records.concat(initial_knowledge_records(tx: tx, root: root, knowledge: knowledge))
643
- unless records.empty?
644
- appended = tx.journals.append(agent_id, expected_position: 0, records: records)
645
- root = root.with(
646
- agent_revision: 1,
647
- context_revision: 1,
648
- journal_position: appended.length
649
- )
650
- tx.agents.save(agent_id, expected_revision: 0, root: root)
651
- end
652
- end
653
- root
654
- end
655
-
656
- def initial_context_records(tx:, root:, context:)
657
- return [] unless context
658
-
659
- imported = context.respond_to?(:records) ? context :
660
- Agent::ContextImporter.import_messages(context)
661
- imported.records.map do |record|
662
- content_ref = case record.content_format
663
- when :text then tx.contents.put_text(record.content)
664
- when :json then tx.contents.put_json(record.content)
665
- else
666
- raise ArgumentError,
667
- "unsupported imported content format: #{record.content_format.inspect}"
668
- end
669
- Agent::JournalRecord.new(
670
- agent_id: agent_id,
671
- kind: record.kind,
672
- channel: record.channel,
673
- role: record.role,
674
- content_ref: content_ref,
675
- context_generation: root.transcript_generation,
676
- context_candidate: true,
677
- metadata: record.metadata
678
- )
679
- end
680
- end
681
-
682
- def initial_knowledge_records(tx:, root:, knowledge:)
683
- Array(knowledge).map do |content|
684
- build_knowledge_record(
685
- tx: tx,
686
- root: root,
687
- content: content,
688
- metadata: {}
689
- )
690
- end
691
- end
692
-
693
- def build_knowledge_record(tx:, root:, content:, metadata:)
694
- Agent::JournalRecord.new(
695
- agent_id: agent_id,
696
- kind: :knowledge,
697
- channel: :context,
698
- role: :user,
699
- content_ref: tx.contents.put_text(String(content)),
700
- context_generation: root.transcript_generation,
701
- context_candidate: true,
702
- metadata: metadata || {}
609
+ state_writer.create_root(
610
+ definition: definition, context: context, knowledge: knowledge, metadata: metadata
703
611
  )
704
612
  end
705
613
 
706
614
  def mutate_context!(kind, context_affecting: true)
707
615
  __assert_live_agent!
708
616
  current = agent_root
709
- next_root = nil
710
- appended = nil
711
- persistence.transaction do |tx|
712
- tx.executions.assert_idle!(agent_id)
713
- record = Agent::JournalRecord.new(
714
- agent_id: agent_id,
715
- kind: kind,
716
- channel: :state,
717
- context_generation: current.transcript_generation,
718
- context_candidate: false
719
- )
720
- appended = tx.journals.append(
721
- agent_id,
722
- expected_position: current.journal_position,
723
- records: [record]
724
- )
725
- proposed = yield(current)
726
- next_root = proposed.with(
727
- journal_position: current.journal_position + appended.length,
728
- context_revision: context_affecting ?
729
- yield_context_revision(current, proposed) : current.context_revision
730
- )
731
- tx.agents.save(
732
- agent_id,
733
- expected_revision: current.agent_revision,
734
- root: next_root
735
- )
736
- end
737
- _append_journal_records(appended)
738
- @root = next_root
617
+ update = state_writer.mutate_context(
618
+ root: current, kind: kind, context_affecting: context_affecting
619
+ ) { |root| yield(root) }
620
+ apply_state_update(update)
621
+ end
622
+
623
+ def state_writer
624
+ StateWriter.new(persistence: persistence, agent_id: agent_id)
739
625
  end
740
626
 
741
- def yield_context_revision(current, proposed)
742
- (proposed.context_revision == current.context_revision) ?
743
- current.context_revision + 1 : proposed.context_revision
627
+ def apply_state_update(update)
628
+ _append_journal_records(update.records)
629
+ @root = update.root
744
630
  end
745
631
 
746
632
  def _journal_records_snapshot
@@ -789,7 +675,8 @@ module Phronomy
789
675
  return true if @_phronomy_runtime_owner_state == :constructing
790
676
 
791
677
  runtime = @_phronomy_runtime_owner
792
- unless runtime&.__agent_owned?(self)
678
+ registry = OwnershipRegistry.existing_for(runtime) if runtime
679
+ unless registry&.owned?(self)
793
680
  raise Phronomy::RuntimeShutdownError,
794
681
  "Agent #{agent_id.inspect} is no longer the live owner in its Runtime"
795
682
  end
@@ -799,18 +686,12 @@ module Phronomy
799
686
  def _prepare_invocation_config(config, invocation_context)
800
687
  __assert_live_agent!
801
688
  _reject_removed_generic_identity_keys!(config)
802
- effective_config = invocation_context ?
689
+ invocation_context ?
803
690
  config.merge(invocation_context: invocation_context) : config
804
691
 
805
- if invocation_context && effective_config[:cancellation_token].nil?
806
- if (tok = invocation_context.effective_timeout_token)
807
- effective_config = effective_config.merge(
808
- cancellation_token: tok,
809
- phronomy_timeout_deadline: invocation_context.deadline
810
- )
811
- end
812
- end
813
- effective_config
692
+ # ExecutionCoordinator connects context and individual controls together
693
+ # at admission, before returning the result. Do not arm a second timer
694
+ # here or replace one cancellation source with another.
814
695
  end
815
696
 
816
697
  def _reject_removed_generic_identity_keys!(config)
@@ -949,7 +830,7 @@ module Phronomy
949
830
  )
950
831
  end
951
832
  projection.tool_classes.each do |tool_class|
952
- chat.with_tool(prepare_tool_class(tool_class, invocation: invocation))
833
+ chat.with_tools(prepare_tool_class(tool_class, invocation: invocation))
953
834
  end
954
835
  projection.messages.each { |message| chat.messages << message }
955
836
  chat
@@ -960,25 +841,9 @@ module Phronomy
960
841
  "model" => self.class.model,
961
842
  "provider" => self.class.provider,
962
843
  "temperature" => self.class.temperature,
963
- "max_output_tokens" => self.class.max_output_tokens,
964
- "parallel_tool_execution" => Phronomy.configuration.parallel_tool_execution
844
+ "max_output_tokens" => self.class.max_output_tokens
965
845
  }
966
- opts = {}
967
- model = config["model"]
968
- opts[:model] = model if model
969
- provider = config["provider"]
970
- if provider
971
- opts[:provider] = provider.to_sym
972
- opts[:assume_model_exists] = true
973
- end
974
- parallel_class = config["parallel_tool_execution"] ?
975
- Phronomy::MultiAgent::ParallelToolChat : nil
976
- chat = parallel_class ? parallel_class.new(**opts) : RubyLLM.chat(**opts)
977
- chat.with_temperature(config["temperature"]) if config["temperature"]
978
- if config["max_output_tokens"] && chat.respond_to?(:with_max_output_tokens)
979
- chat.with_max_output_tokens(config["max_output_tokens"])
980
- end
981
- chat
846
+ RuntimeChatBuilder.build(config)
982
847
  end
983
848
 
984
849
  def build_instructions(input)
@@ -994,12 +859,7 @@ module Phronomy
994
859
  end
995
860
 
996
861
  def apply_instructions(chat, text, cache: false, provider: nil)
997
- if cache && provider.to_s == "anthropic"
998
- content = RubyLLM::Providers::Anthropic::Content.new(text, cache: true)
999
- chat.with_instructions(content)
1000
- else
1001
- chat.with_instructions(text)
1002
- end
862
+ RuntimeChatBuilder.apply_instructions(chat, text, cache: cache, provider: provider)
1003
863
  end
1004
864
 
1005
865
  def extract_message(input)
@@ -1027,66 +887,10 @@ module Phronomy
1027
887
  def prepare_tool_class(tool_class, invocation: nil)
1028
888
  return tool_class unless tool_class.is_a?(Class)
1029
889
 
1030
- resolved = if (alias_name = self.class.tool_aliases[tool_class])
1031
- Class.new(tool_class) do
1032
- tool_name alias_name
1033
- end
1034
- else
1035
- tool_class
1036
- end
1037
-
1038
- result_filters = _tool_result_filters_for(tool_class)
1039
- return resolved if result_filters.empty?
1040
-
1041
- effective_name = resolved.new.name
1042
- custom_async_call =
1043
- resolved.instance_method(:call_async).owner !=
1044
- Phronomy::Agent::Context::Capability::Base
1045
-
1046
- Class.new(resolved) do
1047
- tool_name effective_name
1048
- define_method(:call) do |args, **kwargs|
1049
- result = super(args, **kwargs)
1050
- result_filters.inject(result) { |val, filter|
1051
- filter.call(val, tool_name: name, args: args)
1052
- }
1053
- end
1054
-
1055
- if custom_async_call
1056
- define_method(:call_async) do |args, **kwargs|
1057
- source = super(args, **kwargs)
1058
- filtered = Phronomy::Concurrency::PhysicalCompletionTask.deferred(
1059
- name: "tool-filter-#{name}"
1060
- )
1061
- source_has_physical_signal = source.respond_to?(:on_physical_complete)
1062
- source.on_physical_complete { filtered.mark_physical_complete! } if
1063
- source_has_physical_signal
1064
- source.on_complete do |value, error|
1065
- if error
1066
- filtered.mark_physical_complete! unless source_has_physical_signal
1067
- if source.respond_to?(:status) && source.status == :cancelled
1068
- filtered.cancel!(error)
1069
- else
1070
- filtered.fail(error)
1071
- end
1072
- next
1073
- end
1074
-
1075
- begin
1076
- result = result_filters.inject(value) { |val, filter|
1077
- filter.call(val, tool_name: name, args: args)
1078
- }
1079
- filtered.mark_physical_complete! unless source_has_physical_signal
1080
- filtered.complete(result)
1081
- rescue => filter_error
1082
- filtered.mark_physical_complete! unless source_has_physical_signal
1083
- filtered.fail(filter_error)
1084
- end
1085
- end
1086
- filtered
1087
- end
1088
- end
1089
- end
890
+ binding = ToolBinding.new(
891
+ tool_class, alias_name: self.class.tool_aliases[tool_class]
892
+ )
893
+ binding.prepare(result_filters: _tool_result_filters_for(tool_class))
1090
894
  end
1091
895
  end
1092
896
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This convenience API composes a fresh ephemeral Persistence with an Agent.
4
+ # The application entry loads the method definition; Agent execution does not
5
+ # delegate to or require this higher-level composition implementation.
3
6
  module Phronomy
4
7
  module Agent
5
- StreamEvent = Data.define(:type, :payload)
6
-
7
8
  def self.run_once(
8
9
  definition:,
9
10
  input:,
@@ -17,7 +18,7 @@ module Phronomy
17
18
  raise ArgumentError, "Provide either on_event: or a block, not both"
18
19
  end
19
20
 
20
- persistence = Phronomy::Persistence::InMemory.new
21
+ persistence = Phronomy::Persistence.in_memory
21
22
  agent = definition.create(
22
23
  context: context,
23
24
  knowledge: knowledge,
@@ -29,13 +30,3 @@ module Phronomy
29
30
  end
30
31
  end
31
32
  end
32
-
33
- require_relative "agent/async_event_api"
34
-
35
- unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
36
- Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
37
- end
38
-
39
- # Durable Agent Recovery and Agent-incarnation event binding.
40
- require_relative "agent/recovery_support"
41
- require_relative "agent/recovery_coordinator"
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../../engine/concurrency/worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  module Agent
5
7
  module Context
6
8
  module Capability
7
9
  # Base class extending RubyLLM::Tool with Phronomy-specific DSL.
8
10
  class Base < RubyLLM::Tool
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
9
13
  class << self
10
14
  # @api public
11
15
  def tool_name(value = nil)
12
- return @tool_name if value.nil?
16
+ return @tool_name || super() if value.nil?
13
17
 
14
18
  @tool_name = value.to_s
15
19
  end
@@ -30,41 +34,62 @@ module Phronomy
30
34
  end
31
35
  alias_method :desc, :description
32
36
 
33
- # RubyLLM stores declared parameters in a class-instance variable.
34
- # Copy the parent's registry on first access so child classes inherit
35
- # existing parameters while remaining free to add their own.
37
+ # Phronomy declarations inherit through anonymous Tool decorators.
38
+ # RubyLLM owns schema construction and provider request rendering.
39
+ # @api public
40
+ def declared_parameters
41
+ return @declared_parameters if instance_variable_defined?(:@declared_parameters)
42
+
43
+ @declared_parameters = superclass.declared_parameters.dup
44
+ end
45
+
46
+ # Keep Phronomy's parameter reader and support RubyLLM's schema DSL.
36
47
  # @api public
37
- def parameters
38
- return @parameters if instance_variable_defined?(:@parameters)
48
+ def parameters(schema = :__phronomy_read__, &block)
49
+ return declared_parameters if schema == :__phronomy_read__ && !block
39
50
 
40
- parent = superclass.respond_to?(:parameters) ? superclass.parameters : {}
41
- @parameters = parent.dup
51
+ super((schema == :__phronomy_read__) ? nil : schema, &block)
52
+ end
53
+
54
+ # @api public
55
+ def parameters_schema_definition
56
+ return @parameters_schema_definition if instance_variable_defined?(:@parameters_schema_definition)
57
+
58
+ superclass.parameters_schema_definition
42
59
  end
43
60
 
44
- # RubyLLM stores an explicit .params schema definition in a
45
- # class-instance variable. Readers must fall back to the parent.
46
61
  # @api public
47
62
  def params_schema_definition
48
- return @params_schema_definition if instance_variable_defined?(:@params_schema_definition)
49
- return superclass.params_schema_definition if superclass.respond_to?(:params_schema_definition)
63
+ parameters_schema_definition
64
+ end
50
65
 
51
- nil
66
+ # @api public
67
+ def params(schema = nil, &block)
68
+ parameters(schema, &block)
69
+ end
70
+
71
+ # @api public
72
+ def provider_options(options = :__phronomy_read__)
73
+ return super unless options == :__phronomy_read__
74
+ return @provider_options if instance_variable_defined?(:@provider_options)
75
+
76
+ @provider_options = duplicate_configuration(superclass.provider_options)
52
77
  end
53
78
 
54
- # RubyLLM provider params are also class-instance state. Copy them on
55
- # first access to preserve inheritance without sharing the top-level
56
- # mutable Hash between parent and child.
57
79
  # @api public
58
80
  def provider_params
59
- return @provider_params if instance_variable_defined?(:@provider_params)
81
+ provider_options
82
+ end
60
83
 
61
- parent = superclass.respond_to?(:provider_params) ? superclass.provider_params : {}
62
- @provider_params = duplicate_configuration(parent)
84
+ # @api public
85
+ def with_params(**options)
86
+ provider_options(options)
63
87
  end
64
88
 
65
89
  # @api public
66
- def param(name, enum: nil, properties: nil, **options)
67
- super(name, **options)
90
+ def param(name, enum: nil, properties: nil, desc: nil, **options)
91
+ options[:description] = desc if desc
92
+ parameter(name, **options)
68
93
  param_enums[name] = duplicate_configuration(enum) if enum
69
94
  param_schemas[name] = normalize_nested_schema(properties) if properties
70
95
  end
@@ -175,8 +200,8 @@ module Phronomy
175
200
  end
176
201
  @requires_approval = block
177
202
  elsif value == :__unset__
178
- return @requires_approval if instance_variable_defined?(:@requires_approval)
179
- return superclass.requires_approval if superclass.respond_to?(:requires_approval)
203
+ return @requires_approval unless @requires_approval.nil?
204
+ return superclass.requires_approval if superclass < RubyLLM::Tool
180
205
 
181
206
  false
182
207
  else
@@ -225,7 +250,7 @@ module Phronomy
225
250
  self.class.tool_name || super
226
251
  end
227
252
 
228
- def params_schema
253
+ def parameters_schema
229
254
  schema = super
230
255
  return schema if schema.nil?
231
256
 
@@ -264,6 +289,22 @@ module Phronomy
264
289
  schema
265
290
  end
266
291
 
292
+ # Phronomy retains these readers as part of its Tool contract.
293
+ # @api public
294
+ def params_schema
295
+ parameters_schema
296
+ end
297
+
298
+ # @api public
299
+ def provider_params
300
+ provider_options
301
+ end
302
+
303
+ # @api public
304
+ def parameters
305
+ self.class.declared_parameters
306
+ end
307
+
267
308
  # @api public
268
309
  def call(args, cancellation_token: nil)
269
310
  cancellation_token&.raise_if_cancelled!
@@ -281,7 +322,7 @@ module Phronomy
281
322
  if cancellation_token && execute_accepts_cancellation_token?
282
323
  validated_args = validated_args.merge(cancellation_token: cancellation_token)
283
324
  end
284
- result = super(validated_args)
325
+ result = super(**(validated_args || {}).transform_keys(&:to_sym))
285
326
  truncate_result_if_needed(result)
286
327
  rescue Phronomy::ToolError, Phronomy::CancellationError
287
328
  raise
@@ -307,7 +348,7 @@ module Phronomy
307
348
  cancellation_token: nil,
308
349
  config: {}
309
350
  )
310
- Phronomy::Agent::ToolExecutor.call_async(
351
+ Phronomy::Agent::Context::Capability::ToolExecutor.call_async(
311
352
  tool: self,
312
353
  args: args,
313
354
  cancellation_token: cancellation_token,