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
@@ -21,7 +21,7 @@ module Phronomy
21
21
  knowledge: Array(knowledge).freeze,
22
22
  tools: Array(tools).freeze,
23
23
  conversation: Array(conversation).map { |group| Array(group).freeze }.freeze,
24
- metadata: Immutable.copy(metadata || {})
24
+ metadata: Phronomy::Values::Immutable.copy(metadata || {})
25
25
  )
26
26
  freeze
27
27
  end
@@ -50,8 +50,8 @@ module Phronomy
50
50
  knowledge: normalized_knowledge,
51
51
  tools: normalized_tools,
52
52
  conversation: normalized_conversation,
53
- model_config: Immutable.copy(values[:model_config] || {}),
54
- metadata: Immutable.copy(values[:metadata] || {})
53
+ model_config: Phronomy::Values::Immutable.copy(values[:model_config] || {}),
54
+ metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})
55
55
  ))
56
56
  freeze
57
57
  end
@@ -145,7 +145,7 @@ module Phronomy
145
145
  ) do
146
146
  def initialize(**values)
147
147
  provenance = ContextPolicyInput.send(:normalize_provenance, values[:provenance])
148
- definition = Immutable.copy(values.fetch(:definition))
148
+ definition = Phronomy::Values::Immutable.copy(values.fetch(:definition))
149
149
  unless definition.is_a?(Hash) && definition["name"].to_s != ""
150
150
  raise ArgumentError, "ContextPolicyInput ToolItem definition requires a non-empty name"
151
151
  end
@@ -156,7 +156,7 @@ module Phronomy
156
156
  estimated_tokens: ContextPolicyInput.send(:normalize_estimated_tokens, values[:estimated_tokens]),
157
157
  required: !!values[:required],
158
158
  provenance: provenance,
159
- metadata: Immutable.copy(values[:metadata] || {})
159
+ metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})
160
160
  )
161
161
  raise ArgumentError, "ContextPolicyInput ToolItem id must not be empty" if id.empty?
162
162
  freeze
@@ -212,12 +212,12 @@ module Phronomy
212
212
  id: id,
213
213
  kind: kind,
214
214
  role: role,
215
- content: Immutable.copy(values[:content]),
215
+ content: Phronomy::Values::Immutable.copy(values[:content]),
216
216
  content_format: content_format,
217
217
  estimated_tokens: normalize_estimated_tokens(values[:estimated_tokens]),
218
218
  required: !!values[:required],
219
219
  provenance: normalize_provenance(values[:provenance]),
220
- metadata: Immutable.copy(values[:metadata] || {})
220
+ metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})
221
221
  }
222
222
  end
223
223
 
@@ -9,7 +9,7 @@ module Phronomy
9
9
  :config, :call_sequence
10
10
  ) do
11
11
  def initialize(**values)
12
- super(**values.merge(config: Immutable.copy(values.fetch(:config, {}))))
12
+ super(**values.merge(config: Phronomy::Values::Immutable.copy(values.fetch(:config, {}))))
13
13
  freeze
14
14
  end
15
15
  end
@@ -3,7 +3,7 @@
3
3
  module Phronomy
4
4
  module Agent
5
5
  # ContentStore-backed durable codec boundary for one Provider input manifest.
6
- # It is intentionally separate from Persistence::DurableRecord.
6
+ # It is intentionally separate from Storage::DurableRecord.
7
7
  class LLMInputManifest
8
8
  VERSION = "0.1"
9
9
  CALL_MODES = %i[ask complete].freeze
@@ -18,7 +18,7 @@ module Phronomy
18
18
  if delivery.nil? || !SEGMENT_DELIVERIES.include?(delivery.to_s)
19
19
  raise ArgumentError, "unknown Segment delivery: #{delivery.inspect}"
20
20
  end
21
- super(**values.merge(metadata: Immutable.copy(values[:metadata] || {})))
21
+ super(**values.merge(metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})))
22
22
  freeze
23
23
  end
24
24
 
