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,179 +1,150 @@
1
- # Persistence backend contract
1
+ # Persistence and neutral Storage SPI 2
2
2
 
3
- `Phronomy::Persistence` is the single durable-state backend abstraction used by
4
- stateful Agents and durable Workflows. This document is the normative contract
5
- for authors of custom Persistence backends.
3
+ [ADR-058](decisions/058-neutral-storage-primitives.md) defines the current
4
+ extension contract. This is an intentional breaking replacement of the old
5
+ fixed-repository Backend SPI. Application code keeps `Persistence.new(backend:)`,
6
+ `Persistence.in_memory`, its eight domain repositories and result queries.
6
7
 
7
- The Backend SPI is **Beta**. It may evolve in a minor pre-1.0 release, but a
8
- backend should not depend on Phronomy private APIs or Runtime internals.
8
+ ## Ownership and composition
9
9
 
10
- ## Architecture boundary
10
+ Storage owns `Resource`, `Backend`, `View`, `Records`, `Streams`, `Blobs`, immutable
11
+ `Entry` values, guards, conditions and neutral exceptions. It has no Agent/Team
12
+ record types, active-execution policy, content digest algorithm or watermark API.
11
13
 
12
- A backend implements durable storage only:
14
+ Agent, Team, Workflow and ContentStore declare their own resource schemas.
15
+ `PersistenceComposition::StorageSchema` gathers those declarations and
16
+ `PersistenceComposition::Repositories` assembles domain wrappers over a View.
17
+ The Team and Workflow schema files live beside their features in nested loader
18
+ roots; loading metadata does not load their runtime implementations.
19
+ `ContentStore::StoredContents` owns SHA-256 identity and digest verification.
20
+ The Agent-owned `Watermark` composes guarded revision and stream-head conditions.
13
21
 
14
- ```text
15
- Application
16
- ↓
17
- Agent / Workflow
18
- ↓
19
- Runtime / EventLoop / ExecutionCoordinator
20
- ↓
21
- Phronomy::Persistence synchronous Backend SPI
22
- ↓
23
- Database / durable storage
24
- ```
25
-
26
- Persistence does not own live Agent identity, top-level Runtime admission, or
27
- live execution state. In particular, a backend must not persist or reconstruct
28
- the following as part of this SPI:
29
-
30
- - Runtime Agent ownership-registry entries;
31
- - EventLoop Agent top-level admission entries;
32
- - EventLoop Agent execution-directory entries;
33
- - `AgentInvocation`;
34
- - `FSMSession`;
35
- - `TaskResult` or callbacks;
36
- - EventLoop queue contents;
37
- - Runtime Workflow admission entries;
38
- - in-flight provider operations.
39
-
40
- Persistence operations are synchronous. Framework-owned blocking Persistence I/O
41
- is submitted to the Runtime OffloadPool by Phronomy; a backend must not post
42
- EventLoop events or introduce `load_async` / `save_async` variants into this
43
- contract.
44
-
45
- ## Required root surface
22
+ InMemory receives `resources:` and supplies one Monitor/snapshot transaction
23
+ across the catalog. SQL reference composition supplies the same catalog and a
24
+ separate physical table/column mapping to the neutral driver in examples
25
+ `shared/storage`. Table names, columns, indexes, DurableRecord envelopes, payloads,
26
+ format versions and content identities remain unchanged.
46
27
 
47
- A Persistence backend exposes eight durable repositories:
28
+ The [S3 closure review](architecture/refactoring-closure.md) records why these
29
+ names and placements remain and separates applied evidence from the candidate.
48
30
 
49
- ```text
50
- contents
51
- agents
52
- journals
53
- executions
54
- workflow_states
55
- handoff_states
56
- teams
57
- team_executions
58
- ```
59
-
60
- and two root operations:
31
+ ## Backend and View
61
32
 
