phronomy 0.18.0 → 0.20.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/CONTRIBUTING.md +30 -0
  4. data/README.md +2 -0
  5. data/docs/decisions/009-state-store-abstraction.md +1 -1
  6. data/docs/decisions/014-unified-persistence-durable-state.md +273 -0
  7. data/docs/features.md +27 -1
  8. data/docs/getting-started.md +37 -1
  9. data/docs/migrations/0.19.md +154 -0
  10. data/docs/persistence-backends.md +504 -0
  11. data/docs/runtime-and-concurrency.md +93 -2
  12. data/lib/phronomy/agent/agent_execution.rb +29 -0
  13. data/lib/phronomy/agent/base.rb +81 -36
  14. data/lib/phronomy/agent/context_assembler.rb +13 -3
  15. data/lib/phronomy/agent/execution_coordinator.rb +420 -249
  16. data/lib/phronomy/agent/journal_projection.rb +5 -1
  17. data/lib/phronomy/agent/llm_call_record.rb +20 -0
  18. data/lib/phronomy/configuration.rb +2 -1
  19. data/lib/phronomy/engine/event_loop.rb +86 -8
  20. data/lib/phronomy/engine/fsm_session.rb +6 -4
  21. data/lib/phronomy/engine/runtime.rb +7 -0
  22. data/lib/phronomy/persistence/in_memory.rb +113 -8
  23. data/lib/phronomy/persistence.rb +109 -6
  24. data/lib/phronomy/testing/persistence_contract/a_content_store.rb +50 -0
  25. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +164 -0
  26. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +215 -0
  27. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +119 -0
  28. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +99 -0
  29. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +202 -0
  30. data/lib/phronomy/testing/persistence_contract.rb +41 -0
  31. data/lib/phronomy/version.rb +1 -1
  32. data/lib/phronomy/workflow.rb +10 -9
  33. data/lib/phronomy/workflow_runner.rb +361 -95
  34. data/lib/phronomy.rb +9 -0
  35. metadata +12 -5
  36. data/lib/phronomy/state_store/base.rb +0 -48
  37. data/lib/phronomy/state_store/in_memory.rb +0 -62
  38. data/scripts/check_private_enforcement.rb +0 -93
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ded63907d387d139d6f9017448da33707e9105ce45f49f246aac1aa764058434
4
- data.tar.gz: 7e0125a2bec397563df61421474f0599e9773a9a6b6636f6949a5a858db8cb3d
3
+ metadata.gz: 8a9f9f2ab4acdb4e17edeb4e0b7f0b33660b7102e5b345c2bd8d411e6b0760de
4
+ data.tar.gz: 331265c52dfadbe4be8f85b9a48b35de384042664538f4fa74db0d34829e3b62
5
5
  SHA512:
