phronomy 0.22.0 → 0.24.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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +215 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +147 -0
  15. data/docs/architecture/persistence.md +186 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +89 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
  45. data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
  46. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
  47. data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
  48. data/docs/decisions/README.md +176 -0
  49. data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
  50. data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
  51. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
  52. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
  53. data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
  54. data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
  55. data/docs/features.md +65 -11
  56. data/docs/getting-started.md +77 -45
  57. data/docs/migrations/0.19.md +14 -7
  58. data/docs/migrations/0.22.md +390 -0
  59. data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
  60. data/docs/persistence-backends.md +129 -40
  61. data/docs/runtime-and-concurrency.md +227 -33
  62. data/examples/README.md +13 -0
  63. data/lib/phronomy/agent/agent_execution.rb +21 -17
  64. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  65. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  66. data/lib/phronomy/agent/agent_root.rb +3 -3
  67. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  68. data/lib/phronomy/agent/async_event_api.rb +162 -72
  69. data/lib/phronomy/agent/base.rb +416 -181
  70. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  71. data/lib/phronomy/agent/context_assembler.rb +437 -178
  72. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  73. data/lib/phronomy/agent/context_plan.rb +18 -13
  74. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  75. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  76. data/lib/phronomy/agent/context_policy.rb +109 -3
  77. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  78. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  79. data/lib/phronomy/agent/exact_execution.rb +153 -0
  80. data/lib/phronomy/agent/execution_cancellation.rb +25 -0
  81. data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
  82. data/lib/phronomy/agent/handoff.rb +43 -0
  83. data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
  84. data/lib/phronomy/agent/handoff_context.rb +120 -0
  85. data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
  86. data/lib/phronomy/agent/handoff_policy.rb +143 -0
  87. data/lib/phronomy/agent/handoff_projection.rb +207 -0
  88. data/lib/phronomy/agent/handoff_request.rb +45 -0
  89. data/lib/phronomy/agent/handoff_runner.rb +178 -0
  90. data/lib/phronomy/agent/handoff_state.rb +43 -0
  91. data/lib/phronomy/agent/journal_record.rb +17 -4
  92. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  93. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  94. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  95. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  96. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  97. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
  98. data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
  99. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
  100. data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
  101. data/lib/phronomy/agent/recovery_support.rb +504 -0
  102. data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
  103. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  104. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  105. data/lib/phronomy/agent/shared_state.rb +38 -1
  106. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  107. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  108. data/lib/phronomy/agent/tool_invocation.rb +339 -103
  109. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  110. data/lib/phronomy/agent.rb +20 -2
  111. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  112. data/lib/phronomy/agent_purged_error.rb +5 -0
  113. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  114. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  115. data/lib/phronomy/engine/event_loop.rb +622 -63
  116. data/lib/phronomy/engine/fsm_session.rb +194 -21
  117. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  118. data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
  119. data/lib/phronomy/engine/runtime.rb +92 -20
  120. data/lib/phronomy/generator_verifier.rb +12 -14
  121. data/lib/phronomy/invocation_context.rb +9 -29
  122. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  123. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
  124. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  125. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  126. data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
  127. data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
  128. data/lib/phronomy/multi_agent/team_execution.rb +44 -0
  129. data/lib/phronomy/multi_agent/team_root.rb +41 -0
  130. data/lib/phronomy/persistence/durable_codec.rb +706 -0
  131. data/lib/phronomy/persistence/durable_record.rb +117 -0
  132. data/lib/phronomy/persistence/in_memory.rb +471 -133
  133. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  134. data/lib/phronomy/persistence/repository_facades.rb +535 -0
  135. data/lib/phronomy/persistence.rb +174 -40
  136. data/lib/phronomy/recovery.rb +186 -0
  137. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  138. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
  139. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  140. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  141. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  142. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
  143. data/lib/phronomy/testing/persistence_contract.rb +5 -0
  144. data/lib/phronomy/tools/agent.rb +1 -1
  145. data/lib/phronomy/tracing/automatic.rb +176 -0
  146. data/lib/phronomy/tracing/base.rb +11 -2
  147. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  148. data/lib/phronomy/version.rb +1 -1
  149. data/lib/phronomy/workflow.rb +3 -6
  150. data/lib/phronomy/workflow_context.rb +14 -5
  151. data/lib/phronomy/workflow_recovery.rb +123 -0
  152. data/lib/phronomy/workflow_runner.rb +468 -256
  153. data/lib/phronomy.rb +6 -0
  154. data/scripts/api_snapshot.rb +12 -0
  155. data/sig/phronomy/agent.rbs +209 -7
  156. data/sig/phronomy/handoff.rbs +41 -0
  157. data/sig/phronomy/multi_agent.rbs +35 -0
  158. data/sig/phronomy/persistence.rbs +123 -4
  159. data/sig/phronomy/runtime.rbs +1 -4
  160. data/sig/phronomy/workflow.rbs +2 -2
  161. data/sig/phronomy.rbs +10 -0
  162. metadata +84 -18
  163. data/examples/workflows/agent_event_mapping.rb +0 -101
  164. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  165. data/lib/phronomy/agent/activation_registry.rb +0 -28
  166. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  167. data/lib/phronomy/agent/context_candidate.rb +0 -47
  168. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  169. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  170. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  171. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  172. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  173. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  174. data/lib/phronomy/agent/context_request.rb +0 -35
  175. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  176. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  177. data/lib/phronomy/agent/runner.rb +0 -97
  178. data/lib/phronomy/multi_agent/handoff.rb +0 -64
