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.
- checksums.yaml +4 -4
- data/.mutant.yml +3 -4
- data/CHANGELOG.md +215 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +147 -0
- data/docs/architecture/persistence.md +186 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +89 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
- data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
- data/docs/decisions/README.md +176 -0
- data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
- data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
- data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
- data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
- data/docs/features.md +65 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +129 -40
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +21 -17
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +162 -72
- data/lib/phronomy/agent/base.rb +416 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/exact_execution.rb +153 -0
- data/lib/phronomy/agent/execution_cancellation.rb +25 -0
- data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
- data/lib/phronomy/agent/handoff.rb +43 -0
- data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
- data/lib/phronomy/agent/handoff_context.rb +120 -0
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
- data/lib/phronomy/agent/handoff_policy.rb +143 -0
- data/lib/phronomy/agent/handoff_projection.rb +207 -0
- data/lib/phronomy/agent/handoff_request.rb +45 -0
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
- data/lib/phronomy/agent/recovery_support.rb +504 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +339 -103
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +92 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
- data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
- data/lib/phronomy/multi_agent/team_execution.rb +44 -0
- data/lib/phronomy/multi_agent/team_root.rb +41 -0
- data/lib/phronomy/persistence/durable_codec.rb +706 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +471 -133
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +535 -0
- data/lib/phronomy/persistence.rb +174 -40
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
- data/lib/phronomy/testing/persistence_contract.rb +5 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +35 -0
- data/sig/phronomy/persistence.rbs +123 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +84 -18
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
- data/lib/phronomy/multi_agent/handoff.rb +0 -64
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# ADR-025: Process-Local Agent Ownership and Runtime Admission
|
|
2
|
+
|
|
3
|
+
**Status**: Accepted
|
|
4
|
+
**Date**: 2026-08-24
|
|
5
|
+
**Partially supersedes**: [ADR-014](014-unified-persistence-durable-state.md) for same-process Agent live-instance ownership and top-level execution admission
|
|
6
|
+
**Complements**: [ADR-018](018-durability-guarantees-and-failure-model.md), [ADR-022](022-agent-execution-parent-identity-and-runtime-routing-boundary.md), [ADR-023](023-fsm-session-incarnation-identity-and-routing.md), [ADR-024](024-event-loop-single-writer-agent-runtime.md)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
Phronomy's durable Agent identity is `agent_id`. The architecture baseline treats
|
|
13
|
+
that value as the identity of one logical Agent, not as a database lookup key
|
|
14
|
+
that may be reused by several independent mutable Ruby objects.
|
|
15
|
+
|
|
16
|
+
Before this decision, `Agent::Base.load(agent_id, persistence:)` could hydrate a
|
|
17
|
+
new mutable Agent every time it was called. The same process could therefore
|
|
18
|
+
hold two independent live objects that both represented the same logical Agent.
|
|
19
|
+
Separately, top-level `invoke` requests reached EventLoop, but the first concrete
|
|
20
|
+
same-Agent exclusion was still `Persistence#executions.create_active` inside the
|
|
21
|
+
initial Offload/Persistence operation.
|
|
22
|
+
|
|
23
|
+
That placement confused three different authorities:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
Runtime
|
|
27
|
+
process-local live Agent ownership
|
|
28
|
+
|
|
29
|
+
EventLoop
|
|
30
|
+
process-local top-level execution admission and live execution progression
|
|
31
|
+
|
|
32
|
+
Persistence
|
|
33
|
+
last confirmed durable representation, atomic durable transition,
|
|
34
|
+
optimistic conflict detection and recovery source
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
ADR-024 made EventLoop the single writer of Phronomy-managed live Agent
|
|
38
|
+
execution state. ACS-12 adds the process-local logical-Agent ownership and
|
|
39
|
+
admission layer on top of that foundation without turning Persistence into a
|
|
40
|
+
live ownership service.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
### One `agent_id` has one mutable live Agent owner per Runtime
|
|
45
|
+
|
|
46
|
+
A Runtime owns a purpose-specific Agent ownership registry keyed only by
|
|
47
|
+
`agent_id`:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
agent_id
|
|
51
|
+
-> one mutable live Agent instance
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The registry is an ownership authority, not a cache. Immutable snapshots,
|
|
55
|
+
durable records, read-only projections and handles may still have multiple
|
|
56
|
+
representations of the same Agent identity.
|
|
57
|
+
|
|
58
|
+
Agent ownership is reserved before durable create/load and before the mutable
|
|
59
|
+
Agent is published. Concurrent materialization of the same identity therefore
|
|
60
|
+
cannot create two independent live objects.
|
|
61
|
+
|
|
62
|
+
The registry is separate from EventLoop's `execution_id -> AgentExecutionState`
|
|
63
|
+
directory. Agent lifetime and Execution lifetime are different semantic
|
|
64
|
+
lifetimes and must not be represented by one registry.
|
|
65
|
+
|
|
66
|
+
### Public Agent construction/resolution semantics
|
|
67
|
+
|
|
68
|
+
The public operations have distinct meanings:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
new / create
|
|
72
|
+
create a new logical Agent
|
|
73
|
+
existing live or durable identity -> AgentAlreadyExistsError
|
|
74
|
+
|
|
75
|
+
load(agent_id, persistence:)
|
|
76
|
+
resolve an existing logical Agent
|
|
77
|
+
live owner -> return the exact same Ruby object without Persistence reload
|
|
78
|
+
durable-only -> hydrate once and publish as the live owner
|
|
79
|
+
missing durable Agent -> Persistence::NotFoundError
|
|
80
|
+
|
|
81
|
+
get(agent_id)
|
|
82
|
+
process-local live-owner lookup only
|
|
83
|
+
live -> exact same Ruby object
|
|
84
|
+
not live -> nil
|
|
85
|
+
never loads Persistence
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A live identity resolved through an incompatible Agent class/definition is an
|
|
89
|
+
explicit configuration error; it is not treated as a cache miss. A `load` call
|
|
90
|
+
that supplies a different Persistence instance from the already-live Agent is
|
|
91
|
+
also rejected rather than silently ignoring the caller's backend argument.
|
|
92
|
+
|
|
93
|
+
Ruby `.new` remains supported because it is an established application-facing
|
|
94
|
+
construction path. Its meaning is creation, not lookup: `.new(agent_id: "A")`
|
|
95
|
+
does not return an already-existing A.
|
|
96
|
+
|
|
97
|
+
### Ownership normally lasts for the Runtime lifetime
|
|
98
|
+
|
|
99
|
+
Once a mutable Agent is live, the Runtime keeps a strong ownership reference.
|
|
100
|
+
Execution completion, idleness and Ruby GC do not release that identity.
|
|
101
|
+
|
|
102
|
+
A clean Runtime shutdown detaches all live Agent objects from that Runtime. An
|
|
103
|
+
old Ruby reference is no longer a usable mutable Agent after shutdown; attempts
|
|
104
|
+
to operate on it fail with `RuntimeShutdownError`. This prevents a stale object
|
|
105
|
+
from remaining mutable while a new Runtime hydrates the same `agent_id`.
|
|
106
|
+
|
|
107
|
+
Live Agent entries do not themselves keep Runtime shutdown from completing.
|
|
108
|
+
Only an in-progress ownership transition such as construction or purge must
|
|
109
|
+
settle before clean ownership detachment.
|
|
110
|
+
|
|
111
|
+
### `purge!` is explicit logical-Agent destruction
|
|
112
|
+
|
|
113
|
+
`purge!` is the explicit exception to Runtime-lifetime ownership. It first moves
|
|
114
|
+
the exact current live owner into a process-local purging state, preventing new
|
|
115
|
+
materialization/admission, then deletes the durable Agent state.
|
|
116
|
+
|
|
117
|
+
On known successful purge:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
old Agent object -> permanently purged / unusable
|
|
121
|
+
Runtime registry -> identity released
|
|
122
|
+
Persistence -> Agent/Journal/Execution records deleted
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The same textual `agent_id` may then be used to create a new logical Agent. A
|
|
126
|
+
stale Ruby reference to the old purged object remains invalid and cannot purge
|
|
127
|
+
or mutate the replacement. Repeating `purge!` on that already-purged stale
|
|
128
|
+
object is an idempotent no-op.
|
|
129
|
+
|
|
130
|
+
If the purge is known not to have committed, the process-local purging state is
|
|
131
|
+
rolled back to live. If durable outcome is uncertain, the transition becomes a
|
|
132
|
+
stable `RECOVERY_REQUIRED` ownership state. The identity stays fail-closed, but
|
|
133
|
+
load/shutdown waiters are not left blocked on a transition that can no longer
|
|
134
|
+
settle by itself. ACS-15 recovery/reconciliation work is responsible for
|
|
135
|
+
resolving that uncertainty.
|
|
136
|
+
|
|
137
|
+
### EventLoop owns same-process top-level execution admission
|
|
138
|
+
|
|
139
|
+
A live Agent may be idle while still owned. Top-level Execution admission is a
|
|
140
|
+
separate EventLoop-owned map keyed by `agent_id`.
|
|
141
|
+
|
|
142
|
+
For one logical Agent, at most one nonterminal top-level Execution is admitted:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
IDLE + invoke(E1)
|
|
146
|
+
-> ADMITTING(E1)
|
|
147
|
+
|
|
148
|
+
ADMITTING / EXECUTING / SUSPENDED / RECOVERY_REQUIRED + invoke(E2)
|
|
149
|
+
-> AgentBusyError
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The admission is acquired on EventLoop before the initial Offload/Persistence
|
|
153
|
+
operation. `Persistence#executions.create_active` remains in that operation as a
|
|
154
|
+
durable second line of defense.
|
|
155
|
+
|
|
156
|
+
A short-lived opaque owner token protects the pre-durable `ADMITTING` entry so
|
|
157
|
+
only the request that acquired it may release/bind it. The token is a
|
|
158
|
+
process-local coordination capability only. It is not a domain identity,
|
|
159
|
+
semantic operation ID, generic generation counter or asynchronous result
|
|
160
|
+
authority. After durable establishment, the admission is bound to the canonical
|
|
161
|
+
`execution_id`.
|
|
162
|
+
|
|
163
|
+
### Admission follows the logical Execution lifetime
|
|
164
|
+
|
|
165
|
+
`preparing`, `active` and `suspended` are all nonterminal. Suspension keeps the
|
|
166
|
+
same admission; approval resume continues the same `execution_id` and does not
|
|
167
|
+
create a new top-level Execution.
|
|
168
|
+
|
|
169
|
+
The slot is released only after a known-successful durable terminal transition.
|
|
170
|
+
Caller-facing Task settlement and terminal callbacks are notification boundaries
|
|
171
|
+
and do not extend the logical Execution lifetime.
|
|
172
|
+
|
|
173
|
+
A known pre-durable failure releases the process-local admission. An uncertain
|
|
174
|
+
durable establishment/terminal outcome does not. It is marked
|
|
175
|
+
`RECOVERY_REQUIRED`/fail-closed so another top-level Execution cannot be admitted
|
|
176
|
+
from an unproven state lineage.
|
|
177
|
+
|
|
178
|
+
### Persistence admission remains a durable integrity capability
|
|
179
|
+
|
|
180
|
+
`atomic_admission` and `executions.create_active` remain required Persistence
|
|
181
|
+
capabilities. They continue to guarantee durable execution-ID uniqueness and
|
|
182
|
+
that durable nonterminal executions for one `agent_id` do not overlap.
|
|
183
|
+
|
|
184
|
+
They are no longer described as the primary same-process live/execution
|
|
185
|
+
ownership mechanism. Their role is durable integrity and defensive conflict
|
|
186
|
+
detection, including protection against stale code paths and unsupported
|
|
187
|
+
multi-process races.
|
|
188
|
+
|
|
189
|
+
Optimistic revision, Journal position and watermark guards remain unchanged.
|
|
190
|
+
Phronomy does not reload/merge durable state after a conflict to continue the
|
|
191
|
+
same logical execution.
|
|
192
|
+
|
|
193
|
+
## Required invariants
|
|
194
|
+
|
|
195
|
+
1. One Runtime never publishes two independent mutable Agent objects for the same `agent_id`.
|
|
196
|
+
2. Repeated `load` of a live Agent returns the same Ruby object and does not reload durable state.
|
|
197
|
+
3. `get` is process-local and never performs Persistence I/O.
|
|
198
|
+
4. Agent ownership and EventLoop execution-state directories remain separate responsibilities.
|
|
199
|
+
5. EventLoop admission occurs before initial Persistence execution establishment.
|
|
200
|
+
6. Competing same-Agent top-level requests are rejected with `AgentBusyError`; core does not promise automatic queueing.
|
|
201
|
+
7. Suspension retains the same logical Execution admission.
|
|
202
|
+
8. Known durable terminal success releases admission before caller notification is required to settle.
|
|
203
|
+
9. Unknown durable outcome never causes a blind admission release.
|
|
204
|
+
10. Persistence `create_active`/CAS/revision/watermark protection remains enabled as durable defense.
|
|
205
|
+
11. Runtime shutdown makes old live Agent objects unusable before a later Runtime can authoritatively hydrate the same identity.
|
|
206
|
+
12. Successful `purge!` invalidates the old object before the identity may represent a replacement live Agent.
|
|
207
|
+
|
|
208
|
+
## Explicitly deferred work
|
|
209
|
+
|
|
210
|
+
This decision does **not** implement:
|
|
211
|
+
|
|
212
|
+
- cross-process Agent ownership, lease/fencing or stable routing (ACS-14);
|
|
213
|
+
- durable outcome reconciliation/Agent execution rehydration (ACS-15);
|
|
214
|
+
- cancellation-wide physical-work quiescence supervision (ACS-16);
|
|
215
|
+
- semantic external-operation retry/causal barriers (ACS-17);
|
|
216
|
+
- a general Agent duplication contract.
|
|
217
|
+
|
|
218
|
+
The future Agent duplication operation is named **`copy`**, not `fork`. It must
|
|
219
|
+
create a new `agent_id`. Which Context, Knowledge, Journal history, metadata or
|
|
220
|
+
provenance is copied, and how nonterminal Execution state is handled, remain a
|
|
221
|
+
separate API/semantic decision. ACS-12 does not add `copy` to the runtime API.
|
|
222
|
+
|
|
223
|
+
## Rejected alternatives
|
|
224
|
+
|
|
225
|
+
### Use Persistence `create_active` as the only same-process exclusion
|
|
226
|
+
|
|
227
|
+
Rejected. It preserves durable exclusion behavior but leaves Runtime authority
|
|
228
|
+
ambiguous and allows competing requests to reach durable I/O before the
|
|
229
|
+
process-local owner has decided which continuation is authoritative.
|
|
230
|
+
|
|
231
|
+
### Put Agent live ownership into EventLoop's execution directory
|
|
232
|
+
|
|
233
|
+
Rejected. A live Agent exists while idle and across many sequential Executions.
|
|
234
|
+
`agent_id` ownership and `execution_id` Runtime state have different lifetimes.
|
|
235
|
+
|
|
236
|
+
### Let repeated `load` create a new object and rely on CAS later
|
|
237
|
+
|
|
238
|
+
Rejected. CAS detects stale durable writes after two mutable owners already
|
|
239
|
+
exist; it does not satisfy the logical Agent identity invariant.
|
|
240
|
+
|
|
241
|
+
### Make `.new` return an existing instance
|
|
242
|
+
|
|
243
|
+
Rejected. Ruby `.new` is creation semantics. Returning an existing object would
|
|
244
|
+
make identity lookup implicit and surprising; `load`/`get` provide resolution.
|
|
245
|
+
|
|
246
|
+
### Release ownership when an Execution completes or when GC collects the Agent
|
|
247
|
+
|
|
248
|
+
Rejected. Execution lifetime is shorter than logical Agent live lifetime, and GC
|
|
249
|
+
timing is not an architecture ownership protocol.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# ADR-026: Workflow Runtime Admission and Durable Terminal Barrier
|
|
2
|
+
|
|
3
|
+
**Status**: Accepted
|
|
4
|
+
**Date**: 2026-08-24
|
|
5
|
+
**Partially supersedes**: [ADR-014](014-unified-persistence-durable-state.md) for same-process Workflow admission ownership and terminal durable-barrier ordering
|
|
6
|
+
**Refines**: [ADR-023](023-fsm-session-incarnation-identity-and-routing.md) by removing the transitional Workflow FSMSession-identity reservation bridge
|
|
7
|
+
**Complements**: [ADR-018](018-durability-guarantees-and-failure-model.md), [ADR-020](020-canonical-workflow-instance-identity.md), [ADR-024](024-event-loop-single-writer-agent-runtime.md), [ADR-025](025-process-local-agent-ownership-and-runtime-admission.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`workflow_instance_id` is the canonical logical/durable Workflow identity.
|
|
14
|
+
`fsm_session_id` identifies one concrete Runtime FSMSession incarnation. Those
|
|
15
|
+
identities have different responsibilities and lifetimes.
|
|
16
|
+
|
|
17
|
+
Before this decision, WorkflowRunner reserved the future FSMSession identity
|
|
18
|
+
before durable hydration and reused that value as the process-local Workflow
|
|
19
|
+
admission owner. This preserved admission-before-load ordering, but it conflated
|
|
20
|
+
Runtime coordination ownership with concrete event-routing identity.
|
|
21
|
+
|
|
22
|
+
Durable Workflow terminalization also had the wrong lifecycle order. A
|
|
23
|
+
FSMSession first became `halted`/`finished`, EventLoop removed the session and
|
|
24
|
+
settled its source completion, and only then WorkflowRunner saved the final
|
|
25
|
+
Workflow snapshot. A save failure could therefore occur after the runtime
|
|
26
|
+
lifecycle had already declared the execution segment terminal.
|
|
27
|
+
|
|
28
|
+
The Architecture Baseline requires both inconsistencies to be removed:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
acquire Workflow admission
|
|
32
|
+
↓
|
|
33
|
+
load / hydrate durable Workflow state
|
|
34
|
+
↓
|
|
35
|
+
create and run one concrete FSMSession
|
|
36
|
+
↓
|
|
37
|
+
logical halt / completion reached
|
|
38
|
+
↓
|
|
39
|
+
persist terminal snapshot
|
|
40
|
+
↓
|
|
41
|
+
known-successful persistence result returns to that FSMSession
|
|
42
|
+
↓
|
|
43
|
+
HALTED / COMPLETED
|
|
44
|
+
↓
|
|
45
|
+
release Workflow admission
|
|
46
|
+
↓
|
|
47
|
+
settle caller-facing Task
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Decision
|
|
51
|
+
|
|
52
|
+
### Workflow admission uses an opaque Runtime owner token
|
|
53
|
+
|
|
54
|
+
EventLoop owns a process-local Workflow admission entry keyed by
|
|
55
|
+
`workflow_instance_id`. Admission ownership uses a fresh opaque owner token that
|
|
56
|
+
is independent from the later concrete `fsm_session_id`:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
workflow_instance_id = W
|
|
60
|
+
↓
|
|
61
|
+
admission owner token = T1
|
|
62
|
+
↓
|
|
63
|
+
concrete FSMSession id = S1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The token is a Runtime coordination capability only. It is not a domain
|
|
67
|
+
identity, durable identifier, application correlation value, generic generation
|
|
68
|
+
counter, or asynchronous semantic-result authority.
|
|
69
|
+
|
|
70
|
+
Only the exact owner token that acquired the admission may bind or release it.
|
|
71
|
+
A competing/stale token cannot release the current Workflow execution segment.
|
|
72
|
+
|
|
73
|
+
### Admission precedes mutable durable hydration
|
|
74
|
+
|
|
75
|
+
Start/resume requests are first posted to EventLoop. EventLoop acquires the
|
|
76
|
+
`workflow_instance_id` admission before any `workflow_states.load` is submitted
|
|
77
|
+
to OffloadPool.
|
|
78
|
+
|
|
79
|
+
A durable load returns only operation-specific value/snapshot data. Hydration,
|
|
80
|
+
WorkflowContext construction, concrete FSMSession construction, admission-to-
|
|
81
|
+
FSMSession binding, and registration are applied on EventLoop.
|
|
82
|
+
|
|
83
|
+
This preserves the ordering:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
admit W with T1
|
|
87
|
+
↓
|
|
88
|
+
load W
|
|
89
|
+
↓
|
|
90
|
+
hydrate live Workflow state
|
|
91
|
+
↓
|
|
92
|
+
create S1
|
|
93
|
+
↓
|
|
94
|
+
bind T1 -> S1 for routing
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`fsm_session_id` remains the direct EventLoop routing identity. Application
|
|
98
|
+
`Workflow#signal(workflow_instance_id: ...)` resolves the current admission to
|
|
99
|
+
its bound `fsm_session_id`; the owner token is never used as an event target.
|
|
100
|
+
|
|
101
|
+
### Durable Workflow terminalization is part of the FSMSession lifecycle
|
|
102
|
+
|
|
103
|
+
A durable Workflow that reaches logical halt/completion does not immediately
|
|
104
|
+
become runtime-terminal. The logical Workflow phase and the terminal-persistence
|
|
105
|
+
lifecycle are separate.
|
|
106
|
+
|
|
107
|
+
Conceptually:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
RUNNING
|
|
111
|
+
↓ logical halt/completion result
|
|
112
|
+
PERSISTING_TERMINAL
|
|
113
|
+
↓ terminal persistence result
|
|
114
|
+
├─ known success -> HALTED / COMPLETED
|
|
115
|
+
├─ known failure -> ERROR
|
|
116
|
+
└─ outcome unknown -> RECOVERY_REQUIRED
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`PERSISTING_TERMINAL` is not injected into the application-defined Workflow
|
|
120
|
+
state graph. It is private FSMSession/runtime lifecycle state.
|
|
121
|
+
|
|
122
|
+
When a durable terminal boundary is reached, FSMSession keeps its concrete
|
|
123
|
+
session alive and asks WorkflowRunner to persist the terminal snapshot.
|
|
124
|
+
WorkflowRunner submits a Workflow-specific immutable/value persistence command
|
|
125
|
+
to OffloadPool. The persistence result returns through the same FSMSession's
|
|
126
|
+
session-local event sink. Only that FSMSession may accept the result and advance
|
|
127
|
+
its terminal lifecycle.
|
|
128
|
+
|
|
129
|
+
Ephemeral Workflow executions that do not require durable terminal persistence
|
|
130
|
+
retain their direct terminal behavior.
|
|
131
|
+
|
|
132
|
+
### The FSM consumes semantic persistence outcomes, not backend details
|
|
133
|
+
|
|
134
|
+
FSMSession does not know whether the backend is local, remote, SQL, HTTP-based,
|
|
135
|
+
or otherwise networked. It does not classify database-driver or transport
|
|
136
|
+
exceptions.
|
|
137
|
+
|
|
138
|
+
The Workflow persistence operation normalizes the save result into three
|
|
139
|
+
semantic outcomes:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
success
|
|
143
|
+
Phronomy has a known-successful durable result.
|
|
144
|
+
|
|
145
|
+
known_failure
|
|
146
|
+
the Phronomy Persistence contract establishes that the intended terminal
|
|
147
|
+
save did not become the successful durable transition.
|
|
148
|
+
|
|
149
|
+
outcome_unknown
|
|
150
|
+
Phronomy cannot establish whether the durable transition committed.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For the durable-barrier question, only `success` is permission to proceed.
|
|
154
|
+
Both other outcomes keep the success barrier closed.
|
|
155
|
+
|
|
156
|
+
Portable Persistence semantic errors whose contract establishes ordinary
|
|
157
|
+
failure, such as optimistic conflict or serialization rejection, may be treated
|
|
158
|
+
as `known_failure`. An arbitrary backend/storage/transport error is not assumed
|
|
159
|
+
to mean "not committed"; when non-commit is not established by contract, the
|
|
160
|
+
result is conservatively `outcome_unknown`.
|
|
161
|
+
|
|
162
|
+
This classification depends on semantic certainty, not on whether the physical
|
|
163
|
+
backend is on the same machine or reached over a network.
|
|
164
|
+
|
|
165
|
+
### Known failure follows the Workflow error path
|
|
166
|
+
|
|
167
|
+
A `known_failure` terminal save result is delivered back to the owning
|
|
168
|
+
FSMSession. The FSMSession does not enter `HALTED`/`COMPLETED`; it terminalizes
|
|
169
|
+
through its error path instead. EventLoop then releases the admission using the
|
|
170
|
+
opaque owner token and the caller-facing Task settles as failed.
|
|
171
|
+
|
|
172
|
+
Phronomy does not automatically retry the entire Workflow segment merely because
|
|
173
|
+
the terminal durable save failed. External-effect retry/duplicate semantics are
|
|
174
|
+
separate architecture concerns.
|
|
175
|
+
|
|
176
|
+
### Outcome uncertainty fails closed
|
|
177
|
+
|
|
178
|
+
An `outcome_unknown` result is not converted to ordinary failure and is not
|
|
179
|
+
assumed to be success. The known-success durable barrier remains closed.
|
|
180
|
+
|
|
181
|
+
The concrete FSMSession loses normal continuation authority and is retired. The
|
|
182
|
+
process-local Workflow admission becomes `recovery_required` and remains owned,
|
|
183
|
+
so the Runtime cannot admit a fresh top-level segment from an unproven lineage.
|
|
184
|
+
The caller-facing Workflow Task is not falsely settled as success or failure.
|
|
185
|
+
|
|
186
|
+
Actual persistence-outcome reconciliation and restart-safe continuation are
|
|
187
|
+
ACS-15 responsibilities. ACS-13 establishes the fail-closed boundary but does
|
|
188
|
+
not claim that recovery is already implemented.
|
|
189
|
+
|
|
190
|
+
### Successful terminalization releases admission after FSM acceptance
|
|
191
|
+
|
|
192
|
+
On a known-successful terminal save, the result first returns to the same
|
|
193
|
+
FSMSession. The FSMSession accepts it and only then emits its normal
|
|
194
|
+
`halted`/`finished` terminal event.
|
|
195
|
+
|
|
196
|
+
EventLoop removes the concrete session, and WorkflowRunner releases the
|
|
197
|
+
`workflow_instance_id` admission using its opaque owner token before settling
|
|
198
|
+
the caller-facing Task.
|
|
199
|
+
|
|
200
|
+
The save completing on a worker thread is therefore not itself Workflow
|
|
201
|
+
completion. Authoritative logical completion occurs only after the EventLoop-
|
|
202
|
+
owned lifecycle accepts that save result.
|
|
203
|
+
|
|
204
|
+
## Required invariants
|
|
205
|
+
|
|
206
|
+
1. One Runtime admits at most one live Workflow execution segment for a `workflow_instance_id`.
|
|
207
|
+
2. Workflow admission is acquired before mutable durable Workflow load/hydration.
|
|
208
|
+
3. Workflow admission owner token and `fsm_session_id` are different Runtime concepts.
|
|
209
|
+
4. Only the exact admission owner token may bind/release its Workflow admission.
|
|
210
|
+
5. `fsm_session_id` remains the concrete EventLoop event-routing identity.
|
|
211
|
+
6. Durable halt/completion does not become runtime-terminal before a known-successful terminal snapshot save is accepted by the owning FSMSession.
|
|
212
|
+
7. Terminal Persistence I/O never blocks EventLoop and never mutates the live FSMSession from a worker.
|
|
213
|
+
8. FSMSession receives backend-independent semantic save outcomes, not backend/transport classifications.
|
|
214
|
+
9. Known terminal save failure does not become successful `HALTED`/`COMPLETED`.
|
|
215
|
+
10. Unknown terminal save outcome does not release admission or settle the caller Workflow Task as a terminal success/failure.
|
|
216
|
+
11. Ephemeral Workflows do not acquire a fake durable barrier merely to match the durable path.
|
|
217
|
+
12. Persistence optimistic revision remains a durable integrity/conflict defense and is not the same-process ownership mechanism.
|
|
218
|
+
|
|
219
|
+
## Explicitly deferred work
|
|
220
|
+
|
|
221
|
+
This decision does **not** implement:
|
|
222
|
+
|
|
223
|
+
- cross-process Workflow ownership, routing, lease/fencing (ACS-14);
|
|
224
|
+
- Persistence F1 outcome reconciliation or restart-safe Workflow rehydration (ACS-15);
|
|
225
|
+
- cancellation/semantic-deadline quiescence and terminalization integration (ACS-16);
|
|
226
|
+
- general external semantic-operation retry/idempotency protocol (ACS-17);
|
|
227
|
+
- a new public Workflow lifecycle-state API;
|
|
228
|
+
- Persistence-backend-specific network/driver exception taxonomies in FSM code.
|
|
229
|
+
|
|
230
|
+
## Rejected alternatives
|
|
231
|
+
|
|
232
|
+
### Continue using the future `fsm_session_id` as admission owner
|
|
233
|
+
|
|
234
|
+
Rejected. It preserves pre-load exclusion but conflates coordination ownership
|
|
235
|
+
with concrete routing identity and makes the admission authority depend on a
|
|
236
|
+
session that does not yet exist.
|
|
237
|
+
|
|
238
|
+
### Save only after FSMSession has emitted `halted`/`finished`
|
|
239
|
+
|
|
240
|
+
Rejected. Runtime terminalization would precede the durable outcome that is
|
|
241
|
+
required to justify it, violating the Workflow durable barrier.
|
|
242
|
+
|
|
243
|
+
### Block EventLoop until Persistence returns
|
|
244
|
+
|
|
245
|
+
Rejected. A durable barrier is logical execution ordering, not EventLoop-wide
|
|
246
|
+
blocking. Persistence remains OffloadPool work.
|
|
247
|
+
|
|
248
|
+
### Treat every save exception as known failure
|
|
249
|
+
|
|
250
|
+
Rejected. A backend may have committed even though its success response was not
|
|
251
|
+
observed. Blind release/retry could branch Workflow lineage or duplicate later
|
|
252
|
+
semantic work.
|
|
253
|
+
|
|
254
|
+
### Teach FSMSession about SQL/network/backend exception classes
|
|
255
|
+
|
|
256
|
+
Rejected. Physical backend topology and driver error taxonomies belong below the
|
|
257
|
+
Workflow FSM boundary. FSMSession consumes semantic persistence outcomes only.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# ADR-027: Phronomy-Owned LLM Adapter and RubyLLM Default Provider Boundary
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
ADR-001 selected RubyLLM as Phronomy's LLM provider layer. Its original
|
|
10
|
+
Decision text also encoded an implementation boundary in which Agent code and
|
|
11
|
+
the legacy Chain API called `RubyLLM.chat` directly.
|
|
12
|
+
|
|
13
|
+
The current architecture has evolved:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Phronomy Agent / Context / Manifest
|
|
17
|
+
|
|
|
18
|
+
v
|
|
19
|
+
RubyLLMMaterializer
|
|
20
|
+
|
|
|
21
|
+
v
|
|
22
|
+
Phronomy::LLMAdapter::Base
|
|
23
|
+
|
|
|
24
|
+
v
|
|
25
|
+
configured adapter
|
|
26
|
+
(default: Phronomy::LLMAdapter::RubyLLM)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Phronomy now owns Agent lifecycle, Runtime coordination, canonical Journal /
|
|
30
|
+
Context Policy / Manifest semantics, durable execution state, and the
|
|
31
|
+
framework-owned asynchronous/offload bridge around Provider calls.
|
|
32
|
+
|
|
33
|
+
At the same time, the current materialization path is still RubyLLM-specific:
|
|
34
|
+
the LLMAdapter SPI receives the configured/materialized chat runtime object.
|
|
35
|
+
Making the Provider-call boundary replaceable does not by itself make the
|
|
36
|
+
complete LLM-input materialization pipeline provider-neutral.
|
|
37
|
+
|
|
38
|
+
The public LLMAdapter SPI is currently classified Beta. Architecture
|
|
39
|
+
reconciliation must not silently promote its API stability.
|
|
40
|
+
|
|
41
|
+
## Decision
|
|
42
|
+
|
|
43
|
+
1. Phronomy owns Agent lifecycle, Context/Manifest authority, Runtime
|
|
44
|
+
coordination, and durable execution semantics. These responsibilities are
|
|
45
|
+
not delegated to `RubyLLM::Agent`.
|
|
46
|
+
2. `Phronomy::LLMAdapter::Base#complete` and `#stream` are the public
|
|
47
|
+
Provider-call extension boundary. Phronomy owns the framework-side
|
|
48
|
+
asynchronous/offload wrappers around that synchronous adapter contract.
|
|
49
|
+
3. `Phronomy::LLMAdapter::RubyLLM` remains the default configured LLM adapter
|
|
50
|
+
and RubyLLM remains Phronomy's default Provider integration.
|
|
51
|
+
4. A custom LLMAdapter may replace Provider-call behavior, but the LLMAdapter
|
|
52
|
+
SPI does not imply that the complete input-materialization pipeline is
|
|
53
|
+
Provider-neutral. The current Agent pipeline still materializes canonical
|
|
54
|
+
LLM input through RubyLLM-specific runtime objects.
|
|
55
|
+
5. Provider transport timeout, retry, backoff, jitter, and rate-limit handling
|
|
56
|
+
remain adapter/provider-client responsibilities, consistent with
|
|
57
|
+
[`011-delegate-transport-policy-to-adapters`](011-delegate-transport-policy-to-adapters.md).
|
|
58
|
+
6. This decision does not expand Phronomy scope merely because RubyLLM or
|
|
59
|
+
another Provider exposes additional features.
|
|
60
|
+
7. This decision does not change the existing API stability classification of
|
|
61
|
+
the LLMAdapter SPI.
|
|
62
|
+
|
|
63
|
+
## Consequences
|
|
64
|
+
|
|
65
|
+
### Positive
|
|
66
|
+
|
|
67
|
+
- The normative architecture matches the current Phronomy-owned Agent /
|
|
68
|
+
Context / Manifest pipeline.
|
|
69
|
+
- RubyLLM remains the default integration without making direct RubyLLM calls
|
|
70
|
+
the Phronomy extension contract.
|
|
71
|
+
- Applications can supply a custom call adapter through one explicit boundary.
|
|
72
|
+
- Runtime/offload semantics stay Phronomy-owned while transport policy stays
|
|
73
|
+
adapter-owned.
|
|
74
|
+
- Provider-call replaceability is not confused with full materialization
|
|
75
|
+
neutrality.
|
|
76
|
+
|
|
77
|
+
### Tradeoffs
|
|
78
|
+
|
|
79
|
+
- Custom adapters currently receive Phronomy's materialized chat runtime object
|
|
80
|
+
and therefore may still depend on the RubyLLM-shaped materialization boundary.
|
|
81
|
+
- Replacing the current RubyLLM-specific materializer would require a separate
|
|
82
|
+
architecture/API decision if Phronomy later wants end-to-end Provider-neutral
|
|
83
|
+
materialization.
|
|
84
|
+
- The Beta LLMAdapter SPI may still evolve according to the repository's
|
|
85
|
+
compatibility policy.
|
|
86
|
+
|
|
87
|
+
## Supersession
|
|
88
|
+
|
|
89
|
+
This decision supersedes
|
|
90
|
+
[`001-rubyllm-as-provider-layer`](001-rubyllm-as-provider-layer.md).
|
|
91
|
+
|
|
92
|
+
ADR-001 remains preserved as historical rationale for adopting RubyLLM, but its
|
|
93
|
+
direct-call boundary is no longer normative.
|