6
- metadata.gz: 694de5028ce07c43599b55de9380316f7210d9936a683924acd2d68c5f05348fb772fdc978726ef0782abf2d4d7c03146b32bf2eea3b78e7e54505161fe77c2c
7
- data.tar.gz: 9d0217ddd076c29985d48eb1061cc92338054f54cf017c21425d263b056f22072c2657f3c06ef4db8142c08cf4b126b3d2c4c7cdc87409b718cc6cdcffdda2f4
6
+ metadata.gz: f3c7e0b15ede3129a2ac426ed86ebde36e353c664b5c64fc559878996ab6331b86f88c289e2f47179f0d1c58e91ec23c0c3d23060f066168c1a441e349dd534f
7
+ data.tar.gz: 527e858a300d3e8fb305782cdab29d6195b3836686df5b4d41a7abf8a40b584de8a8f0dcdab8ccd5b7b5dcfbcbf0c0dad45a7be2f092eae69c2c643ff69e9734
data/CHANGELOG.md CHANGED
@@ -12,6 +12,31 @@ Release history for 0.14.0 and earlier is archived in
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ### Unified Persistence and durable-state ownership
16
+
17
+ #### Added
18
+
19
+ - `Persistence#workflow_states` with optimistic revision checks for durable Workflow snapshots.
20
+ - Runtime-local Agent Activation ownership; live `AgentExecutionActivation` values are no longer Persistence repositories.
21
+ - `Agent::Base.live_for_execution(execution_id)` for resolving the current process's live owner Agent without reloading Agent or Execution state from Persistence.
22
+ - Owner-aware Workflow admission keyed by durable `thread_id` and owned by a per-execution internal `fsm_session_id`.
23
+ - ADR-014 and the 0.19 migration guide for the unified durable-state architecture.
24
+
25
+ #### Changed
26
+
27
+ - Live Agent instances now remain the authoritative logical-state owners after hydration. Context Policy and follow-up Manifest preparation use the Agent-local root, Journal view, and Activation state instead of reloading mutable Agent state for freshness.
28
+ - `Phronomy.configuration.persistence` is the global durable backend for Workflows and for Agent `new`/`create` calls that do not explicitly inject another Persistence instance.
29
+ - Agent durable writes use optimistic revision/Journal-position guardrails; conflicting external writes fail instead of being silently reloaded or merged.
30
+ - Approval suspension/resume preserves the same live Agent/Activation/AgentInvocation. Approval remains an Agent-instance operation; callers with only an `execution_id` resolve the live owner with `Agent::Base.live_for_execution` (or the expected concrete Agent class) before calling `agent.approve` / `agent.approve_async`.
31
+ - Workflow durable I/O runs outside EventLoop through OffloadPool, while `thread_id` admission remains owned until terminal/halted snapshot persistence completes inside the current Runtime. Workflow admission is process-local; optimistic revisions detect stale commits across processes but do not prevent duplicate execution or undo already-performed external side effects.
32
+ - Workflow `thread_id`, Runtime `fsm_session_id`, and application `session_id` now have distinct responsibilities.
33
+
34
+ #### Removed
35
+
36
+ - `Phronomy::StateStore`, `StateStore::InMemory`, `Workflow.define(..., state_store:)`, `Configuration#state_store`, and per-invocation `config[:state_store]`.
37
+ - `Persistence#activations`; ActivationRegistry is transient Runtime state.
38
+ - Class-level `Agent::Base.approve` / `Agent::Base.approve_async` routing APIs and their caller-supplied `persistence:` argument; approval execution now goes through the resolved live Agent instance.
39
+
15
40
  ### OffloadPool execution model
16
41
 
17
42
  #### Changed
data/CONTRIBUTING.md CHANGED
@@ -52,6 +52,36 @@ When adding, removing, or renaming a public method or class:
52
52
  bundle exec ruby scripts/api_snapshot.rb --write