@@ -23,10 +23,13 @@ Phronomy::Persistence synchronous Backend SPI
23
23
  Database / durable storage
24
24
  ```
25
25
 
26
- Persistence does not own live execution state. In particular, a backend must not
27
- persist or reconstruct the following as part of this SPI:
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:
28
29
 
29
- - `AgentExecutionActivation`;
30
+ - Runtime Agent ownership-registry entries;
31
+ - EventLoop Agent top-level admission entries;
32
+ - EventLoop Agent execution-directory entries;
30
33
  - `AgentInvocation`;
31
34
  - `FSMSession`;
32
35
  - `Task` or callbacks;
@@ -41,7 +44,7 @@ contract.
41
44
 
42
45
  ## Required root surface
43
46
 
44
- A Persistence backend exposes five durable repositories:
47
+ A Persistence backend exposes eight durable repositories:
45
48
 
46
49
  ```text
47
50
  contents
@@ -49,6 +52,9 @@ agents
49
52
  journals
50
53
  executions
51
54
  workflow_states
55
+ handoff_states
56
+ teams
57
+ team_executions
52
58
  ```
53
59
 
54
60
  and two root operations:
@@ -63,7 +69,7 @@ persistence.assert_agent_watermark!(
63
69
  ```
64
70
 
65
71
  The object yielded by `transaction` is a transaction-scoped Persistence view. It
66
- must respond to all five repository accessors and
72
+ must respond to all eight repository accessors and
67
73
  `assert_agent_watermark!`. It may be the Persistence instance itself, but SQL
68
74
  backends may instead yield an object bound to a checked-out connection or
69
75
  transaction session.
@@ -87,7 +93,7 @@ this requirement.
87
93
 
88
94
  All durable repositories must be able to participate in one atomic transaction
89
95
  domain. A transaction may change `contents`, `agents`, `journals`, `executions`,
90
- and `workflow_states` and then either commit all changes or roll them all back.
96
+ `workflow_states`, `handoff_states`, `teams`, and `team_executions`, and then either commit all changes or roll them all back.
91
97
 
92
98
  This requirement deliberately does not claim exactly-once semantics after an
93
99
  indeterminate database/network failure. If the underlying database cannot tell
@@ -96,8 +102,9 @@ failure rather than pretending the outcome is known.
96
102
 
97
103
  ### `atomic_admission`
98
104
 
99
- This capability refers to **Agent execution admission**, not Workflow distributed
100
- locking.
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.
101
108
 
102
109
  For one Agent, `executions.create_active` must atomically guarantee both:
103
110
 
@@ -110,9 +117,15 @@ no active/suspended execution already exists for agent_id
110
117
  A conflict with an existing active/suspended execution raises
111
118
  `Phronomy::AgentBusyError`.
112
119
 
113
- Workflow admission remains Runtime/process-local. Cross-process Workflow
114
- lease/fencing is an application/distributed-coordination concern and is not part
115
- of this Backend SPI.
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.
116
129
 
117
130
  ### `optimistic_revision`
118
131
 
@@ -137,8 +150,10 @@ identity, duplicate-ID, or compare-and-swap conflicts.
137
150
 
138
151
  ### `Phronomy::AgentBusyError`
139
152
 