@@ -62,7 +62,7 @@ module Phronomy
62
62
  label: label
63
63
  )
64
64
  unless SEGMENT_DELIVERIES.include?(delivery)
65
- raise Phronomy::Persistence::SerializationError,
65
+ raise Phronomy::Storage::SerializationError,
66
66
  "LLMInputManifest segment delivery must be one of #{SEGMENT_DELIVERIES.inspect}, got #{delivery.inspect}"
67
67
  end
68
68
  tool_call_id = LLMInputManifest.send(
@@ -87,10 +87,10 @@ module Phronomy
87
87
  tool_call_id: tool_call_id,
88
88
  metadata: metadata
89
89
  )
90
- rescue Phronomy::Persistence::SerializationError
90
+ rescue Phronomy::Storage::SerializationError
91
91
  raise
92
92
  rescue => error
93
- raise Phronomy::Persistence::SerializationError,
93
+ raise Phronomy::Storage::SerializationError,
94
94
  "invalid LLMInputManifest segment: #{error.class}: #{error.message}"
95
95
  end
96
96
 
@@ -162,7 +162,7 @@ module Phronomy
162
162
  label = "LLMInputManifest"
163
163
  version = source.fetch("version")
164
164
  unless version.is_a?(String) && version == VERSION
165
- raise Phronomy::Persistence::SerializationError,
165
+ raise Phronomy::Storage::SerializationError,
166
166
  "unsupported LLMInputManifest version: #{version.inspect}; " \
167
167
  "current version is #{VERSION.inspect}"
168
168
  end
@@ -229,10 +229,10 @@ module Phronomy
229
229
  adapter_name: adapter_name,
230
230
  adapter_version: adapter_version
231
231
  )
232
- rescue Phronomy::Persistence::SerializationError
232
+ rescue Phronomy::Storage::SerializationError
233
233
  raise
234
234
  rescue => error
235
- raise Phronomy::Persistence::SerializationError,
235
+ raise Phronomy::Storage::SerializationError,
236
236
  "invalid LLMInputManifest: #{error.class}: #{error.message}"
237
237
  end
238
238
 
@@ -262,11 +262,11 @@ module Phronomy
262
262
 
263
263
  def strict_source!(hash, required:, optional:, label:)
264
264
  unless hash.is_a?(Hash)
265
- raise Phronomy::Persistence::SerializationError,
265
+ raise Phronomy::Storage::SerializationError,
266
266
  "#{label} must be a Hash"
267
267
  end
268
268
  unless hash.keys.all? { |key| key.is_a?(String) }
269
- raise Phronomy::Persistence::SerializationError,
269
+ raise Phronomy::Storage::SerializationError,
270
270
  "#{label} keys must all be String"
271
271
  end
272
272
 
@@ -277,13 +277,13 @@ module Phronomy
277
277
  details = []
278
278
  details << "missing=#{missing.inspect}" unless missing.empty?
279
279
  details << "unknown=#{unknown.inspect}" unless unknown.empty?
280
- raise Phronomy::Persistence::SerializationError,
280
+ raise Phronomy::Storage::SerializationError,
281
281
  "#{label} schema mismatch (#{details.join(", ")})"
282
282
  end
283
283
  Phronomy::CanonicalJSON.dump(hash)
284
284
  hash
285
285
  rescue ArgumentError => error
286
- raise Phronomy::Persistence::SerializationError,
286
+ raise Phronomy::Storage::SerializationError,
287
287
  "#{label} is not canonical JSON compatible: #{error.message}"
288
288
  end
289
289
 
@@ -291,7 +291,7 @@ module Phronomy
291
291
  value = hash.fetch(key)
292
292
  return value if value.is_a?(Integer)
293
293
 
294
- raise Phronomy::Persistence::SerializationError,
294
+ raise Phronomy::Storage::SerializationError,
295
295
  "#{label} #{key} must be an Integer"
296
296
  end
297
297
 
@@ -299,7 +299,7 @@ module Phronomy
299
299
  value = hash.fetch(key)
