phronomy 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +14 -7
  5. data/VERIFY.sh +27 -27
  6. data/benchmark/bench_agent_invoke.rb +26 -22
  7. data/benchmark/bench_context_assembler.rb +4 -5
  8. data/benchmark/bench_regression.rb +1 -1
  9. data/docs/architecture/agent-chat-and-state-ownership.md +147 -0
  10. data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
  11. data/docs/architecture/agent-context.md +5 -3
  12. data/docs/architecture/agent-transition-ownership.md +89 -0
  13. data/docs/architecture/before-llm-input.md +6 -0
  14. data/docs/architecture/context-management.md +30 -0
  15. data/docs/architecture/context-preparation-steps.md +90 -0
  16. data/docs/architecture/entry-action-and-team-wording.md +72 -0
  17. data/docs/architecture/execution-metadata-and-values.md +80 -0
  18. data/docs/architecture/generator-verifier-ownership.md +111 -0
  19. data/docs/architecture/multi-agent-handoff.md +8 -2
  20. data/docs/architecture/persistence-refactoring-plan.md +50 -0
  21. data/docs/architecture/persistence.md +29 -6
  22. data/docs/architecture/refactoring-closure.md +334 -0
  23. data/docs/architecture/remaining-refactoring-plan.md +374 -0
  24. data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
  25. data/docs/architecture/tool-schema-recording-gap.md +50 -0
  26. data/docs/architecture/tracing.md +4 -4
  27. data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
  28. data/docs/architecture.md +209 -0
  29. data/docs/async-composition.md +205 -0
  30. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  31. data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
  32. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
  33. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
  34. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
  35. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +5 -0
  36. data/docs/decisions/031-durable-multi-agent-coordination.md +5 -0
  37. data/docs/decisions/032-storage-backend-composition.md +80 -0
  38. data/docs/decisions/033-domain-persistence-ownership.md +79 -0
  39. data/docs/decisions/034-handoff-runner-coordination-ownership.md +76 -0
  40. data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
  41. data/docs/decisions/036-context-contract-ownership.md +86 -0
  42. data/docs/decisions/037-common-definition-ownership.md +62 -0
  43. data/docs/decisions/038-responsibility-based-source-layout.md +131 -0
  44. data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +74 -0
  45. data/docs/decisions/040-configuration-default-composition.md +94 -0
  46. data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
  47. data/docs/decisions/042-feature-owned-execution-state.md +106 -0
  48. data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
  49. data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
  50. data/docs/decisions/045-worker-input-restriction-ownership.md +100 -0
  51. data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
  52. data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
  53. data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
  54. data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
  55. data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
  56. data/docs/decisions/051-execution-outcome-worker-ownership.md +151 -0
  57. data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
  58. data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
  59. data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
  60. data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
  61. data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
  62. data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
  63. data/docs/decisions/058-neutral-storage-primitives.md +78 -0
  64. data/docs/decisions/README.md +35 -6
  65. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
  66. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
  67. data/docs/features.md +21 -12
  68. data/docs/getting-started.md +10 -10
  69. data/docs/migrations/0.15.md +5 -0
  70. data/docs/migrations/durable-semantic-coordination-v2.md +7 -1
  71. data/docs/migrations/handoff-runner-multi-agent.md +35 -0
  72. data/docs/migrations/neutral-storage-spi.md +43 -0
  73. data/docs/migrations/parallel-tool-chat-removal.md +49 -0
  74. data/docs/migrations/shared-state-multi-agent.md +44 -0
  75. data/docs/migrations/storage-backend-composition.md +117 -0
  76. data/docs/migrations/storage-transaction-boundaries.md +74 -0
  77. data/docs/persistence-backends.md +162 -204
  78. data/docs/runtime-and-concurrency.md +205 -60
  79. data/lib/phronomy/agent/api/agent.rb +17 -0
  80. data/lib/phronomy/agent/async_event_api.rb +2 -2
  81. data/lib/phronomy/agent/base.rb +55 -251
  82. data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
  83. data/lib/phronomy/agent/context/capability/base.rb +67 -26
  84. data/lib/phronomy/agent/context/capability/tool_executor.rb +62 -0
  85. data/lib/phronomy/agent/{context_assembler.rb → context_assembly/context_assembler.rb} +143 -88
  86. data/lib/phronomy/agent/{context_importer.rb → context_assembly/context_importer.rb} +2 -2
  87. data/lib/phronomy/agent/{ruby_llm_materializer.rb → context_assembly/ruby_llm_materializer.rb} +4 -7
  88. data/lib/phronomy/agent/context_assembly/runtime_chat_builder.rb +36 -0
  89. data/lib/phronomy/agent/context_assembly/saved_context_reader.rb +53 -0
  90. data/lib/phronomy/agent/context_assembly/state_writer.rb +165 -0
  91. data/lib/phronomy/agent/context_assembly/token_budget_resolver.rb +22 -0
  92. data/lib/phronomy/agent/{context_plan.rb → context_contract/context_plan.rb} +1 -1
  93. data/lib/phronomy/agent/{context_policy_input.rb → context_contract/context_policy_input.rb} +6 -6
  94. data/lib/phronomy/agent/{llm_input_build_context.rb → context_contract/llm_input_build_context.rb} +1 -1
  95. data/lib/phronomy/agent/{llm_input_manifest.rb → context_contract/llm_input_manifest.rb} +20 -20
  96. data/lib/phronomy/agent/{llm_input_patch.rb → context_contract/llm_input_patch.rb} +2 -2
  97. data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
  98. data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +8 -13
  99. data/lib/phronomy/agent/{agent_invocation_session_builder.rb → execution/agent_invocation_session_builder.rb} +20 -96
  100. data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
  101. data/lib/phronomy/agent/execution/dispatch_preparation.rb +305 -0
  102. data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +12 -13
  103. data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +2 -3
  104. data/lib/phronomy/agent/execution/execution_coordinator.rb +1925 -0
  105. data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
  106. data/lib/phronomy/agent/execution/execution_metadata.rb +53 -0
  107. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +344 -0
  108. data/lib/phronomy/agent/execution/execution_registry.rb +459 -0
  109. data/lib/phronomy/agent/execution/execution_session_runner.rb +118 -0
  110. data/lib/phronomy/agent/execution/initial_preparation.rb +421 -0
  111. data/lib/phronomy/agent/execution/invocation_transitions.rb +86 -0
  112. data/lib/phronomy/agent/{phase_machine_builder.rb → execution/phase_machine_builder.rb} +22 -73
  113. data/lib/phronomy/agent/{provider_call_outcome.rb → execution/provider_call_outcome.rb} +9 -9
  114. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
  115. data/lib/phronomy/agent/{handoff_context.rb → handoff/handoff_context.rb} +2 -2
  116. data/lib/phronomy/agent/handoff/handoff_execution_coordinator.rb +15 -0
  117. data/lib/phronomy/agent/handoff/handoff_outcome_committer.rb +131 -0
  118. data/lib/phronomy/agent/{handoff_state.rb → handoff/handoff_state.rb} +1 -1
  119. data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
  120. data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
  121. data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
  122. data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
  123. data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
  124. data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -10
  125. data/lib/phronomy/{agent_already_exists_error.rb → agent/lifecycle_contract/agent_already_exists_error.rb} +2 -0
  126. data/lib/phronomy/{agent_busy_error.rb → agent/lifecycle_contract/agent_busy_error.rb} +2 -0
  127. data/lib/phronomy/{agent_purged_error.rb → agent/lifecycle_contract/agent_purged_error.rb} +2 -0
  128. data/lib/phronomy/agent/lifecycle_contract/handoff_error.rb +7 -0
  129. data/lib/phronomy/{stream_callback_error.rb → agent/lifecycle_contract/stream_callback_error.rb} +2 -0
  130. data/lib/phronomy/agent/persistence/agent_repository.rb +61 -0
  131. data/lib/phronomy/agent/persistence/codec.rb +358 -0
  132. data/lib/phronomy/agent/persistence/execution_repository.rb +108 -0
  133. data/lib/phronomy/agent/persistence/handoff_state_repository.rb +58 -0
  134. data/lib/phronomy/agent/persistence/journal_repository.rb +54 -0
  135. data/lib/phronomy/agent/persistence/queries.rb +61 -0
  136. data/lib/phronomy/agent/persistence/storage_schema.rb +24 -0
  137. data/lib/phronomy/agent/persistence/watermark.rb +27 -0
  138. data/lib/phronomy/agent/recovery/invocation_restorer.rb +132 -0
  139. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +21 -45
  140. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +36 -41
  141. data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +37 -40
  142. data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +9 -13
  143. data/lib/phronomy/agent/recovery/recovery_support.rb +227 -0
  144. data/lib/phronomy/agent/selection/candidate.rb +1 -1
  145. data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
  146. data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
  147. data/lib/phronomy/agent/tool_execution/tool_binding.rb +90 -0
  148. data/lib/phronomy/agent/{tool_call_intercepted.rb → tool_execution/tool_call_intercepted.rb} +2 -2
  149. data/lib/phronomy/agent/{tool_definition_set.rb → tool_execution/tool_definition_set.rb} +9 -4
  150. data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +69 -34
  151. data/lib/phronomy/agent/{tool_invocation_session_builder.rb → tool_execution/tool_invocation_session_builder.rb} +2 -2
  152. data/lib/phronomy/common/configuration_error.rb +7 -0
  153. data/lib/phronomy/common/error.rb +5 -0
  154. data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
  155. data/lib/phronomy/common/values/serializable.rb +32 -0
  156. data/lib/phronomy/{configuration.rb → configuration/configuration.rb} +14 -5
  157. data/lib/phronomy/configuration/global_configuration.rb +26 -0
  158. data/lib/phronomy/content_store/storage_schema.rb +11 -0
  159. data/lib/phronomy/content_store/stored_contents.rb +43 -0
  160. data/lib/phronomy/engine/backpressure_error.rb +7 -0
  161. data/lib/phronomy/{blocking.rb → engine/blocking.rb} +15 -8
  162. data/lib/phronomy/engine/cancellation_error.rb +7 -0
  163. data/lib/phronomy/engine/concurrency/cancellation_token.rb +4 -0
  164. data/lib/phronomy/engine/concurrency/offload_pool.rb +29 -16
  165. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  166. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  167. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  168. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  169. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  170. data/lib/phronomy/engine/concurrency/worker_input_restricted.rb +14 -0
  171. data/lib/phronomy/engine/event_loop.rb +160 -626
  172. data/lib/phronomy/engine/event_loop_reentrancy_error.rb +8 -0
  173. data/lib/phronomy/engine/execution.rb +229 -0
  174. data/lib/phronomy/engine/execution_cancellation_error.rb +14 -0
  175. data/lib/phronomy/engine/execution_receiver.rb +65 -0
  176. data/lib/phronomy/engine/execution_timeout_error.rb +14 -0
  177. data/lib/phronomy/engine/fsm_protocol.rb +14 -0
  178. data/lib/phronomy/engine/fsm_session.rb +38 -31
  179. data/lib/phronomy/{invalid_async_entry_action_error.rb → engine/invalid_async_entry_action_error.rb} +3 -1
  180. data/lib/phronomy/{invalid_async_transition_action_error.rb → engine/invalid_async_transition_action_error.rb} +3 -1
  181. data/lib/phronomy/{invalid_async_workflow_action_error.rb → engine/invalid_async_workflow_action_error.rb} +3 -1
  182. data/lib/phronomy/{invocation_context.rb → engine/invocation_context.rb} +13 -1
  183. data/lib/phronomy/engine/pool_shutdown_error.rb +7 -0
  184. data/lib/phronomy/engine/recursion_limit_error.rb +7 -0
  185. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  186. data/lib/phronomy/engine/runtime.rb +80 -84
  187. data/lib/phronomy/engine/runtime_shutdown_error.rb +7 -0
  188. data/lib/phronomy/engine/runtime_shutdown_reentrancy_error.rb +7 -0
  189. data/lib/phronomy/engine/scheduler_reentrancy_error.rb +9 -0
  190. data/lib/phronomy/engine/{task.rb → task_result.rb} +101 -42
  191. data/lib/phronomy/engine/timeout_error.rb +7 -0
  192. data/lib/phronomy/filter/contract/filter_block_error.rb +14 -0
  193. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
  194. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
  195. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
  196. data/lib/phronomy/generation/generator_verifier.rb +118 -0
  197. data/lib/phronomy/generation/low_confidence_error.rb +14 -0
  198. data/lib/phronomy/llm_adapter/base.rb +2 -2
  199. data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
  200. data/lib/phronomy/llm_contract/authentication_error.rb +7 -0
  201. data/lib/phronomy/{context_budget_exceeded_error.rb → llm_contract/context_budget_exceeded_error.rb} +2 -0
  202. data/lib/phronomy/llm_contract/context_length_error.rb +7 -0
  203. data/lib/phronomy/llm_contract/rate_limit_error.rb +7 -0
  204. data/lib/phronomy/{token_usage.rb → llm_contract/token_usage.rb} +2 -2
  205. data/lib/phronomy/llm_contract/transport_error.rb +7 -0
  206. data/lib/phronomy/multi_agent/admission_registry.rb +22 -2
  207. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +8 -8
  208. data/lib/phronomy/{agent → multi_agent}/handoff_runner.rb +18 -17
  209. data/lib/phronomy/multi_agent/orchestrator.rb +36 -68
  210. data/lib/phronomy/multi_agent/persistence/codec.rb +55 -0
  211. data/lib/phronomy/multi_agent/persistence/queries.rb +30 -0
  212. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +108 -0
  213. data/lib/phronomy/multi_agent/persistence/team_repository.rb +61 -0
  214. data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
  215. data/lib/phronomy/multi_agent/storage_contract/team_storage_schema.rb +15 -0
  216. data/lib/phronomy/multi_agent/team_coordinator.rb +21 -18
  217. data/lib/phronomy/multi_agent/team_execution.rb +1 -1
  218. data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +14 -4
  219. data/lib/phronomy/multi_agent/team_root.rb +1 -1
  220. data/lib/phronomy/output_parser/contract/parse_error.rb +7 -0
  221. data/lib/phronomy/persistence/api/persistence.rb +140 -0
  222. data/lib/phronomy/persistence/migration/initial_format_migration.rb +19 -19
  223. data/lib/phronomy/persistence_composition/repositories.rb +77 -0
  224. data/lib/phronomy/persistence_composition/storage_schema.rb +24 -0
  225. data/lib/phronomy/{execution_rehydration_required_error.rb → recovery/execution_rehydration_required_error.rb} +2 -0
  226. data/lib/phronomy/{recovery.rb → recovery/recovery.rb} +1 -1
  227. data/lib/phronomy/runtime_composition/agent_defaults.rb +7 -0
  228. data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
  229. data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
  230. data/lib/phronomy/storage/backend.rb +101 -0
  231. data/lib/phronomy/storage/backends/in_memory.rb +157 -0
  232. data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
  233. data/lib/phronomy/storage/blobs.rb +31 -0
  234. data/lib/phronomy/storage/condition.rb +25 -0
  235. data/lib/phronomy/storage/condition_failed_error.rb +16 -0
  236. data/lib/phronomy/storage/conflict_error.rb +9 -0
  237. data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
  238. data/lib/phronomy/storage/entry.rb +33 -0
  239. data/lib/phronomy/storage/guard_ref.rb +13 -0
  240. data/lib/phronomy/storage/not_found_error.rb +9 -0
  241. data/lib/phronomy/storage/record_codec.rb +177 -0
  242. data/lib/phronomy/storage/records.rb +61 -0
  243. data/lib/phronomy/storage/resource.rb +126 -0
  244. data/lib/phronomy/storage/scope.rb +25 -0
  245. data/lib/phronomy/storage/serialization_error.rb +9 -0
  246. data/lib/phronomy/storage/streams.rb +44 -0
  247. data/lib/phronomy/storage/transaction_error.rb +10 -0
  248. data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
  249. data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
  250. data/lib/phronomy/storage/validation.rb +53 -0
  251. data/lib/phronomy/storage/view.rb +131 -0
  252. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  253. data/lib/phronomy/testing/fake_clock.rb +13 -9
  254. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
  255. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +4 -4
  256. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +10 -7
  257. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +2 -2
  258. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +6 -6
  259. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +6 -6
  260. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +8 -8
  261. data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
  262. data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +123 -0
  263. data/lib/phronomy/testing/persistence_contract.rb +4 -0
  264. data/lib/phronomy/tool/contract/tool_error.rb +7 -0
  265. data/lib/phronomy/tools/agent.rb +6 -6
  266. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  267. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  268. data/lib/phronomy/version.rb +1 -1
  269. data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
  270. data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +4 -0
  271. data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
  272. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +188 -0
  273. data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +126 -75
  274. data/lib/phronomy/workflow/execution/workflow_terminal_policy.rb +40 -0
  275. data/lib/phronomy/workflow/persistence/codec.rb +153 -0
  276. data/lib/phronomy/workflow/persistence/state_repository.rb +57 -0
  277. data/lib/phronomy/workflow/phase_machine_builder.rb +8 -8
  278. data/lib/phronomy/workflow/storage_contract/workflow_storage_schema.rb +9 -0
  279. data/lib/phronomy.rb +52 -94
  280. data/scripts/api_snapshot.rb +4 -2
  281. data/scripts/storage_spi_snapshot.rb +36 -0
  282. data/sig/phronomy/agent.rbs +4 -5
  283. data/sig/phronomy/execution_receiver.rbs +34 -0
  284. data/sig/phronomy/extensions.rbs +5 -5
  285. data/sig/phronomy/handoff.rbs +4 -2
  286. data/sig/phronomy/multi_agent.rbs +19 -3
  287. data/sig/phronomy/persistence.rbs +7 -91
  288. data/sig/phronomy/runtime.rbs +34 -7
  289. data/sig/phronomy/storage.rbs +174 -0
  290. data/sig/phronomy/tool.rbs +11 -2
  291. data/sig/phronomy/workflow.rbs +1 -1
  292. metadata +236 -99
  293. data/lib/phronomy/agent/execution_coordinator.rb +0 -3138
  294. data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
  295. data/lib/phronomy/agent/recovery_support.rb +0 -504
  296. data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
  297. data/lib/phronomy/agent/tool_executor.rb +0 -55
  298. data/lib/phronomy/generator_verifier.rb +0 -369
  299. data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
  300. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  301. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
  302. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
  303. data/lib/phronomy/persistence/durable_codec.rb +0 -706
  304. data/lib/phronomy/persistence/in_memory.rb +0 -690
  305. data/lib/phronomy/persistence/repository_facades.rb +0 -535
  306. data/lib/phronomy/persistence.rb +0 -276
  307. data/lib/phronomy/ruby_llm_patches.rb +0 -24
  308. data/lib/phronomy/workflow_recovery.rb +0 -123
  309. /data/lib/phronomy/agent/{context_candidate_resolver.rb → context_assembly/context_candidate_resolver.rb} +0 -0
  310. /data/lib/phronomy/agent/{context_policy_input_builder.rb → context_assembly/context_policy_input_builder.rb} +0 -0
  311. /data/lib/phronomy/agent/{context_plan_validator.rb → context_contract/context_plan_validator.rb} +0 -0
  312. /data/lib/phronomy/agent/{context_policy.rb → context_contract/context_policy.rb} +0 -0
  313. /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
  314. /data/lib/phronomy/agent/{handoff.rb → handoff/handoff.rb} +0 -0
  315. /data/lib/phronomy/agent/{handoff_capability_factory.rb → handoff/handoff_capability_factory.rb} +0 -0
  316. /data/lib/phronomy/agent/{handoff_policy.rb → handoff/handoff_policy.rb} +0 -0
  317. /data/lib/phronomy/agent/{handoff_projection.rb → handoff/handoff_projection.rb} +0 -0
  318. /data/lib/phronomy/agent/{handoff_request.rb → handoff/handoff_request.rb} +0 -0
  319. /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
  320. /data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +0 -0
  321. /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
  322. /data/lib/phronomy/{metrics.rb → engine/metrics.rb} +0 -0
  323. /data/lib/phronomy/{runnable.rb → engine/runnable.rb} +0 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ module Context
