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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/CONTRIBUTING.md +30 -0
- data/README.md +2 -0
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/014-unified-persistence-durable-state.md +273 -0
- data/docs/features.md +27 -1
- data/docs/getting-started.md +37 -1
- data/docs/migrations/0.19.md +154 -0
- data/docs/persistence-backends.md +504 -0
- data/docs/runtime-and-concurrency.md +93 -2
- data/lib/phronomy/agent/agent_execution.rb +29 -0
- data/lib/phronomy/agent/base.rb +81 -36
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +420 -249
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/llm_call_record.rb +20 -0
- data/lib/phronomy/configuration.rb +2 -1
- data/lib/phronomy/engine/event_loop.rb +86 -8
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime.rb +7 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +109 -6
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +50 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +164 -0
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +215 -0
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +119 -0
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +99 -0
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +202 -0
- data/lib/phronomy/testing/persistence_contract.rb +41 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +361 -95
- data/lib/phronomy.rb +9 -0
- metadata +12 -5
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/scripts/check_private_enforcement.rb +0 -93
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a9f9f2ab4acdb4e17edeb4e0b7f0b33660b7102e5b345c2bd8d411e6b0760de
|
|
4
|
+
data.tar.gz: 331265c52dfadbe4be8f85b9a48b35de384042664538f4fa74db0d34829e3b62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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).
|
|
@@ -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).
|
data/docs/getting-started.md
CHANGED
|
@@ -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
|
-
|
|
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.
|