53
53
  ```
54
54
 
55
+ ### `@api` classification vs Ruby visibility
56
+
57
+ Phronomy's YARD `@api` annotation describes the compatibility boundary; it is
58
+ not a synonym for Ruby's `public` / `protected` / `private` keywords.
59
+
60
+ - `@api public` means consumers or extension implementers may rely on the
61
+ documented contract. Ruby visibility still follows the intended calling
62
+ model: ordinary methods may be public, subclass extension helpers may be
63
+ protected, and `initialize` remains Ruby-private while construction is
64
+ exposed through `.new`.
65
+ - `@api private` means the method is internal and carries no public compatibility
66
+ promise. It may still be Ruby-public when Phronomy components need to call it
67
+ through an explicit receiver.
68
+ - Ruby visibility is therefore not inferred from the `@api` annotation in
69
+ either direction.
70
+
71
+ Run the annotation coverage guard when changing documented methods:
72
+
73
+ ```bash
74
+ ruby scripts/check_api_annotations.rb
75
+ ```
76
+
77
+ Ruby-public compatibility for the primary Stable/Beta product surface is
78
+ protected by `scripts/api_snapshot.rb` and
79
+ `spec/phronomy/api_compatibility_spec.rb`. Extension contracts whose calling
80
+ model is protected/private should be protected by focused specs for that
81
+ contract rather than by a repository-wide visibility inference rule.
82
+
83
+ Do not change Ruby visibility merely to make it match an `@api` annotation.
84
+
55
85
  ---
56
86
 
57
87
  ## Architecture Decision Records
data/README.md CHANGED
@@ -16,6 +16,7 @@ tracking `main` directly.
16
16
  ## Core concepts
17
17
 
18
18
  - **Agent** — stateful, persistence-backed LLM agent with canonical execution history.
19
+ - **Persistence** — unified durable backend for Agent state and Workflow `workflow_states`.
19
20
  - **Workflow** — state-machine-driven application workflow with explicit events and wait states.
20
21
  - **Tool / Capability** — callable application capability exposed to an Agent.
21
22
  - **EventLoop + FSMSession** — the framework control plane for logical lifecycle coordination.
@@ -117,6 +118,7 @@ contracts, timeout/cancellation semantics, metrics, and callback rules.
117
118
  - [MCP client](docs/mcp-client.md) — Model Context Protocol (MCP) integration and supported schema subset.
118
119
  - [Migration from 0.15-era APIs](docs/migrations/0.15.md).
119
120
  - [0.16 cleanup migration](docs/migrations/0.16.md).
121
+ - [0.19 unified Persistence migration](docs/migrations/0.19.md).
120
122
  - [Architecture Decision Records](docs/decisions/) — design rationale and superseding decisions.
121
123
  - [CHANGELOG](CHANGELOG.md) — current development and recent release history.
122
124
  - [Changelog archive: 0.14.0 and earlier](docs/changelog/0.14-and-earlier.md).
@@ -1,6 +1,6 @@
1
1
  # ADR 009: StateStore Abstraction for Workflow Persistence
2
2
 
3
- **Status**: Accepted
3
+ **Status**: Superseded by ADR-014
4
4
  **Date**: 2025-01
5
5
  **Issue**: [#250](https://github.com/Raizo-TCS/phronomy/issues/250)
6
6
 
@@ -0,0 +1,273 @@
1
+ # ADR 014: Unified Persistence for Durable State
2
+
3
+ **Status**: Accepted
4
+ **Date**: 2026-08-14
5
+ **Supersedes**: ADR-009
6
+
7
+ ---
8
+
9
+ ## Context
10
+
11
+ Phronomy historically had two durable-state abstractions:
12
+
13
+ - `Phronomy::Persistence` for Agent roots, Journals, content and executions;
14
+ - `Phronomy::StateStore` for Workflow snapshots.
15
+
16
+ At the same time, `Persistence` exposed `activations`, although
17
+ `AgentExecutionActivation` contains live process state such as the active
18
+ AgentInvocation, FSMSession, runtime projection, callbacks and uncommitted runtime
19
+ facts. That state is not a durable database record.
20
+
21
+ Keeping two durable backend abstractions makes SQL/database backends implement the
22
+ same responsibility twice. Treating live Activations as Persistence also mixes
23
+ process-local execution ownership with durable storage ownership.
24
+
25
+ A second problem is state freshness. Re-reading mutable Agent records from
26
+ Persistence before every semantic boundary makes the durable copy act as the
27
+ source of truth even while a live Agent instance is executing. That makes
28
+ ownership ambiguous and encourages implicit merge/reload semantics.
29
+
30
+ Workflow runtime identity also needs to be distinct from its durable identity.
31
+ `InvocationContext#session_id` already means application session metadata (for
32
+ example a Rails session), while Workflow `thread_id` identifies durable Workflow
33
+ state. Neither value is the identity of one concrete FSMSession execution.
34
+
35
+ ## Decision
36
+
37
+ ### Persistence is the only durable backend abstraction
38
+
39
+ `Phronomy::Persistence` exposes the durable repositories:
40
+
41
+ ```text
42
+ contents
43
+ agents
44
+ journals
45
+ executions
46
+ workflow_states
47
+ ```
48
+
49
+ `Phronomy::StateStore` is removed without a compatibility adapter. Workflow
50
+ persistence is configured with `persistence:` or
51
+ `Phronomy.configuration.persistence` and stored through
52
+ `Persistence#workflow_states`. Agent `new`/`create` also uses the global
53
+ Persistence when no explicit backend is injected, so an application can select
54
+ one durable backend for both domains. Explicit injection takes precedence.
55
+
56
+ The `workflow_states` repository uses optimistic revision metadata:
57
+
58
+ ```ruby
59
+ record = persistence.workflow_states.load(thread_id)
60
+ # => nil or { snapshot: ..., revision: Integer }
61
+
62
+ persistence.workflow_states.save(
63
+ thread_id,
64
+ expected_revision: record&.fetch(:revision),
65
+ snapshot: snapshot
66
+ )
67
+ ```
68
+
69
+ A stale expected revision raises `Phronomy::Persistence::ConflictError`. Upper
70
+ layers do not automatically reload and merge after such a conflict.
71
+
72
+ The normative contract for custom durable backends, including repository
73
+ semantics, transaction requirements, capabilities, durable codecs, and the Agent
74
+ watermark precondition, is documented in
75
+ [`docs/persistence-backends.md`](../persistence-backends.md).
76
+
77
+ ### Activation is transient Runtime state
78
+
79
+ `AgentExecutionActivation` is not part of the Persistence contract. Live
80
+ Activations are held by a Runtime-local `Agent::ActivationRegistry`.
81
+
82
+ Durable execution rehydration is a separate future capability. When no live
83
+ Activation exists, approval resume raises
84
+ `ExecutionRehydrationRequiredError`; the framework does not silently construct a
85
+ new Agent and pretend that the process-local execution was recovered.
86
+
87
+ ### A live Agent instance owns its logical current state
88
+
89
+ After hydration, the live Agent instance and its Activation are authoritative for
90
+ mutable logical state:
91
+
92
+ ```text
93
+ Agent instance
94
+ ├─ current AgentRoot
95
+ ├─ hydrated Journal/context view
96
+ └─ active execution entry
97
+
98
+ AgentExecutionActivation
99
+ ├─ current AgentExecution
100
+ ├─ runtime projection
101
+ ├─ Provider results
102
+ ├─ Tool/runtime events
103
+ └─ active AgentInvocation / call state
104
+ ```
105
+
106
+ Persistence is the last committed durable representation and recovery source.
107
+ Normal LLM/Tool cycles do not re-read mutable AgentRoot, AgentExecution or Journal
108
+ records merely to obtain freshness. Successful optimistic commits advance the
109
+ local root/execution/Journals; failed revision checks surface a conflict.
110
+
111
+ Before a next-LLM durable barrier, the backend verifies the live owner's Agent
112
+ revision and Journal position as a durable watermark. This check is a precondition
113
+ only: it does not return replacement state to the live Agent. If another writer
114
+ has advanced either value, the barrier raises `ConflictError` and no Provider
115
+ Call starts from the stale local owner.
116
+
117
+ Content-addressed `contents` are immutable values and may still be dereferenced by
118
+ content reference on demand. That is not a mutable-state refresh.
119
+
120
+ Context Policy therefore sees the current local logical state rather than a
121
+ Persistence readback. Runtime facts captured after a Persistence snapshot remain
122
+ in the Activation and are eligible at the next semantic boundary.
123
+
124
+ ### Approval resumes the same owner
125
+
126
+ An approval suspension retains the original Agent instance,
127
+ `AgentExecutionActivation` and `AgentInvocation`. Instance-level
128
+ `agent.approve` / `agent.approve_async` resume that live Activation.
129
+
130
+ When an application has only an `execution_id`, it resolves the current process's
131
+ live owner first:
132
+
133
+ ```text
134
+ execution_id
135
+ -> Agent::Base.live_for_execution(...)
136
+ -> Runtime ActivationRegistry
137
+ -> live Activation
138
+ -> activation.agent
139
+ ```
140
+
141
+ A concrete Agent class may call `MyAgent.live_for_execution(execution_id)` to
142
+ also verify that the live owner is an instance of that class. The lookup does not
143
+ load Agent or Execution state from Persistence. If the live Activation no longer
144
+ exists, `ExecutionRehydrationRequiredError` is raised rather than silently
145
+ rehydrating another owner.
146
+
147
+ Approval remains an Agent-instance operation after lookup:
148
+
149
+ ```ruby
150
+ agent = MyAgent.live_for_execution(execution_id)
151
+ agent.approve_async(execution_id, approval_request_id: request_id)
152
+ ```
153
+
154
+ ### Workflow has three distinct identities
155
+
156
+ The following identities must not be conflated:
157
+
158
+ ```text
159
+ application session_id
160
+ caller/tracing identity, e.g. Rails session
161
+
162
+ thread_id
163
+ durable Workflow identity / workflow_states key
164
+
165
+ fsm_session_id
166
+ Runtime-only identity of one FSMSession invocation or resume
167
+ ```
168
+
169
+ Each Workflow invocation/resume receives a fresh `fsm_session_id`. The EventLoop
170
+ registers the session by that ID.
171
+
172
+ For a Workflow with a durable `thread_id`, EventLoop also owns a separate
173
+ admission table:
174
+
175
+ ```text
176
+ thread_id -> owner_fsm_session_id
177
+ ```
178
+
179
+ Admission is acquired before Persistence load and held through FSM execution and
180
+ terminal snapshot save. Release succeeds only when the supplied
181
+ `owner_fsm_session_id` is the current owner. A failed competing invocation can
182
+ therefore never release another invocation's reservation.
183
+
184
+ The admission table belongs to one Runtime and is process-local. It prevents
185
+ competing execution of the same `thread_id` inside that Runtime, but it is not a
186
+ distributed lease and is not shared by multiple Ruby processes, containers, or
187
+ service replicas. Two processes may therefore execute the same durable
188
+ `thread_id` concurrently if an application routes work that way.
189
+
190
+ `workflow_states` optimistic revisions still detect stale/double commits between
191
+ those processes. They do not prevent both executions from starting and cannot
192
+ roll back external side effects already performed before one terminal save loses
193
+ the revision race. CAS is therefore stale/double-commit protection, not
194
+ distributed execution exclusion or duplicate-side-effect prevention.
195
+
196
+ `fsm_session_id` is Runtime metadata. It is not an application Workflow field and
197
+ is not stored in `workflow_states` snapshots.
198
+
199
+ ### Persistence I/O stays off EventLoop
200
+
201
+ The Persistence repository contract remains synchronous. Persistence operations
202
+ started by EventLoop-driven Workflow/Agent lifecycle code execute through the
203
+ bounded OffloadPool. Completion is converted back into an EventLoop event where
204
+ lifecycle ordering requires it.
205
+
206
+ No worker waits synchronously for an FSMSession or Task that requires EventLoop
207
+ progress.
208
+
209
+ ### Backend synchronization is an implementation detail
210
+
211
+ `Persistence::InMemory` uses one shared `Monitor` for all durable repositories,
212
+ including `workflow_states`. Workflow values retain StateStore-like deep-copy
213
+ semantics and are kept outside the Marshal snapshot used by the existing Agent
214
+ state container, while both remain inside the same transaction domain.
215
+
216
+ A SQL backend should implement the same repository/transaction contract using DB
217
+ transactions, isolation and optimistic revision constraints. Mutex/Monitor and
218
+ Ruby object identity are not part of the public repository contract.
219
+
220
+ ## Consequences
221
+
222
+ ### Positive
223
+
224
+ - Agent and Workflow durability share one backend configuration and transaction
225
+ abstraction.
226
+ - Process-local Activation state is no longer presented as durable Persistence.
227
+ - Active Agent ownership is explicit; Context Policy freshness no longer depends
228
+ on repeated mutable-state reloads.
229
+ - Approval owner lookup and approval execution are separate: class-level lookup
230
+ resolves the existing live Agent, while approval remains an instance operation.
231
+ - Revision conflicts are visible instead of being hidden by automatic merge.
232
+ - Workflow durable identity is independent of one Runtime FSMSession execution.
233
+ - Owner-aware Workflow admission closes the stale-load window during terminal
234
+ save inside one Runtime.
235
+ - InMemory and future SQL backends can share one contract test suite.
236
+
237
+ ### Trade-offs
238
+
239
+ - Removing `StateStore` is a pre-1.0 breaking change.
240
+ - A process restart cannot resume an approval until durable Activation/FSM
241
+ rehydration is implemented separately.
242
+ - Runtime Activation lookup and Workflow admission are process-local. Applications
243
+ requiring cross-process ownership or duplicate-execution prevention need a
244
+ separate routing/lease/fencing design.
245
+ - Optimistic Workflow revisions can reject a stale commit but cannot undo
246
+ duplicate external side effects performed before that conflict is observed.
247
+ - Applications that intentionally allow another process to edit the same live
248
+ Agent or Workflow must handle `ConflictError` and explicitly reload/reconcile.
249
+ - `Persistence` implementations must provide `workflow_states` and optimistic
250
+ revision semantics.
251
+
252
+ ## Rejected alternatives
253
+
254
+ ### Keep StateStore as a compatibility facade
255
+
256
+ Rejected. It preserves the duplicate durable-backend abstraction and makes future
257
+ backend behavior harder to reason about.
258
+
259
+ ### Reload Agent state before every LLM call
260
+
261
+ Rejected. It makes Persistence the live source of truth, creates implicit merge
262
+ semantics and conflicts with Agent-instance ownership.
263
+
264
+ ### Use Workflow thread_id as FSMSession id
265
+
266
+ Rejected. Durable identity and one Runtime execution have different lifetimes.
267
+ It also makes owner-aware admission and application `session_id` terminology
268
+ ambiguous.
269
+
270
+ ### Release Workflow admission by thread_id only
271
+
272
+ Rejected. Cleanup from an invocation that failed to acquire admission could then
273
+ release the real owner's reservation. Release is owner-aware by construction.
data/docs/features.md CHANGED
@@ -19,6 +19,7 @@ for production deployments.
19
19
  |---|---|
