phronomy 0.22.0 → 0.23.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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +200 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +152 -0
  15. data/docs/architecture/persistence.md +175 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +82 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/README.md +172 -0
  45. data/docs/features.md +31 -11
  46. data/docs/getting-started.md +77 -45
  47. data/docs/migrations/0.19.md +14 -7
  48. data/docs/migrations/0.22.md +390 -0
  49. data/docs/persistence-backends.md +88 -38
  50. data/docs/runtime-and-concurrency.md +227 -33
  51. data/examples/README.md +13 -0
  52. data/lib/phronomy/agent/agent_execution.rb +19 -15
  53. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  54. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  55. data/lib/phronomy/agent/agent_root.rb +3 -3
  56. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  57. data/lib/phronomy/agent/async_event_api.rb +145 -72
  58. data/lib/phronomy/agent/base.rb +388 -181
  59. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  60. data/lib/phronomy/agent/context_assembler.rb +437 -178
  61. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  62. data/lib/phronomy/agent/context_plan.rb +18 -13
  63. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  64. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  65. data/lib/phronomy/agent/context_policy.rb +109 -3
  66. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  67. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  68. data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
  69. data/lib/phronomy/agent/journal_record.rb +17 -4
  70. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  71. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  72. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  73. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  74. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  75. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
  76. data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
  77. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
  78. data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
  79. data/lib/phronomy/agent/recovery_support.rb +512 -0
  80. data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
  81. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  82. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  83. data/lib/phronomy/agent/shared_state.rb +38 -1
  84. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  85. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  86. data/lib/phronomy/agent/tool_invocation.rb +336 -102
  87. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  88. data/lib/phronomy/agent.rb +20 -2
  89. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  90. data/lib/phronomy/agent_purged_error.rb +5 -0
  91. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  92. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  93. data/lib/phronomy/engine/event_loop.rb +622 -63
  94. data/lib/phronomy/engine/fsm_session.rb +194 -21
  95. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  96. data/lib/phronomy/engine/runtime.rb +77 -20
  97. data/lib/phronomy/generator_verifier.rb +12 -14
  98. data/lib/phronomy/invocation_context.rb +9 -29
  99. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  100. data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
  101. data/lib/phronomy/multi_agent/coordinator.rb +154 -0
  102. data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
  103. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  104. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  105. data/lib/phronomy/multi_agent/handoff.rb +24 -45
  106. data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
  107. data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
  108. data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
  109. data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
  110. data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
  111. data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
  112. data/lib/phronomy/multi_agent/runner.rb +98 -0
  113. data/lib/phronomy/persistence/durable_codec.rb +646 -0
  114. data/lib/phronomy/persistence/durable_record.rb +117 -0
  115. data/lib/phronomy/persistence/in_memory.rb +210 -134
  116. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  117. data/lib/phronomy/persistence/repository_facades.rb +316 -0
  118. data/lib/phronomy/persistence.rb +81 -41
  119. data/lib/phronomy/recovery.rb +186 -0
  120. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  121. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
  122. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  123. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  124. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  125. data/lib/phronomy/tracing/automatic.rb +176 -0
  126. data/lib/phronomy/tracing/base.rb +11 -2
  127. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  128. data/lib/phronomy/version.rb +1 -1
  129. data/lib/phronomy/workflow.rb +3 -6
  130. data/lib/phronomy/workflow_context.rb +14 -5
  131. data/lib/phronomy/workflow_recovery.rb +123 -0
  132. data/lib/phronomy/workflow_runner.rb +468 -256
  133. data/lib/phronomy.rb +6 -0
  134. data/scripts/api_snapshot.rb +12 -0
  135. data/sig/phronomy/agent.rbs +209 -7
  136. data/sig/phronomy/multi_agent.rbs +39 -0
  137. data/sig/phronomy/persistence.rbs +62 -4
  138. data/sig/phronomy/runtime.rbs +1 -4
  139. data/sig/phronomy/workflow.rbs +2 -2
  140. data/sig/phronomy.rbs +10 -0
  141. metadata +65 -17
  142. data/examples/workflows/agent_event_mapping.rb +0 -101
  143. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  144. data/lib/phronomy/agent/activation_registry.rb +0 -28
  145. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  146. data/lib/phronomy/agent/context_candidate.rb +0 -47
  147. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  148. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  149. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  150. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  151. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  152. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  153. data/lib/phronomy/agent/context_request.rb +0 -35
  154. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  155. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  156. data/lib/phronomy/agent/runner.rb +0 -97