140
- An Agent already has an active or suspended execution and another execution
141
- cannot be admitted.
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.
142
157
 
143
158
  ### `Phronomy::Persistence::SerializationError`
144
159
 
@@ -202,6 +217,11 @@ def delete(agent_id)
202
217
  - returns `Phronomy::Agent::AgentRoot`, not a raw database Hash;
203
218
  - raises `NotFoundError` when missing.
204
219
 
220
+ These repository operations are durable-storage primitives. The higher-level
221
+ `Agent::Base.load` API first consults Runtime's process-local live ownership
222
+ registry and does not call the repository when the requested Agent is already
223
+ live.
224
+
205
225
  `save` atomically checks:
206
226
 
207
227
  ```text
@@ -252,13 +272,17 @@ def create_active(execution)
252
272
  def load(execution_id)
253
273
  def save(execution_id, expected_revision:, execution:)
254
274
  def list_active(agent_id)
275
+ def list(agent_id, after: nil, limit: 100)
255
276
  def delete(execution_id)
256
277
  def delete_for_agent(agent_id)
257
278
  def assert_idle!(agent_id)
258
279
  ```
259
280
 
260
- `create_active` performs atomic Agent admission. A duplicate `execution_id`
261
- raises `ConflictError`; an already busy Agent raises `AgentBusyError`.
281
+ `create_active` performs atomic **durable** Agent execution admission. A duplicate
282
+ `execution_id` raises `ConflictError`; an already busy Agent raises
283
+ `AgentBusyError`. Runtime/EventLoop has already acquired the process-local
284
+ logical execution slot on the normal Phronomy path before this repository method
285
+ runs.
262
286
 
263
287
  `load` returns `Phronomy::Agent::AgentExecution`, not a raw database Hash, and
264
288
  raises `NotFoundError` when missing.
@@ -278,17 +302,19 @@ A failed precondition raises `ConflictError`.
278
302
  `assert_idle!` is used inside transactions before Agent context/Knowledge changes
279
303
  and destructive operations. It must raise `AgentBusyError` if an active/suspended
280
304
  execution exists. A SQL implementation must make this check part of a consistency
281
- boundary that cannot race with Agent execution admission; a best-effort SELECT
282
- outside the transaction is not sufficient.
305
+ boundary that cannot race with durable Agent execution admission; a best-effort
306
+ SELECT outside the transaction is not sufficient. Process-local Runtime
307
+ admission is an additional upstream coordination layer, not a replacement for
308
+ this durable check.
283
309
 
284
310
  ## Workflow states repository
285
311
 
286
312
  Required surface:
287
313
 
288
314
  ```ruby
289
- def load(thread_id)
290
- def save(thread_id, expected_revision:, snapshot:)
291
- def delete(thread_id, expected_revision:)
315
+ def load(workflow_instance_id)
316
+ def save(workflow_instance_id, expected_revision:, snapshot:)
317
+ def delete(workflow_instance_id, expected_revision:)
292
318
  ```
293
319
 
294
320
  `load` returns `nil` when no row exists. Otherwise it returns a Hash containing a
@@ -315,6 +341,37 @@ snapshot and revision. String or Symbol Hash keys are accepted by Phronomy:
315
341
 
316
342
  Caller mutation of a loaded snapshot must not mutate durable storage.
317
343
 
344
+ ### Workflow Runtime admission and terminal-save outcome
345
+
346
+ Same-process Workflow admission is owned by Runtime/EventLoop, not by this
347
+ repository. EventLoop acquires an opaque owner token for `workflow_instance_id`
348
+ before mutable durable load/hydration, then binds a separately generated
349
+ `fsm_session_id` only after the concrete FSMSession is constructed.
350
+
351
+ A durable Workflow terminal/halt snapshot is saved through OffloadPool while the
352
+ owning FSMSession remains nonterminal. The backend still implements only the
353
+ synchronous `save` contract above; it does not post Runtime events or decide FSM
354
+ state.
355
+
356
+ Phronomy interprets terminal-save results by semantic certainty:
357
+
358
+ ```text
359
+ known successful save
360
+ -> durable barrier may be crossed
361
+
362
+ portable known failure / known not committed
363
+ -> barrier remains closed; Workflow error path
364
+
365
+ arbitrary storage/transport failure whose commit outcome is not established
366
+ -> outcome unknown; barrier remains closed and Runtime fails closed
367
+ ```
368
+
369
+ This distinction is independent of physical topology. A local backend can have
370
+ an uncertain outcome, and a remote backend can return a definite optimistic
371
+ conflict. Backends must therefore preserve meaningful portable errors when the
372
+ contract establishes them, and must surface other storage/transport failures
373
+ honestly rather than converting them into `ConflictError`.
374
+
318
375
  ### Workflow value serialization