6
+ module Capability
7
+ # Routes Tool work according to the Tool execution contract.
8
+ #
9
+ # Both execution modes return {Phronomy::TaskResult}; only the execution mechanism
10
+ # differs.
11
+ #
12
+ # :cooperative Tool calls execute inline and must return quickly. call_async
13
+ # wraps their result in an already-settled TaskResult and never consumes an
14
+ # OffloadPool worker.
15
+ #
16
+ # :offloaded Tool calls route synchronous work through OffloadPool, whose
17
+ # caller-facing completion handle is also a TaskResult. Phronomy does not distinguish
18
+ # whether the reason is blocking I/O, CPU-bound work, or another long
19
+ # synchronous operation.
20
+ #
21
+ # @api private
22
+ module ToolExecutor
23
+ # @api private
24
+ def self.call_async(
25
+ tool:,
26
+ args:,
27
+ cancellation_token: nil,
28
+ config: {},
29
+ runtime: nil,
30
+ on_full: :raise
31
+ )
32
+ mode = tool.class.execution_mode
33
+
34
+ case mode
35
+ when :cooperative
36
+ task = Phronomy::TaskResult.deferred(name: "tool-#{tool.name}")
37
+ begin
38
+ task.complete(
39
+ tool.call(args, cancellation_token: cancellation_token)
40
+ )
41
+ rescue => error
42
+ task.fail(error)
43
+ end
44
+ task
45
+ when :offloaded
46
+ runtime ||= Phronomy::Runtime.instance
47
+ runtime.offload.submit(
48
+ cancellation_token: cancellation_token,
49
+ on_full: on_full
50
+ ) do
51
+ tool.call(args, cancellation_token: cancellation_token)
52
+ end
53
+ else
54
+ raise Phronomy::ConfigurationError,
55
+ "unknown Tool execution_mode: #{mode.inspect}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,7 +3,7 @@
3
3
  module Phronomy
