phronomy 0.26.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 (304) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +4 -0
  4. data/VERIFY.sh +27 -27
  5. data/benchmark/bench_agent_invoke.rb +26 -22
  6. data/benchmark/bench_context_assembler.rb +4 -5
  7. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  8. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  9. data/docs/architecture/agent-context.md +4 -2
  10. data/docs/architecture/agent-transition-ownership.md +89 -0
  11. data/docs/architecture/before-llm-input.md +6 -0
  12. data/docs/architecture/context-management.md +30 -0
  13. data/docs/architecture/context-preparation-steps.md +90 -0
  14. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  15. data/docs/architecture/execution-metadata-and-values.md +80 -0
  16. data/docs/architecture/generator-verifier-ownership.md +111 -0
  17. data/docs/architecture/multi-agent-handoff.md +8 -2
  18. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  19. data/docs/architecture/persistence.md +27 -4
  20. data/docs/architecture/refactoring-closure.md +334 -0
  21. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  22. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  23. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  24. data/docs/architecture/tracing.md +1 -1
  25. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  26. data/docs/architecture.md +209 -0
  27. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  28. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  29. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  30. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  31. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  32. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  33. data/docs/decisions/032-storage-backend-composition.md +80 -0
  34. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  35. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  36. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  37. data/docs/decisions/036-context-contract-ownership.md +86 -0
  38. data/docs/decisions/037-common-definition-ownership.md +62 -0
  39. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  40. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  41. data/docs/decisions/040-configuration-default-composition.md +94 -0
  42. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  43. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  44. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  45. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  46. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  47. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  48. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  49. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  50. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  51. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  52. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  53. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  54. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  55. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  56. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  57. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  58. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  59. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  60. data/docs/decisions/README.md +35 -6
  61. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  62. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  63. data/docs/features.md +9 -3
  64. data/docs/getting-started.md +4 -4
  65. data/docs/migrations/0.15.md +5 -0
  66. data/docs/migrations/durable-semantic-coordination-v2.md +6 -0
  67. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  68. data/docs/migrations/neutral-storage-spi.md +43 -0
  69. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  70. data/docs/migrations/shared-state-multi-agent.md +44 -0
  71. data/docs/migrations/storage-backend-composition.md +117 -0
  72. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  73. data/docs/persistence-backends.md +162 -204
  74. data/docs/runtime-and-concurrency.md +117 -32
  75. data/lib/phronomy/agent/api/agent.rb +17 -0
  76. data/lib/phronomy/agent/async_event_api.rb +2 -2
  77. data/lib/phronomy/agent/base.rb +51 -241
  78. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  79. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  80. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  81. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  82. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  83. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  84. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  85. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  86. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  87. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  88. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  89. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  90. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  91. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  92. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  93. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  94. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  95. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +19 -95
  96. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  97. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  98. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +9 -10
  99. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  100. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  101. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  102. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  103. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  104. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  105. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  106. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  107. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  108. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +20 -71
  109. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  110. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  111. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  112. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  113. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  114. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  115. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  116. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  117. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  118. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  119. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  120. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  121. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  122. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  123. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  124. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  125. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  126. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  127. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  128. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  129. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  130. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  131. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  132. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  133. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  134. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  135. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  136. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +32 -37
  137. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  138. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +6 -10
  139. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  140. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  141. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  142. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  143. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  144. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  145. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  146. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  147. data/lib/phronomy/common/configuration_error.rb +7 -0
  148. data/lib/phronomy/common/error.rb +5 -0
  149. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  150. data/lib/phronomy/common/values/serializable.rb +32 -0
  151. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  152. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  153. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  154. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  155. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  156. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +1 -1
  157. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  158. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  159. data/lib/phronomy/engine/concurrency/offload_pool.rb +4 -0
  160. data/lib/phronomy/engine/concurrency/operation_binding.rb +1 -1
  161. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  162. data/lib/phronomy/engine/event_loop.rb +158 -624
  163. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  164. data/lib/phronomy/{execution.rb → engine/execution.rb} +0 -7
  165. data/lib/phronomy/{execution_cancellation_error.rb → engine/execution_cancellation_error.rb} +2 -0
  166. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  167. data/lib/phronomy/{execution_timeout_error.rb → engine/execution_timeout_error.rb} +2 -0
  168. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  169. data/lib/phronomy/engine/fsm_session.rb +35 -28
  170. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +2 -0
  171. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +2 -0
  172. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +2 -0
  173. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  174. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  175. data/lib/phronomy/engine/runtime.rb +80 -84
  176. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  177. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  178. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  179. data/lib/phronomy/engine/task_result.rb +4 -0
  180. data/lib/phronomy/engine/timeout_error.rb +7 -0
  181. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  182. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  183. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  184. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  185. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  186. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  187. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  188. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  189. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  190. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  191. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  192. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  193. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  194. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  195. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +5 -5
  196. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  197. data/lib/phronomy/multi_agent/orchestrator.rb +2 -2
  198. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  199. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  200. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  201. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  202. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  203. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  204. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  205. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  206. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  207. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  208. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  209. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  210. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  211. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  212. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  213. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  214. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  215. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  216. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  217. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  218. data/lib/phronomy/storage/backend.rb +101 -0
  219. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  220. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  221. data/lib/phronomy/storage/blobs.rb +31 -0
  222. data/lib/phronomy/storage/condition.rb +25 -0
  223. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  224. data/lib/phronomy/storage/conflict_error.rb +9 -0
  225. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  226. data/lib/phronomy/storage/entry.rb +33 -0
  227. data/lib/phronomy/storage/guard_ref.rb +13 -0
  228. data/lib/phronomy/storage/not_found_error.rb +9 -0
  229. data/lib/phronomy/storage/record_codec.rb +177 -0
  230. data/lib/phronomy/storage/records.rb +61 -0
  231. data/lib/phronomy/storage/resource.rb +126 -0
  232. data/lib/phronomy/storage/scope.rb +25 -0
  233. data/lib/phronomy/storage/serialization_error.rb +9 -0
  234. data/lib/phronomy/storage/streams.rb +44 -0
  235. data/lib/phronomy/storage/transaction_error.rb +10 -0
  236. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  237. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  238. data/lib/phronomy/storage/validation.rb +53 -0
  239. data/lib/phronomy/storage/view.rb +131 -0
  240. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  241. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  242. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  243. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  244. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  245. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  246. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  247. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  248. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  249. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  250. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  251. data/lib/phronomy/tools/agent.rb +1 -1
  252. data/lib/phronomy/version.rb +1 -1
  253. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +8 -6
  254. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  255. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  256. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  257. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +122 -71
  258. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  259. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  260. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  261. data/lib/phronomy/workflow/phase_machine_builder.rb +2 -2
  262. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  263. data/lib/phronomy.rb +52 -94
  264. data/scripts/api_snapshot.rb +1 -1
  265. data/scripts/storage_spi_snapshot.rb +36 -0
  266. data/sig/phronomy/agent.rbs +0 -1
  267. data/sig/phronomy/execution_receiver.rbs +34 -0
  268. data/sig/phronomy/handoff.rbs +4 -2
  269. data/sig/phronomy/multi_agent.rbs +18 -0
  270. data/sig/phronomy/persistence.rbs +7 -91
  271. data/sig/phronomy/storage.rbs +174 -0
  272. data/sig/phronomy/tool.rbs +10 -1
  273. metadata +230 -99
  274. data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
  275. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  276. data/lib/phronomy/agent/recovery_support.rb +0 -504
  277. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  278. data/lib/phronomy/agent/tool_executor.rb +0 -55
  279. data/lib/phronomy/generator_verifier.rb +0 -369
  280. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  281. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  282. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  283. data/lib/phronomy/persistence/in_memory.rb +0 -690
  284. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  285. data/lib/phronomy/persistence.rb +0 -276
  286. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  287. data/lib/phronomy/workflow_recovery.rb +0 -123
  288. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  289. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  290. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  291. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  292. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  293. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  294. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  295. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  296. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  297. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  298. /data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +0 -0
  299. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  300. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  301. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  302. /data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +0 -0
  303. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  304. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -10,7 +10,7 @@ module Phronomy