62
33
  ```ruby
63
- persistence.transaction { |tx| ... }
64
- persistence.assert_agent_watermark!(
65
- agent_id:,
66
- agent_revision:,
67
- journal_position:
68
- )
69
- ```
70
-
71
- The object yielded by `transaction` is a transaction-scoped Persistence view. It
72
- must respond to all eight repository accessors and
73
- `assert_agent_watermark!`. It may be the Persistence instance itself, but SQL
74
- backends may instead yield an object bound to a checked-out connection or
75
- transaction session.
76
-
77
- ## Required capabilities
78
-
79
- Every backend must advertise:
80
-
81
- ```ruby
82
- {
83
- atomic_all: true,
84
- atomic_admission: true,
85
- optimistic_revision: true
86
- }
87
- ```
88
-
89
- `Phronomy::Persistence::REQUIRED_CAPABILITIES` is the executable definition of
90
- this requirement.
91
-
92
- ### `atomic_all`
93
-
94
- All durable repositories must be able to participate in one atomic transaction
95
- domain. A transaction may change `contents`, `agents`, `journals`, `executions`,
96
- `workflow_states`, `handoff_states`, `teams`, and `team_executions`, and then either commit all changes or roll them all back.
97
-
98
- This requirement deliberately does not claim exactly-once semantics after an
99
- indeterminate database/network failure. If the underlying database cannot tell
100
- the caller whether a commit happened, the backend should surface the storage
101
- failure rather than pretending the outcome is known.
102
-
103
- ### `atomic_admission`
104
-
105
- This capability is a **durable Agent execution integrity defense**. It is not the
106
- primary same-process Agent ownership/admission mechanism and it is not Workflow
107
- distributed locking.
108
-
109
- For one Agent, `executions.create_active` must atomically guarantee both:
110
-
111
- ```text
112
- execution_id is unique
113
- AND
114
- no active/suspended execution already exists for agent_id
34
+ backend = Phronomy::Storage::Backends::InMemory.new(resources: resources)
35
+ backend.view.records(resource)
36
+ backend.view.streams(resource)
37
+ backend.view.blobs(resource)
38
+ backend.transaction { |view| ... }
39
+ view.check!(guards: guards, conditions: conditions)
115
40
  ```
116
41
 