319
376
 
320
377
  `WorkflowContext#to_h` may contain ordinary Ruby application values. The
@@ -414,6 +471,17 @@ Phronomy::Agent::AgentExecution.from_h(hash)
414
471
  String and Symbol top-level keys are accepted by these new execution/call codecs,
415
472
  which permits adapters to use parsed JSON without reimplementing constructors.
416
473
 
474
+ The current canonical `JournalRecord` Hash does not contain `correlation_id`.
475
+ Legacy durable Journal Hashes that still contain that key may be passed to
476
+ `JournalRecord.from_h`; the legacy key is accepted and ignored. Backends must not
477
+ synthesize or populate `correlation_id` for new canonical Journal records, and
478
+ they are not required to eagerly rewrite existing durable rows solely to remove
479
+ the old physical value.
480
+
481
+ This is a targeted migration rule for the removed generic identity field. It does
482
+ not establish a general unknown-field or long-term codec/schema-versioning
483
+ policy.
484
+
417
485
  The canonical Hash representation is the Phronomy/domain boundary. A backend is
418
486
  free to map that representation to normalized SQL columns, JSON, or another
419
487
  storage format internally.
@@ -457,6 +525,9 @@ RSpec.describe MyPersistenceBackend do
457
525
  it_behaves_like "a Journal repository"
458
526
  it_behaves_like "an Execution repository"
459
527
  it_behaves_like "a workflow state repository"
528
+ it_behaves_like "a Handoff state repository"
529
+ it_behaves_like "a Team repository"
530
+ it_behaves_like "a Team execution repository"
460
531
  it_behaves_like "a Persistence backend"
461
532
  end
462
533
  ```
@@ -467,10 +538,12 @@ require wrappers only; the authoritative shared-example implementations live
467
538
  under `lib/phronomy/testing/persistence_contract/` so the core suite and external
468
539
  backends cannot drift through copied definitions.
469
540
 
470
- The generic suite verifies repository behavior, CAS semantics, admission,
471
- mutation isolation, and whole-backend transaction behavior. Database-specific
472
- concurrency/locking mechanisms remain backend integration-test concerns; the SPI
473
- specifies outcomes rather than a particular SQL locking strategy.
541
+ The generic suite verifies repository behavior, CAS semantics, durable execution
542
+ admission, mutation isolation, and whole-backend transaction behavior. Runtime
543
+ same-process ownership/admission is tested separately because it is not a
544
+ Persistence Backend SPI responsibility. Database-specific concurrency/locking
545
+ mechanisms remain backend integration-test concerns; the SPI specifies outcomes
546
+ rather than a particular SQL locking strategy.
474
547
 
475
548
  ## SQL implementation guidance
476
549
 
@@ -481,24 +554,40 @@ may use combinations of:
481
554
  - conditional `UPDATE ... WHERE revision = ?`;
482
555
  - row locks;
483
556
  - serializable/repeatable-read isolation where appropriate;
484
- - partial unique indexes for active Agent execution admission;
557
+ - partial unique indexes for durable active Agent execution admission;
485
558
  - transaction-scoped checks for Agent revision + Journal head.
486
559
 
487
560
  Backend-specific database exceptions should be translated to the Phronomy error
488
561
  contract where their meaning is known.
489
562
 
490
- ## Explicit non-goals
491
-
492
- This Backend SPI does not provide:
493
-
494
- - durable reconstruction of a lost Agent Activation;
495
- - serialization of Runtime objects;
496
- - cross-process Workflow execution exclusion;
497
- - exactly-once external Tool side effects;
498
- - automatic conflict reload/merge;
499
- - a generic serializer registry for arbitrary Workflow field classes;
500
- - an async Persistence API.
501
-
502
- For the architectural reasons behind these boundaries, see
503
- [ADR-014: Unified Persistence for Durable State](decisions/014-unified-persistence-durable-state.md)
504
- and [Runtime and concurrency](runtime-and-concurrency.md).
563
+ ## V2 coordination Backend SPI (clean break)
564
+
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.
570
+
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.
@@ -8,24 +8,40 @@ the bounded `OffloadPool`.
8
8
 
9
9
  For the design rationale, see [ADR-010](decisions/010-cooperative-first-concurrency.md).
10
10
  Durable-state ownership is defined by
11
- [ADR-014](decisions/014-unified-persistence-durable-state.md).
11
+ [ADR-014](decisions/014-unified-persistence-durable-state.md), with live Agent
12
+ Runtime execution-state ownership refined by
13
+ [ADR-024](decisions/024-event-loop-single-writer-agent-runtime.md) and process-local
14
+ Agent identity/admission ownership defined by
15
+ [ADR-025](decisions/025-process-local-agent-ownership-and-runtime-admission.md).
16
+ Canonical Workflow instance identity is defined by
17
+ [ADR-020](decisions/020-canonical-workflow-instance-identity.md).
18
+ Concrete FSMSession incarnation identity and session-local Runtime routing are
19
+ defined by [ADR-023](decisions/023-fsm-session-incarnation-identity-and-routing.md).
20
+ Same-process Workflow admission ownership and durable terminal-barrier ordering
21
+ are defined by
22
+ [ADR-026](decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md).
12
23
 
13
24
  ## Runtime model
14
25
 
15
26
  ```text