4
4
  module Agent
5
5
  class ContextAssembler
6
- ASSEMBLY_POLICY_VERSION = 8
6
+ ASSEMBLY_POLICY_VERSION = 9
7
7
  SEGMENT_ORIGIN_METADATA_KEY = "phronomy_origin"
8
8
  POLICY_ORIGIN_METADATA_KEY = "context_policy_origin"
9
9
  POLICY_ITEM_ID_METADATA_KEY = "context_policy_item_id"
@@ -59,81 +59,33 @@ module Phronomy
59
59
  excluded = [execution.metadata["current_input_record_id"]].compact
60
60
  handoff_context = config[:phronomy_handoff_context]
61
61
 
62
- instructions = []
63
- unless system_text.to_s.empty?
64
- instructions << ContextPolicyInput::InstructionItem.new(
65
- id: "instruction:agent:#{execution.execution_id}:1",
66
- kind: :instruction,
67
- role: :system,
68
- content: system_text.to_s,
69
- content_format: :text,
70
- estimated_tokens: estimate_value(system_text.to_s),
71
- required: true,
72
- provenance: ContextPolicyInput::Provenance.new(origin: :agent_configuration),
73
- metadata: {}
74
- )
75
- end
76
- if handoff_context
77
- instructions << ContextPolicyInput::InstructionItem.new(
78
- id: "instruction:handoff:#{execution.execution_id}:1",
79
- kind: :handoff_responsibility,
80
- role: :user,
81
- content: handoff_context.responsibility.to_s,
82
- content_format: :text,
83
- estimated_tokens: estimate_value(handoff_context.responsibility.to_s),
84
- required: true,
85
- provenance: ContextPolicyInput::Provenance.new(origin: :handoff_context),
86
- metadata: {SEGMENT_ORIGIN_METADATA_KEY => HANDOFF_CONTEXT_ORIGIN}
87
- )
88
- end
62
+ instructions = initial_instruction_items(
63
+ system_text,
64
+ handoff_context,
65
+ execution: execution
66
+ )
89
67
 