20
20
  | **Workflow** — Stateful, branching workflows with `wait_state` and explicit events | Stable |
21
21
  | **Agent** — Stateful ReAct-style agents with stable `agent_id`, persistence-backed execution state, canonical history, guardrails, and conversation context | Stable |
22
+ | **Unified Persistence** — One durable backend abstraction for Agent state and Workflow `workflow_states`; live Agent/Workflow state remains owned by the active instance/session between durable commits; custom backends implement the documented Backend SPI and repository/transaction semantics | Beta |
22
23
  | **Before-Large-Language-Model (LLM) Input Hook** — Three-tier per-call LLM input customization via `before_llm_input` and `LLMInputPatch` | Stable |
23
24
  | **Context Management** — Journal + Context Policy + per-LLM-call Manifest with token-budget-aware selection and protocol-safe Tool Call / Tool message dependencies | Stable |
24
25
  | **Filters** — Input/output transformation and blocking via `Filter::Base` | Beta |
@@ -44,6 +45,7 @@ for production deployments.
44
45
  | Feature | Stability |
45
46
  |---|---|
46
47
  | **EventLoop** — Runtime-owned event-driven execution core shared by Agent, ToolInvocation, Workflow, and MultiAgent sessions | Beta |
48
+ | **Workflow durable admission** — Durable `thread_id` ownership is held by a Runtime-only `fsm_session_id` from load through terminal save | Beta |
47
49
  | **`invoke` / `invoke_async`** — Blocking and non-blocking Agent/Workflow entry points | Stable |