16
27
  Runtime
28
+ ├─ Agent ownership registry
29
+ │ └─ agent_id -> one mutable live Agent instance
17
30
  ├─ EventLoop (one control-plane operating-system Thread)
18
- └─ FSMSession
19
- ├─ Agent
20
- ├─ Workflow
21
- ├─ ToolInvocation
22
- └─ MultiAgent fan-out
23
- ├─ process-local Agent ActivationRegistry
31
+ ├─ FSMSession
32
+ ├─ Agent
33
+ ├─ Workflow
34
+ ├─ ToolInvocation
35
+ └─ MultiAgent fan-out
36
+ ├─ Agent top-level admission
37
+ │ │ └─ agent_id -> one nonterminal logical Execution admission
38
+ │ └─ Agent execution directory
39
+ │ └─ execution_id -> immutable live-state record
24
40
  ├─ OffloadPool (bounded operating-system Threads)
25
41
  │ ├─ private Operation records
26
42
  │ ├─ blocking input/output (I/O)
27
43
  │ ├─ central-processing-unit (CPU)-bound synchronous work
28
- │ └─ other long synchronous work
44
+ │ └─ operation-specific durable Agent/Workflow work
29
45
  ├─ named OffloadPools
30
46
  └─ EventLoop-driven timers
31
47
 
@@ -44,12 +60,22 @@ A live Agent or Workflow owns its current logical state. `Persistence` is the
44
60
  last committed durable representation and recovery source; it is not reloaded at
45
61
  every semantic boundary.
46
62
 
47
- For Agents, the live owner consists of the Agent instance plus its current
48
- `AgentRoot`, hydrated Journal view, and `AgentExecutionActivation`. Mutable
49
- Agent/Execution/Journal state is not automatically reloaded before every LLM or
50
- Tool step. Durable writes use optimistic revision/position guardrails; an
51
- external writer that advances the durable base causes `Persistence::ConflictError`
52
- rather than automatic reload or merge.
63
+ For active Agents, **EventLoop is the single writer of Phronomy-managed live
64
+ execution state**. EventLoop owns a process-local execution directory keyed by
65
+ canonical `execution_id`. Each directory value is immutable and is replaced on
66
+ EventLoop when the current AgentExecution, RuntimeProjection, AgentInvocation, or
67
+ owning FSMSession changes. The former mutex-protected
68
+ `AgentExecutionActivation` / `ActivationRegistry` model is removed.
69
+
70
+ `AgentInvocation` is the FSM-local mutable context and holds uncommitted Provider
71
+ outcomes, Tool/runtime events, active Provider-call provenance, and callback
72
+ failure state. These fields are advanced only by EventLoop-driven FSMSession
73
+ handling; workers do not receive AgentInvocation as a mutable state authority.
74
+
75
+ Mutable Agent/Execution/Journal state is not automatically reloaded before every
76
+ LLM or Tool step. Durable writes use optimistic revision/position guardrails; an
77
+ external writer that advances the durable base causes
78
+ `Persistence::ConflictError` rather than automatic reload or merge.
53
79
 