300
300
  return value if value.is_a?(Integer) && value.positive?
301
301
 
302
- raise Phronomy::Persistence::SerializationError,
302
+ raise Phronomy::Storage::SerializationError,
303
303
  "#{label} #{key} must be a positive Integer"
304
304
  end
305
305
 
@@ -307,7 +307,7 @@ module Phronomy
307
307
  value = hash.fetch(key)
308
308
  return value if value.is_a?(Integer) && value >= 0
309
309
 
310
- raise Phronomy::Persistence::SerializationError,
310
+ raise Phronomy::Storage::SerializationError,
311
311
  "#{label} #{key} must be a non-negative Integer"
312
312
  end
313
313
 
@@ -315,7 +315,7 @@ module Phronomy
315
315
  value = hash.fetch(key)
316
316
  return value if value.is_a?(String) && !value.empty?
317
317
 
318
- raise Phronomy::Persistence::SerializationError,
318
+ raise Phronomy::Storage::SerializationError,
319
319
  "#{label} #{key} must be a non-empty String"
320
320
  end
321
321
 
@@ -323,7 +323,7 @@ module Phronomy
323
323
  value = hash[key]
324
324
  return value if value.nil? || value.is_a?(String)
325
325
 
326
- raise Phronomy::Persistence::SerializationError,
326
+ raise Phronomy::Storage::SerializationError,
327
327
  "#{label} #{key} must be a String or nil"
328
328
  end
329
329
 
@@ -331,7 +331,7 @@ module Phronomy
331
331
  value = hash.fetch(key)
332
332
  return value if value.is_a?(String) && allowed.include?(value)
333
333
 
334
- raise Phronomy::Persistence::SerializationError,
334
+ raise Phronomy::Storage::SerializationError,
335
335
  "#{label} #{key} must be one of #{allowed.inspect}"
336
336
  end
337
337
 
@@ -339,7 +339,7 @@ module Phronomy
339
339
  value = hash.fetch(key)
340
340
  return value if value.is_a?(Array)
341
341
 
342
- raise Phronomy::Persistence::SerializationError,
342
+ raise Phronomy::Storage::SerializationError,
343
343
  "#{label} #{key} must be an Array"
344
344
  end
345
345
 
@@ -347,7 +347,7 @@ module Phronomy
347
347
  value = hash.fetch(key)
348
348
  return value if value.is_a?(Hash)
349
349
 
350
- raise Phronomy::Persistence::SerializationError,
350
+ raise Phronomy::Storage::SerializationError,
351
351
  "#{label} #{key} must be a Hash"
352
352
  end
353
353
  end
@@ -7,8 +7,8 @@ module Phronomy
7
7
  LLMInputPatch = Data.define(:model_config_patch, :segment_candidates) do
8
8
  def initialize(model_config_patch: nil, segment_candidates: nil)
9
9
  super(
10
- model_config_patch: model_config_patch && Immutable.copy(model_config_patch),
11
- segment_candidates: segment_candidates && Immutable.copy(Array(segment_candidates))
10
+ model_config_patch: model_config_patch && Phronomy::Values::Immutable.copy(model_config_patch),
11
+ segment_candidates: segment_candidates && Phronomy::Values::Immutable.copy(Array(segment_candidates))
12
12
  )
13
13
  freeze
14
14
  end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
3
5
  require "securerandom"
4
6
  require "time"
5
7
 
6
8
  module Phronomy
7
9
  module Agent
8
10
  class AgentExecution
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
9
13
  ACTIVE_STATUSES = %i[preparing active suspended].freeze
10
14
  TERMINAL_STATUSES = %i[completed handed_off failed cancelled rejected blocked].freeze