117
- A conflict with an existing active/suspended execution raises
118
- `Phronomy::AgentBusyError`.
119
-
120
- Within one process, Runtime/EventLoop admission is acquired before the initial
121
- Persistence operation and is the primary competing-execution exclusion
122
- mechanism. `atomic_admission` remains required as the durable second line of
123
- defense against stale paths, durable conflicts, and unsupported cross-process
124
- races. It must not be removed merely because Runtime admission exists.
125
-
126
- Workflow admission remains Runtime/process-local. Cross-process Agent or
127
- Workflow ownership/lease/fencing is a separate distributed-coordination concern
128
- and is not part of this Backend SPI.
129
-
130
- ### `optimistic_revision`
131
-
132
- The backend must implement compare-and-swap semantics used by Agent roots,
133
- Agent executions, Journals, Workflow snapshots, and the durable Agent watermark.
134
- Stale writers must receive `Phronomy::Persistence::ConflictError`; they must not
135
- silently overwrite newer durable state.
136
-
137
- ## Error contract
138
-
139
- Backends should translate backend-specific constraint errors into the following
140
- portable Phronomy errors when the meaning matches.
141
-
142
- ### `Phronomy::Persistence::NotFoundError`
143
-
144
- A requested durable record does not exist.
145
-
146
- ### `Phronomy::Persistence::ConflictError`
147
-
148
- A persistence precondition failed, including revision, Journal position,
149
- identity, duplicate-ID, or compare-and-swap conflicts.
150
-
151
- ### `Phronomy::AgentBusyError`
152
-
153
- A durable nonterminal Agent execution already exists and another durable
154
- execution record cannot be established. Phronomy also uses the same public error
155
- for a competing process-local top-level request rejected by Runtime/EventLoop
156
- before the backend is called.
157
-
158
- ### `Phronomy::Persistence::SerializationError`
159
-
160
- The backend cannot encode a value into its supported durable representation.
161
- This is intended primarily for durable backends whose Workflow state domain is
162
- narrower than the InMemory backend's Ruby-object domain.
163
-
164
- ### `Phronomy::Persistence::UnsupportedBackendError`
165
-
166
- The backend does not provide a required structural or capability contract.
167
-
168
- Database availability, connection loss, and other transport/storage failures
169
- must not be misreported as ordinary optimistic conflicts merely to fit this
170
- error taxonomy.
42
+ The backend declares `spi_version: 2` and true values for `atomic_resources`,
43
+ `record_cas`, `stream_cas`, `conditional_unique`, `guarded_checks` and
44
+ `nested_savepoints`. Persistence validates these capabilities and the required
45
+ resource declarations before exposing repositories. Old duck-typed backends are
46
+ rejected with `UnsupportedBackendError`; there is no eight-slot compatibility
47
+ adapter. Public Persistence capabilities retain `atomic_all`, `atomic_admission`
48
+ and `optimistic_revision`, derived by composition from these primitives.
49
+
50
+ A root handle routes to the current transaction on the same backend/thread.
51
+ A bound view and all its handles expire on commit or rollback and reject use from
52
+ another thread. Explicit nested transactions use savepoints on the same SQL
53
+ connection, or nested InMemory snapshots. Inner success depends on outer commit;
54
+ an inner exception is re-raised after rollback and can be caught by the outer
55
+ scope. `ActiveRecord::Rollback` also propagates.
56
+
57
+ A failure during physical work marks the scope failed. Catching it inside that
58
+ same scope does not permit further operations or successful commit. Establish an
59
+ explicit inner transaction before a recoverable operation and catch outside it.
60
+ Input validation occurs before physical work. A successful optional read returning
61
+ nil is not a failed physical operation; domain required-load errors may be raised
62
+ after that read. Domain decode/returned-metadata failures stay inside the atomic
63
+ boundary so a corrupt response after a write causes rollback.
64
+
65
+ Transaction blocks must finish normally. `return`, `break` and `throw` escaping
66
+ them cause rollback and `TransactionError`. These exits are not commit controls.
67
+ Commit/rollback transport failures remain database failures; this contract does
68
+ not promise exactly-once external effects or infer commit certainty.
69
+
70
+ ## Resource declarations
71
+
72
+ A `Resource` is an immutable value with `id`, `kind`, `attributes`,
73
+ `immutable_attributes`, `indexes`, `unique` and optional `guard`. Attributes use
74
+ `:string`, `:integer`, `:boolean` and their explicit `:nullable_*` forms. Keys and
75
+ text attributes use valid UTF-8 without NUL; keys are nonempty. Binary data belongs
76
+ to Blobs. No Proc, SQL expression or payload predicate is accepted.
77
+
78
+ Named equality indexes specify exact fields. Records may declare conditional
79
+ unique constraints with a symbol name, fields and equality `where` values. Null
80
+ unique-key fields are distinct, matching the default SQL unique-index semantics.
81
+ Streams have no indexed attributes; Blobs have attributes but no indexes/guards.
82
+ A record guard uses its key or an immutable non-null string attribute. A stream
83
+ guard uses its stream ID. Its anchor must be a registered Records resource.
84
+ Required anchors must exist; missing parents raise `NotFoundError` in all drivers.
85
+
86
+ ## Records
87
+
88
+ | Operation | Contract |
89
+ |---|---|
90
+ | `insert(key:, revision:, attributes:, record:)` | Insert only when absent; return an independent immutable `Entry::Record`. Primary duplicates and named unique failures differ. |
91
+ | `read(key)` / `fetch(key)` | Optional nil / required `NotFoundError`. |
92
+ | `replace(key:, expected_revision:, next_revision:, attributes:, record:, expected_attributes: {})` | Check existence, revision, expected attributes, immutable fields and uniqueness atomically; next revision must equal expected + 1. Replace all attributes. |
93
+ | `delete(key:, expected_revision: Records::UNCHECKED)` | Unchecked deletion is idempotent. A supplied revision requires existence and equality. Return nil. |
94
+ | `scan(index:, equals:, after: nil, limit: nil)` | Exact named-index fields, UTF-8 byte order, exclusive key cursor, positive limit or nil for all. |
95
+ | `delete_matching(index:, equals:)` | Delete equality matches atomically; a parent-guarded resource must constrain the guard attribute. Return nil. |
96
+
97
+ An Entry carries key, revision, attributes and an opaque `DurableRecord`.
98
+ The driver does not reconstruct metadata from payload. Domain wrappers verify
99
+ that returned identity, revision and attributes agree with decoded domain data.
100
+ Workflow/Handoff choose initial revision 1 and route nil expected revision to
101
+ insert; nil is never an unchecked update. Team terminal-to-active rejection is a
102
+ domain `expected_attributes: {active: true}` precondition when saving active state.
103
+
104
+ ## Streams and Blobs
105
+
106
+ Streams expose `append(stream:, expected_head:, entries:)`,
107
+ `read(stream:, after: 0, limit: nil)`, `head(stream:)`, and `delete(stream:)`.
108
+ Append takes `Entry::Append(id:, record:)`, validates the entire batch before any
109
+ write, enforces unique entry IDs within a stream, assigns contiguous positions
110
+ and updates the head atomically. Empty append still checks the expected head.
111
+ Reads return immutable `Entry::Stream(position:, id:, record:)` values in position
112
+ order. Delete removes head and entries together. The domain Journal wrapper
113
+ preserves its existing `limit: 0` empty-result behavior without a raw zero-limit
114
+ operation.
115
+
116
+ Blobs expose `put_if_absent(key:, bytes:, attributes:)`, `fetch(key)` and
117
+ `exist?(key)`. Same bytes retain the first attributes; different bytes for an
118
+ existing key raise `BlobConflictError`. Entry bytes are independent immutable
119
+ binary strings. Blob keys are arbitrary storage keys; ContentStore adds the
120
+ `sha256:<digest>` contract and maps integrity failures to its own `IntegrityError`.
121
+
122
+ ## Guards, conditions and errors
123
+
124
+ `GuardRef(resource:, key:)` names a stable existing parent record. `View#check!`
125
+ acquires guards in resource/key byte order before evaluating the closed condition
126
+ set: `RevisionIs`, `StreamHeadIs`, `NoRows`. A condition must include the guard
127
+ required by its resource and scope. PostgreSQL locks parents before child heads
128
+ or records; SQLite relies on the transaction/CAS/unique constraints and must not
129
+ claim a SELECT alone reserves a writer. Transactions spanning multiple owners
130
+ must establish a consistent owner lock order; database deadlocks remain database
131
+ errors, never optimistic conflicts.
132
+
133
+ `UniqueConstraintError < ConflictError` carries the resource ID and constraint
134
+ name. Agent/Team translate only their exact `one_active_owner` constraint to
135
+ `AgentBusyError`. Their idle checks translate their own `NoRows` condition failure.
136
+ `ConditionFailedError` carries the failed condition. Duplicate identities,
137
+ stale revisions and ordinary constraint conflicts remain `ConflictError`.
138
+ `NotFoundError`, `SerializationError`, `UnsupportedBackendError` retain their
139
+ meanings; `TransactionError` identifies invalid scope use. The removed
140
+ `ActiveExecutionConflictError` and `Storage::Repositories` have no aliases.
141
+
142
+ The following sections describe the retained **domain Persistence** surface.
171
143
 