54
80
  For Workflows, the current `WorkflowContext` and FSMSession own the active
55
81
  logical state. A durable Workflow hydrates once at invocation/resume and saves at
@@ -58,34 +84,198 @@ the halted/terminal boundary.
58
84
  Content-addressed `Persistence#contents` values are immutable. Fetching a known
59
85
  content reference is value materialization rather than mutable state refresh.
60
86
 
61
- ## Workflow identities and durable admission
87
+ ## Process-local Agent identity ownership and admission
88
+
89
+ `agent_id` identifies one logical Agent, not a reusable lookup key for independent
90
+ mutable objects. One Runtime therefore publishes at most one mutable live Agent
91
+ instance for a given `agent_id`. The Runtime-owned registry is an authority, not a
92
+ cache, and reserves the identity before create/load materialization.
93
+
94
+ The application-facing identity operations are distinct:
95
+
96
+ ```text
97
+ new / create
98
+ create a new Agent; existing live or durable identity is an error
99
+
100
+ load(agent_id, persistence:)
101
+ live -> exact same Ruby object, with no Persistence reload
102
+ durable-only -> hydrate and publish once
103
+ missing -> Persistence::NotFoundError
104
+
105
+ get(agent_id)
106
+ live Runtime lookup only; missing -> nil
107
+ ```
108
+
109
+ A live Agent is strongly owned for the Runtime lifetime even while idle and
110
+ across sequential Executions. Execution completion does not evict it. Clean
111
+ Runtime shutdown detaches old Agent objects so they cannot remain mutable beside
112
+ a later Runtime owner. `purge!` is the explicit earlier destruction boundary: it
113
+ invalidates the old object, deletes durable state, releases the process-local
114
+ identity, and allows a later new Agent to reuse the textual ID.
115
+
116
+ Live Agent ownership and top-level Execution admission are separate lifetimes.
117
+ For one live Agent, EventLoop admits at most one nonterminal top-level Execution.
118
+ Admission is acquired **before** the initial Offload/Persistence operation:
62
119
 