@@ -8,24 +8,40 @@ the bounded `OffloadPool`.
8
8
 
9
9
  For the design rationale, see [ADR-010](decisions/010-cooperative-first-concurrency.md).
10
10
  Durable-state ownership is defined by
11
- [ADR-014](decisions/014-unified-persistence-durable-state.md).
11
+ [ADR-014](decisions/014-unified-persistence-durable-state.md), with live Agent
12
+ Runtime execution-state ownership refined by
13
+ [ADR-024](decisions/024-event-loop-single-writer-agent-runtime.md) and process-local
14
+ Agent identity/admission ownership defined by
15
+ [ADR-025](decisions/025-process-local-agent-ownership-and-runtime-admission.md).
16
+ Canonical Workflow instance identity is defined by
17
+ [ADR-020](decisions/020-canonical-workflow-instance-identity.md).
18
+ Concrete FSMSession incarnation identity and session-local Runtime routing are
19
+ defined by [ADR-023](decisions/023-fsm-session-incarnation-identity-and-routing.md).
20
+ Same-process Workflow admission ownership and durable terminal-barrier ordering
21
+ are defined by
22
+ [ADR-026](decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md).
12
23
 
13
24
  ## Runtime model
14
25
 
15
26
  ```text
16
27
  Runtime
28
+ ├─ Agent ownership registry
29
+ │ └─ agent_id -> one mutable live Agent instance
17
30
  ├─ EventLoop (one control-plane operating-system Thread)
18
- │ └─ FSMSession
19
- │ ├─ Agent
20
- │ ├─ Workflow
21
- │ ├─ ToolInvocation
22
- │ └─ MultiAgent fan-out
23
- ├─ process-local Agent ActivationRegistry
31
+ │ ├─ FSMSession
32
+ │ │ ├─ Agent
33
+ │ │ ├─ Workflow
34
+ │ │ ├─ ToolInvocation
35
+ │ │ └─ MultiAgent fan-out
36
+ │ ├─ Agent top-level admission
37
+ │ │ └─ agent_id -> one nonterminal logical Execution admission
38
+ │ └─ Agent execution directory
39
+ │ └─ execution_id -> immutable live-state record
24
40
  ├─ OffloadPool (bounded operating-system Threads)
25
41
  │ ├─ private Operation records
26
42
  │ ├─ blocking input/output (I/O)
27
43
  │ ├─ central-processing-unit (CPU)-bound synchronous work
28
- │ └─ other long synchronous work
44
+ │ └─ operation-specific durable Agent/Workflow work
29
45
  ├─ named OffloadPools
30
46
  └─ EventLoop-driven timers
31
47
 
@@ -44,12 +60,22 @@ A live Agent or Workflow owns its current logical state. `Persistence` is the
44
60
  last committed durable representation and recovery source; it is not reloaded at
45
61
  every semantic boundary.
46
62
 
47
- For Agents, the live owner consists of the Agent instance plus its current
48
- `AgentRoot`, hydrated Journal view, and `AgentExecutionActivation`. Mutable
49
- Agent/Execution/Journal state is not automatically reloaded before every LLM or
50
- Tool step. Durable writes use optimistic revision/position guardrails; an
51
- external writer that advances the durable base causes `Persistence::ConflictError`
52
- rather than automatic reload or merge.
63
+ For active Agents, **EventLoop is the single writer of Phronomy-managed live
64
+ execution state**. EventLoop owns a process-local execution directory keyed by
65
+ canonical `execution_id`. Each directory value is immutable and is replaced on
66
+ EventLoop when the current AgentExecution, RuntimeProjection, AgentInvocation, or
67
+ owning FSMSession changes. The former mutex-protected
68
+ `AgentExecutionActivation` / `ActivationRegistry` model is removed.
69
+
70
+ `AgentInvocation` is the FSM-local mutable context and holds uncommitted Provider
71
+ outcomes, Tool/runtime events, active Provider-call provenance, and callback
72
+ failure state. These fields are advanced only by EventLoop-driven FSMSession
73
+ handling; workers do not receive AgentInvocation as a mutable state authority.
74
+
75
+ Mutable Agent/Execution/Journal state is not automatically reloaded before every
76
+ LLM or Tool step. Durable writes use optimistic revision/position guardrails; an
77
+ external writer that advances the durable base causes
78
+ `Persistence::ConflictError` rather than automatic reload or merge.
53
79
 
54
80
  For Workflows, the current `WorkflowContext` and FSMSession own the active
55
81
  logical state. A durable Workflow hydrates once at invocation/resume and saves at
@@ -58,34 +84,198 @@ the halted/terminal boundary.
58
84
  Content-addressed `Persistence#contents` values are immutable. Fetching a known