48
50
  | **Agent async events** — `invoke_async(..., on_event:)` and `stream_async(..., on_event:)`; streaming additionally emits `:token` | Beta |
49
51
  | **`stream` / `stream_async`** — Event callbacks execute on EventLoop and must return quickly | Beta |
@@ -66,7 +68,7 @@ for production deployments.
66
68
  | **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
67
69
  | **`Phronomy::MultiAgent::TeamCoordinator`** — LLM coordinator with stateful worker Agents | Beta |
68
70
  | **SharedState** — Peer-agent shared-state coordination | Experimental |
69
- | **Human-in-the-loop approval** — Suspension and approval/resume of Tool requests | Beta |
71
+ | **Human-in-the-loop approval** — Suspension and approval/resume of Tool requests on the same live Agent/Activation owner | Beta |
70
72
  | **`tool_approval_policy`** — Application-defined allow/approve/reject policy | Beta |
71
73
 
72
74
  ## Public API boundary
@@ -76,12 +78,36 @@ Source declarations marked `@api private`, including most EventLoop/FSMSession a
76
78
  OffloadPool internals, are implementation details and may change without the same
77
79
  compatibility guarantees.
78
80
 
81
+ The YARD `@api` classification is independent from Ruby language visibility in
82
+ both directions. `@api public` marks a compatibility contract, but the Ruby
83
+ visibility still follows the intended calling model: ordinary APIs may be
84
+ public, subclass extension helpers may be protected, and constructors use
85
+ Ruby-private `initialize` behind `.new`. `@api private` means "internal/no
86
+ compatibility promise" and does not require a Ruby `private` declaration; some
87
+ internal methods remain Ruby-public because Phronomy components call them
88
+ through explicit receivers.
89
+
90
+ Persistence Backend SPI methods are a deliberate exception to the ordinary
91
+ application-facing interpretation of `@api public`: they are public extension
92
+ contracts for backend implementers, but application business logic should usually
93
+ interact with Agents/Workflows instead of calling low-level repository operations.
94
+ See [Persistence backend contract](persistence-backends.md).
95
+
96
+ `Phronomy::StateStore` is no longer a public backend abstraction. Workflow
97
+ durability is provided through `Phronomy::Persistence#workflow_states`; see the
98
+ 0.19 migration guide when upgrading code that used `state_store:`.
99
+
79
100
  ## Advanced and internal APIs