63
- Workflow execution keeps three identities separate:
120
+ ```text
121
+ invoke
122
+ -> EventLoop Agent admission
123
+ -> Offload/Persistence executions.create_active
124
+ -> EventLoop live execution state
125
+ ```
126
+
127
+ `preparing`, `active`, and `suspended` all retain the slot. A competing request is
128
+ rejected with `AgentBusyError`; core does not promise automatic queueing. A
129
+ known-successful durable terminal transition releases the slot. A known
130
+ pre-durable failure may release it; an uncertain durable outcome remains
131
+ fail-closed/recovery-required.
132
+
133
+ `Persistence#executions.create_active`, optimistic revision, Journal position, and
134
+ watermark checks remain required durable defenses. They do not become the
135
+ primary same-process live ownership/admission mechanism and do not provide
136
+ cross-process exclusion.
137
+
138
+ ## EventLoop single-writer and Offload result application
139
+
140
+ Persistence repositories are synchronous, so durable work must remain off the
141
+ EventLoop thread. The ownership rule is therefore not "run everything on
142
+ EventLoop". It is:
143
+
144
+ ```text
145
+ EventLoop
146
+ capture operation-specific immutable state
147
+
148
+ OffloadPool
149
+ blocking I/O / CPU / operation-local calculation
150
+ durable commit
151
+ ↓ operation-specific result
152
+ EventLoop
153
+ validate current authority
154
+ apply committed result to live state
155
+ ```
156
+
157
+ Agent initial preparation, follow-up Manifest preparation, approval resume, and
158
+ terminal commit use distinct command/result values. An Offload worker may commit
159
+ Persistence but does not update the live Agent root, Journal view, current
160
+ AgentExecution, RuntimeProjection, AgentInvocation runtime queues, or EventLoop
161
+ execution directory.
162
+
163
+ Completion callbacks are lightweight bridges that enqueue the result back to the
164
+ EventLoop. If EventLoop no longer accepts the result, the callback does not fall
165
+ back to direct live mutation.
166
+
167
+ ## Provider Call result authority
168
+
169
+ Provider Call identity is purpose-specific semantic provenance. EventLoop
170
+ allocates `llm_call_id` before transport begins and binds it to the Manifest used
171
+ for that call.
172
+
173
+ Provider completion and streaming chunks return through the owning FSMSession's
174
+ EventSink and carry the `llm_call_id`. A result is applicable only when:
175
+
176
+ - it still targets the current FSMSession incarnation;
177
+ - the FSM is in the state that accepts that result; and
178
+ - the AgentInvocation still owns the same active `llm_call_id`.
179
+
180
+ A callback to an old FSMSession incarnation is dropped by session-local routing.
181
+ A result with a stale `llm_call_id` is consumed without advancing the current
182
+ FSM. Phronomy does not add a generic generation/correlation token as another
183
+ result authority.
184
+
185
+ Tool operations follow the same ownership direction. `tool_invocation_id` is the
186
+ semantic Tool-operation identity, while FSMSession ID is Runtime routing identity.
187
+ Tool authorization captures Agent identity and Tool description data as values on
188
+ EventLoop before offload. The authorization worker receives no live Agent, Tool, or
189
+ ToolInvocation reference. Application-owned approval/facts/requirement callables
190
+ are explicitly classified behavior handles and receive a value-only
191
+ `ApprovalEvaluationRequest`.
192
+
193
+ Hash, Array, and String authorization command data is recursively copied/frozen.
194
+ Phronomy-managed live domain objects are rejected from that value data. A complete
195
+ value-type/serialization contract for arbitrary Application-owned opaque objects is
196
+ deferred; such objects remain Application-owned and must be worker-safe.
197
+
198
+ Worker authorization/execution outcomes return as values carrying
199
+ `tool_invocation_id`; the Tool FSMSession consumes a mismatched semantic result
200
+ without advancing its current state.
201
+
202
+ ## Approval suspension and live owner lookup
203
+
204
+ Approval suspension retains the same process-local Agent and AgentInvocation but
205
+ has no active owning FSMSession until resume. EventLoop retains the suspended
206
+ execution entry.
207
+
208
+ `Agent::Base.live_for_execution(execution_id)` resolves a read-only Runtime owner
209
+ view and returns the existing Agent instance. `agent.approve_async(...)` routes to
210
+ the same live coordinator. Neither operation reloads a replacement Agent or
211
+ Execution from Persistence.
212
+
213
+ A resume performs its durable approval transition through OffloadPool, applies
214
+ the result on EventLoop, and then builds a **fresh** FSMSession incarnation.
215
+ If the process-local owner no longer exists, durable continuation reconstruction
216
+ is not implied; `ExecutionRehydrationRequiredError` is raised.
217
+
218
+ ## Workflow identities, admission, and durable terminal barrier
219
+
220
+ Workflow runtime keeps identity and coordination responsibilities separate:
64
221
 
65
222
  ```text
66
223
  session_id
67
- application session/correlation identity
224
+ application session/correlation metadata
68
225
 
69
- thread_id
70
- durable Workflow identity and Persistence#workflow_states key
226
+ workflow_instance_id
227
+ logical/durable Workflow identity and Persistence#workflow_states key
228
+
229
+ admission owner token
230
+ opaque process-local Runtime coordination capability
71
231
 
72
232
  fsm_session_id
73
- one Runtime FSMSession execution identity; generated again for each invoke/resume
233
+ one concrete Runtime FSMSession routing identity
234
+ ```
235
+
236
+ EventLoop acquires the `workflow_instance_id` admission with a fresh opaque owner
237
+ token **before** `workflow_states.load(workflow_instance_id)` is submitted. The
238
+ token is not a domain identity and is never an Event target. After durable
239
+ hydration, EventLoop constructs the concrete FSMSession and binds its fresh
240
+ `fsm_session_id` to the existing admission for `Workflow#signal` routing.
241
+
242
+ ```text
243
+ admit workflow_instance_id with owner token
244
+
245
+ Offload workflow_states.load
246
+
247
+ EventLoop hydrate / create FSMSession
248
+
249
+ bind fsm_session_id for routing
74
250
  ```
75
251
 
76
- The application `session_id` is tracing/caller metadata and is not used for
77
- durable Workflow ownership. EventLoop registers active FSMs by `fsm_session_id`;
78
- durable Workflow admission is a separate owner map:
252
+ A durable Workflow also keeps terminal persistence inside the FSMSession
253
+ lifecycle. Logical halt/completion first enters a private
254
+ `persisting_terminal` lifecycle condition; the FSMSession remains nonterminal
255
+ while WorkflowRunner saves the terminal snapshot through OffloadPool. Only a
256
+ known-successful save result returned to that same FSMSession permits
257
+ `HALTED`/`COMPLETED`, admission release, and caller Task settlement.
79
258
 