59
85
  content reference is value materialization rather than mutable state refresh.
60
86
 
61
- ## Workflow identities and durable admission
87
+ ## Process-local Agent identity ownership and admission
88
+
89
+ `agent_id` identifies one logical Agent, not a reusable lookup key for independent
90
+ mutable objects. One Runtime therefore publishes at most one mutable live Agent
91
+ instance for a given `agent_id`. The Runtime-owned registry is an authority, not a
92
+ cache, and reserves the identity before create/load materialization.
93
+
94
+ The application-facing identity operations are distinct:
95
+
96
+ ```text
97
+ new / create
98
+ create a new Agent; existing live or durable identity is an error
99
+
100
+ load(agent_id, persistence:)
101
+ live -> exact same Ruby object, with no Persistence reload
102
+ durable-only -> hydrate and publish once
103
+ missing -> Persistence::NotFoundError
104
+
105
+ get(agent_id)
106
+ live Runtime lookup only; missing -> nil
107
+ ```
108
+
109
+ A live Agent is strongly owned for the Runtime lifetime even while idle and
110
+ across sequential Executions. Execution completion does not evict it. Clean
111
+ Runtime shutdown detaches old Agent objects so they cannot remain mutable beside
112
+ a later Runtime owner. `purge!` is the explicit earlier destruction boundary: it
113
+ invalidates the old object, deletes durable state, releases the process-local
114
+ identity, and allows a later new Agent to reuse the textual ID.
115
+
116
+ Live Agent ownership and top-level Execution admission are separate lifetimes.
117
+ For one live Agent, EventLoop admits at most one nonterminal top-level Execution.
118
+ Admission is acquired **before** the initial Offload/Persistence operation:
62
119
 