90
- generation = agent_root.transcript_generation
91
- eligible_working = Array(execution.working_records).select do |record|
92
- record.context_candidate && record.context_generation == generation
93
- end
94
- candidates = @candidate_resolver.resolve(
95
- prior_records: projection.context_records,
96
- working_records: eligible_working,
68
+ candidates = resolve_record_candidates(
69
+ projection,
70
+ agent_root: agent_root,
71
+ execution: execution,
97
72
  excluded_record_ids: excluded
98
73
  )
99
- candidates = merge_hook_candidates(
74
+ candidates = merge_context_candidates(
100
75
  candidates,
101
76
  hook_candidates,
77
+ handoff_context,
102
78
  agent_root: agent_root,
103
79
  execution: execution,
104
80
  call_sequence: 1
105
81
  )
106
- candidates = merge_handoff_candidates(
107
- candidates,
108
- handoff_context,
82
+ current_input = current_input_item(
83
+ current_input_content,
84
+ input_ref: input_ref,
85
+ candidates: candidates,
86
+ agent_root: agent_root,
109
87
  execution: execution
110
88
  )
111
- next_sequence = Array(candidates).filter_map(&:sequence).max.to_i + 1
112
- current_input = ContextPolicyInput::ConversationItem.new(
113
- id: "current-input:#{execution.execution_id}",
114
- kind: :current_input,
115
- role: :user,
116
- content: current_input_content,
117
- content_format: :text,
118
- sequence: next_sequence,
119
- estimated_tokens: estimate_value(current_input_content),
120
- required: true,
121
- provenance: ContextPolicyInput::Provenance.new(
122
- origin: :working,
123
- content_ref: input_ref,
124
- record_id: execution.metadata["current_input_record_id"],
125
- agent_id: agent_root.agent_id,
126
- execution_id: execution.execution_id
127
- ),
128
- tool_call_id: nil,
129
- tool_call_ids: [],
130
- delivery: :ask_argument,
131
- metadata: {
132
- "source_agent_id" => agent_root.agent_id,
133
- "source_execution_id" => execution.execution_id,
134
- "handoff_policy_category" => "current_request"
135
- }
136
- )
137
89
 
138
90
  prepare(
139
91
  agent_root: agent_root,
@@ -168,33 +120,23 @@ module Phronomy
168
120
  additional_tools: handoff_tool_classes(config)
169
121
  )
170
122
 
171
- instructions = base_manifest.segments.filter_map do |segment|
172
- next unless retained_base_instruction?(segment)
173
- instruction_item_from_manifest(segment)
174
- end
123
+ instructions = retained_instruction_items(base_manifest)
175
124
 
176
- generation = agent_root.transcript_generation
177
- eligible_working = Array(execution.working_records).select do |record|
178
- record.context_candidate && record.context_generation == generation
179
- end
180
- candidates = @candidate_resolver.resolve(
181
- prior_records: projection.context_records,
182
- working_records: eligible_working,
125
+ candidates = resolve_record_candidates(
126
+ projection,
127
+ agent_root: agent_root,
128
+ execution: execution,
183
129
  excluded_record_ids: []
184
130
  )
185
131
  call_sequence = execution.llm_calls.length + 1
186
- candidates = merge_hook_candidates(
132
+ candidates = merge_context_candidates(
187
133
  candidates,
188
134
  hook_candidates,
135
+ handoff_context,
189
136
  agent_root: agent_root,
190
137
  execution: execution,
191
138
  call_sequence: call_sequence
192
139
  )
193
- candidates = merge_handoff_candidates(
194
- candidates,
195
- handoff_context,
196
- execution: execution
197
- )
198
140
 
199
141
  prepare(
200
142
  agent_root: agent_root,
@@ -274,6 +216,111 @@ module Phronomy
274
216
 
275
217
  private
276
218
 
219
+ def initial_instruction_items(system_text, handoff_context, execution:)
220
+ instructions = []
221
+ unless system_text.to_s.empty?
222
+ instructions << agent_instruction_item(system_text, execution: execution)
223
+ end
224
+ if handoff_context
225
+ instructions << handoff_instruction_item(handoff_context, execution: execution)
226
+ end
227
+ instructions
228
+ end
229
+
230
+ def agent_instruction_item(system_text, execution:)
231
+ ContextPolicyInput::InstructionItem.new(
232
+ id: "instruction:agent:#{execution.execution_id}:1",
233
+ kind: :instruction,
234
+ role: :system,
235
+ content: system_text.to_s,
236
+ content_format: :text,
237
+ estimated_tokens: estimate_value(system_text.to_s),
238
+ required: true,
239
+ provenance: ContextPolicyInput::Provenance.new(origin: :agent_configuration),
240
+ metadata: {}
241
+ )
242
+ end
243
+
244
+ def handoff_instruction_item(handoff_context, execution:)
245
+ ContextPolicyInput::InstructionItem.new(
246
+ id: "instruction:handoff:#{execution.execution_id}:1",
247
+ kind: :handoff_responsibility,
248
+ role: :user,
249
+ content: handoff_context.responsibility.to_s,
250
+ content_format: :text,
251
+ estimated_tokens: estimate_value(handoff_context.responsibility.to_s),
252
+ required: true,
253
+ provenance: ContextPolicyInput::Provenance.new(origin: :handoff_context),
254
+ metadata: {SEGMENT_ORIGIN_METADATA_KEY => HANDOFF_CONTEXT_ORIGIN}
255
+ )
256
+ end
257
+
258
+ def retained_instruction_items(base_manifest)
259
+ base_manifest.segments.filter_map do |segment|
260
+ next unless retained_base_instruction?(segment)
261
+ instruction_item_from_manifest(segment)
262
+ end
263
+ end
264
+
265
+ def resolve_record_candidates(projection, agent_root:, execution:, excluded_record_ids:)
266
+ generation = agent_root.transcript_generation
267
+ eligible_working = Array(execution.working_records).select do |record|
268
+ record.context_candidate && record.context_generation == generation
269
+ end
270
+ @candidate_resolver.resolve(
271
+ prior_records: projection.context_records,
272
+ working_records: eligible_working,
273
+ excluded_record_ids: excluded_record_ids
274
+ )
275
+ end
276
+
277
+ def merge_context_candidates(
278
+ candidates, hook_candidates, handoff_context,
279
+ agent_root:, execution:, call_sequence:
280
+ )
281
+ candidates = merge_hook_candidates(
282
+ candidates,
283
+ hook_candidates,
284
+ agent_root: agent_root,
285
+ execution: execution,
286
+ call_sequence: call_sequence
287
+ )
288
+ merge_handoff_candidates(
289
+ candidates,
290
+ handoff_context,
291
+ execution: execution
292
+ )
293
+ end
294
+
295
+ def current_input_item(content, input_ref:, candidates:, agent_root:, execution:)
296
+ next_sequence = Array(candidates).filter_map(&:sequence).max.to_i + 1
297
+ ContextPolicyInput::ConversationItem.new(
298
+ id: "current-input:#{execution.execution_id}",
299
+ kind: :current_input,
300
+ role: :user,
301
+ content: content,
302
+ content_format: :text,
303
+ sequence: next_sequence,
304
+ estimated_tokens: estimate_value(content),
305
+ required: true,
306
+ provenance: ContextPolicyInput::Provenance.new(
307
+ origin: :working,
308
+ content_ref: input_ref,
309
+ record_id: execution.metadata["current_input_record_id"],
310
+ agent_id: agent_root.agent_id,
311
+ execution_id: execution.execution_id
312
+ ),
313
+ tool_call_id: nil,
314
+ tool_call_ids: [],
315
+ delivery: :ask_argument,
316
+ metadata: {
317
+ "source_agent_id" => agent_root.agent_id,
318
+ "source_execution_id" => execution.execution_id,
319
+ "handoff_policy_category" => "current_request"
320
+ }
321
+ )
322
+ end
323
+
277
324
  def prepare(
278
325
  agent_root:,
279
326
  execution:,
@@ -286,7 +333,7 @@ module Phronomy
286
333
  tools:,
287
334
  current_input:
288
335
  )
289
- token_budget = TokenBudgetResolver.new(agent: @agent).resolve(model_config)
336
+ token_budget = TokenBudgetResolver.new.resolve(model_config)
290
337
  policy_input = @input_builder.build(
291
338
  agent_id: agent_root.agent_id,
292
339
  execution_id: execution.execution_id,
@@ -438,7 +485,7 @@ module Phronomy
438
485
 
439
486
  def sanitized_item_metadata(item)
440
487
  raw = item.metadata.to_h.transform_keys(&:to_s)
441
- # :working origin is Phronomy-controlled (from perform_initial_preparation),
488
+ # :working origin is Phronomy-controlled (from InitialPreparation),
442
489
  # so handoff routing metadata on working records is equally trusted.
443
490
  trusted_handoff = if item.provenance.origin == :handoff || item.provenance.origin == :working
444
491
  raw.slice(*TRUSTED_HANDOFF_METADATA_KEYS)
@@ -474,15 +521,23 @@ module Phronomy
474
521
  "provider" => @agent.class.provider&.to_s,
475
522
  "temperature" => @agent.class.temperature,
476
523
  "max_output_tokens" => @agent.class.max_output_tokens,
477
- "context_window" => @agent.class.context_window,
478
- "cache_instructions" => !!@agent.class.cache_instructions,
479
- "parallel_tool_execution" => !!Phronomy.configuration.parallel_tool_execution
524
+ "cache_instructions" => !!@agent.class.cache_instructions
480
525
  }.compact
481
526
  end
482
527
 
483
528
  def apply_model_config_patch(base, patch)
484
529
  return base unless patch
485
- base.merge(patch.to_h.transform_keys(&:to_s)).compact
530
+ values = patch.to_h.transform_keys(&:to_s)
531
+ if values.key?("context_window")
532
+ raise Phronomy::ConfigurationError,
533
+ "context_window is model metadata; configure the RubyLLM model registry"
534
+ end
535
+ if values.key?("max_output_tokens") && !values["max_output_tokens"].nil?
536
+ cap = Integer(values["max_output_tokens"])
537
+ raise ArgumentError, "max_output_tokens must be positive" unless cap.positive?
538
+ values["max_output_tokens"] = cap
539
+ end
540
+ base.merge(values).compact
486
541
  end
487
542
 
488
543
  def normalize_candidates(candidates)
@@ -8,8 +8,8 @@ module Phronomy
8
8
  ) do
9
9
  def initialize(**values)
10
10
  super(**values.merge(
11
- content: Immutable.copy(values[:content]),
12
- metadata: Immutable.copy(values[:metadata] || {})
11
+ content: Phronomy::Values::Immutable.copy(values[:content]),
12
+ metadata: Phronomy::Values::Immutable.copy(values[:metadata] || {})
13
13
  ))
14
14
  freeze
15
15
  end
@@ -43,7 +43,7 @@ module Phronomy
43
43
  messages: materialize_message_segments(message_segments).freeze,
44
44
  tool_classes: tool_set.runtime_tools,
45
45
  ask_message: ask_message,
46
- model_config: Immutable.copy(model_config),
46
+ model_config: Phronomy::Values::Immutable.copy(model_config),
47
47
  manifest: manifest,
48
48
  manifest_ref: manifest_ref
49
49
  )
@@ -151,22 +151,19 @@ module Phronomy
151
151
  end
152
152
 
153
153
  tool_calls = materialize_tool_calls(payload["tool_calls"])
154
- message = RubyLLM::Message.new(
154
+ RubyLLM::Message.new(
155
155
  role: role,
156
156
  content: initial_content_for(role, payload.fetch("content", nil), tool_calls),
157
157
  tool_calls: tool_calls.empty? ? nil : tool_calls,
158
158
  tool_call_id: payload["tool_call_id"],
159
- model_id: payload["model_id"]
159
+ model: payload["model_id"]
160
160
  )
161
-
162
- message.content = payload["content"] if payload.key?("content")
163
- message
164
161
  end
165
162
 
166
163
  def initial_content_for(role, content, tool_calls)
167
164
  return "" if role == :assistant && content.nil? && !tool_calls.empty?
168
165
 
169
- content.nil? ? "" : content
166
+ (content.is_a?(Hash) || content.is_a?(Array)) ? JSON.generate(content) : content
170
167
  end
171
168
 
172
169
  def materialize_tool_calls(payloads)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Constructs provider chat objects from already selected model settings.
6
+ # Agent hooks retain projection installation and invocation-specific Tools.
7
+ # @api private
8
+ class RuntimeChatBuilder
9
+ def self.build(config)
10
+ chat = RubyLLM.chat(**chat_options(config))
11
+ chat.with_temperature(config["temperature"]) if config["temperature"]
12
+ if config["max_output_tokens"]
13
+ chat.with_max_output_tokens(config["max_output_tokens"])
14
+ end
15
+ chat
16
+ end
17
+
18
+ def self.apply_instructions(chat, text, cache:, provider:)
19
+ chat.with_instructions(text, cache_until_here: cache)
20
+ end
21
+
22
+ def self.chat_options(config)
23
+ opts = {}
24
+ model = config["model"]
25
+ opts[:model] = model if model
26
+ provider = config["provider"]
27
+ if provider
28
+ opts[:provider] = provider.to_sym
29
+ opts[:assume_model_exists] = true
30
+ end
31
+ opts
32
+ end
33
+ private_class_method :chat_options
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Reads saved manifests and Provider output for continuation preparation.
6
+ # Callers perform these reads before applying live state on EventLoop.
7
+ # @api private
8
+ module SavedContextReader
9
+ module_function
10
+
11
+ def manifest_from_ref(agent, ref)
12
+ raw = agent.persistence.contents.fetch_json(ref)
13
+ Phronomy::Agent::LLMInputManifest.from_h(raw)
14
+ end
15
+
16
+ def materialize_projection(agent, manifest_ref)
17
+ manifest = manifest_from_ref(agent, manifest_ref)
18
+ projection = Phronomy::Agent::RubyLLMMaterializer.new(
19
+ agent: agent,
20
+ persistence: agent.persistence
21
+ ).materialize(manifest: manifest, manifest_ref: manifest_ref)
22
+ [manifest, projection]
23
+ end
24
+
25
+ def latest_assistant_record(execution, llm_call_id: nil)
26
+ Array(execution.working_records).reverse.find do |record|
27
+ record.kind.to_sym == :assistant_message &&
28
+ (llm_call_id.nil? || record.llm_call_id.to_s == llm_call_id.to_s)
29
+ end
30
+ end
31
+
32
+ def provider_output_and_usage(agent, execution)
33
+ assistant = latest_assistant_record(execution)
34
+ payload = assistant ? agent.persistence.contents.fetch_json(assistant.content_ref) : {}
35
+ output = payload["content"]
36
+
37
+ call = execution.llm_calls.last
38
+ usage_hash = if call&.usage_ref
39
+ agent.persistence.contents.fetch_json(call.usage_ref)
40
+ else
41
+ {}
42
+ end
43
+ usage = Phronomy::TokenUsage.new(
44
+ input: usage_hash["input"] || usage_hash[:input] || usage_hash["input_tokens"] || usage_hash[:input_tokens],
45
+ output: usage_hash["output"] || usage_hash[:output] || usage_hash["output_tokens"] || usage_hash[:output_tokens],
46
+ cached: usage_hash["cached"] || usage_hash[:cached] || usage_hash["cache_read_tokens"] || usage_hash[:cache_read_tokens] || usage_hash["cached_tokens"] || usage_hash[:cached_tokens],
47
+ cache_creation: usage_hash["cache_creation"] || usage_hash[:cache_creation] || usage_hash["cache_write_tokens"] || usage_hash[:cache_write_tokens] || usage_hash["cache_creation_tokens"] || usage_hash[:cache_creation_tokens]
48
+ )
49
+ [output, usage]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Persists initial state and explicit idle-Agent context/lifecycle changes.
6
+ # Base owns live-state publication; execution commits have separate owners.
7
+ # @api private
8
+ class StateWriter
9
+ Update = Data.define(:root, :records)
10
+ private_constant :Update
11
+
12
+ def initialize(persistence:, agent_id:)
13
+ @persistence = persistence
14
+ @agent_id = agent_id
15
+ end
16
+
17
+ def create_root(definition:, context:, knowledge:, metadata:)
18
+ root = Agent::AgentRoot.create(
19
+ agent_id: agent_id,
20
+ agent_definition_id: definition.fetch(:id),
21
+ agent_definition_version: definition.fetch(:version),
22
+ metadata: metadata
23
+ )
24
+ persistence.transaction do |tx|
25
+ tx.agents.create(root)
26
+ records = initial_context_records(tx: tx, root: root, context: context)
27
+ records.concat(initial_knowledge_records(tx: tx, root: root, knowledge: knowledge))
28
+ unless records.empty?
29
+ appended = tx.journals.append(agent_id, expected_position: 0, records: records)
30
+ root = root.with(
31
+ agent_revision: 1,
32
+ context_revision: 1,
33
+ journal_position: appended.length
34
+ )
35
+ tx.agents.save(agent_id, expected_revision: 0, root: root)
36
+ end
37
+ end
38
+ root
39
+ end
40
+
41
+ def add_knowledge(root:, content:, metadata:)
42
+ current = root
43
+ next_root = nil
44
+ appended = nil
45
+ persistence.transaction do |tx|
46
+ tx.executions.assert_idle!(agent_id)
47
+ record = build_knowledge_record(
48
+ tx: tx,
49
+ root: current,
50
+ content: content,
51
+ metadata: metadata
52
+ )
53
+ appended = tx.journals.append(
54
+ agent_id,
55
+ expected_position: current.journal_position,
56
+ records: [record]
57
+ )
58
+ next_root = current.with(
59
+ agent_revision: current.agent_revision + 1,
60
+ context_revision: current.context_revision + 1,
61
+ journal_position: current.journal_position + appended.length
62
+ )
63
+ tx.agents.save(
64
+ agent_id,
65
+ expected_revision: current.agent_revision,
66
+ root: next_root
67
+ )
68
+ end
69
+ Update.new(root: next_root, records: appended)
70
+ end
71
+
72
+ def mutate_context(root:, kind:, context_affecting:)
73
+ current = root
74
+ next_root = nil
75
+ appended = nil
76
+ persistence.transaction do |tx|
77
+ tx.executions.assert_idle!(agent_id)
78
+ record = Agent::JournalRecord.new(
79
+ agent_id: agent_id,
80
+ kind: kind,
81
+ channel: :state,
82
+ context_generation: current.transcript_generation,
83
+ context_candidate: false
84
+ )
85
+ appended = tx.journals.append(
86
+ agent_id,
87
+ expected_position: current.journal_position,
88
+ records: [record]
89
+ )
90
+ proposed = yield(current)
91
+ next_root = proposed.with(
92
+ journal_position: current.journal_position + appended.length,
93
+ context_revision: context_affecting ?
94
+ yield_context_revision(current, proposed) : current.context_revision
95
+ )
96
+ tx.agents.save(
97
+ agent_id,
98
+ expected_revision: current.agent_revision,
99
+ root: next_root
100
+ )
101
+ end
102
+ Update.new(root: next_root, records: appended)
103
+ end
104
+
105
+ private
106
+
107
+ attr_reader :persistence, :agent_id
108
+
109
+ def initial_context_records(tx:, root:, context:)
110
+ return [] unless context
111
+
112
+ imported = context.respond_to?(:records) ? context :
113
+ Agent::ContextImporter.import_messages(context)
114
+ imported.records.map do |record|
115
+ content_ref = case record.content_format
116
+ when :text then tx.contents.put_text(record.content)
117
+ when :json then tx.contents.put_json(record.content)
118
+ else
119
+ raise ArgumentError,
120
+ "unsupported imported content format: #{record.content_format.inspect}"
121
+ end
122
+ Agent::JournalRecord.new(
123
+ agent_id: agent_id,
124
+ kind: record.kind,
125
+ channel: record.channel,
126
+ role: record.role,
127
+ content_ref: content_ref,
128
+ context_generation: root.transcript_generation,
129
+ context_candidate: true,
130
+ metadata: record.metadata
131
+ )
132
+ end
133
+ end
134
+
135
+ def initial_knowledge_records(tx:, root:, knowledge:)
136
+ Array(knowledge).map do |content|
137
+ build_knowledge_record(
138
+ tx: tx,
139
+ root: root,
140
+ content: content,
141
+ metadata: {}
142
+ )
143
+ end
144
+ end
145
+
146
+ def build_knowledge_record(tx:, root:, content:, metadata:)
147
+ Agent::JournalRecord.new(
148
+ agent_id: agent_id,
149
+ kind: :knowledge,
150
+ channel: :context,
151
+ role: :user,
152
+ content_ref: tx.contents.put_text(String(content)),
153
+ context_generation: root.transcript_generation,
154
+ context_candidate: true,
155
+ metadata: metadata || {}
156
+ )
157
+ end
158
+
159
+ def yield_context_revision(current, proposed)
160
+ (proposed.context_revision == current.context_revision) ?
161
+ current.context_revision + 1 : proposed.context_revision
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Resolves the input limit from RubyLLM's authoritative model registry.
6
+ class TokenBudgetResolver
7
+ def resolve(model_config)
8
+ config = model_config.to_h.transform_keys(&:to_s)
9
+ model_name = config["model"]
10
+ return nil unless model_name
11
+
12
+ model = RubyLLM.models.find(model_name, provider: config["provider"])
13
+ limit = model&.context_window
14
+ return nil unless limit.is_a?(Integer) && limit.positive?
15
+
16
+ Phronomy::LlmContextWindow::TokenBudget.new(max_input_tokens: limit)
17
+ rescue RubyLLM::ModelNotFoundError
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end