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
@@ -0,0 +1,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../engine/concurrency/worker_input_restricted"
4
+
5
+ require "securerandom"
6
+ require "time"
7
+
8
+ module Phronomy
9
+ module Agent
10
+ # Admits and prepares an execution, or replays its saved preparation inputs.
11
+ # Runtime admission, live-state application and result delivery stay with
12
+ # the execution owner. Per-operation state is kept in local variables.
13
+ # @api private
14
+ class InitialPreparation
15
+ include Phronomy::Concurrency::WorkerInputRestricted
16
+
17
+ Command = Data.define(
18
+ :root, :journal_records, :input, :config, :preparation_replayable
19
+ )
20
+ RecoveryCommand = Data.define(:execution, :root, :journal_records)
21
+ Result = Data.define(
22
+ :execution, :root, :runtime_projection, :filtered_input,
23
+ :config, :appended_records, :error, :admission_outcome
24
+ )
25
+
26
+ # @api private
27
+ def initialize(agent:, persistence:)
28
+ @agent = agent
29
+ @persistence = persistence
30
+ end
31
+
32
+ # @api private
33
+ def prepare(operation)
34
+ begin
35
+ raw_message = @agent.send(:extract_message, operation.input)
36
+ rescue => error
37
+ return unadmitted_result(operation, error, :not_established)
38
+ end
39
+
40
+ begin
41
+ execution, active_root = admit_execution(
42
+ raw_message,
43
+ root: operation.root,
44
+ mode: (operation.config[:phronomy_recovery_mode] || :invoke).to_sym,
45
+ config: operation.config,
46
+ preparation_replayable: operation.preparation_replayable
47
+ )
48
+ rescue Phronomy::AgentBusyError => error
49
+ # A durable nonterminal execution keeps Runtime admission fail-closed
50
+ # until recovery can establish its lineage, including after F4 loss.
51
+ return unadmitted_result(operation, error, :recovery_required)
52
+ rescue Phronomy::Storage::ConflictError,
53
+ Phronomy::Storage::NotFoundError,
54
+ Phronomy::Storage::SerializationError,
55
+ ArgumentError,
56
+ Phronomy::ConfigurationError => error
57
+ return unadmitted_result(operation, error, :not_established)
58
+ rescue => error
59
+ return unadmitted_result(operation, error, :outcome_unknown)
60
+ end
61
+
62
+ prepare_admitted(
63
+ input: operation.input,
64
+ config: operation.config,
65
+ execution: execution,
66
+ active_root: active_root,
67
+ journal_records: operation.journal_records
68
+ )
69
+ end
70
+
71
+ # @api private
72
+ def recover(operation)
73
+ execution = operation.execution
74
+ unless execution.metadata["preparation_replayable"] == true
75
+ raise Phronomy::ExecutionRehydrationRequiredError,
76
+ "execution #{execution.execution_id} has no replay-safe initial preparation contract"
77
+ end
78
+
79
+ input = @persistence.contents.fetch_text(execution.metadata.fetch("current_input_ref"))
80
+ config = recovered_config(execution)
81
+ prepare_admitted(
82
+ input: input,
83
+ config: config,
84
+ execution: execution,
85
+ active_root: operation.root,
86
+ journal_records: operation.journal_records
87
+ )
88
+ end
89
+
90
+ private
91
+
92
+ def unadmitted_result(operation, error, outcome)
93
+ Result.new(
94
+ execution: nil,
95
+ root: operation.root,
96
+ runtime_projection: nil,
97
+ filtered_input: nil,
98
+ config: operation.config,
99
+ appended_records: [].freeze,
100
+ error: translated(error),
101
+ admission_outcome: outcome
102
+ )
103
+ end
104
+
105
+ def prepare_admitted(input:, config:, execution:, active_root:, journal_records:)
106
+ current_execution = execution
107
+ begin
108
+ filtered_input = filter_input(input, config)
109
+ staged = stage_filtered_input(current_execution, active_root, filtered_input)
110
+ assembler, prepared = prepare_context(staged, active_root, filtered_input, config, journal_records)
111
+ active_execution, manifest, manifest_ref = commit_preparation(
112
+ staged, active_root, assembler, prepared
113
+ )
114
+ # Advance the failure base only after the commit response is known.
115
+ # F1 response loss must not be reported as a confirmed terminal result.
116
+ current_execution = active_execution
117
+ projection = RubyLLMMaterializer.new(
118
+ agent: @agent, persistence: @persistence
119
+ ).materialize(manifest: manifest, manifest_ref: manifest_ref)
120
+ Result.new(
121
+ execution: active_execution,
122
+ root: active_root,
123
+ runtime_projection: projection,
124
+ filtered_input: filtered_input,
125
+ config: config,
126
+ appended_records: [].freeze,
127
+ error: nil,
128
+ admission_outcome: :active
129
+ )
130
+ rescue => error
131
+ preparation_failure_result(current_execution, active_root, config, error)
132
+ end
133
+ end
134
+
135
+ def filter_input(input, config)
136
+ @agent.send(:check_cancellation!, config, "invocation cancelled before input filtering")
137
+ filtered = @agent.send(:run_input_filters!, input)
138
+ @agent.send(:check_cancellation!, config, "invocation cancelled before context assembly")
139
+ filtered
140
+ end
141
+
142
+ def stage_filtered_input(execution, root, filtered_input)
143
+ message = @agent.send(:extract_message, filtered_input)
144
+ # Content-addressed source material may precede Policy. Mutable execution
145
+ # and Agent records are only advanced by the later commit transaction.
146
+ filtered_ref = @persistence.contents.put_text(message)
147
+ record = JournalRecord.new(
148
+ agent_id: @agent.agent_id,
149
+ execution_id: execution.execution_id,
150
+ kind: :external_message,
151
+ channel: :external,
152
+ role: :user,
153
+ content_ref: filtered_ref,
154
+ context_generation: root.transcript_generation,
155
+ context_candidate: true,
156
+ metadata: {"handoff_policy_category" => "current_request"}
157
+ )
158
+ execution.with(
159
+ execution_revision: execution.execution_revision,
160
+ working_records: execution.working_records + [record],
161
+ metadata: execution.metadata.merge(
162
+ "current_input_ref" => filtered_ref,
163
+ "current_input_record_id" => record.record_id
164
+ )
165
+ )
166
+ end
167
+
168
+ def prepare_context(staged, root, input, config, journal_records)
169
+ assembler = ContextAssembler.new(
170
+ agent: @agent, persistence: @persistence, journal_records: journal_records
171
+ )
172
+ prepared = assembler.prepare_initial(
173
+ input: input,
174
+ agent_root: root,
175
+ execution: staged,
176
+ config: config,
177
+ patch: @agent.send(:run_before_llm_input_hooks, call_sequence: 1, config: config)
178
+ )
179
+ @agent.send(:check_cancellation!, config, "invocation cancelled after context policy")
180
+ [assembler, prepared]
181
+ end
182
+
183
+ def commit_preparation(staged, root, assembler, prepared)
184
+ active = manifest = manifest_ref = nil
185
+ @persistence.transaction do |tx|
186
+ assert_local_durable_base!(tx, root)
187
+ manifest, manifest_ref = assembler.finalize(prepared, persistence: tx)
188
+ active = execution_with_initial_manifest(staged, manifest_ref)
189
+ tx.executions.save(
190
+ staged.execution_id,
191
+ expected_revision: staged.execution_revision,
192
+ execution: active
193
+ )
194
+ end
195
+ [active, manifest, manifest_ref]
196
+ end
197
+
198
+ def execution_with_initial_manifest(staged, manifest_ref)
199
+ staged.with(
200
+ status: :active,
201
+ phase: :calling_llm,
202
+ metadata: staged.metadata.merge(
203
+ "base_manifest_ref" => manifest_ref,
204
+ "manifest_ref" => manifest_ref,
205
+ "manifest_refs" => [manifest_ref]
206
+ )
207
+ )
208
+ end
209
+
210
+ def admit_execution(raw_message, root:, mode: :invoke, config: {}, preparation_replayable: false)
211
+ if root.lifecycle_status == :closed
212
+ raise Phronomy::Error, "agent is closed: #{@agent.agent_id}"
213
+ end
214
+
215
+ execution = next_root = nil
216
+ preparation_metadata = initial_preparation_metadata(mode.to_sym, preparation_replayable)
217
+ @persistence.transaction do |tx|
218
+ execution = build_admitted_execution(
219
+ tx, raw_message, root: root, config: config,
220
+ preparation_metadata: preparation_metadata
221
+ )
222
+ validate_coordination_admission!(tx, execution)
223
+ tx.executions.create_active(execution)
224
+ next_root = root.with(
225
+ agent_revision: root.agent_revision + 1,
226
+ lifecycle_status: :active
227
+ )
228
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
229
+ end
230
+ [execution, next_root]
231
+ end
232
+
233
+ def initial_preparation_metadata(mode, replayable)
234
+ {
235
+ "preparation_replayable" => !!replayable,
236
+ ExecutionMetadata::CONTRACT_VERSION_KEY => ExecutionMetadata::CONTRACT_VERSION,
237
+ ExecutionMetadata::INVOCATION_MODE_KEY => mode.to_s,
238
+ ExecutionMetadata::PENDING_LLM_ID_KEY => SecureRandom.uuid.to_s.freeze,
239
+ ExecutionMetadata::PENDING_LLM_STARTED_AT_KEY => Time.now.utc.iso8601(6).freeze
240
+ }
241
+ end
242
+
243
+ def build_admitted_execution(tx, raw_message, root:, config:, preparation_metadata:)
244
+ input_ref = tx.contents.put_text(raw_message)
245
+ durable_context_ref = if config.key?(:durable_context)
246
+ tx.contents.put_json(config.fetch(:durable_context))
247
+ end
248
+ input_record = JournalRecord.new(
249
+ agent_id: @agent.agent_id,
250
+ kind: :input_received,
251
+ channel: :external,
252
+ role: :user,
253
+ content_ref: input_ref,
254
+ context_generation: root.transcript_generation,
255
+ context_candidate: false
256
+ )
257
+ execution = AgentExecution.start(
258
+ agent_root: root,
259
+ input_record: input_record,
260
+ execution_id: config[:phronomy_reserved_execution_id] || SecureRandom.uuid,
261
+ metadata: {
262
+ "coordination" => config[:phronomy_coordination],
263
+ "current_input_ref" => input_ref,
264
+ "durable_context_ref" => durable_context_ref
265
+ }.merge(preparation_metadata).compact
266
+ )
267
+ input_record = JournalRecord.from_h(input_record.to_h.merge("execution_id" => execution.execution_id))
268
+ execution.with(execution_revision: 0, working_records: [input_record])
269
+ end
270
+
271
+ def validate_coordination_admission!(tx, execution)
272
+ owner = execution.metadata["coordination"]
273
+ return unless owner
274
+
275
+ case owner.fetch("kind")
276
+ when "team" then validate_team_admission!(tx, execution, owner)
277
+ when "subagent" then validate_subagent_admission!(tx, execution, owner)
278
+ when "handoff" then validate_handoff_admission!(tx, execution, owner)
279
+ else
280
+ raise Phronomy::ConfigurationError, "Unknown coordination owner kind"
281
+ end
282
+ end
283
+
284
+ def validate_team_admission!(tx, execution, owner)
285
+ team = tx.team_executions.load(owner.fetch("team_execution_id"))
286
+ unless team.team_id == owner.fetch("team_id") && team.active? && !team.metadata["cancel_requested"]
287
+ raise Phronomy::CancellationError, "Team run is no longer dispatchable"
288
+ end
289
+ slot = if owner.fetch("slot") == "coordinator"
290
+ team.coordinator
291
+ else
292
+ assignment = team.assignments.find { |entry| entry.fetch("task_id") == owner.fetch("slot") }
293
+ worker = assignment && team.workers.fetch(assignment.fetch("worker"))
294
+ worker&.merge("execution_id" => assignment.fetch("execution_id"))
295
+ end
296
+ unless slot && slot.fetch("execution_id") == execution.execution_id && slot.fetch("agent_id") == execution.agent_id
297
+ raise Phronomy::Storage::ConflictError, "Team reserved child identity mismatch"
298
+ end
299
+ end
300
+
301
+ def validate_subagent_admission!(tx, execution, owner)
302
+ parent = tx.executions.load(owner.fetch("parent_execution_id"))
303
+ unless parent.agent_id == owner.fetch("parent_agent_id") && parent.active? && !parent.metadata["coordination_cancel_requested"]
304
+ raise Phronomy::CancellationError, "Parent run is no longer dispatchable"
305
+ end
306
+ snapshot = tx.contents.fetch_json(parent.metadata.fetch("multi_agent_coordination_ref"))
307
+ slot = snapshot.fetch("children").find { |entry| entry.fetch("slot") == owner.fetch("slot") }
308
+ unless slot && slot.fetch("agent_id") == execution.agent_id && slot.fetch("execution_id") == execution.execution_id
309
+ raise Phronomy::Storage::ConflictError, "Parent reserved child identity mismatch"
310
+ end
311
+ end
312
+
313
+ def validate_handoff_admission!(tx, execution, owner)
314
+ routing = tx.handoff_states.load(owner.fetch("main_agent_id"))
315
+ unless routing && routing.active_agent_id == execution.agent_id
316
+ raise Phronomy::Storage::ConflictError, "Handoff responsibility changed before admission"
317
+ end
318
+ if Array(routing.metadata["cancelled_execution_ids"]).include?(execution.execution_id)
319
+ raise Phronomy::CancellationError, "Handoff reservation was cancelled"
320
+ end
321
+ if routing.phase != "stable" && routing.pending_target_execution_id != execution.execution_id
322
+ raise Phronomy::Storage::ConflictError, "Handoff reserved Target identity mismatch"
323
+ end
324
+ end
325
+
326
+ def recovered_config(execution)
327
+ mode = (execution.metadata[ExecutionMetadata::INVOCATION_MODE_KEY] || "invoke").to_sym
328
+ config = {phronomy_recovery_mode: mode}.merge(@agent.__coordination_config)
329
+ if execution.metadata.key?("durable_context_ref")
330
+ context = @persistence.contents.fetch_json(execution.metadata.fetch("durable_context_ref"))
331
+ unless context.is_a?(Hash)
332
+ raise Phronomy::ExecutionRehydrationRequiredError,
333
+ "execution #{execution.execution_id} has a non-Hash durable_context"
334
+ end
335
+ Phronomy::Values::Immutable.validate_canonical_json!(context, label: "Recovered durable_context")
336
+ config[:durable_context] = Phronomy::Values::Immutable.copy(context)
337
+ end
338
+ config.freeze
339
+ end
340
+
341
+ def preparation_failure_result(execution, root, config, error)
342
+ failure = commit_preparation_failure(execution, root, error)
343
+ Result.new(
344
+ execution: failure.fetch(:execution),
345
+ root: failure.fetch(:root),
346
+ runtime_projection: nil,
347
+ filtered_input: nil,
348
+ config: config,
349
+ appended_records: failure.fetch(:appended_records),
350
+ error: failure.fetch(:error),
351
+ admission_outcome: :terminal
352
+ )
353
+ end
354
+
355
+ def commit_preparation_failure(execution, root, error)
356
+ translated_error = translated(error)
357
+ failed = next_root = appended = nil
358
+ @persistence.transaction do |tx|
359
+ error_ref = tx.contents.put_json(
360
+ "class" => translated_error.class.name,
361
+ "message" => translated_error.message
362
+ )
363
+ status = ExecutionFailure.status_for(translated_error)
364
+ records = preparation_failure_records(execution, root, status, error_ref)
365
+ appended = tx.journals.append(
366
+ root.agent_id, expected_position: root.journal_position, records: records
367
+ )
368
+ failed = execution.with(
369
+ status: status,
370
+ phase: status,
371
+ working_records: [],
372
+ error_ref: error_ref,
373
+ terminal_reason: translated_error.class.name
374
+ )
375
+ tx.executions.save(
376
+ execution.execution_id, expected_revision: execution.execution_revision, execution: failed
377
+ )
378
+ next_root = root.with(
379
+ agent_revision: root.agent_revision + 1,
380
+ journal_position: root.journal_position + appended.length,
381
+ lifecycle_status: :idle
382
+ )
383
+ tx.agents.save(root.agent_id, expected_revision: root.agent_revision, root: next_root)
384
+ end
385
+ {
386
+ error: translated_error,
387
+ execution: failed,
388
+ root: next_root,
389
+ appended_records: Array(appended).freeze
390
+ }.freeze
391
+ end
392
+
393
+ def preparation_failure_records(execution, root, status, error_ref)
394
+ records = execution.working_records.map do |record|
395
+ JournalRecord.from_h(record.to_h.merge("context_candidate" => false))
396
+ end
397
+ records << JournalRecord.new(
398
+ agent_id: @agent.agent_id,
399
+ execution_id: execution.execution_id,
400
+ kind: ExecutionFailure.journal_kind_for(status),
401
+ channel: :audit,
402
+ content_ref: error_ref,
403
+ context_generation: root.transcript_generation,
404
+ context_candidate: false
405
+ )
406
+ end
407
+
408
+ def assert_local_durable_base!(tx, root)
409
+ tx.assert_agent_watermark!(
410
+ agent_id: root.agent_id,
411
+ agent_revision: root.agent_revision,
412
+ journal_position: root.journal_position
413
+ )
414
+ end
415
+
416
+ def translated(error)
417
+ @agent.send(:_translated_error, error)
418
+ end
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module Agent
5
+ # Agent phase vocabulary and ordered external transitions. Both the phase
6
+ # machine and FSMSession read this definition; Engine has no Agent policy.
7
+ # Automatic transition behavior and entry actions retain their own owners.
8
+ # @api private
9
+ module InvocationTransitions
10
+ ENTRY_POINT = :idle
11
+
12
+ AUTO_STATE_SET = {
13
+ idle: true,
14
+ filtering_input: true,
15
+ building_context: true,
16
+ starting_tools: true,
17
+ evaluating_tools: true,
18
+ recording_tool_results: true,
19
+ output_filtering: true
20
+ }.freeze
21
+
22
+ DECLARED_STATES = %i[
23
+ idle filtering_input building_context calling_llm starting_tools
24
+ evaluating_tools waiting_for_tools dispatching_tools
25
+ recording_tool_results suspended output_filtering handed_off completed blocked failed
26
+ ].freeze
27
+
28
+ WAIT_STATES = %i[suspended].freeze
29
+
30
+ TOOL_EVENTS = %i[
31
+ tool_authorized
32
+ tool_approval_required
33
+ tool_completed
34
+ tool_failed
35
+ tool_rejected
36
+ tool_cancelled
37
+ ].freeze
38
+
39
+ EXTERNAL_EVENTS = begin
40
+ tool_transitions = TOOL_EVENTS.to_h do |event_name|
41
+ [event_name, [{from: :waiting_for_tools, to: :evaluating_tools, guard: nil}]]
42
+ end
43
+
44
+ events = tool_transitions.merge(
45
+ llm_completed: [
46
+ {from: :calling_llm, to: :failed, guard: ->(ctx) { ctx.callback_failed? }},
47
+ {from: :calling_llm, to: :failed, guard: ->(ctx) { ctx.handoff_failed? }},
48
+ {from: :calling_llm, to: :handed_off, guard: ->(ctx) { ctx.handoff_requested? }},
49
+ {from: :calling_llm, to: :starting_tools, guard: ->(ctx) { ctx.tool_call_pending? }},
50
+ {from: :calling_llm, to: :output_filtering, guard: nil}
51
+ ],
52
+ llm_failed: [
53
+ {from: :calling_llm, to: :failed, guard: nil}
54
+ ],
55
+ llm_setup_failed: [
56
+ {from: :calling_llm, to: :failed, guard: nil}
57
+ ],
58
+ tool_setup_failed: [
59
+ {from: :dispatching_tools, to: :failed, guard: nil}
60
+ ],
61
+ tool_dispatch_prepared: [
62
+ {from: :dispatching_tools, to: :evaluating_tools, guard: nil}
63
+ ],
64
+ resume: [
65
+ {from: :suspended, to: :waiting_for_tools, guard: nil}
66
+ ],
67
+ application_callback_failed: [
68
+ {from: :filtering_input, to: :failed, guard: nil},
69
+ {from: :building_context, to: :failed, guard: nil},
70
+ {from: :calling_llm, to: :failed, guard: nil},
71
+ {from: :starting_tools, to: :failed, guard: nil},
72
+ {from: :evaluating_tools, to: :failed, guard: nil},
73
+ {from: :waiting_for_tools, to: :failed, guard: nil},
74
+ {from: :dispatching_tools, to: :failed, guard: nil},
75
+ {from: :recording_tool_results, to: :failed, guard: nil},
76
+ {from: :output_filtering, to: :failed, guard: nil}
77
+ ]
78
+ )
79
+ events.each_value do |transitions|
80
+ transitions.each(&:freeze).freeze
81
+ end
82
+ events.freeze
83
+ end
84
+ end
85
+ end
86
+ end
@@ -5,15 +5,6 @@ require "state_machines"
5
5
  module Phronomy