11
15
  TRANSITIONS = {
@@ -55,13 +59,13 @@ module Phronomy
55
59
  ATTRIBUTES.each do |name|
56
60
  value = attributes.fetch(name)
57
61
  value = value.to_sym if %i[status phase].include?(name)
58
- instance_variable_set("@#{name}", Immutable.copy(value))
62
+ instance_variable_set("@#{name}", Phronomy::Values::Immutable.copy(value))
59
63
  end
60
64
  raise ArgumentError, "unknown execution status: #{status.inspect}" unless TRANSITIONS.key?(status)
61
65
  raise ArgumentError, "execution_revision must be non-negative" if execution_revision.negative?
62
- Immutable.validate_canonical_json!(metadata, label: "Execution metadata")
66
+ Phronomy::Values::Immutable.validate_canonical_json!(metadata, label: "Execution metadata")
63
67
  if approval_request
64
- Immutable.validate_canonical_json!(approval_request, label: "Approval request")
68
+ Phronomy::Values::Immutable.validate_canonical_json!(approval_request, label: "Approval request")
65
69
  end
66
70
  freeze
67
71
  end
@@ -86,7 +90,7 @@ module Phronomy
86
90
  end
87
91
 
88
92
  # Current semantic payload representation. Persistence format identity and
89
- # compatibility validation are owned by Persistence::DurableCodec.
93
+ # compatibility validation are owned by Agent::Persistence::Codec.
90
94
  # @api public
91
95
  def to_h
92
96
  ATTRIBUTES.to_h do |name|
@@ -1,22 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
3
5
  require "securerandom"
4
6
  require "time"
5
7
 
6
8
  module Phronomy
7
9
  module Agent
8
10
  class AgentInvocation
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
9
13
  # execution_id is the logical Agent execution parent. Concrete Runtime
10
14
  # routing identity belongs only to the owning FSMSession incarnation.
11
- TOOL_EVENT_TYPES = %i[
12
- tool_authorized
13
- tool_approval_required
14
- tool_completed
15
- tool_failed
16
- tool_rejected
17
- tool_cancelled
18
- ].freeze
19
-
20
15
  LLM_EVENT_TYPES = %i[llm_completed llm_failed].freeze
21
16
  CALLBACK_FAILED_EVENTS = %i[application_callback_failed].freeze
22
17
  SETUP_FAILED_EVENTS = %i[llm_setup_failed tool_setup_failed].freeze
@@ -147,11 +142,11 @@ module Phronomy
147
142
  if durable_id.nil? &&
148
143
  Phronomy::Runtime.instance.event_loop.current?
149
144
  state =
150
- Phronomy::Runtime.instance.event_loop.agent_execution_state(
145
+ Phronomy::Agent::ExecutionRegistry.for(Phronomy::Runtime.instance.event_loop).agent_execution_state(
151
146
  execution_id
152
147
  )
153
148
  durable_id = state&.execution&.metadata&.fetch(
154
- RecoverySupport::PENDING_LLM_ID_KEY,
149
+ ExecutionMetadata::PENDING_LLM_ID_KEY,
155
150
  nil
156
151
  )
157
152
  end
@@ -209,7 +204,7 @@ module Phronomy
209
204
  return true
210
205
  end
211
206
 
212
- return false unless TOOL_EVENT_TYPES.include?(event.type)
207
+ return false unless InvocationTransitions::TOOL_EVENTS.include?(event.type)
213
208
 
214
209
  invocation = tool_invocation(event.payload&.fetch(:tool_invocation_id, nil))
215
210
  return true unless invocation
@@ -540,7 +535,7 @@ module Phronomy
540
535
  # listener receives the notification event separately.
541
536
  @runtime_events << StreamEvent.new(
542
537
  type: event.type,
543
- payload: Immutable.copy(event.payload)
538
+ payload: Phronomy::Values::Immutable.copy(event.payload)
544
539
  )
545
540
  listener = @callback_failure ? nil : @event_listener
546
541
  return unless listener
@@ -3,33 +3,6 @@
3
3
  module Phronomy
4
4
  module Agent
5
5
  class AgentInvocationSessionBuilder
6
- AUTO_STATE_SET = {
7
- idle: true,
8
- filtering_input: true,
9
- building_context: true,
10
- starting_tools: true,
11
- evaluating_tools: true,
12
- recording_tool_results: true,
13
- output_filtering: true
14
- }.freeze
15
-
16
- DECLARED_STATES = %i[
17
- idle filtering_input building_context calling_llm starting_tools
18
- evaluating_tools waiting_for_tools dispatching_tools
19
- recording_tool_results suspended output_filtering handed_off completed blocked failed
20
- ].freeze
21
-
22
- WAIT_STATES = %i[suspended].freeze
23
-
24
- TOOL_EVENTS = %i[
25
- tool_authorized
26
- tool_approval_required
27
- tool_completed
28
- tool_failed
29
- tool_rejected
30
- tool_cancelled
31
- ].freeze
32
-
33
6
  def self.build(
34
7
  agent:,
35
8
  input:,
@@ -93,13 +66,13 @@ module Phronomy
93
66
  Phronomy::FSMSession.new(
94
67
  context: agent_invocation,
95
68
  event_sink: event_sink,
96
- entry_point: :idle,
69
+ entry_point: InvocationTransitions::ENTRY_POINT,
97
70
  phase_machine_class: phase_machine,
98
71
  entry_actions: {},
99
- auto_state_set: AUTO_STATE_SET,
100
- declared_states: DECLARED_STATES,
101
- wait_state_names: WAIT_STATES,
102
- external_events: external_events,
72
+ auto_state_set: InvocationTransitions::AUTO_STATE_SET,
73
+ declared_states: InvocationTransitions::DECLARED_STATES,
74
+ wait_state_names: InvocationTransitions::WAIT_STATES,
75
+ external_events: InvocationTransitions::EXTERNAL_EVENTS,
103
76
  recursion_limit: 12 + (iterations * 8),
104
77
  event_loop: runtime.event_loop,
105
78
  resume_event: resume_event,
@@ -108,49 +81,6 @@ module Phronomy
108
81
  end
109
82
  private_class_method :build_session
110
83
 
111
- def self.external_events
112
- tool_transitions = TOOL_EVENTS.to_h do |event_name|
113
- [event_name, [{from: :waiting_for_tools, to: :evaluating_tools, guard: nil}]]
114
- end
115
-
116
- tool_transitions.merge(
117
- llm_completed: [
118
- {from: :calling_llm, to: :failed, guard: ->(ctx) { ctx.callback_failed? }},
119
- {from: :calling_llm, to: :failed, guard: ->(ctx) { ctx.handoff_failed? }},
120
- {from: :calling_llm, to: :handed_off, guard: ->(ctx) { ctx.handoff_requested? }},
121
- {from: :calling_llm, to: :starting_tools, guard: ->(ctx) { ctx.tool_call_pending? }},
122
- {from: :calling_llm, to: :output_filtering, guard: nil}
123
- ],
124
- llm_failed: [
125
- {from: :calling_llm, to: :failed, guard: nil}
126
- ],
127
- llm_setup_failed: [
128
- {from: :calling_llm, to: :failed, guard: nil}
129
- ],
130
- tool_setup_failed: [
131
- {from: :dispatching_tools, to: :failed, guard: nil}
132
- ],
133
- tool_dispatch_prepared: [
134
- {from: :dispatching_tools, to: :evaluating_tools, guard: nil}
135
- ],
136
- resume: [
137
- {from: :suspended, to: :waiting_for_tools, guard: nil}
138
- ],
139
- application_callback_failed: [
140
- {from: :filtering_input, to: :failed, guard: nil},
141
- {from: :building_context, to: :failed, guard: nil},
142
- {from: :calling_llm, to: :failed, guard: nil},
143
- {from: :starting_tools, to: :failed, guard: nil},
144
- {from: :evaluating_tools, to: :failed, guard: nil},
145
- {from: :waiting_for_tools, to: :failed, guard: nil},
146
- {from: :dispatching_tools, to: :failed, guard: nil},
147
- {from: :recording_tool_results, to: :failed, guard: nil},
148
- {from: :output_filtering, to: :failed, guard: nil}
149
- ]
150
- )
151
- end
152
- private_class_method :external_events
153
-
154
84
  def self.build_entry_actions(agent, runtime, mode:, event_sink:)
155
85
  calling_action = if mode.to_sym == :stream
156
86
  method(:calling_llm_stream_action).curry.call(agent, runtime, event_sink)
@@ -159,8 +89,8 @@ module Phronomy
159
89
  end
160
90
 
161
91
  {
162
- filtering_input: [method(:filtering_input_action).curry.call(agent)],
163
- building_context: [method(:building_context_action).curry.call(agent)],
92
+ filtering_input: [method(:apply_prepared_input_action).curry.call(agent)],
93
+ building_context: [method(:build_runtime_chat_action).curry.call(agent)],
164
94
  calling_llm: [calling_action],
165
95
  starting_tools: [method(:starting_tools_action).curry.call(runtime, event_sink)],
166
96
  dispatching_tools: [method(:dispatching_tools_action).curry.call(runtime, event_sink)],
@@ -172,13 +102,13 @@ module Phronomy
172
102
  end
173
103
  private_class_method :build_entry_actions
174
104
 
175
- def self.filtering_input_action(_agent, invocation)
105
+ def self.apply_prepared_input_action(_agent, invocation)
176
106
  invocation.input = invocation.config.fetch(:phronomy_filtered_input)
177
107
  invocation
178
108
  end
179
- private_class_method :filtering_input_action
109
+ private_class_method :apply_prepared_input_action
180
110
 
181
- def self.building_context_action(agent, invocation)
111
+ def self.build_runtime_chat_action(agent, invocation)
182
112
  projection = invocation.config.fetch(:phronomy_runtime_projection)
183
113
  invocation.chat = agent.send(:build_chat, model_config: projection.model_config)
184
114
  agent.send(
@@ -189,22 +119,16 @@ module Phronomy
189
119
  )
190
120
  invocation
191
121
  end
192
- private_class_method :building_context_action
122
+ private_class_method :build_runtime_chat_action
193
123
 
194
124
  def self.install_tool_interceptors(chat, llm_call_id:)
195
- unless chat.respond_to?(:before_tool_call)
196
- raise Phronomy::ConfigurationError,
197
- "Agent-owned Tool execution requires RubyLLM >= 1.15 (before_tool_call callback)"
198
- end
199
-
200
- if chat.respond_to?(:on_tool_call_batch)
201
- chat.on_tool_call_batch do |tool_calls|
202
- raise build_tool_interception(chat, tool_calls, llm_call_id)
203
- end
204
- end
125
+ # RubyLLM 2 records the complete assistant message before this callback,
126
+ # and checks its own approval gate afterwards. Capture here so Phronomy
127
+ # owns authorization and dispatch for the entire Tool batch.
128
+ chat.after_message do |message|
129
+ next unless message.tool_call?
205
130
 
206
- chat.before_tool_call do |tool_call|
207
- raise build_tool_interception(chat, [tool_call], llm_call_id)
131
+ raise build_tool_interception(chat, [], llm_call_id)
208
132
  end
209
133
  end
210
134
  private_class_method :install_tool_interceptors
@@ -347,7 +271,7 @@ module Phronomy
347
271
  chat, message, config: config
348
272
  )
349
273
  end
350
- runtime.event_loop.supervise_agent_operation(
274
+ Phronomy::Agent::ExecutionRegistry.for(runtime.event_loop).supervise_agent_operation(
351
275
  invocation.execution_id,
352
276
  operation
353
277
  )
@@ -456,7 +380,7 @@ module Phronomy
456
380
  children = invocation.pending_tool_calls.map do |tool_call|
457
381
  tool = invocation.chat.tools[tool_call.name.to_sym]
458
382
  tool_invocation_id =
459
- RecoverySupport.semantic_tool_id(
383
+ ToolInvocation.semantic_id(
460
384
  execution_id: invocation.execution_id,
461
385
  llm_call_id: invocation.tool_batch_llm_call_id,
462
386
  tool_call_id: (
@@ -542,7 +466,7 @@ module Phronomy
542
466
  end
543
467
 
544
468
  def self.register_child_session(runtime, child, session, parent_event_sink)
545
- completion = Phronomy::Task.deferred(name: "tool-session:#{child.id}")
469
+ completion = Phronomy::TaskResult.deferred(name: "tool-session:#{child.id}")
546
470
  completion.on_complete do |_result, error|
547
471
  next unless error
548
472
 
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
5
+ module Phronomy
6
+ module Agent
7
+ # Persists one approval decision with its captured recovery facts.
8
+ # Admission, live state and session resumption stay with the execution owner.
9
+ # @api private
10
+ class ApprovalResumeCommit
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
13
+ Command = Data.define(
14
+ :execution_id, :expected_execution_revision,
15
+ :root, :execution, :approval_request_id, :approved, :tool_batch_snapshot
16
+ )
17
+ Result = Data.define(:execution, :root)
18
+
19
+ # @api private
20
+ def initialize(agent_id:, persistence:)
21
+ @agent_id = agent_id
22
+ @persistence = persistence
23
+ end
24
+
25
+ # @api private
26
+ def commit(operation)
27
+ request = validated_approval_request(operation)
28
+ execution = stage_recovery_snapshot(operation)
29
+ persist_decision(execution, operation.root, request, operation.approved)
30
+ end
31
+
32
+ private
33
+
34
+ def validated_approval_request(operation)
35
+ request = operation.execution.approval_request || {}
36
+ request_id = request["id"] || request[:id]
37
+ unless request_id.to_s == operation.approval_request_id
38
+ raise ArgumentError,
39
+ "approval request does not match execution #{operation.execution.execution_id}"
40
+ end
41
+ request
42
+ end
43
+
44
+ def stage_recovery_snapshot(operation)
45
+ return operation.execution unless operation.tool_batch_snapshot
46
+
47
+ ExecutionMetadata.with_values(
48
+ operation.execution,
49
+ ExecutionMetadata::TOOL_BATCH_METADATA_KEY => operation.tool_batch_snapshot
50
+ )
51
+ end
52
+
53
+ def persist_decision(execution, root, request, approved)
54
+ updated = next_root = nil
55
+ @persistence.transaction do |tx|
56
+ decision = record_decision(tx, execution, root, request, approved)
57
+ updated = save_resumed_execution(tx, execution, request, approved, decision)
58
+ next_root = save_resumed_root(tx, root)
59
+ end
60
+ Result.new(execution: updated, root: next_root)
61
+ end
62
+
63
+ def record_decision(tx, execution, root, request, approved)
64
+ decision_ref = tx.contents.put_json(
65
+ "approval_request_id" => (request["id"] || request[:id]).to_s,
66
+ "approved" => approved
67
+ )
68
+ JournalRecord.new(
69
+ agent_id: @agent_id,
70
+ execution_id: execution.execution_id,
71
+ kind: :approval_decided,
72
+ channel: :approval,
73
+ content_ref: decision_ref,
74
+ context_generation: root.transcript_generation,
75
+ context_candidate: false
76
+ )
77
+ end
78
+
79
+ def save_resumed_execution(tx, current, request, approved, decision)
80
+ updated = current.with(
81
+ status: :active,
82
+ phase: :resuming,
83
+ working_records: current.working_records + [decision],
84
+ approval_request: request.merge("approved" => approved)
85
+ )
86
+ tx.executions.save(
87
+ current.execution_id,
88
+ expected_revision: current.execution_revision,
89
+ execution: updated
90
+ )
91
+ updated
92
+ end
93
+
94
+ def save_resumed_root(tx, current)
95
+ updated = current.with(
96
+ agent_revision: current.agent_revision + 1,
97
+ lifecycle_status: :active
98
+ )
99
+ tx.agents.save(
100
+ current.agent_id,
101
+ expected_revision: current.agent_revision,
102
+ root: updated
103
+ )
104
+ updated
105
+ end
106
+ end
107
+ end
108
+ end