63
- Workflow execution keeps three identities separate:
120
+ ```text
121
+ invoke
122
+ -> EventLoop Agent admission
123
+ -> Offload/Persistence executions.create_active
124
+ -> EventLoop live execution state
125
+ ```
126
+
127
+ `preparing`, `active`, and `suspended` all retain the slot. A competing request is
128
+ rejected with `AgentBusyError`; core does not promise automatic queueing. A
129
+ known-successful durable terminal transition releases the slot. A known
130
+ pre-durable failure may release it; an uncertain durable outcome remains
131
+ fail-closed/recovery-required.
132
+
133
+ `Persistence#executions.create_active`, optimistic revision, Journal position, and
134
+ watermark checks remain required durable defenses. They do not become the
135
+ primary same-process live ownership/admission mechanism and do not provide
136
+ cross-process exclusion.
137
+
138
+ ## EventLoop single-writer and Offload result application
139
+
140
+ Persistence repositories are synchronous, so durable work must remain off the
141
+ EventLoop thread. The ownership rule is therefore not "run everything on
142
+ EventLoop". It is:
143
+
144
+ ```text
145
+ EventLoop
146
+ capture operation-specific immutable state
147
+ ↓
148
+ OffloadPool
149
+ blocking I/O / CPU / operation-local calculation
150
+ durable commit
151
+ ↓ operation-specific result
152
+ EventLoop
153
+ validate current authority
154
+ apply committed result to live state
155
+ ```
156
+
157
+ Agent initial preparation, follow-up Manifest preparation, approval resume, and
158
+ terminal commit use distinct command/result values. An Offload worker may commit
159
+ Persistence but does not update the live Agent root, Journal view, current
160
+ AgentExecution, RuntimeProjection, AgentInvocation runtime queues, or EventLoop
161
+ execution directory.
162
+
163
+ Completion callbacks are lightweight bridges that enqueue the result back to the
164
+ EventLoop. If EventLoop no longer accepts the result, the callback does not fall
165
+ back to direct live mutation.
166
+
167
+ ## Provider Call result authority
168
+
169
+ Provider Call identity is purpose-specific semantic provenance. EventLoop
170
+ allocates `llm_call_id` before transport begins and binds it to the Manifest used
171
+ for that call.
172
+
173
+ Provider completion and streaming chunks return through the owning FSMSession's
174
+ EventSink and carry the `llm_call_id`. A result is applicable only when:
175
+
176
+ - it still targets the current FSMSession incarnation;
177
+ - the FSM is in the state that accepts that result; and
178
+ - the AgentInvocation still owns the same active `llm_call_id`.
179
+
180
+ A callback to an old FSMSession incarnation is dropped by session-local routing.
181
+ A result with a stale `llm_call_id` is consumed without advancing the current
182
+ FSM. Phronomy does not add a generic generation/correlation token as another
183
+ result authority.
184
+
185
+ Tool operations follow the same ownership direction. `tool_invocation_id` is the
186
+ semantic Tool-operation identity, while FSMSession ID is Runtime routing identity.
187
+ Tool authorization captures Agent identity and Tool description data as values on
188
+ EventLoop before offload. The authorization worker receives no live Agent, Tool, or
189
+ ToolInvocation reference. Application-owned approval/facts/requirement callables
190
+ are explicitly classified behavior handles and receive a value-only
191
+ `ApprovalEvaluationRequest`.
192
+
193
+ Hash, Array, and String authorization command data is recursively copied/frozen.
194
+ Phronomy-managed live domain objects are rejected from that value data. A complete
195
+ value-type/serialization contract for arbitrary Application-owned opaque objects is
196
+ deferred; such objects remain Application-owned and must be worker-safe.
197
+
198
+ Worker authorization/execution outcomes return as values carrying
199
+ `tool_invocation_id`; the Tool FSMSession consumes a mismatched semantic result
200
+ without advancing its current state.
201
+
202
+ ## Approval suspension and live owner lookup
203
+
204
+ Approval suspension retains the same process-local Agent and AgentInvocation but
205
+ has no active owning FSMSession until resume. EventLoop retains the suspended
206
+ execution entry.
207
+
208
+ `Agent::Base.live_for_execution(execution_id)` resolves a read-only Runtime owner
209
+ view and returns the existing Agent instance. `agent.approve_async(...)` routes to
210
+ the same live coordinator. Neither operation reloads a replacement Agent or
211
+ Execution from Persistence.
212
+
213
+ A resume performs its durable approval transition through OffloadPool, applies
214
+ the result on EventLoop, and then builds a **fresh** FSMSession incarnation.
215
+ If the process-local owner no longer exists, durable continuation reconstruction
216
+ is not implied; `ExecutionRehydrationRequiredError` is raised.
217
+
218
+ ## Workflow identities, admission, and durable terminal barrier
219
+
220
+ Workflow runtime keeps identity and coordination responsibilities separate:
64
221
 
65
222
  ```text
66
223
  session_id
67
- application session/correlation identity
224
+ application session/correlation metadata
68
225
 
69
- thread_id
70
- durable Workflow identity and Persistence#workflow_states key
226
+ workflow_instance_id
227
+ logical/durable Workflow identity and Persistence#workflow_states key
228
+
229
+ admission owner token
230
+ opaque process-local Runtime coordination capability
71
231
 
72
232
  fsm_session_id
73
- one Runtime FSMSession execution identity; generated again for each invoke/resume
233
+ one concrete Runtime FSMSession routing identity
234
+ ```
235
+
236
+ EventLoop acquires the `workflow_instance_id` admission with a fresh opaque owner
237
+ token **before** `workflow_states.load(workflow_instance_id)` is submitted. The
238
+ token is not a domain identity and is never an Event target. After durable
239
+ hydration, EventLoop constructs the concrete FSMSession and binds its fresh
240
+ `fsm_session_id` to the existing admission for `Workflow#signal` routing.
241
+
242
+ ```text
243
+ admit workflow_instance_id with owner token
244
+ ↓
245
+ Offload workflow_states.load
246
+ ↓
247
+ EventLoop hydrate / create FSMSession
248
+ ↓
249
+ bind fsm_session_id for routing
74
250
  ```