6
6
  module Agent
7
7
  class PhaseMachineBuilder
8
- TOOL_EVENTS = %i[
9
- tool_authorized
10
- tool_approval_required
11
- tool_completed
12
- tool_failed
13
- tool_rejected
14
- tool_cancelled
15
- ].freeze
16
-
17
8
  def initialize(entry_actions: {})
18
9
  @entry_actions = entry_actions
19
10
  end
@@ -21,26 +12,16 @@ module Phronomy
21
12
  def build
22
13
  entry_actions = @entry_actions
23
14
  callback_builder = method(:build_entry_callback)
15
+ condition_builder = method(:build_transition_condition)
16
+ external_events = InvocationTransitions::EXTERNAL_EVENTS
17
+ declared_states = InvocationTransitions::DECLARED_STATES
18
+ entry_point = InvocationTransitions::ENTRY_POINT
24
19
 
25
20
  Class.new do
26
21
  attr_accessor :context, :current_event
27
22
 
28
- state_machine :phase, initial: :idle do
29
- state :idle
30
- state :filtering_input
31
- state :building_context
32
- state :calling_llm
33
- state :starting_tools
34
- state :evaluating_tools
35
- state :waiting_for_tools
36
- state :dispatching_tools
37
- state :recording_tool_results
38
- state :suspended
39
- state :output_filtering
40
- state :handed_off
41
- state :completed
42
- state :blocked
43
- state :failed
23
+ state_machine :phase, initial: entry_point do
24
+ declared_states.each { |state_name| state state_name }
44
25
 