172
144
  ## Contents repository
173
145
 
174
- The content repository should normally inherit from
175
- `Phronomy::ContentStore::Base`, which supplies text/JSON helpers and the canonical
176
- content-ID calculation.
146
+ `ContentStore::StoredContents < ContentStore::Base` supplies this domain surface
147
+ over neutral Blobs, with text/JSON helpers and canonical content-ID calculation.
177
148
 
178
149
  Required primitive surface:
179
150
 
@@ -188,7 +159,7 @@ Required semantics:
188
159
  - content is immutable and content-addressed;
189
160
  - writing identical bytes is idempotent and returns the same content ID;
190
161
  - `fetch` returns a binary `String` isolated from caller mutation;
191
- - a missing content ID raises `Persistence::NotFoundError`;
162
+ - a missing content ID raises `Storage::NotFoundError`;
192
163
  - one content ID must never resolve to different bytes; a digest-integrity
193
164
  violation raises `ContentStore::IntegrityError`.
194
165
 
@@ -374,10 +345,9 @@ honestly rather than converting them into `ConflictError`.
374
345
 
375
346
  ### Workflow value serialization
376
347
 
377
- `WorkflowContext#to_h` may contain ordinary Ruby application values. The
378
- InMemory backend can preserve a broader set of Ruby values than a JSON database.
379
- A durable backend is not required to serialize arbitrary Ruby objects such as
380
- `Proc`, IO objects, sockets, or runtime callbacks.
348
+ The Workflow domain codec accepts canonical JSON-compatible snapshot values in
349
+ all drivers, including InMemory. Proc, IO, sockets and runtime callbacks are not
350
+ durable snapshot values.
381
351
 