75
251
 
76
- The application `session_id` is tracing/caller metadata and is not used for
77
- durable Workflow ownership. EventLoop registers active FSMs by `fsm_session_id`;
78
- durable Workflow admission is a separate owner map:
252
+ A durable Workflow also keeps terminal persistence inside the FSMSession
253
+ lifecycle. Logical halt/completion first enters a private
254
+ `persisting_terminal` lifecycle condition; the FSMSession remains nonterminal
255
+ while WorkflowRunner saves the terminal snapshot through OffloadPool. Only a
256
+ known-successful save result returned to that same FSMSession permits
257
+ `HALTED`/`COMPLETED`, admission release, and caller Task settlement.
79
258
 
80
259
  ```text
81
- thread_id -> owner_fsm_session_id
260
+ RUNNING
261
+ ↓ logical halt/completion
262
+ PERSISTING_TERMINAL
263
+ ├─ known success -> HALTED / COMPLETED -> release
264
+ ├─ known failure -> ERROR -> release
265
+ └─ outcome unknown -> RECOVERY_REQUIRED (fail closed)
82
266
  ```
83
267
 
84
- The owner is acquired before `workflow_states.load(thread_id)` and remains held
85
- until the halted/terminal `workflow_states.save(...)` completes. The admission map
86
- is process-local. Cross-process duplicate execution requires application-level
87
- distributed coordination; optimistic revisions detect stale terminal commits but
88
- do not prevent duplicate side effects before that conflict is detected.
268
+ The FSMSession does not know whether Persistence is local, remote, SQL, HTTP, or
269
+ networked. The Workflow persistence operation normalizes the result into
270
+ `success`, `known_failure`, or `outcome_unknown`. Only known success crosses the
271
+ durable barrier. If the backend/storage error does not establish non-commit,
272
+ Phronomy treats the terminal outcome as uncertain rather than guessing.
273
+
274
+ `recovery_required` prevents a fresh same-Workflow execution segment from being
275
+ admitted, but ACS-13 does not claim restart-safe reconciliation; that remains
276
+ ACS-15 work. The admission map itself is process-local. Cross-process duplicate
277
+ execution requires the later coordination/fencing work; optimistic revisions
278
+ remain durable conflict defense rather than distributed ownership.
89
279
 
90
280
  ## Tool execution modes
91
281
 
@@ -132,8 +322,7 @@ waiting at the same time.
132
322
  `Persistence` repositories expose synchronous operations. Framework lifecycle
133
323
  code must not perform potentially blocking durable reads/writes on EventLoop.
134
324
  Agent preparation/commit and Workflow hydrate/save operations are submitted to
135
- `OffloadPool`; completion continues through Task callbacks or explicit EventLoop
136
- events.
325
+ `OffloadPool`; completion continues through explicit EventLoop events.
137
326
 
138
327
  A durable barrier may pause one logical lifecycle without blocking EventLoop.
139
328
  Persistence does not implement async repository variants and must not depend on
@@ -174,8 +363,9 @@ abandonment state.
174
363
  `Task#on_complete` registers an independent notification callback. Callback
175
364
  execution thread is not guaranteed. A callback may be delivered by an OffloadPool
176
365
  worker, a timer/cancellation caller, an EventLoop-related control path, or the
177
- thread that registers after settlement. Callbacks must be thread-safe and should
178
- complete quickly.
366
+ thread that registers after settlement. Callbacks must therefore be thread-safe
367
+ and should complete quickly. Framework lifecycle code normally converts worker
368
+ completion into an explicit EventLoop event before applying live state.
179
369
 
180
370
  `Task#map` is application-level composition. A transformation exception settles
181
371
  the mapped Task as failed.
