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
@@ -23,37 +23,38 @@ are defined by
23
23
 
24
24
  ## Runtime model
25
25
 
26
- ```text
27
- Runtime
28
- ├─ Agent ownership registry
29
- │ └─ agent_id -> one mutable live Agent instance
30
- ├─ EventLoop (one control-plane operating-system Thread)
31
- │ ├─ FSMSession
32
- │ │ ├─ Agent
33
- │ │ ├─ Workflow
34
- │ │ ├─ ToolInvocation
35
- │ │ └─ MultiAgent fan-out
36
- │ ├─ Agent top-level admission
37
- │ │ └─ agent_id -> one nonterminal logical Execution admission
38
- │ └─ Agent execution directory
39
- │ └─ execution_id -> immutable live-state record
40
- ├─ OffloadPool (bounded operating-system Threads)
41
- │ ├─ private Operation records
42
- │ ├─ blocking input/output (I/O)
43
- │ ├─ central-processing-unit (CPU)-bound synchronous work
44
- │ └─ operation-specific durable Agent/Workflow work
45
- ├─ named OffloadPools
46
- └─ EventLoop-driven timers
47
-
48
- EventLoop / FSMSession ─┐
49
- ├─> TaskResult = completion handle
50
- OffloadPool ────────────┘
51
- ```
26
+ | Runtime-retained component | Implementation owner and responsibility |
27
+ |---|---|
28
+ | Shutdown participants | Feature-owned registries: Agent identity, Team identity, and MultiAgent synchronous-call admission |
29
+ | EventLoop | Engine dispatcher thread, FSMSessions and registered feature receivers |
30
+ | Execution receivers | Agent/Workflow own execution admission, state and idle policies; mutations run on EventLoop |
31
+ | OffloadPool and named pools | Engine bounded workers and private physical-operation records |
32
+ | Timers | Engine EventLoop-driven scheduling |
33
+
34
+ Runtime retains participants through a generic lifecycle protocol; it does not
35
+ construct Agent or Team identity registries. Each feature registers its own
36
+ instance. See [ADR-041](decisions/041-feature-owned-identity-registries.md).
52
37
 
53
38
  The framework does not allocate one operating-system Thread per logical
54
39
  Agent/Workflow/Tool lifecycle. Logical waits remain explicit states plus later
55
40
  EventLoop events.
56
41
 
42
+ ## Feature execution receivers
43
+
44
+ [ADR-042](decisions/042-feature-owned-execution-state.md) separates implementation
45
+ ownership from the single-writer execution context. Agent and Workflow create
46
+ `Agent::ExecutionRegistry` and `WorkflowExecutionRegistry` respectively; Engine
47
+ retains registered receivers without selecting concrete feature types.
48
+
49
+ Receiver registration and new request admission close before idle checks.
50
+ Already queued requests, pending durable operations and supervised physical
51
+ work are counted until their feature continuation settles. Worker callbacks
52
+ only enqueue results. Workflow instance routing and generic FSM enqueue share
53
+ one lock. After a clean join, receiver cleanup invalidates retained state;
54
+ a failing receiver makes Runtime cleanup incomplete. Dispatcher failure instead
55
+ notifies receivers on the failing loop thread with the original error.
56
+
57
+
57
58
  ## Live state and durable state
58
59
 
59
60
  A live Agent or Workflow owns its current logical state. `Persistence` is the
@@ -61,7 +62,7 @@ last committed durable representation and recovery source; it is not reloaded at
61
62
  every semantic boundary.
62
63
 
63
64
  For active Agents, **EventLoop is the single writer of Phronomy-managed live
64
- execution state**. EventLoop owns a process-local execution directory keyed by
65
+ execution state**. `Agent::ExecutionRegistry` owns the process-local directory keyed by
65
66
  canonical `execution_id`. Each directory value is immutable and is replaced on
66
67
  EventLoop when the current AgentExecution, RuntimeProjection, AgentInvocation, or