80
259
  ```text
81
- thread_id -> owner_fsm_session_id
260
+ RUNNING
261
+ ↓ logical halt/completion
262
+ PERSISTING_TERMINAL
263
+ ├─ known success -> HALTED / COMPLETED -> release
264
+ ├─ known failure -> ERROR -> release
265
+ └─ outcome unknown -> RECOVERY_REQUIRED (fail closed)
82
266
  ```
83
267
 
84
- The owner is acquired before `workflow_states.load(thread_id)` and remains held
85
- until the halted/terminal `workflow_states.save(...)` completes. The admission map
86
- is process-local. Cross-process duplicate execution requires application-level
87
- distributed coordination; optimistic revisions detect stale terminal commits but
88
- do not prevent duplicate side effects before that conflict is detected.
268
+ The FSMSession does not know whether Persistence is local, remote, SQL, HTTP, or
269
+ networked. The Workflow persistence operation normalizes the result into
270
+ `success`, `known_failure`, or `outcome_unknown`. Only known success crosses the
271
+ durable barrier. If the backend/storage error does not establish non-commit,
272
+ Phronomy treats the terminal outcome as uncertain rather than guessing.
273
+
274
+ `recovery_required` prevents a fresh same-Workflow execution segment from being
275
+ admitted, but ACS-13 does not claim restart-safe reconciliation; that remains
276
+ ACS-15 work. The admission map itself is process-local. Cross-process duplicate
277
+ execution requires the later coordination/fencing work; optimistic revisions
278
+ remain durable conflict defense rather than distributed ownership.
89
279
 
90
280
  ## Tool execution modes
91
281
 
@@ -132,8 +322,7 @@ waiting at the same time.
132
322
  `Persistence` repositories expose synchronous operations. Framework lifecycle
133
323
  code must not perform potentially blocking durable reads/writes on EventLoop.
134
324
  Agent preparation/commit and Workflow hydrate/save operations are submitted to
135
- `OffloadPool`; completion continues through Task callbacks or explicit EventLoop
136
- events.
325
+ `OffloadPool`; completion continues through explicit EventLoop events.
137
326
 
138
327
  A durable barrier may pause one logical lifecycle without blocking EventLoop.
139
328
  Persistence does not implement async repository variants and must not depend on
@@ -174,8 +363,9 @@ abandonment state.
174
363
  `Task#on_complete` registers an independent notification callback. Callback
175
364
  execution thread is not guaranteed. A callback may be delivered by an OffloadPool
176
365
  worker, a timer/cancellation caller, an EventLoop-related control path, or the
177
- thread that registers after settlement. Callbacks must be thread-safe and should
178
- complete quickly.
366
+ thread that registers after settlement. Callbacks must therefore be thread-safe
367
+ and should complete quickly. Framework lifecycle code normally converts worker
368
+ completion into an explicit EventLoop event before applying live state.
179
369
 
180
370
  `Task#map` is application-level composition. A transformation exception settles
181
371
  the mapped Task as failed.
@@ -301,6 +491,10 @@ Workflow durable admission participates in EventLoop idleness: a Workflow whose
301
491
  FSMSession has ended but whose durable save is still in flight remains owned until
302
492
  that save completes and owner-aware admission is released.
303
493
 
494
+ Suspended Agent execution owner entries are process-local continuation state and
495
+ do not by themselves keep Runtime shutdown waiting. Runtime/process loss does not
496
+ imply durable Agent continuation reconstruction.
497
+
304
498
  `Phronomy.reset_runtime!` exists primarily for test isolation and performs a real
305
499
  Runtime shutdown before resetting configuration.
306
500
 
@@ -0,0 +1,13 @@
1
+ # Phronomy examples
2
+
3
+ Runnable application examples are maintained in the dedicated
4
+ [`Raizo-TCS/phronomy-examples`](https://github.com/Raizo-TCS/phronomy-examples)
5
+ repository.
6
+
7
+ This repository intentionally does not keep a second runnable example set under
8
+ `examples/`. Keeping one canonical sample repository avoids API drift between
9
+ Phronomy's implementation/documentation and independently maintained copies of
10
+ sample code.
11
+
12
+ For framework-level executable behavior and regression coverage, use the specs
13
+ under `spec/`. For application-oriented examples, use `phronomy-examples`.