@@ -301,6 +491,10 @@ Workflow durable admission participates in EventLoop idleness: a Workflow whose
301
491
  FSMSession has ended but whose durable save is still in flight remains owned until
302
492
  that save completes and owner-aware admission is released.
303
493
 
494
+ Suspended Agent execution owner entries are process-local continuation state and
495
+ do not by themselves keep Runtime shutdown waiting. Runtime/process loss does not
496
+ imply durable Agent continuation reconstruction.
497
+
304
498
  `Phronomy.reset_runtime!` exists primarily for test isolation and performs a real
305
499
  Runtime shutdown before resetting configuration.
306
500
 
@@ -0,0 +1,13 @@
1
+ # Phronomy examples
2
+
3
+ Runnable application examples are maintained in the dedicated
4
+ [`Raizo-TCS/phronomy-examples`](https://github.com/Raizo-TCS/phronomy-examples)
5
+ repository.
6
+
7
+ This repository intentionally does not keep a second runnable example set under
8
+ `examples/`. Keeping one canonical sample repository avoids API drift between
9
+ Phronomy's implementation/documentation and independently maintained copies of
10
+ sample code.
11
+
12
+ For framework-level executable behavior and regression coverage, use the specs
13
+ under `spec/`. For application-oriented examples, use `phronomy-examples`.
@@ -7,12 +7,13 @@ module Phronomy
7
7
  module Agent
8
8
  class AgentExecution
9
9
  ACTIVE_STATUSES = %i[preparing active suspended].freeze
10
- TERMINAL_STATUSES = %i[completed failed cancelled rejected blocked].freeze
10
+ TERMINAL_STATUSES = %i[completed handed_off failed cancelled rejected blocked].freeze
11
11
  TRANSITIONS = {
12
12
  preparing: %i[preparing active failed cancelled blocked],
13
- active: %i[active suspended completed failed cancelled rejected blocked],
13
+ active: %i[active suspended completed handed_off failed cancelled rejected blocked],
14
14
  suspended: %i[suspended active failed cancelled],
15
15
  completed: %i[completed],
16
+ handed_off: %i[handed_off],
16
17
  failed: %i[failed],
17
18
  cancelled: %i[cancelled],
18
19
  rejected: %i[rejected],
@@ -84,10 +85,8 @@ module Phronomy
84
85
  self.class.new(**values)
85
86
  end
86
87
 
87
- # Returns the canonical durable representation of this execution.
88
- # Nested JournalRecord and LLMCallRecord values are recursively encoded.
89
- #
90
- # @return [Hash{String => Object}]
88
+ # Current semantic payload representation. Persistence format identity and
89
+ # compatibility validation are owned by Persistence::DurableCodec.
91
90
  # @api public
92
91
  def to_h
93
92
  ATTRIBUTES.to_h do |name|
@@ -98,18 +97,23 @@ module Phronomy
98
97
  end
99
98
  end
100
99
 
101
- # Restores an execution from its canonical durable representation.
102
- # String and Symbol top-level keys are accepted. Nested working Journal
103
- # records and LLM Call records are restored through their public codecs so
104
- # storage backends do not need to know their constructor details.
105
- #
106
- # @param hash [Hash]
107
- # @return [AgentExecution]
100
+ # Restores only the current semantic payload shape. Historical durable
101
+ # representations must go through explicit Persistence migration first.
108
102
  # @api public
109
103
  def self.from_h(hash)
104
+ source = hash.to_h { |key, value| [key.to_s, value] }
105
+ expected = ATTRIBUTES.map(&:to_s).sort
106
+ actual = source.keys.sort
107
+ unless actual == expected
108
+ missing = expected - actual
109
+ unknown = actual - expected
110
+ raise ArgumentError,
111
+ "AgentExecution payload schema mismatch: " \
112
+ "missing=#{missing.inspect}, unknown=#{unknown.inspect}"
113
+ end
114
+
110
115
  attributes = ATTRIBUTES.to_h do |name|
111
- key = hash.key?(name.to_s) ? name.to_s : name
112
- [name, hash.fetch(key)]
116
+ [name, source.fetch(name.to_s)]
113
117
  end
114
118
 
115
119
  attributes[:working_records] = attributes.fetch(:working_records).map do |record|