67
68
  owning FSMSession changes. The former mutex-protected
@@ -75,7 +76,7 @@ handling; workers do not receive AgentInvocation as a mutable state authority.
75
76
  Mutable Agent/Execution/Journal state is not automatically reloaded before every
76
77
  LLM or Tool step. Durable writes use optimistic revision/position guardrails; an
77
78
  external writer that advances the durable base causes
78
- `Persistence::ConflictError` rather than automatic reload or merge.
79
+ `Storage::ConflictError` rather than automatic reload or merge.
79
80
 
80
81
  For Workflows, the current `WorkflowContext` and FSMSession own the active
81
82
  logical state. A durable Workflow hydrates once at invocation/resume and saves at
@@ -88,8 +89,9 @@ content reference is value materialization rather than mutable state refresh.
88
89
 
89
90
  `agent_id` identifies one logical Agent, not a reusable lookup key for independent
90
91
  mutable objects. One Runtime therefore publishes at most one mutable live Agent
91
- instance for a given `agent_id`. The Runtime-owned registry is an authority, not a
92
- cache, and reserves the identity before create/load materialization.
92
+ instance for a given `agent_id`. The Agent-owned registry, strongly retained by
93
+ Runtime, is an authority, not a cache, and reserves the identity before
94
+ create/load materialization.
93
95
 
94
96
  The application-facing identity operations are distinct:
95
97
 
@@ -100,7 +102,7 @@ new / create
100
102
  load(agent_id, persistence:)
101
103
  live -> exact same Ruby object, with no Persistence reload
102
104
  durable-only -> hydrate and publish once
103
- missing -> Persistence::NotFoundError
105
+ missing -> Storage::NotFoundError
104
106
 
105
107
  get(agent_id)
106
108
  live Runtime lookup only; missing -> nil
@@ -114,7 +116,8 @@ invalidates the old object, deletes durable state, releases the process-local
114
116
  identity, and allows a later new Agent to reuse the textual ID.
115
117
 
116
118
  Live Agent ownership and top-level Execution admission are separate lifetimes.
117
- For one live Agent, EventLoop admits at most one nonterminal top-level Execution.
119
+ For one live Agent, `Agent::ExecutionRegistry` admits at most one nonterminal
120
+ top-level Execution on EventLoop.
118
121
  Admission is acquired **before** the initial Offload/Persistence operation:
119
122
 