45
26
  event :state_completed do
46
27
  transition idle: :filtering_input
@@ -73,56 +54,17 @@ module Phronomy
73
54
  if: ->(machine) { machine.context&.output_blocked? }
74
55
  end
75
56
 
76
- event :llm_completed do
77
- transition calling_llm: :failed,
78
- if: ->(machine) { machine.context&.callback_failed? }
79
- transition calling_llm: :failed,
80
- if: ->(machine) { machine.context&.handoff_failed? }
81
- transition calling_llm: :handed_off,
82
- if: ->(machine) { machine.context&.handoff_requested? }
83
- transition calling_llm: :starting_tools,
84
- if: ->(machine) { machine.context&.tool_call_pending? }
85
- transition calling_llm: :output_filtering
86
- end
87
-
88
- event :llm_failed do
89
- transition calling_llm: :failed
90
- end
91
-
92
- event :llm_setup_failed do
93
- transition calling_llm: :failed
94
- end
95
-
96
- event :tool_setup_failed do
97
- transition dispatching_tools: :failed
98
- end
99
-
100
- event :tool_dispatch_prepared do
101
- transition dispatching_tools: :evaluating_tools
102
- end
103
-
104
- TOOL_EVENTS.each do |event_name|
57
+ external_events.each do |event_name, transitions|
105
58
  event event_name do