382
352
  A JSON/JSONB backend should document its supported value domain. A recommended
383
353
  domain is:
@@ -391,7 +361,7 @@ Array of supported values
391
361
  Hash with String/Symbol keys and supported values
392
362
  ```
393
363
 
394
- If a value cannot be represented, raise `Persistence::SerializationError` rather
364
+ If a value cannot be represented, raise `Storage::SerializationError` rather
395
365
  than silently converting it into a lossy form. JSON backends may return String
396
366
  keys after decoding; `WorkflowRunner` deliberately accepts String and Symbol keys
397
367
  and normalizes them when comparing durable snapshots.
@@ -401,8 +371,8 @@ particular database backend accept arbitrary Workflow values.
401
371
 
402
372
  ## Durable Agent watermark
403
373
 
404
- `assert_agent_watermark!` is a public **Backend SPI** operation. It is not an
405
- ordinary application API.
374
+ `assert_agent_watermark!` is a domain Persistence operation. Agent-owned
375
+ `Watermark` composes a parent guard, root revision and Journal head conditions.
406
376
 
407
377
  Phronomy uses it at durable barriers because a hydrated live Agent owns the
408
378
  current logical state and Phronomy deliberately does not reload mutable Agent
@@ -482,9 +452,9 @@ This is a targeted migration rule for the removed generic identity field. It doe
482
452
  not establish a general unknown-field or long-term codec/schema-versioning
483
453
  policy.
484
454
 
485
- The canonical Hash representation is the Phronomy/domain boundary. A backend is
486
- free to map that representation to normalized SQL columns, JSON, or another
487
- storage format internally.
455
+ Domain codecs own canonical Hash representation. The raw driver stores the
456
+ DurableRecord envelope and separately supplied metadata without interpreting
457
+ domain payload fields.
488
458
 
489
459
  ## Conformance tests
490
460
 
@@ -508,6 +478,11 @@ an Agent repository
508
478
  a Journal repository
509
479
  an Execution repository
510
480
  a workflow state repository
481
+ a Handoff state repository
482
+ a Team repository
483
+ a Team execution repository
484
+ neutral storage primitives
485
+ storage transaction boundaries
511
486
  a Persistence backend
512
487
  ```
513
488
 
@@ -518,7 +493,7 @@ require "phronomy"
518
493
  require "phronomy/testing/persistence_contract"
519
494
 
520
495
  RSpec.describe MyPersistenceBackend do
521
- let(:persistence) { described_class.new(...) }
496
+ let(:persistence) { Phronomy::Persistence.new(backend: described_class.new(...)) }
522
497
 
523
498
  it_behaves_like "a persistence content store"
524
499
  it_behaves_like "an Agent repository"
@@ -532,7 +507,7 @@ RSpec.describe MyPersistenceBackend do
532
507
  end