120
123
  ```text
@@ -195,6 +198,15 @@ Phronomy-managed live domain objects are rejected from that value data. A comple
195
198
  value-type/serialization contract for arbitrary Application-owned opaque objects is
196
199
  deferred; such objects remain Application-owned and must be worker-safe.
197
200
 
201
+ The existing restricted types declare the internal methodless
202
+ `Concurrency::WorkerInputRestricted` marker at their own definitions.
203
+ ToolInvocation checks that execution-boundary contract instead of enumerating
204
+ Agent/Workflow/Runtime classes. Frozen marked values and subclasses remain
205
+ restricted; Hash keys and values, nested Arrays and behavior handles are all
206
+ checked. The marker does not inspect opaque application fields or closure
207
+ captures and is not a general restriction on every OffloadPool command.
208
+ See [ADR-045](decisions/045-worker-input-restriction-ownership.md).
209
+
198
210
  Worker authorization/execution outcomes return as values carrying
199
211
  `tool_invocation_id`; the Tool FSMSession consumes a mismatched semantic result
200
212
  without advancing its current state.
@@ -279,6 +291,32 @@ remain durable conflict defense rather than distributed ownership.
279
291
 
280
292
  ## Tool execution modes
281
293
 
294
+ The default dispatch helper is the private
295
+ `Phronomy::Agent::Context::Capability::ToolExecutor`, colocated with Capability
296
+ Base. Agent ToolInvocation supplies Runtime and admission policy for the
297
+ standard path, and owns authorization and logical result handling. Custom
298
+ `call_async` implementations keep the public Tool protocol. This ownership is
299
+ defined by [ADR-035](decisions/035-tool-executor-capability-ownership.md); the
300
+ public `Phronomy::Tool::Base` facade and its Class identity remain unchanged.
301
+
302
+ Agent uses ordinary `RubyLLM::Chat` for both complete and streaming Provider
303
+ calls. The `before_tool_call` callback takes the complete assistant Tool-call
304
+ batch before RubyLLM executes the first Tool body. `AgentInvocation` then owns
305
+ authorization, dispatch, approval suspension and result collection. Offloaded
306
+ Tools can overlap within Runtime capacity; short cooperative Tools run according
307
+ to their existing execution contract.
308
+
309
+ On successful batch completion, Agent records one Tool result per call ID in
310
+ request order and includes the whole exchange in the next Provider request.
311
+ Worker completion order does not split the conversation or trigger a partial
312
+ Provider continuation. Failure, cancellation and approval keep their existing
313
+ Agent lifecycle semantics.
314
+
315
+ There is no separate Chat execution path or `parallel_tool_execution` switch.
316
+ The removed internal `MultiAgent::ParallelToolChat` class is not replaced by an
317
+ alias. See the [Chat migration guide](migrations/parallel-tool-chat-removal.md)
318
+ for application configuration and existing stored model-config records.
319
+
282
320
  Phronomy exposes two execution modes for capabilities:
283
321
 
284
322
  - `:cooperative` — short EventLoop-safe work, or specialized asynchronous work
@@ -547,6 +585,53 @@ Use these to distinguish worker saturation from EventLoop backlog/latency.
547
585
  Runtime-owned EventLoop, then closes pools and timers according to the Runtime
548
586
  shutdown contract.
549
587
 
588
+ Internal subsystems can register one shutdown participant per Runtime-local key
589
+ through `Runtime#__register_shutdown_participant`. This is an internal lifecycle
590
+ contract, not a public extension API or an execution registry. Runtime does not
591
+ construct participants or interpret their admission rules.
592
+
593
+ Each participant implements two required operations:
594
+
595
+ - `begin_draining` closes admission under the participant's admission lock. It
596
+ must be idempotent, short and nonblocking, and must not call back into Runtime.
597
+ Runtime invokes it under its lifecycle lock when shutdown starts or EventLoop
598
+ fails, so no participant can be registered after closure begins.
599
+ - `wait_until_idle(deadline)` returns whether admitted calls have finished before
600
+ the shared absolute monotonic deadline. Runtime closes every participant before
601
+ waiting, and waits without holding its lifecycle lock. A timeout or failed hook
602
+ makes cleanup incomplete; other participants, pools and timers still receive
603
+ their shutdown calls. Incomplete cleanup prevents default Runtime replacement.
604
+
605
+ Participants may also implement `after_runtime_shutdown`. Runtime calls it
606
+ outside its lifecycle lock after every wait succeeds, EventLoop has stopped,
607
+ and pools/timers have shut down successfully. The hook must be idempotent,
608
+ short, and perform no I/O; its return value is ignored. A hook exception marks
609
+ cleanup incomplete, retains the first failure, and does not skip later hooks.
610
+ Completed releases are not rolled back; repeated shutdown returns its cached
611
+ result without retrying hooks. A prior closure/wait failure, timeout, or worker
612
+ shutdown failure skips finalization entirely.
613
+
614
+ `Runtime#__shutdown_participant(key:)` only retrieves an existing participant,
615
+ including during/after shutdown. Registration still requires a running Runtime,
616
+ even for an existing key. Agent/Team `get` uses lookup without registration;
617
+ feature gates reject new ownership changes through retained registries after
618
+ closure. Already admitted transitions may finish. Identity registries wait only
619
+ for active construction/purge, then detach Agent references or clear Team
620
+ owners at finalization; live idle objects do not themselves prolong shutdown.
621
+
622
+ `MultiAgent::AdmissionRegistry.for(runtime)` constructs and registers the shared
623
+ coordination admission registry. MultiAgent::HandoffRunner and TeamCoordinator retain that
624
+ registry, admit each synchronous call directly and release it in `ensure` after
625
+ successful admission. The registry closes admission atomically with respect to
626
+ `admit!`; a call admitted before closure is included in the wait, and a call after
627
+ closure raises `RuntimeShutdownError`, including after EventLoop failure. The
628
+ existing `HandoffError` for duplicate admission remains unchanged.
629
+
630
+ This tracks the current synchronous Handoff/Team call, not the lifetime or
631
+ completion of a durable AgentExecution or TeamExecution. Multi-Agent policy stays
632
+ in `multi_agent`; Runtime depends only on the shutdown operations. Application
633
+ authors do not register these participants themselves.
634
+
550
635
  Workflow durable admission participates in EventLoop idleness: a Workflow whose