106
- transition waiting_for_tools: :evaluating_tools
59
+ transitions.each do |definition|
60
+ transition(
61
+ definition[:from] => definition[:to],
62
+ :if => condition_builder.call(definition)
63
+ )
64
+ end
107
65
  end
108
66
  end
109
67
 
110
- event :resume do
111
- transition suspended: :waiting_for_tools
112
- end
113
-
114
- event :application_callback_failed do
115
- transition filtering_input: :failed
116
- transition building_context: :failed
117
- transition calling_llm: :failed
118
- transition starting_tools: :failed
119
- transition evaluating_tools: :failed
120
- transition waiting_for_tools: :failed
121
- transition dispatching_tools: :failed
122
- transition recording_tool_results: :failed
123
- transition output_filtering: :failed
124
- end
125
-
126
68
  entry_actions.each do |state_name, callables|
127
69
  callables.each do |callable|
128
70
  after_transition(
@@ -137,6 +79,13 @@ module Phronomy
137
79
 
138
80
  private
139
81
 
82
+ def build_transition_condition(definition)
83
+ guard = definition[:guard]
84
+ ->(machine) {
85
+ guard.nil? || (!machine.context.nil? && guard.call(machine.context))
86
+ }
87
+ end
88
+
140
89
  def build_entry_callback(callable, state_name)
141
90
  ->(machine) {
142
91
  result = callable.call(machine.context)
@@ -20,7 +20,7 @@ module Phronomy
20
20
  {}
21
21
  end
22
22
  metadata = {}
23
- metadata["model_id"] = message.model_id.to_s if message.respond_to?(:model_id) && message.model_id
23
+ metadata["model_id"] = message.model.to_s if message.respond_to?(:model) && message.model
24
24
 
25
25
  new(
26
26
  role: message.respond_to?(:role) ? message.role : :assistant,
@@ -72,15 +72,15 @@ module Phronomy
72
72
  def initialize(role:, content:, tool_calls:, usage: {}, metadata: {})
73
73
  super(
74
74
  role: role&.to_sym,
75
- content: Immutable.copy(content),
76
- tool_calls: Immutable.copy(Array(tool_calls)),
77
- usage: Immutable.copy(usage || {}),
78
- metadata: Immutable.copy(metadata || {})
75
+ content: Phronomy::Values::Immutable.copy(content),
76
+ tool_calls: Phronomy::Values::Immutable.copy(Array(tool_calls)),
77
+ usage: Phronomy::Values::Immutable.copy(usage || {}),
78
+ metadata: Phronomy::Values::Immutable.copy(metadata || {})
79
79
  )
80
- Immutable.validate_canonical_json!(content, label: "Provider assistant content")
81
- Immutable.validate_canonical_json!(tool_calls, label: "Provider Tool Calls")
82
- Immutable.validate_canonical_json!(usage, label: "Provider usage")
83
- Immutable.validate_canonical_json!(metadata, label: "Provider outcome metadata")
80
+ Phronomy::Values::Immutable.validate_canonical_json!(content, label: "Provider assistant content")
81
+ Phronomy::Values::Immutable.validate_canonical_json!(tool_calls, label: "Provider Tool Calls")
82
+ Phronomy::Values::Immutable.validate_canonical_json!(usage, label: "Provider usage")
83
+ Phronomy::Values::Immutable.validate_canonical_json!(metadata, label: "Provider outcome metadata")
84
84
  freeze
85
85
  end
86
86