533
508
  ```
534
509
 
535
- `Persistence::InMemory` is run through the same shipped contract source in
510
+ `Persistence.in_memory` is run through the same shipped contract source in
536
511
  Phronomy CI. The files under `spec/support/shared_examples/` are compatibility
537
512
  require wrappers only; the authoritative shared-example implementations live
538
513
  under `lib/phronomy/testing/persistence_contract/` so the core suite and external
@@ -560,34 +535,17 @@ may use combinations of:
560
535
  Backend-specific database exceptions should be translated to the Phronomy error
561
536
  contract where their meaning is known.
562
537
 
563
- ## V2 coordination Backend SPI (clean break)
538
+ ## Coordination domain repositories
564
539
 
565
- Both `Persistence.new` and `build_transaction_view` require all eight raw
566
- repositories. Five-repository fallback is removed. Existing record formats remain
567
- `0.1`; three additional record types are `phronomy.handoff_state`,
568
- `phronomy.team_root`, and `phronomy.team_execution`. Backend index metadata is
569
- explicit and MUST NOT be reconstructed by parsing DurableRecord payloads.
540
+ Handoff starts at revision 1 with expected revision nil; later saves advance one
541
+ revision. Team roots and executions begin at revision 0. Team admission retains
542
+ `AgentBusyError`; stale CAS and duplicate identities use `ConflictError`.
543
+ Missing Team/execution loads raise `NotFoundError`; absent Handoff state returns
544
+ nil. The unchanged record types are `phronomy.handoff_state`, `phronomy.team_root`
545
+ and `phronomy.team_execution`, all version `0.1`.
570
546
 
571
- | Raw repository | Required operations and explicit metadata |
572
- |---|---|
573
- | `handoff_states` | `load(main_agent_id)`; `save(main_agent_id, expected_revision:, next_revision:, active_agent_id:, record:)`; `delete(main_agent_id, expected_revision:)` |
574
- | `teams` | `create(team_id:, team_revision:, record:)`; `load(team_id)`; `save(team_id, expected_revision:, next_revision:, record:)`; `delete(team_id)` |
575
- | `team_executions` | `create_active(team_execution_id:, team_id:, execution_revision:, record:)`; `load(id)`; `save(id, expected_revision:, next_revision:, team_id:, active:, record:)`; `list_active(team_id)`; `list(team_id, after: nil, limit: 100)`; `delete(id)`; `delete_for_team(team_id)`; `assert_idle!(team_id)` |
576
- | `executions` extension | `list(agent_id, after: nil, limit: 100)` for retained active and terminal records |
577
-
578
- Handoff starts at revision 1 with expected revision nil; later saves advance
579
- exactly one revision. Team roots and executions begin at revision 0. Team
580
- admission has the existing `AgentBusyError` contract for an already active owner;
581
- stale CAS/duplicate identity uses `ConflictError`. Missing Team/execution loads
582
- raise `NotFoundError`; absent Handoff state returns nil. Unavailable reads and
583
- codec errors must propagate separately.
584
-
585
- Lists sort IDs lexically, use an exclusive ID cursor, return at most the positive
586
- integer limit, include only the requested owner, and return immutable copies.
587
- Backends define retention; enumeration does not implement input correlation or
588
- request deduplication. Terminal-to-active rewrites are invalid.
589
-
590
- For F1, atomic commit does not imply a known response. Phronomy reads back the
591
- same intended ID/fact before advancing. A backend must provide authoritative
592
- reads/CAS; failure of readback is returned without new semantic work. No callback
593
- ACK/index is part of the SPI. Existing cross-process exclusion limitations remain.
547
+ Backend authors should also read the [SPI 2 migration guide](migrations/neutral-storage-spi.md).
548
+ The Stable/Beta product API snapshot and the explicit Storage SPI 2 signature
549
+ snapshot are separate gates. Live PostgreSQL locking and failure tests must run
550
+ against the candidate core and examples revisions; earlier SPI results do not
551
+ satisfy this gate.