551
636
  FSMSession has ended but whose durable save is still in flight remains owned until
552
637
  that save completes and owner-aware admission is released.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ StreamEvent = Data.define(:type, :payload)
6
+ end
7
+ end
8
+
9
+ require_relative "../async_event_api"
10
+
11
+ unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
12
+ Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
13
+ end
14
+
15
+ # Durable Agent Recovery and Agent-incarnation event binding.
16
+ require_relative "../recovery/recovery_support"
17
+ require_relative "../recovery/recovery_coordinator"
@@ -96,7 +96,7 @@ module Phronomy
96
96
 
97
97
  def approve_async(execution_id, approval_request_id:, approved: true, config: {})
98
98
  _reject_removed_generic_identity_keys!(config)
99
- owner = Phronomy::Runtime.instance.__agent_execution_owner(execution_id)
99
+ owner = Phronomy::Agent::ExecutionRegistry.existing_for(Phronomy::Runtime.instance)&.agent_execution_owner(execution_id)
100
100
  coordinator = if owner&.agent&.equal?(self)
101
101
  owner.coordinator
102
102
  else
@@ -190,7 +190,7 @@ module Phronomy
190
190
  end
191
191
 
192
192
  bytes = Phronomy::CanonicalJSON.dump(value)
193
- snapshot = Phronomy::Agent::Immutable.copy(
193
+ snapshot = Phronomy::Values::Immutable.copy(
194
194
  Phronomy::CanonicalJSON.load(bytes)
195
195
  )
196
196
  config.merge(durable_context: snapshot)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../engine/concurrency/worker_input_restricted"
4
+
3
5
  require "securerandom"
4
6
  require_relative "concerns/filterable"
5
7
  require_relative "concerns/before_llm_input"
@@ -31,6 +33,8 @@ module Phronomy
31
33
  # max_iterations 15
32
34
  # end
33
35
  class Base
36
+ include Phronomy::Concurrency::WorkerInputRestricted
37
+
34
38
  include Phronomy::Runnable
35
39
  include Concerns::Filterable
36
40
  include Concerns::BeforeLLMInput
@@ -119,15 +123,9 @@ module Phronomy
119
123
  if val.nil?
120
124
  @max_output_tokens
121
125
  else
122
- @max_output_tokens = val.to_i
123
- end
124
- end
125
-
126
- def context_window(val = nil)
127
- if val.nil?
128
- @context_window
129
- else
130
- @context_window = val.to_i
126
+ value = Integer(val)
127
+ raise ArgumentError, "max_output_tokens must be positive" unless value.positive?
128
+ @max_output_tokens = value
131
129
  end
132
130
  end
133
131
 
@@ -204,7 +202,7 @@ module Phronomy
204
202
  runtime = Phronomy::Runtime.instance
205
203
  materialized = false
206
204
 
207
- agent = runtime.__load_agent(key, expected_class: self) do |owner_runtime|
205
+ agent = OwnershipRegistry.for(runtime).load(key, expected_class: self) do |owner_runtime|
208
206
  materialized = true
209
207
  instance = __construct_owned_agent(
210
208
  owner_runtime,
@@ -235,14 +233,17 @@ module Phronomy
235
233
  # Returns only the process-local live Agent owner. Does not access
236
234
  # Persistence and returns nil when this Runtime has no live owner.
237
235
  def get(agent_id)
238
- Phronomy::Runtime.instance.__get_agent(agent_id, expected_class: self)
236
+ key = agent_id.to_s
237
+ raise ArgumentError, "agent_id must not be empty" if key.empty?
238
+
239
+ OwnershipRegistry.existing_for(Phronomy::Runtime.instance)&.get(key, expected_class: self)
239
240
  end
240
241
 
241
242
  # Resolves the live Agent instance that currently owns execution_id in
242
243
  # this process. The Runtime returns only a read-only ownership view;
243
244
  # mutable Agent execution state remains EventLoop-owned.
244
245
  def live_for_execution(execution_id)
245
- owner = Phronomy::Runtime.instance.__agent_execution_owner(execution_id)
246
+ owner = Phronomy::Agent::ExecutionRegistry.existing_for(Phronomy::Runtime.instance)&.agent_execution_owner(execution_id)
246
247
  unless owner
247
248
  raise Phronomy::ExecutionRehydrationRequiredError,
248
249
  "no live execution owner for #{execution_id}; durable rehydration is required"
@@ -338,7 +339,7 @@ module Phronomy
338
339
 
339
340
  runtime = Phronomy::Runtime.instance
340
341
  begin
341
- runtime.__create_agent(effective_agent_id, expected_class: self.class) do |owner_runtime|
342
+ OwnershipRegistry.for(runtime).create(effective_agent_id, expected_class: self.class) do |owner_runtime|
342
343
  __prepare_runtime_owner!(owner_runtime, effective_agent_id)
343
344
  initialize_owned_state(
344
345
  agent_id: effective_agent_id,
@@ -350,7 +351,7 @@ module Phronomy
350
351
  )
351
352
  self
352
353
  end
353
- rescue Phronomy::Persistence::ConflictError => error
354
+ rescue Phronomy::Storage::ConflictError => error
354
355
  raise Phronomy::AgentAlreadyExistsError,
355
356
  "Agent #{effective_agent_id.inspect} already exists durably: #{error.message}"
356
357
  end
@@ -395,34 +396,8 @@ module Phronomy
395
396
  def add_knowledge(content, metadata: {})
396
397
  __assert_live_agent!
397
398
  current = agent_root
398
- next_root = nil
399
- appended = nil
400
- persistence.transaction do |tx|
401
- tx.executions.assert_idle!(agent_id)
402
- record = build_knowledge_record(
403
- tx: tx,
404
- root: current,
405
- content: content,
406
- metadata: metadata
407
- )
408
- appended = tx.journals.append(
409
- agent_id,
410
- expected_position: current.journal_position,
411
- records: [record]
412
- )
413
- next_root = current.with(
414
- agent_revision: current.agent_revision + 1,
415
- context_revision: current.context_revision + 1,
416
- journal_position: current.journal_position + appended.length
417
- )
418
- tx.agents.save(
419
- agent_id,
420
- expected_revision: current.agent_revision,
421
- root: next_root
422
- )
423
- end
424
- _append_journal_records(appended)
425
- @root = next_root
399
+ update = state_writer.add_knowledge(root: current, content: content, metadata: metadata)
400
+ apply_state_update(update)
426
401
  self
427
402
  end
428
403
 
@@ -450,9 +425,10 @@ module Phronomy
450
425
 
451
426
  __assert_live_agent!
452
427
  runtime = @_phronomy_runtime_owner
453
- token = runtime.__begin_agent_purge(self)
454
- if runtime.__agent_execution_admitted?(agent_id)
455
- runtime.__abort_agent_purge(self, token)
428
+ registry = OwnershipRegistry.for(runtime)
429
+ token = registry.begin_purge(self)
430
+ if Phronomy::Agent::ExecutionRegistry.existing_for(runtime)&.agent_execution_admitted?(agent_id)
431
+ registry.abort_purge(self, token)
456
432
  raise Phronomy::AgentBusyError,
457
433
  "Agent #{agent_id.inspect} has a nonterminal top-level execution"
458
434
  end
@@ -471,20 +447,20 @@ module Phronomy
471
447
  tx.agents.delete(agent_id)
472
448
  end
473
449
  rescue Phronomy::AgentBusyError,
474
- Phronomy::Persistence::ConflictError,
475
- Phronomy::Persistence::NotFoundError,
476
- Phronomy::Persistence::SerializationError,
450
+ Phronomy::Storage::ConflictError,
451
+ Phronomy::Storage::NotFoundError,
452
+ Phronomy::Storage::SerializationError,
477
453
  ArgumentError
478
- runtime.__abort_agent_purge(self, token)
454
+ registry.abort_purge(self, token)
479
455
  raise
480
456
  rescue
481
457
  # Without F1 reconciliation support we cannot infer that a failed
482
458
  # durable delete did not commit. Keep the identity fail-closed.
483
- runtime.__leave_agent_purge_uncertain(self, token)
459
+ registry.leave_purge_uncertain(self, token)
484
460
  raise
485
461
  end
486
462
 
487
- runtime.__complete_agent_purge(self, token)
463
+ registry.complete_purge(self, token)
488
464
  true
489
465
  end
490
466
 
@@ -585,7 +561,7 @@ module Phronomy
585
561
  )
586
562
  @persistence = persistence ||
587
563
  Phronomy.configuration.persistence ||
588
- Phronomy::Persistence::InMemory.new
564
+ DefaultPersistence.build
589
565
  @agent_id = agent_id.to_s.freeze
590
566
 
591
567
  if load_existing
@@ -630,117 +606,27 @@ module Phronomy
630
606
 
631
607
  def create_agent_root!(context:, knowledge:, metadata:)
632
608
  definition = self.class.agent_definition
633
- root = Agent::AgentRoot.create(
634
- agent_id: agent_id,
635
- agent_definition_id: definition.fetch(:id),
636
- agent_definition_version: definition.fetch(:version),
637
- metadata: metadata
638
- )
639
- persistence.transaction do |tx|
640
- tx.agents.create(root)
641
- records = initial_context_records(tx: tx, root: root, context: context)
642
- records.concat(initial_knowledge_records(tx: tx, root: root, knowledge: knowledge))
643
- unless records.empty?
644
- appended = tx.journals.append(agent_id, expected_position: 0, records: records)
645
- root = root.with(
646
- agent_revision: 1,
647
- context_revision: 1,
648
- journal_position: appended.length
649
- )
650
- tx.agents.save(agent_id, expected_revision: 0, root: root)
651
- end
652
- end
653
- root
654
- end
655
-
656
- def initial_context_records(tx:, root:, context:)
657
- return [] unless context
658
-
659
- imported = context.respond_to?(:records) ? context :
660
- Agent::ContextImporter.import_messages(context)
661
- imported.records.map do |record|
662
- content_ref = case record.content_format
663
- when :text then tx.contents.put_text(record.content)
664
- when :json then tx.contents.put_json(record.content)
665
- else
666
- raise ArgumentError,
667
- "unsupported imported content format: #{record.content_format.inspect}"
668
- end
669
- Agent::JournalRecord.new(
670
- agent_id: agent_id,
671
- kind: record.kind,
672
- channel: record.channel,
673
- role: record.role,
674
- content_ref: content_ref,
675
- context_generation: root.transcript_generation,
676
- context_candidate: true,
677
- metadata: record.metadata
678
- )
679
- end
680
- end
681
-
682
- def initial_knowledge_records(tx:, root:, knowledge:)
683
- Array(knowledge).map do |content|
684
- build_knowledge_record(
685
- tx: tx,
686
- root: root,
687
- content: content,
688
- metadata: {}
689
- )
690
- end
691
- end
692
-
693
- def build_knowledge_record(tx:, root:, content:, metadata:)
694
- Agent::JournalRecord.new(
695
- agent_id: agent_id,
696
- kind: :knowledge,
697
- channel: :context,
698
- role: :user,
699
- content_ref: tx.contents.put_text(String(content)),
700
- context_generation: root.transcript_generation,
701
- context_candidate: true,
702
- metadata: metadata || {}
609
+ state_writer.create_root(
610
+ definition: definition, context: context, knowledge: knowledge, metadata: metadata
703
611
  )
704
612
  end
705
613
 
706
614
  def mutate_context!(kind, context_affecting: true)
707
615
  __assert_live_agent!
708
616
  current = agent_root
709
- next_root = nil
710
- appended = nil
711
- persistence.transaction do |tx|
712
- tx.executions.assert_idle!(agent_id)
713
- record = Agent::JournalRecord.new(
714
- agent_id: agent_id,
715
- kind: kind,
716
- channel: :state,
717
- context_generation: current.transcript_generation,
718
- context_candidate: false
719
- )
720
- appended = tx.journals.append(
721
- agent_id,
722
- expected_position: current.journal_position,
723
- records: [record]
724
- )
725
- proposed = yield(current)
726
- next_root = proposed.with(
727
- journal_position: current.journal_position + appended.length,
728
- context_revision: context_affecting ?
729
- yield_context_revision(current, proposed) : current.context_revision
730
- )
731
- tx.agents.save(
732
- agent_id,
733
- expected_revision: current.agent_revision,
734
- root: next_root
735
- )
736
- end
737
- _append_journal_records(appended)
738
- @root = next_root
617
+ update = state_writer.mutate_context(
618
+ root: current, kind: kind, context_affecting: context_affecting
619
+ ) { |root| yield(root) }
620
+ apply_state_update(update)
739
621
  end
740
622
 
741
- def yield_context_revision(current, proposed)
742
- (proposed.context_revision == current.context_revision) ?
743
- current.context_revision + 1 : proposed.context_revision
623
+ def state_writer
624
+ StateWriter.new(persistence: persistence, agent_id: agent_id)
625
+ end
626
+
627
+ def apply_state_update(update)
628
+ _append_journal_records(update.records)
629
+ @root = update.root
744
630
  end
745
631
 
746
632
  def _journal_records_snapshot
@@ -789,7 +675,8 @@ module Phronomy
789
675
  return true if @_phronomy_runtime_owner_state == :constructing
790
676
 
791
677
  runtime = @_phronomy_runtime_owner
792
- unless runtime&.__agent_owned?(self)
678
+ registry = OwnershipRegistry.existing_for(runtime) if runtime
679
+ unless registry&.owned?(self)
793
680
  raise Phronomy::RuntimeShutdownError,
794
681
  "Agent #{agent_id.inspect} is no longer the live owner in its Runtime"
795
682
  end
@@ -943,7 +830,7 @@ module Phronomy
943
830
  )
944
831
  end
945
832
  projection.tool_classes.each do |tool_class|
946
- chat.with_tool(prepare_tool_class(tool_class, invocation: invocation))
833
+ chat.with_tools(prepare_tool_class(tool_class, invocation: invocation))
947
834
  end
948
835
  projection.messages.each { |message| chat.messages << message }
949
836
  chat
@@ -954,25 +841,9 @@ module Phronomy
954
841
  "model" => self.class.model,
955
842
  "provider" => self.class.provider,
956
843
  "temperature" => self.class.temperature,
957
- "max_output_tokens" => self.class.max_output_tokens,
958
- "parallel_tool_execution" => Phronomy.configuration.parallel_tool_execution
844
+ "max_output_tokens" => self.class.max_output_tokens
959
845
  }
