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
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # This convenience API composes a fresh ephemeral Persistence with an Agent.
4
+ # The application entry loads the method definition; Agent execution does not
5
+ # delegate to or require this higher-level composition implementation.
3
6
  module Phronomy
4
7
  module Agent
5
- StreamEvent = Data.define(:type, :payload)
6
-
7
8
  def self.run_once(
8
9
  definition:,
9
10
  input:,
@@ -17,7 +18,7 @@ module Phronomy
17
18
  raise ArgumentError, "Provide either on_event: or a block, not both"
18
19
  end
19
20
 
20
- persistence = Phronomy::Persistence::InMemory.new
21
+ persistence = Phronomy::Persistence.in_memory
21
22
  agent = definition.create(
22
23
  context: context,
23
24
  knowledge: knowledge,
@@ -29,13 +30,3 @@ module Phronomy
29
30
  end
30
31
  end
31
32
  end
32
-
33
- require_relative "agent/async_event_api"
34
-
35
- unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
36
- Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
37
- end
38
-
39
- # Durable Agent Recovery and Agent-incarnation event binding.
40
- require_relative "agent/recovery_support"
41
- require_relative "agent/recovery_coordinator"
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../../engine/concurrency/worker_input_restricted"
4
+
3
5
  module Phronomy
4
6
  module Agent
5
7
  module Context
6
8
  module Capability
7
9
  # Base class extending RubyLLM::Tool with Phronomy-specific DSL.
8
10
  class Base < RubyLLM::Tool
11
+ include Phronomy::Concurrency::WorkerInputRestricted
12
+
9
13
  class << self
10
14
  # @api public
11
15
  def tool_name(value = nil)
12
- return @tool_name if value.nil?
16
+ return @tool_name || super() if value.nil?
13
17
 
14
18
  @tool_name = value.to_s
15
19
  end
@@ -30,41 +34,62 @@ module Phronomy
30
34
  end
31
35
  alias_method :desc, :description
32
36
 
33
- # RubyLLM stores declared parameters in a class-instance variable.
34
- # Copy the parent's registry on first access so child classes inherit
35
- # existing parameters while remaining free to add their own.
37
+ # Phronomy declarations inherit through anonymous Tool decorators.
38
+ # RubyLLM owns schema construction and provider request rendering.
39
+ # @api public
40
+ def declared_parameters
41
+ return @declared_parameters if instance_variable_defined?(:@declared_parameters)
42
+
43
+ @declared_parameters = superclass.declared_parameters.dup
44
+ end
45
+
46
+ # Keep Phronomy's parameter reader and support RubyLLM's schema DSL.
36
47
  # @api public
37
- def parameters
38
- return @parameters if instance_variable_defined?(:@parameters)
48
+ def parameters(schema = :__phronomy_read__, &block)
49
+ return declared_parameters if schema == :__phronomy_read__ && !block
39
50
 
40
- parent = superclass.respond_to?(:parameters) ? superclass.parameters : {}
41
- @parameters = parent.dup
51
+ super((schema == :__phronomy_read__) ? nil : schema, &block)
52
+ end
53
+
54
+ # @api public
55
+ def parameters_schema_definition
56
+ return @parameters_schema_definition if instance_variable_defined?(:@parameters_schema_definition)
57
+
58
+ superclass.parameters_schema_definition
42
59
  end
43
60
 
44
- # RubyLLM stores an explicit .params schema definition in a
45
- # class-instance variable. Readers must fall back to the parent.
46
61
  # @api public
47
62
  def params_schema_definition
48
- return @params_schema_definition if instance_variable_defined?(:@params_schema_definition)
49
- return superclass.params_schema_definition if superclass.respond_to?(:params_schema_definition)
63
+ parameters_schema_definition
64
+ end
50
65
 
51
- nil
66
+ # @api public
67
+ def params(schema = nil, &block)
68
+ parameters(schema, &block)
69
+ end
70
+
71
+ # @api public
72
+ def provider_options(options = :__phronomy_read__)
73
+ return super unless options == :__phronomy_read__
74
+ return @provider_options if instance_variable_defined?(:@provider_options)
75
+
76
+ @provider_options = duplicate_configuration(superclass.provider_options)
52
77
  end
53
78
 
54
- # RubyLLM provider params are also class-instance state. Copy them on
55
- # first access to preserve inheritance without sharing the top-level
56
- # mutable Hash between parent and child.
57
79
  # @api public
58
80
  def provider_params
59
- return @provider_params if instance_variable_defined?(:@provider_params)
81
+ provider_options
82
+ end
60
83
 
61
- parent = superclass.respond_to?(:provider_params) ? superclass.provider_params : {}
62
- @provider_params = duplicate_configuration(parent)
84
+ # @api public
85
+ def with_params(**options)
86
+ provider_options(options)
63
87
  end
64
88
 
65
89
  # @api public
66
- def param(name, enum: nil, properties: nil, **options)
67
- super(name, **options)
90
+ def param(name, enum: nil, properties: nil, desc: nil, **options)
91
+ options[:description] = desc if desc
92
+ parameter(name, **options)
68
93
  param_enums[name] = duplicate_configuration(enum) if enum
69
94
  param_schemas[name] = normalize_nested_schema(properties) if properties
70
95
  end
@@ -175,8 +200,8 @@ module Phronomy
175
200
  end
176
201
  @requires_approval = block
177
202
  elsif value == :__unset__
178
- return @requires_approval if instance_variable_defined?(:@requires_approval)
179
- return superclass.requires_approval if superclass.respond_to?(:requires_approval)
203
+ return @requires_approval unless @requires_approval.nil?
204
+ return superclass.requires_approval if superclass < RubyLLM::Tool
180
205
 
181
206
  false
182
207
  else
@@ -225,7 +250,7 @@ module Phronomy
225
250
  self.class.tool_name || super
226
251
  end
227
252
 
228
- def params_schema
253
+ def parameters_schema
229
254
  schema = super
230
255
  return schema if schema.nil?
231
256
 
@@ -264,6 +289,22 @@ module Phronomy
264
289
  schema
265
290
  end
266
291
 
292
+ # Phronomy retains these readers as part of its Tool contract.
293
+ # @api public
294
+ def params_schema
295
+ parameters_schema
296
+ end
297
+
298
+ # @api public
299
+ def provider_params
300
+ provider_options
301
+ end
302
+
303
+ # @api public
304
+ def parameters
305
+ self.class.declared_parameters
306
+ end
307
+
267
308
  # @api public
268
309
  def call(args, cancellation_token: nil)
269
310
  cancellation_token&.raise_if_cancelled!
@@ -281,7 +322,7 @@ module Phronomy
281
322
  if cancellation_token && execute_accepts_cancellation_token?
282
323
  validated_args = validated_args.merge(cancellation_token: cancellation_token)
283
324
  end
284
- result = super(validated_args)
325
+ result = super(**(validated_args || {}).transform_keys(&:to_sym))
285
326
  truncate_result_if_needed(result)
286
327
  rescue Phronomy::ToolError, Phronomy::CancellationError
287
328
  raise
@@ -307,7 +348,7 @@ module Phronomy
307
348
  cancellation_token: nil,
308
349
  config: {}
309
350
  )
310
- Phronomy::Agent::ToolExecutor.call_async(
351
+ Phronomy::Agent::Context::Capability::ToolExecutor.call_async(
311
352
  tool: self,
312
353
  args: args,
313
354
  cancellation_token: cancellation_token,
@@ -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