10
10
 
11
11
  def begin_resolve_on_event_loop(request)
12
12
  event_loop = @runtime.event_loop
13
- state = event_loop.agent_execution_state(
13
+ state = Phronomy::Agent::ExecutionRegistry.for(event_loop).agent_execution_state(
14
14
  request.execution_id
15
15
  )
16
16
  unless state && state.agent.equal?(agent)
@@ -26,7 +26,7 @@ module Phronomy
26
26
  unless current.execution_revision ==
27
27
  request.expected_execution_revision
28
28
  request.completion.fail(
29
- Phronomy::Persistence::ConflictError.new(
29
+ Phronomy::Storage::ConflictError.new(
30
30
  "Recovery resolution revision conflict: expected " \
31
31
  "#{request.expected_execution_revision}, actual " \
32
32
  "#{current.execution_revision}"
@@ -69,7 +69,7 @@ module Phronomy
69
69
  result: request.result,
70
70
  failure: request.failure
71
71
  )
72
- event_loop.mark_agent_execution_admission(
72
+ Phronomy::Agent::ExecutionRegistry.for(event_loop).mark_agent_execution_admission(
73
73
  agent.agent_id,
74
74
  execution_id: current.execution_id,
75
75
  state: :recovery_required
@@ -116,9 +116,9 @@ module Phronomy
116
116
  llm_call_id =
117
117
  operation.subject.fetch(:llm_call_id).to_s
118
118
  unless current.metadata[
119
- RecoverySupport::PENDING_LLM_ID_KEY
119
+ ExecutionMetadata::PENDING_LLM_ID_KEY
120
120
  ].to_s == llm_call_id
121
- raise Phronomy::Persistence::ConflictError,
121
+ raise Phronomy::Storage::ConflictError,
122
122
  "LLM Recovery subject is no longer pending"
123
123
  end
124
124
 
@@ -136,27 +136,27 @@ module Phronomy
136
136
  "automatic semantic redispatch is unavailable without a replay contract"
137
137
  }.freeze
138
138
  recovery = {
139
- "version" => RecoverySupport::CONTRACT_VERSION,
139
+ "version" => ExecutionMetadata::CONTRACT_VERSION,
140
140
  "resolution_outcome" => "not_performed",
141
141
  "failure" => failure
142
142
  }.freeze
143
143
  updated = current.with(
144
144
  phase: :recovery_resolved_failed,
145
145
  metadata: current.metadata.merge(
146
- RecoverySupport::RECOVERY_METADATA_KEY => recovery
146
+ ExecutionMetadata::RECOVERY_METADATA_KEY => recovery
147
147
  )
148
148
  )
149
149
  intended = ResolutionResult.new(execution: updated)
150
150
  save_resolution_result(current, intended)
151
151
  when :failed
152
152
  recovery = {
153
- "version" => RecoverySupport::CONTRACT_VERSION,
153
+ "version" => ExecutionMetadata::CONTRACT_VERSION,
154
154
  "failure" => operation.failure
155
155
  }
156
156
  updated = current.with(
157
157
  phase: :recovery_resolved_failed,
158
158
  metadata: current.metadata.merge(
159
- RecoverySupport::RECOVERY_METADATA_KEY =>
159
+ ExecutionMetadata::RECOVERY_METADATA_KEY =>
160
160
  recovery
161
161
  )
162
162
  )
@@ -170,7 +170,6 @@ module Phronomy
170
170
  )
171
171
  updated = nil
172
172
  with_resolution_f1_capture do |tx|
173
- main = agent.send(:execution_coordinator_for, agent.__coordination_config)
174
173
  snapshot = {
175
174
  llm_results: [{
176
175
  llm_call_id: llm_call_id,
@@ -178,22 +177,22 @@ module Phronomy
178
177
  error: nil,
179
178
  streaming: (
180
179
  current.metadata[
181
- RecoverySupport::INVOCATION_MODE_KEY
180
+ ExecutionMetadata::INVOCATION_MODE_KEY
182
181
  ].to_s == "stream"
183
182
  ),
184
183
  manifest_ref: current.metadata.fetch(
185
184
  "manifest_ref"
186
185
  ),
187
186
  started_at: current.metadata[
188
- RecoverySupport::PENDING_LLM_STARTED_AT_KEY
187
+ ExecutionMetadata::PENDING_LLM_STARTED_AT_KEY
189
188
  ] || current.updated_at
190
189
  }.freeze].freeze,
191
190
  runtime_events: [].freeze,
192
191
  active_call: nil
193
192
  }.freeze
194
- records, calls = main.send(
195
- :encode_runtime_records,
193
+ records, calls = RuntimeRecordEncoder.encode(
196
194
  current,
195
+ agent_id: agent.agent_id,
197
196
  tx: tx,
198
197
  snapshot: snapshot,
199
198
  context_candidate: true,
@@ -210,15 +209,15 @@ module Phronomy
210
209
  :recovery_provider_completed : :recovery_tools
211
210
  metadata = current.metadata.dup
212
211
  metadata.delete(
213
- RecoverySupport::PENDING_LLM_ID_KEY
212
+ ExecutionMetadata::PENDING_LLM_ID_KEY
214
213
  )
215
214
  metadata.delete(
216
- RecoverySupport::PENDING_LLM_STARTED_AT_KEY
215
+ ExecutionMetadata::PENDING_LLM_STARTED_AT_KEY
217
216
  )
218
217
  metadata.delete("framework_calls_pending")
219
218
  metadata["framework_calls_pending"] = true unless framework_subjects.empty?
220
- if subjects.any? && Array(metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY]).empty?
221
- metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY] = subjects.map do |entry|
219
+ if subjects.any? && Array(metadata[ExecutionMetadata::TOOL_BATCH_METADATA_KEY]).empty?
220
+ metadata[ExecutionMetadata::TOOL_BATCH_METADATA_KEY] = subjects.map do |entry|
222
221
  {
223
222
  "tool_invocation_id" => entry.fetch("tool_invocation_id"),
224
223
  "llm_call_id" => entry.fetch("llm_call_id"),
@@ -231,11 +230,11 @@ module Phronomy
231
230
  end
232
231
  if external_subjects.empty?
233
232
  metadata.delete(
234
- RecoverySupport::RECOVERY_METADATA_KEY
233
+ ExecutionMetadata::RECOVERY_METADATA_KEY
235
234
  )
236
235
  else
237
236
  metadata[
238
- RecoverySupport::RECOVERY_METADATA_KEY
237
+ ExecutionMetadata::RECOVERY_METADATA_KEY
239
238
  ] = RecoverySupport.build_recovery_hash(
240
239
  external_subjects
241
240
  )
@@ -268,7 +267,7 @@ module Phronomy
268
267
  recovery = RecoverySupport.build_recovery_hash(subjects)
269
268
  end
270
269
  unless recovery
271
- raise Phronomy::Persistence::ConflictError,
270
+ raise Phronomy::Storage::ConflictError,
272
271
  "Tool Recovery state is missing"
273
272
  end
274
273
 
@@ -281,7 +280,7 @@ module Phronomy
281
280
  hash.fetch("state", "unresolved") == "unresolved"
282
281
  end
283
282
  unless subject_entry
284
- raise Phronomy::Persistence::ConflictError,
283
+ raise Phronomy::Storage::ConflictError,
285
284
  "Tool Recovery subject is no longer unresolved"
286
285
  end
287
286
  subject_entry =
@@ -309,7 +308,7 @@ module Phronomy
309
308
  updated = current.with(
310
309
  phase: :recovery_resolved_failed,
311
310
  metadata: current.metadata.merge(
312
- RecoverySupport::RECOVERY_METADATA_KEY =>
311
+ ExecutionMetadata::RECOVERY_METADATA_KEY =>
313
312
  resolved_recovery
314
313
  )
315
314
  )
@@ -329,7 +328,7 @@ module Phronomy
329
328
  updated = current.with(
330
329
  phase: :recovery_resolved_failed,
331
330
  metadata: current.metadata.merge(
332
- RecoverySupport::RECOVERY_METADATA_KEY =>
331
+ ExecutionMetadata::RECOVERY_METADATA_KEY =>
333
332
  recovery_with_failure
334
333
  )
335
334
  )
@@ -338,9 +337,7 @@ module Phronomy
338
337
  when :succeeded
339
338
  updated = nil
340
339
  with_resolution_f1_capture do |tx|
341
- main = agent.send(:execution_coordinator_for, agent.__coordination_config)
342
- result_ref = main.send(
343
- :put_runtime_content,
340
+ result_ref = RuntimeRecordEncoder.put_runtime_content(
344
341
  tx,
345
342
  operation.result
346
343
  )
@@ -363,9 +360,9 @@ module Phronomy
363
360
  subject_entry.fetch("llm_call_id").to_s
364
361
  }.freeze
365
362
  )
366
- records, _calls = main.send(
367
- :encode_runtime_records,
363
+ records, _calls = RuntimeRecordEncoder.encode(
368
364
  current,
365
+ agent_id: agent.agent_id,
369
366
  tx: tx,
370
367
  snapshot: {
371
368
  llm_results: [].freeze,
@@ -388,7 +385,7 @@ module Phronomy
388
385
  RecoverySupport.unresolved_subjects(
389
386
  next_recovery
390
387
  )
391
- tool_batch = Array(current.metadata[RecoverySupport::TOOL_BATCH_METADATA_KEY]).map do |entry|
388
+ tool_batch = Array(current.metadata[ExecutionMetadata::TOOL_BATCH_METADATA_KEY]).map do |entry|
392
389
  if entry.fetch("tool_invocation_id") == subject_entry.fetch("tool_invocation_id")
393
390
  entry.merge("status" => "completed", "result" => operation.result)
394
391
  else
@@ -402,8 +399,8 @@ module Phronomy
402
399
  working_records:
403
400
  current.working_records + records,
404
401
  metadata: current.metadata.merge(
405
- RecoverySupport::RECOVERY_METADATA_KEY => next_recovery,
406
- RecoverySupport::TOOL_BATCH_METADATA_KEY => tool_batch
402
+ ExecutionMetadata::RECOVERY_METADATA_KEY => next_recovery,
403
+ ExecutionMetadata::TOOL_BATCH_METADATA_KEY => tool_batch
407
404
  )
408
405
  )
409
406
  tx.executions.save(
@@ -418,10 +415,10 @@ module Phronomy
418
415
  # simplecov:enable
419
416
 
420
417
  def known_non_f1_error?(error)
421
- error.is_a?(Phronomy::Persistence::ConflictError) ||
422
- error.is_a?(Phronomy::Persistence::NotFoundError) ||
423
- error.is_a?(Phronomy::Persistence::SerializationError) ||
424
- error.is_a?(Phronomy::Persistence::UnsupportedBackendError) ||
418
+ error.is_a?(Phronomy::Storage::ConflictError) ||
419
+ error.is_a?(Phronomy::Storage::NotFoundError) ||
420
+ error.is_a?(Phronomy::Storage::SerializationError) ||
421
+ error.is_a?(Phronomy::Storage::UnsupportedBackendError) ||
425
422
  error.is_a?(ArgumentError) ||
426
423
  error.is_a?(Phronomy::ConfigurationError)
427
424
  end
@@ -477,27 +474,27 @@ module Phronomy
477
474
  if current.to_h == operation.execution.to_h
478
475
  raise(error.is_a?(ResolutionOutcomeUnknownError) ? error.original_error : error)
479
476
  end
480
- raise Phronomy::Persistence::ConflictError,
477
+ raise Phronomy::Storage::ConflictError,
481
478
  "Recovery resolution durable outcome conflicts with both expected pre-state and intended post-state"
482
479
  end
483
480
 
484
481
  def apply_resolve_on_event_loop(ready)
485
482
  request = ready.request
486
483
  event_loop = @runtime.event_loop
487
- state = event_loop.agent_execution_state(request.execution_id)
484
+ state = Phronomy::Agent::ExecutionRegistry.for(event_loop).agent_execution_state(request.execution_id)
488
485
  unless state && state.agent.equal?(agent)
489
486
  raise Phronomy::ExecutionRehydrationRequiredError,
490
487
  "recovered execution disappeared before resolution apply"
491
488
  end
492
489
  unless state.execution.equal?(ready.operation.execution) && state.fsm_session_id.nil?
493
- raise Phronomy::Persistence::ConflictError,
490
+ raise Phronomy::Storage::ConflictError,
494
491
  "Recovery execution changed before resolution apply"
495
492
  end
496
493
  raise ready.error if ready.error
497
494
 
498
495
  result = ready.result
499
496
  if result.execution
500
- event_loop.replace_agent_execution(request.execution_id, execution: result.execution)
497
+ Phronomy::Agent::ExecutionRegistry.for(event_loop).replace_agent_execution(request.execution_id, execution: result.execution)
501
498
  end
502
499
  raise result.error if result.error
503
500
 
@@ -57,14 +57,14 @@ module Phronomy
57
57
  Array(tx.executions.list_active(agent.agent_id))
58
58
  end
59
59
  if active.length > 1
60
- raise Phronomy::Persistence::ConflictError,
60
+ raise Phronomy::Storage::ConflictError,
61
61
  "multiple active AgentExecutions exist for #{agent.agent_id}"
62
62
  end
63
63
  return agent if active.empty?
64
64
 
65
65
  execution = active.first
66
66
  unless execution.agent_id.to_s == agent.agent_id.to_s
67
- raise Phronomy::Persistence::ConflictError,
67
+ raise Phronomy::Storage::ConflictError,
68
68
  "active AgentExecution belongs to another Agent: #{execution.agent_id}"
69
69
  end
70
70
 
@@ -187,14 +187,10 @@ module Phronomy
187
187
  private
188
188
 
189
189
  def post_control(command)
190
- @runtime.event_loop.post(
191
- Phronomy::Event.new(
192
- type: :agent_control,
193
- target_id:
194
- Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
195
- payload: {command: command}.freeze
196
- )
197
- )
190
+ admission = command.is_a?(InstallCommand) || command.is_a?(ResolveCommand)
191
+ completion = admission ? command.completion : command.request.completion
192
+ ExecutionRegistry.for(@runtime.event_loop).post(command,
193
+ admission: admission, completion: completion)
198
194
  rescue Phronomy::RuntimeShutdownError
199
195
  false
200
196
  end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module RecoverySupport
6
+ module_function
7
+
8
+ def canonical_copy(value)
9
+ Phronomy::Values::Serializable.convert(value,
10
+ unsupported_message: "Recovery value is not canonically serializable")
11
+ end
12
+
13
+ def tool_calls_from_outcome(outcome)
14
+ Array(outcome.tool_calls)
15
+ end
16
+
17
+ def normalize_provider_outcome(value)
18
+ return value if value.is_a?(Phronomy::Agent::ProviderCallOutcome)
19
+
20
+ if value.is_a?(Hash)
21
+ return Phronomy::Agent::ProviderCallOutcome.from_h(value)
22
+ end
23
+
24
+ captured = Phronomy::Agent::ProviderCallOutcome.capture(value)
25
+ return captured if captured
26
+
27
+ raise ArgumentError,
28
+ "LLM Recovery :succeeded requires a Provider result or ProviderCallOutcome-compatible Hash"
29
+ end
30
+
31
+ def resolution_failure(error)
32
+ {
33
+ "class" => error.class.name.to_s,
34
+ "message" => error.message.to_s
35
+ }.freeze
36
+ end
37
+
38
+ def error_from_failure(failure)
39
+ hash = failure.to_h { |key, value| [key.to_s, value] }
40
+ Phronomy::Error.new(
41
+ "#{hash.fetch("class", "Error")}: #{hash.fetch("message", "Recovery-resolved failure")}"
42
+ )
43
+ end
44
+
45
+ def event_payload(execution, descriptor)
46
+ {
47
+ execution_id: execution.execution_id,
48
+ execution_revision: execution.execution_revision,
49
+ reason: descriptor.fetch(:reason),
50
+ subject: Phronomy::Recovery.normalize_subject(
51
+ descriptor.fetch(:subject)
52
+ ),
53
+ allowed_outcomes: Array(
54
+ descriptor.fetch(:allowed_outcomes)
55
+ ).map(&:to_sym).freeze,
56
+ facts: Phronomy::Values::Immutable.copy(
57
+ descriptor.fetch(:facts, {})
58
+ )
59
+ }.freeze
60
+ end
61
+
62
+ def pending_llm_descriptor(execution)
63
+ llm_call_id = execution.metadata[ExecutionMetadata::PENDING_LLM_ID_KEY]
64
+ return unless llm_call_id
65
+
66
+ {
67
+ reason: :outcome_unknown,
68
+ subject: {
69
+ type: :llm_call,
70
+ llm_call_id: llm_call_id
71
+ },
72
+ allowed_outcomes: Phronomy::Recovery::OUTCOMES,
73
+ facts: {
74
+ manifest_ref: execution.metadata["manifest_ref"],
75
+ call_sequence: execution.llm_calls.length + 1
76
+ }.compact.freeze
77
+ }.freeze
78
+ end
79
+
80
+ def recovery_hash(execution)
81
+ value = execution.metadata[ExecutionMetadata::RECOVERY_METADATA_KEY]
82
+ value.is_a?(Hash) ? value : nil
83
+ end
84
+
85
+ def current_tool_descriptor(execution)
86
+ recovery = recovery_hash(execution)
87
+ return unless recovery
88
+
89
+ subjects = Array(recovery["subjects"] || recovery[:subjects])
90
+ current = subjects.find do |entry|
91
+ hash = entry.to_h { |key, value| [key.to_s, value] }
92
+ hash.fetch("state", "unresolved") == "unresolved"
93
+ end
94
+ return unless current
95
+
96
+ hash = current.to_h { |key, value| [key.to_s, value] }
97
+ {
98
+ reason: (recovery["reason"] || recovery[:reason] || "outcome_unknown").to_sym,
99
+ subject: {
100
+ type: :tool_invocation,
101
+ tool_invocation_id: hash.fetch("tool_invocation_id")
102
+ },
103
+ allowed_outcomes: Array(
104
+ recovery["allowed_outcomes"] ||
105
+ recovery[:allowed_outcomes] ||
106
+ Phronomy::Recovery::OUTCOMES
107
+ ).map(&:to_sym),
108
+ facts: {
109
+ tool_call_id: hash["tool_call_id"],
110
+ tool_name: hash["tool_name"],
111
+ arguments: hash["arguments"],
112
+ llm_call_id: hash["llm_call_id"]
113
+ }.compact
114
+ }.freeze
115
+ end
116
+
117
+ def pending_tool_subjects(execution)
118
+ Array(execution.metadata[ExecutionMetadata::TOOL_BATCH_METADATA_KEY]).filter_map do |entry|
119
+ hash = entry.to_h { |key, value| [key.to_s, value] }
120
+ next unless %w[authorized awaiting_approval].include?(
121
+ hash.fetch("status")
122
+ )
123
+
124
+ {
125
+ "tool_invocation_id" => hash.fetch("tool_invocation_id"),
126
+ "llm_call_id" => hash["llm_call_id"],
127
+ "tool_call_id" => hash.fetch("tool_call_id"),
128
+ "tool_name" => hash.fetch("tool_name"),
129
+ "arguments" => canonical_copy(
130
+ hash["arguments"] || hash["raw_arguments"] || {}
131
+ ),
132
+ "state" => "unresolved"
133
+ }.compact.freeze
134
+ end.freeze
135
+ end
136
+
137
+ def pending_tool_descriptor(execution)
138
+ subjects = pending_tool_subjects(execution)
139
+ return if subjects.empty?
140
+
141
+ first = subjects.first
142
+ {
143
+ reason: :outcome_unknown,
144
+ subject: {
145
+ type: :tool_invocation,
146
+ tool_invocation_id: first.fetch("tool_invocation_id")
147
+ },
148
+ allowed_outcomes: Phronomy::Recovery::OUTCOMES,
149
+ facts: {
150
+ tool_call_id: first["tool_call_id"],
151
+ tool_name: first["tool_name"],
152
+ arguments: first["arguments"],
153
+ llm_call_id: first["llm_call_id"]
154
+ }.compact
155
+ }.freeze
156
+ end
157
+
158
+ def recovery_descriptor(execution)
159
+ case execution.phase.to_sym
160
+ when :calling_llm
161
+ pending_llm_descriptor(execution)
162
+ when :resuming
163
+ approved = execution.approval_request &&
164
+ (
165
+ execution.approval_request["approved"] ||
166
+ execution.approval_request[:approved]
167
+ )
168
+ approved ? pending_tool_descriptor(execution) : nil
169
+ when :dispatching_tools
170
+ pending_tool_descriptor(execution)
171
+ when :recovery_tools
172
+ current_tool_descriptor(execution)
173
+ end
174
+ end
175
+
176
+ def build_tool_subjects(execution, llm_call_id, outcome)
177
+ tool_calls_from_outcome(outcome).map do |call|
178
+ call_hash = canonical_copy(call)
179
+ tool_call_id = call_hash.fetch("id").to_s
180
+ tool_name = call_hash.fetch("name").to_s
181
+ {
182
+ "tool_invocation_id" => ToolInvocation.semantic_id(
183
+ execution_id: execution.execution_id,
184
+ llm_call_id: llm_call_id,
185
+ tool_call_id: tool_call_id,
186
+ tool_name: tool_name
187
+ ),
188
+ "llm_call_id" => llm_call_id.to_s,
189
+ "tool_call_id" => tool_call_id,
190
+ "tool_name" => tool_name,
191
+ "arguments" => canonical_copy(call_hash.fetch("arguments", {})),
192
+ "state" => "unresolved"
193
+ }.freeze
194
+ end.freeze
195
+ end
196
+
197
+ def build_recovery_hash(subjects, reason: :outcome_unknown, allowed_outcomes: Phronomy::Recovery::OUTCOMES)
198
+ {
199
+ "version" => ExecutionMetadata::CONTRACT_VERSION,
200
+ "reason" => reason.to_s,
201
+ "allowed_outcomes" => Array(allowed_outcomes).map(&:to_s),
202
+ "subjects" => Array(subjects).map { |entry| canonical_copy(entry) }
203
+ }.freeze
204
+ end
205
+
206
+ def update_recovery_subject(recovery, tool_invocation_id:, state:, outcome:, result_ref: nil)
207
+ copy = canonical_copy(recovery)
208
+ copy["subjects"] = Array(copy.fetch("subjects")).map do |entry|
209
+ next entry unless entry.fetch("tool_invocation_id").to_s == tool_invocation_id.to_s
210
+
211
+ entry.merge(
212
+ "state" => state.to_s,
213
+ "outcome" => outcome.to_s,
214
+ "result_ref" => result_ref
215
+ ).compact
216
+ end
217
+ copy.freeze
218
+ end
219
+
220
+ def unresolved_subjects(recovery)
221
+ Array(recovery.fetch("subjects")).select do |entry|
222
+ entry.fetch("state", "unresolved") == "unresolved"
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -41,7 +41,7 @@ module Phronomy
41
41
  sequence: values[:sequence] && Integer(values[:sequence]),
42
42
  constraint: effective_constraint,
43
43
  priority: Integer(values[:priority] || 0),
44
- metadata: Immutable.copy(values[:metadata] || {})
44
+ metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})
45
45
  )
46
46
 
47
47
  super(**normalized)
@@ -102,18 +102,7 @@ module Phronomy
102
102
  private
103
103
 
104
104
  def immutable_copy(value)
105
- case value
106
- when Hash
107
- value.each_with_object({}) do |(key, item), result|
108
- result[immutable_copy(key)] = immutable_copy(item)
109
- end.freeze
110
- when Array
111
- value.map { |item| immutable_copy(item) }.freeze
112
- when String
113
- value.dup.freeze
114
- else
115
- value
116
- end
105
+ Phronomy::Values::Immutable.copy(value)
117
106
  end
118
107
  end
119
108
  end
@@ -75,16 +75,7 @@ module Phronomy
75
75
  private
76
76
 
77
77
  def immutable_copy(value)
78
- case value
79
- when Hash
80
- value.each_with_object({}) { |(k, v), h| h[immutable_copy(k)] = immutable_copy(v) }.freeze
81
- when Array
82
- value.map { |v| immutable_copy(v) }.freeze
83
- when String
84
- value.dup.freeze
85
- else
86
- value
87
- end
78
+ Phronomy::Values::Immutable.copy(value)
88
79
  end
89
80
  end
90
81
 
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Builds invocation-independent Tool decorators from explicit Agent settings.
6
+ # Authorization, execution and Orchestrator context remain with their owners.
7
+ # @api private
8
+ class ToolBinding
9
+ def initialize(tool_class, alias_name:)
10
+ @tool_class = if alias_name
11
+ Class.new(tool_class) do
12
+ tool_name alias_name
13
+ end
14
+ else
15
+ tool_class
16
+ end
17
+ end
18
+
19
+ def prepare(result_filters:)
20
+ return @tool_class if result_filters.empty?
21
+
22
+ effective_name = @tool_class.new.name
23
+ custom_async_call =
24
+ @tool_class.instance_method(:call_async).owner !=
25
+ Phronomy::Agent::Context::Capability::Base
26
+ filter_async_result = method(:filter_async_result)
27
+
28
+ Class.new(@tool_class) do
29
+ tool_name effective_name
30
+ define_method(:call) do |args, **kwargs|
31
+ result = super(args, **kwargs)
32
+ result_filters.inject(result) { |val, filter|
33
+ filter.call(val, tool_name: name, args: args)
34
+ }
35
+ end
36
+
37
+ if custom_async_call
38
+ define_method(:call_async) do |args, **kwargs|
39
+ source = super(args, **kwargs)
40
+ filter_async_result.call(source, name) do |value|
41
+ result_filters.inject(value) { |val, filter|
42
+ filter.call(val, tool_name: name, args: args)
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def filter_async_result(source, tool_name, &filter_result)
53
+ filtered = Phronomy::Concurrency::PhysicalCompletionTask.deferred(
54
+ name: "tool-filter-#{tool_name}"
55
+ )
56
+ source_has_physical_signal = source.respond_to?(:on_physical_complete)
57
+ source.on_physical_complete { filtered.mark_physical_complete! } if
58
+ source_has_physical_signal
59
+ source.on_complete do |value, error|
60
+ if error
61
+ propagate_failure(filtered, source, error, source_has_physical_signal)
62
+ else
63
+ complete_filtered_result(filtered, source_has_physical_signal) do
64
+ filter_result.call(value)
65
+ end
66
+ end
67
+ end
68
+ filtered
69
+ end
70
+
71
+ def propagate_failure(filtered, source, error, source_has_physical_signal)
72
+ filtered.mark_physical_complete! unless source_has_physical_signal
73
+ if source.respond_to?(:status) && source.status == :cancelled
74
+ filtered.cancel!(error)
75
+ else
76
+ filtered.fail(error)
77
+ end
78
+ end
79
+
80
+ def complete_filtered_result(filtered, source_has_physical_signal)
81
+ result = yield
82
+ filtered.mark_physical_complete! unless source_has_physical_signal
83
+ filtered.complete(result)
84
+ rescue => filter_error
85
+ filtered.mark_physical_complete! unless source_has_physical_signal
86
+ filtered.fail(filter_error)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -4,8 +4,8 @@ module Phronomy
4
4
  module Agent
5
5
  # Normal control-transfer signal raised before RubyLLM executes a Tool Call.
6
6
  #
7
- # RubyLLM >= 1.15 guarantees that the complete assistant message has already
8
- # been added to Chat#messages before before_tool_call runs. Keeping that
7
+ # RubyLLM 2.0 guarantees that the complete assistant message has already
8
+ # been added to Chat#messages before after_message runs. Keeping that
9
9
  # message here allows Phronomy to persist the complete Provider outcome even
10
10
  # though the RubyLLM call itself unwinds through this exception.
11
11
  # @api private