960
- opts = {}
961
- model = config["model"]
962
- opts[:model] = model if model
963
- provider = config["provider"]
964
- if provider
965
- opts[:provider] = provider.to_sym
966
- opts[:assume_model_exists] = true
967
- end
968
- parallel_class = config["parallel_tool_execution"] ?
969
- Phronomy::MultiAgent::ParallelToolChat : nil
970
- chat = parallel_class ? parallel_class.new(**opts) : RubyLLM.chat(**opts)
971
- chat.with_temperature(config["temperature"]) if config["temperature"]
972
- if config["max_output_tokens"] && chat.respond_to?(:with_max_output_tokens)
973
- chat.with_max_output_tokens(config["max_output_tokens"])
974
- end
975
- chat
846
+ RuntimeChatBuilder.build(config)
976
847
  end
977
848
 
978
849
  def build_instructions(input)
@@ -988,12 +859,7 @@ module Phronomy
988
859
  end
989
860
 
990
861
  def apply_instructions(chat, text, cache: false, provider: nil)
991
- if cache && provider.to_s == "anthropic"
992
- content = RubyLLM::Providers::Anthropic::Content.new(text, cache: true)
993
- chat.with_instructions(content)
994
- else
995
- chat.with_instructions(text)
996
- end
862
+ RuntimeChatBuilder.apply_instructions(chat, text, cache: cache, provider: provider)
997
863
  end