80
101
 
81
102
  | Feature | Stability |
82
103
  |---|---|
83
104
  | **`Phronomy::Diagnostics`** — Snapshot of EventLoop lag/queue state and OffloadPool activity | Experimental |
84
105
  | **`Phronomy::Testing::FakeClock`** — Test-only deterministic clock helper | Beta |
106
+ | **`Phronomy::Testing::PersistenceContract`** — Explicitly loaded RSpec conformance suite for custom Persistence backends | Beta |
107
+
108
+ `Phronomy::Testing::PersistenceContract` is available only after explicit
109
+ `require "phronomy/testing/persistence_contract"`. Ordinary
110
+ `require "phronomy"` and production eager-load do not load RSpec.
85
111
 
86
112
  For runtime ownership and the distinction between public lifecycle APIs and
87
113
  private execution machinery, see [Runtime and concurrency](runtime-and-concurrency.md).
@@ -107,6 +107,13 @@ agent = ResearchAgent.load(
107
107
  agent.invoke("Continue our previous discussion.")
108
108
  ```
109
109
 
110
+ `load` is the hydration boundary. While that Agent instance is live, the
111
+ instance and its active `AgentExecutionActivation` own the current logical state.
112
+ Phronomy persists snapshots at defined durability boundaries but does not reload
113
+ mutable Agent/Execution/Journal state before every LLM or Tool step. A conflicting
114
+ external durable write is surfaced as `Persistence::ConflictError` rather than
115
+ silently merged into the live instance.
116
+
110
117
  The active transcript and Knowledge views can be advanced independently without
111
118
  deleting the append-only canonical Journal:
112
119
 
@@ -172,6 +179,8 @@ end
172
179
  ```
173
180
 
174
181
  From an EventLoop callback, use `approve_async` rather than blocking EventLoop.
182
+ Approval resume continues the same live Agent instance, Activation, and
183
+ AgentInvocation; it is not an Agent reload boundary.
175
184
 
176
185
  ## Workflow basics
177
186
 
@@ -188,7 +197,12 @@ end
188
197
  write_draft = ->(state) { state.merge(draft: "Draft content") }
189
198
  review_draft = ->(state) { state.merge(feedback: "Feedback on: #{state.draft}") }
190
199
 
191
- workflow = Phronomy::Workflow.define(ReviewContext) do
200
+ persistence = Phronomy::Persistence::InMemory.new
201
+
202
+ workflow = Phronomy::Workflow.define(
203
+ ReviewContext,
204
+ persistence: persistence
205
+ ) do
192
206
  initial :write
193
207
  state :write, action: write_draft
194
208
  state :review, action: review_draft
@@ -207,6 +221,28 @@ final = workflow.send_event(state: state, event: :approve)
207
221
  puts final.approved
208
222
  ```
209
223
 
224
+ `Persistence#workflow_states` is the durable Workflow repository. `thread_id`
225
+ identifies the durable Workflow state and remains stable across resume. Each
226
+ concrete Runtime execution receives a separate internal `fsm_session_id`; the
227
+ application-level `session_id` remains ordinary caller/tracing metadata. Phronomy
228
+ holds owner-aware admission for `thread_id` from durable load through terminal
229
+ save so another local invocation cannot start from a stale snapshot while the
230
+ current owner is still committing.
231
+
232
+ A global Persistence backend can be configured when Agents and Workflows should
233
+ share one durable backend:
234
+
235
+ ```ruby
236
+ Phronomy.configure do |config|
237
+ config.persistence = persistence
238
+ end
239
+ ```
240
+
241
+ Agent `new`/`create` and Workflow definitions use the global backend when they do
242
+ not inject an explicit `persistence:`. Workflow durability is fixed at the
243
+ application/Workflow-definition boundary; there is no per-invocation backend
244
+ switch.
245
+
210
246
  Workflow entry and transition actions are synchronous Run-to-Completion
211
247
  callbacks. If a Workflow needs an Agent or another asynchronous lifecycle, start
212
248
  it asynchronously, return the Workflow context immediately, and deliver its
@@ -0,0 +1,154 @@
1
+ # Migrating to 0.19: Unified Persistence
2
+
3
+ This release removes the separate Workflow `StateStore` abstraction and uses
4
+ `Phronomy::Persistence` for both Agent and Workflow durable state.
5
+
6
+ ## Workflow persistence
7
+
8
+ Before:
9
+
10
+ ```ruby
11
+ store = Phronomy::StateStore::InMemory.new
12
+
13
+ workflow = Phronomy::Workflow.define(
14
+ MyContext,
15
+ state_store: store
16
+ ) do
17
+ # ...
18
+ end
19
+ ```
20
+
21
+ After:
22
+
23
+ ```ruby
24
+ persistence = Phronomy::Persistence::InMemory.new
25
+
26
+ workflow = Phronomy::Workflow.define(
27
+ MyContext,
28
+ persistence: persistence
29
+ ) do
30
+ # ...
31
+ end
32
+ ```
33
+
34
+ Global configuration changes from:
35
+
36
+ ```ruby
37
+ Phronomy.configure do |config|
38
+ config.state_store = store
39
+ end
40
+ ```
41
+
42
+ to:
43
+
44
+ ```ruby
45
+ Phronomy.configure do |config|
46
+ config.persistence = persistence
47
+ end
48
+ ```
49
+
50
+ The global backend is also used by Agent `new`/`create` calls that do not pass an
51
+ explicit `persistence:`. An explicitly injected Agent or Workflow backend still
52
+ takes precedence over the global setting.
53
+
54
+ Per-invocation `config[:state_store]` is removed. Persistence is selected at the
55
+ Workflow definition/application boundary rather than switched on individual
56
+ invocations.
57
+
58
+ `Phronomy::StateStore`, `StateStore::InMemory`, and the `state_store:` Workflow
59
+ parameter are removed without compatibility aliases.
60
+
61
+ ## workflow_states repository
62
+
63
+ Custom Persistence backends must provide `workflow_states` with revision-aware
64
+ load/save/delete semantics:
65
+
66
+ ```ruby
67
+ record = persistence.workflow_states.load("order-123")
68
+
69
+ persistence.workflow_states.save(
70
+ "order-123",
71
+ expected_revision: record&.fetch(:revision),
72
+ snapshot: {
73
+ fields: workflow_context.to_h,
74
+ phase: workflow_context.phase.to_s
75
+ }
76
+ )
77
+ ```
78
+
79
+ A stale revision must raise `Phronomy::Persistence::ConflictError` rather than
80
+ silently overwriting newer durable state.
81
+
82
+ Custom durable backends must also implement the public Backend SPI operation
83
+ `Persistence#assert_agent_watermark!` used before next-LLM durable barriers. It
84
+ verifies the caller's Agent revision and Journal position without returning
85
+ replacement mutable state; mismatches raise `ConflictError`. SQL backends should
86
+ implement this check inside the same database transaction as the subsequent
87
+ durable write.
88
+
89
+ The complete custom-backend contract is documented in
90
+ [`docs/persistence-backends.md`](../persistence-backends.md). In particular,
91
+ custom backends must provide the required capabilities, transaction atomicity,
92
+ repository compare-and-swap semantics, and durable domain-object reconstruction
93
+ described there.
94
+
95
+ ## Agent ownership and approval resume
96
+
97
+ A live Agent instance now remains the logical owner of its mutable state after
98
+ hydration. Phronomy no longer re-loads mutable AgentRoot/AgentExecution/Journal
99
+ state at each LLM boundary just to obtain freshness.
100
+
101
+ Approval resume continues the same live Agent/Activation and is performed through
102
+ the Agent instance APIs:
103
+
104
+ ```ruby
105
+ agent.approve(...)
106
+ agent.approve_async(...)
107
+ ```
108
+
109
+ The former class-level `MyAgent.approve(...)` / `MyAgent.approve_async(...)`
110
+ routing APIs are removed. Applications that only have an `execution_id` should
111
+ first resolve the current process's live owner:
112
+
113
+ ```ruby
114
+ agent = MyAgent.live_for_execution(execution_id)
115
+
116
+ agent.approve_async(
117
+ execution_id,
118
+ approval_request_id: request_id
119
+ )
120
+ ```
121
+
122
+ If the concrete Agent class is not known, use:
123
+
124
+ ```ruby
125
+ agent = Phronomy::Agent::Base.live_for_execution(execution_id)
126
+ ```
127
+
128
+ `live_for_execution` resolves the Runtime-local Activation and returns its
129
+ existing Agent instance. It does not accept `persistence:` and does not load a new
130
+ Agent or Execution from Persistence. If the live Activation has been lost (for
131
+ example after process restart), it raises `ExecutionRehydrationRequiredError`
132
+ until durable execution rehydration is implemented.
133
+
134
+ `execution_id` is not an authorization token. HTTP/API applications must still
135
+ verify that the caller is allowed to act on the resolved Agent and approval
136
+ request.
137
+
138
+ ## Workflow identity
139
+
140
+ Do not treat the following as the same identifier:
141
+
142
+ - `session_id`: application/tracing session metadata;
143
+ - `thread_id`: durable Workflow state identity;
144
+ - `fsm_session_id`: private Runtime identity for one FSMSession execution.
145
+
146
+ The last value is internal and is regenerated for each invoke/resume. Applications
147
+ continue to use `thread_id` when calling `Workflow#signal`.
148
+
149
+ Workflow admission for a durable `thread_id` is Runtime/process-local. Separate
150
+ Ruby processes or service replicas can execute the same `thread_id` concurrently
151
+ unless the application adds distributed coordination. `workflow_states` optimistic
152
+ revisions can reject a stale terminal commit, but they do not prevent duplicate
153
+ execution from starting or undo external side effects already performed before a
154
+ conflict is detected.