998
864
 
999
865
  def extract_message(input)
@@ -1021,66 +887,10 @@ module Phronomy
1021
887
  def prepare_tool_class(tool_class, invocation: nil)
1022
888
  return tool_class unless tool_class.is_a?(Class)
1023
889
 
1024
- resolved = if (alias_name = self.class.tool_aliases[tool_class])
1025
- Class.new(tool_class) do
1026
- tool_name alias_name
1027
- end
1028
- else
1029
- tool_class
1030
- end
1031
-
1032
- result_filters = _tool_result_filters_for(tool_class)
1033
- return resolved if result_filters.empty?
1034
-
1035
- effective_name = resolved.new.name
1036
- custom_async_call =
1037
- resolved.instance_method(:call_async).owner !=
1038
- Phronomy::Agent::Context::Capability::Base
1039
-
1040
- Class.new(resolved) do
1041
- tool_name effective_name
1042
- define_method(:call) do |args, **kwargs|
1043
- result = super(args, **kwargs)
1044
- result_filters.inject(result) { |val, filter|
1045
- filter.call(val, tool_name: name, args: args)
1046
- }
1047
- end
1048
-
1049
- if custom_async_call
1050
- define_method(:call_async) do |args, **kwargs|
1051
- source = super(args, **kwargs)
1052
- filtered = Phronomy::Concurrency::PhysicalCompletionTask.deferred(
1053
- name: "tool-filter-#{name}"
1054
- )
1055
- source_has_physical_signal = source.respond_to?(:on_physical_complete)
1056
- source.on_physical_complete { filtered.mark_physical_complete! } if
1057
- source_has_physical_signal
1058
- source.on_complete do |value, error|
1059
- if error
1060
- filtered.mark_physical_complete! unless source_has_physical_signal
1061
- if source.respond_to?(:status) && source.status == :cancelled
1062
- filtered.cancel!(error)
1063
- else
1064
- filtered.fail(error)
1065
- end
1066
- next
1067
- end
1068
-
1069
- begin
1070
- result = result_filters.inject(value) { |val, filter|
1071
- filter.call(val, tool_name: name, args: args)
1072
- }
1073
- filtered.mark_physical_complete! unless source_has_physical_signal
1074
- filtered.complete(result)
1075
- rescue => filter_error
1076
- filtered.mark_physical_complete! unless source_has_physical_signal
1077
- filtered.fail(filter_error)
1078
- end
1079
- end
1080
- filtered
1081
- end
1082
- end
1083
- end
890
+ binding = ToolBinding.new(
891
+ tool_class, alias_name: self.class.tool_aliases[tool_class]
892
+ )
893
+ binding.prepare(result_filters: _tool_result_filters_for(tool_class))
1084
894
  end
1085
895
  end
1086
896
  end