phronomy 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +215 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +147 -0
  15. data/docs/architecture/persistence.md +186 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +89 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
  45. data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
  46. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
  47. data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
  48. data/docs/decisions/README.md +176 -0
  49. data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
  50. data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
  51. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
  52. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
  53. data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
  54. data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
  55. data/docs/features.md +65 -11
  56. data/docs/getting-started.md +77 -45
  57. data/docs/migrations/0.19.md +14 -7
  58. data/docs/migrations/0.22.md +390 -0
  59. data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
  60. data/docs/persistence-backends.md +129 -40
  61. data/docs/runtime-and-concurrency.md +227 -33
  62. data/examples/README.md +13 -0
  63. data/lib/phronomy/agent/agent_execution.rb +21 -17
  64. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  65. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  66. data/lib/phronomy/agent/agent_root.rb +3 -3
  67. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  68. data/lib/phronomy/agent/async_event_api.rb +162 -72
  69. data/lib/phronomy/agent/base.rb +416 -181
  70. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  71. data/lib/phronomy/agent/context_assembler.rb +437 -178
  72. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  73. data/lib/phronomy/agent/context_plan.rb +18 -13
  74. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  75. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  76. data/lib/phronomy/agent/context_policy.rb +109 -3
  77. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  78. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  79. data/lib/phronomy/agent/exact_execution.rb +153 -0
  80. data/lib/phronomy/agent/execution_cancellation.rb +25 -0
  81. data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
  82. data/lib/phronomy/agent/handoff.rb +43 -0
  83. data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
  84. data/lib/phronomy/agent/handoff_context.rb +120 -0
  85. data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
  86. data/lib/phronomy/agent/handoff_policy.rb +143 -0
  87. data/lib/phronomy/agent/handoff_projection.rb +207 -0
  88. data/lib/phronomy/agent/handoff_request.rb +45 -0
  89. data/lib/phronomy/agent/handoff_runner.rb +178 -0
  90. data/lib/phronomy/agent/handoff_state.rb +43 -0
  91. data/lib/phronomy/agent/journal_record.rb +17 -4
  92. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  93. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  94. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  95. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  96. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  97. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
  98. data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
  99. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
  100. data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
  101. data/lib/phronomy/agent/recovery_support.rb +504 -0
  102. data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
  103. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  104. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  105. data/lib/phronomy/agent/shared_state.rb +38 -1
  106. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  107. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  108. data/lib/phronomy/agent/tool_invocation.rb +339 -103
  109. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  110. data/lib/phronomy/agent.rb +20 -2
  111. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  112. data/lib/phronomy/agent_purged_error.rb +5 -0
  113. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  114. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  115. data/lib/phronomy/engine/event_loop.rb +622 -63
  116. data/lib/phronomy/engine/fsm_session.rb +194 -21
  117. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  118. data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
  119. data/lib/phronomy/engine/runtime.rb +92 -20
  120. data/lib/phronomy/generator_verifier.rb +12 -14
  121. data/lib/phronomy/invocation_context.rb +9 -29
  122. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  123. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
  124. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  125. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  126. data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
  127. data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
  128. data/lib/phronomy/multi_agent/team_execution.rb +44 -0
  129. data/lib/phronomy/multi_agent/team_root.rb +41 -0
  130. data/lib/phronomy/persistence/durable_codec.rb +706 -0
  131. data/lib/phronomy/persistence/durable_record.rb +117 -0
  132. data/lib/phronomy/persistence/in_memory.rb +471 -133
  133. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  134. data/lib/phronomy/persistence/repository_facades.rb +535 -0
  135. data/lib/phronomy/persistence.rb +174 -40
  136. data/lib/phronomy/recovery.rb +186 -0
  137. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  138. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
  139. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  140. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  141. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  142. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
  143. data/lib/phronomy/testing/persistence_contract.rb +5 -0
  144. data/lib/phronomy/tools/agent.rb +1 -1
  145. data/lib/phronomy/tracing/automatic.rb +176 -0
  146. data/lib/phronomy/tracing/base.rb +11 -2
  147. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  148. data/lib/phronomy/version.rb +1 -1
  149. data/lib/phronomy/workflow.rb +3 -6
  150. data/lib/phronomy/workflow_context.rb +14 -5
  151. data/lib/phronomy/workflow_recovery.rb +123 -0
  152. data/lib/phronomy/workflow_runner.rb +468 -256
  153. data/lib/phronomy.rb +6 -0
  154. data/scripts/api_snapshot.rb +12 -0
  155. data/sig/phronomy/agent.rbs +209 -7
  156. data/sig/phronomy/handoff.rbs +41 -0
  157. data/sig/phronomy/multi_agent.rbs +35 -0
  158. data/sig/phronomy/persistence.rbs +123 -4
  159. data/sig/phronomy/runtime.rbs +1 -4
  160. data/sig/phronomy/workflow.rbs +2 -2
  161. data/sig/phronomy.rbs +10 -0
  162. metadata +84 -18
  163. data/examples/workflows/agent_event_mapping.rb +0 -101
  164. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  165. data/lib/phronomy/agent/activation_registry.rb +0 -28
  166. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  167. data/lib/phronomy/agent/context_candidate.rb +0 -47
  168. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  169. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  170. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  171. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  172. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  173. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  174. data/lib/phronomy/agent/context_request.rb +0 -35
  175. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  176. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  177. data/lib/phronomy/agent/runner.rb +0 -97
  178. data/lib/phronomy/multi_agent/handoff.rb +0 -64
@@ -0,0 +1,193 @@
1
+ # ADR 022: Agent Execution Parent Identity and Runtime Routing Boundary
2
+
3
+ **Status**: Accepted
4
+ **Date**: 2026-08-23
5
+ **Related**:
6
+ - [ADR-014](014-unified-persistence-durable-state.md)
7
+ - [ADR-021](021-generic-agent-invocation-identity-removal.md)
8
+ - [ADR-023](023-fsm-session-incarnation-identity-and-routing.md)
9
+ - [ADR-024](024-event-loop-single-writer-agent-runtime.md)
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ Phronomy has a canonical durable Agent execution identity, `execution_id`, but
16
+ Tool and approval paths still expose or carry the older
17
+ `agent_invocation_id`/`parent_agent_invocation_id` terminology.
18
+
19
+ Those older values mix two different concerns:
20
+
21
+ 1. the logical parent Agent execution; and
22
+ 2. the concrete Runtime FSMSession target used to deliver an event.
23
+
24
+ The two lifetimes differ. An Agent execution can survive a suspension while its
25
+ concrete FSMSession incarnation is rebuilt. A Runtime delivery target therefore
26
+ must not become the durable or application-facing parent identity.
27
+
28
+ `AgentInvocation` is the live FSM context of an `AgentExecution`; it is not a
29
+ separate logical/domain entity. `ToolInvocation` has its own Tool lifecycle
30
+ identity and provider Tool Call identity, but its logical parent is the owning
31
+ Agent execution.
32
+
33
+ ## Decision
34
+
35
+ The canonical logical parent of Agent-owned Tool and approval work is
36
+ `execution_id`.
37
+
38
+ The current identity relationship is:
39
+
40
+ ```text
41
+ AgentExecution
42
+ execution_id
43
+
44
+ AgentInvocation
45
+ execution_id # parent reference, not an independent identity
46
+
47
+ ToolInvocation
48
+ execution_id # logical parent
49
+ tool_invocation_id # Tool lifecycle identity
50
+ tool_call_id # Provider-originated Tool Call identity
51
+
52
+ ToolApprovalRequest
53
+ approval request id
54
+ execution_id # logical parent
55
+
56
+ ApprovalEvaluationRequest
57
+ execution_id # logical parent
58
+ tool_invocation_id
59
+ tool_call_id
60
+ ```
61
+
62
+ Application-facing approval surfaces change as a clean break:
63
+
64
+ ```text
65
+ ToolApprovalRequest#agent_invocation_id
66
+ -> ToolApprovalRequest#execution_id
67
+
68
+ ToolApprovalRequest#to_h[:agent_invocation_id]
69
+ -> ToolApprovalRequest#to_h[:execution_id]
70
+
71
+ ApprovalEvaluationRequest#agent_invocation_id
72
+ -> ApprovalEvaluationRequest#execution_id
73
+ ```
74
+
75
+ No deprecated alias is retained. Agent invocation configuration also rejects the
76
+ legacy `agent_invocation_id` key instead of allowing application code to control
77
+ a Runtime routing identifier.
78
+
79
+ ## Runtime routing is separate
80
+
81
+ CG-03a does not redesign Runtime routing.
82
+
83
+ The implementation may temporarily retain private
84
+ `parent_agent_invocation_id`-named storage as the existing parent FSMSession
85
+ routing carrier until the Runtime foundation is reconciled. That temporary
86
+ carrier:
87
+
88
+ - is not the logical/domain parent;
89
+ - is not application-facing identity;
90
+ - is not a durable field;
91
+ - must not be renamed to `execution_id` and then used as an EventLoop target.
92
+
93
+ CG-03b, integrated with ACS-10/ACS-11, will give each concrete FSMSession its own
94
+ fresh ID and replace domain-object routing reuse with session-local Runtime
95
+ bindings. Rebuilt sessions receive rebuilt bindings. Stale completion must not be
96
+ applied to a new session merely because the logical execution is the same.
97
+
98
+ No long-lived `parent_fsm_session_id` field is introduced on `ToolInvocation`.
99
+
100
+ ## Durable approval compatibility
101
+
102
+ Current approval suspension stores `ToolApprovalRequest#to_h` both as referenced
103
+ ContentStore audit content and in `AgentExecution#approval_request`.
104
+
105
+ New canonical approval request representations use `execution_id`.
106
+
107
+ Existing durable `AgentExecution` hashes may contain an embedded approval request
108
+ with `agent_invocation_id`. During `AgentExecution.from_h`, that legacy key is
109
+ discarded and the current logical parent is derived from the enclosing
110
+ `AgentExecution#execution_id`. The old `agent_invocation_id` value is not renamed
111
+ or reinterpreted as an execution ID.
112
+
113
+ Existing content-addressed approval audit bodies are not rewritten. Their bytes
114
+ are historical execution evidence and may retain the legacy field. New writes use
115
+ the current representation.
116
+
117
+ This is a targeted compatibility rule. It does not establish a general durable
118
+ schema/versioning policy, which remains separate persistence work.
119
+
120
+ ## Staging
121
+
122
+ ```text
123
+ CG-03a
124
+ execution_id logical parent
125
+ public approval clean break
126
+ targeted embedded durable-read compatibility
127
+ status: reconciled by the CG-03a change
128
+
129
+ CG-03b
130
+ fresh FSMSession identity
131
+ session-local Runtime routing binding
132
+ stale-session completion rejection
133
+ duplicate AgentInvocation/ToolInvocation session fields cleanup
134
+ status: identity/routing slice implemented by ADR-023; result/live-state
135
+ authority completed by ADR-024 / ACS-11
136
+ ```
137
+
138
+ CG-03's Agent/Tool Runtime-foundation criteria are reconciled by the joint
139
+ ADR-023 / ADR-024 implementation: concrete-session routing is separated from
140
+ domain identity, and worker results return to EventLoop for current-state and
141
+ semantic-ID validation before live apply. Workflow admission ownership remains
142
+ separate ACS-13 work.
143
+
144
+ ## CG-03b routing foundation implementation
145
+
146
+ The Runtime now binds Agent/Tool/Multi-Agent asynchronous completion through a
147
+ session-local `FSMSession::EventSink`. Rebuilt sessions receive fresh sinks and
148
+ IDs; old sinks are never rebound to a new `execution_id` incarnation.
149
+
150
+ `AgentInvocation` therefore has no independent `id`/`session_id`.
151
+ `ToolInvocation#id` remains the semantic Tool lifecycle identity and is no
152
+ longer reused as an FSMSession ID. Workflow uses a private FSMSession-owned
153
+ identity reservation only because its existing pre-load admission still uses the
154
+ future FSMSession ID; opaque Workflow admission ownership is explicitly ACS-13.
155
+
156
+ ## Explicit non-goals
157
+
158
+ This decision does not in CG-03a:
159
+
160
+ - stop using `AgentInvocation#id` as an Agent FSMSession ID;
161
+ - stop using `ToolInvocation#id` as a Tool FSMSession ID;
162
+ - remove the temporary `parent_agent_invocation_id` Runtime routing carrier;
163
+ - remove `AgentInvocation#session_id` or `ToolInvocation#session_id`;
164
+ - add a long-lived `parent_fsm_session_id`;
165
+ - remove `AgentExecutionActivation`;
166
+ - implement EventLoop single-writer ownership;
167
+ - implement recovery, rehydration, fencing, or cross-process ownership;
168
+ - define general Persistence schema/version evolution.
169
+
170
+ ## Consequences
171
+
172
+ ### Positive
173
+
174
+ - Agent, Tool, and approval logical-parent vocabulary uses an existing
175
+ purpose-specific domain identity.
176
+ - Application policy/notification code no longer receives a Runtime-oriented
177
+ Agent invocation identity.
178
+ - Provider `tool_call_id`, Phronomy `tool_invocation_id`, approval request ID, and
179
+ Agent `execution_id` remain distinct.
180
+ - Old embedded suspended-execution data remains readable without inventing a
181
+ false identity mapping.
182
+ - The Runtime foundation can later change FSMSession incarnation/routing
183
+ independently of durable and application-facing parent identity.
184
+
185
+ ### Trade-offs
186
+
187
+ - This is a pre-1.0 breaking application API change.
188
+ - Applications that persisted `ToolApprovalRequest#to_h` must read the new
189
+ `execution_id` key for newly produced requests.
190
+ - Historical content-addressed approval audit bodies are intentionally not
191
+ rewritten and may still contain `agent_invocation_id`.
192
+ - The Agent/Tool portion of CG-03 is complete only when ADR-023 routing and
193
+ ADR-024 EventLoop result authority are both present.
@@ -0,0 +1,139 @@
1
+ # ADR-023: FSMSession Incarnation Identity and Runtime Routing Foundation
2
+
3
+ **Status**: Accepted
4
+ **Date**: 2026-08-23
5
+ **Partially superseded by**: [ADR-026](026-workflow-runtime-admission-and-durable-terminal-barrier.md) for the transitional Workflow FSMSession-identity reservation/admission bridge
6
+ **Related**:
7
+ - [ADR-010](010-cooperative-first-concurrency.md)
8
+ - [ADR-014](014-unified-persistence-durable-state.md)
9
+ - [ADR-020](020-canonical-workflow-instance-identity.md)
10
+ - [ADR-022](022-agent-execution-parent-identity-and-runtime-routing-boundary.md)
11
+ - [ADR-024](024-event-loop-single-writer-agent-runtime.md)
12
+
13
+ ---
14
+
15
+ ## Context
16
+
17
+ Phronomy's EventLoop routes events to concrete `FSMSession` instances. Before
18
+ this decision, Agent, Tool, and Multi-Agent context objects generated IDs that
19
+ were injected into `FSMSession`, and callbacks later reused those object IDs as
20
+ EventLoop routing targets. Workflow likewise pre-generated a Runtime ID because
21
+ its current same-process admission is acquired before durable hydration.
22
+
23
+ This mixes domain/context identity with the identity of one concrete Runtime FSM
24
+ incarnation and makes suspend/resume stale-result safety difficult to reason
25
+ about.
26
+
27
+ ## Decision
28
+
29
+ ### 1. Concrete FSMSession identity is allocated by FSMSession infrastructure
30
+
31
+ A normal concrete `FSMSession` generates and owns a fresh `FSMSession#id` when
32
+ constructed. Agent, Tool, and Multi-Agent domain/context IDs are not injected as
33
+ that identity.
34
+
35
+ Across Runtime object/event boundaries the value is named `fsm_session_id`.
36
+ Terminal management payloads use `fsm_session_id` rather than generic
37
+ `session_id`.
38
+
39
+ Workflow is a narrow transitional case. Its current admission must be acquired
40
+ before durable state load, while the concrete FSMSession is constructed only
41
+ after hydration. Until ACS-13 separates Workflow admission ownership from
42
+ FSMSession routing identity, Workflow obtains a single-use identity reservation
43
+ from `FSMSession.reserve_identity`; the concrete FSMSession later claims exactly
44
+ that Runtime-owned reservation. Arbitrary raw `id:` injection is removed.
45
+
46
+ This reservation is Runtime-only and is not a Workflow/domain identity.
47
+
48
+ ### 2. Async routing uses session-local event sinks
49
+
50
+ A concrete FSMSession has a Runtime-only event sink bound exactly once to its
51
+ `fsm_session_id`. Async work captures the sink belonging to the session that
52
+ started it. A rebuilt session receives a different sink and ID.
53
+
54
+ An old sink is never rebound or retargeted. If its session has terminated,
55
+ `EventLoop#post_to_session` rejects the old target rather than translating it to
56
+ a new session of the same logical execution.
57
+
58
+ This foundation is applied to Agent LLM completion/stream chunks, Tool
59
+ authorization/execution, Tool-to-parent notifications, callback-failure
60
+ notification, and Multi-Agent fan-out completion/timeout/cancellation.
61
+
62
+ ### 3. Agent and Tool live/domain objects do not duplicate routing identity
63
+
64
+ `AgentInvocation` is a live FSM context belonging to `execution_id`; it has no
65
+ independent `id` and no duplicate Runtime `session_id`.
66
+
67
+ `ToolInvocation#id` remains the semantic `tool_invocation_id`. It is not an
68
+ FSMSession ID. ToolInvocation stores neither a duplicate session ID nor a
69
+ long-lived parent FSMSession ID. Parent routing is supplied as a session-local
70
+ sink when a Tool FSMSession is constructed.
71
+
72
+ No `parent_fsm_session_id`, generic generation token, or replacement generic
73
+ correlation identity is introduced.
74
+
75
+ ### 4. Provider completion is routed before live result application
76
+
77
+ Provider completion callbacks post an immutable `LLMOperationResult` through the
78
+ session-local sink. The result carries the Provider Call's semantic `llm_call_id`.
79
+ The AgentInvocation EventLoop handler applies it only when the concrete session
80
+ still owns the event and the `llm_call_id` still matches the current Provider
81
+ Call.
82
+
83
+ ADR-024 completes this result-authority rule by making EventLoop the single
84
+ writer of Phronomy-managed live Agent execution state and by removing the former
85
+ Activation shared-mutable state model.
86
+
87
+ ### 5. Runtime incarnation identity is not durable state
88
+
89
+ `fsm_session_id`, identity reservations, event sinks, callbacks, Tasks, and
90
+ other process-local Runtime values are not persisted as logical recovery state.
91
+ Recovery creates fresh Runtime objects from confirmed durable semantic state.
92
+
93
+ ## Relationship to ACS-11 and ACS-13
94
+
95
+ This decision is the ACS-10 identity/routing foundation and the implementation
96
+ half of CG-03b. ADR-024/ACS-11 builds directly on it: Offload work now returns
97
+ operation-specific results and EventLoop validates/applies those results against
98
+ current Runtime state and purpose-specific semantic identity.
99
+
100
+ Together, ADR-023 and ADR-024 close the Agent/Tool result-routing and live-state
101
+ authority portion of CG-03 without introducing another generic identity.
102
+
103
+ ACS-13 separately owns Workflow's opaque admission owner handle and the durable
104
+ terminal-save barrier. This ADR does not pull those Workflow lifecycle changes
105
+ forward. The current `owner_fsm_session_id` admission representation remains an
106
+ explicit transitional mismatch until ACS-13.
107
+
108
+ ## Explicit non-goals
109
+
110
+ This decision does not implement:
111
+
112
+ - ACS-13 Workflow opaque admission owner or durable-save-before-terminal barrier;
113
+ - restart-safe HITL/Workflow rehydration;
114
+ - same-process Agent admission redesign;
115
+ - cross-process ownership, leases, or fencing;
116
+ - general Persistence schema/version evolution.
117
+
118
+ Those items remain governed by their own later change sets. EventLoop single-
119
+ writer Agent ownership itself is defined by ADR-024 rather than duplicated here.
120
+
121
+ ## Consequences
122
+
123
+ ### Positive
124
+
125
+ - Agent/Tool/Multi-Agent domain IDs are no longer EventLoop routing IDs.
126
+ - Rebuilt sessions receive fresh session-local routing sinks.
127
+ - Old-session Provider completion is not applied to a newer session merely
128
+ because the logical `execution_id` is unchanged.
129
+ - Provider results are additionally protected by current semantic `llm_call_id`.
130
+ - Workflow preserves pre-load admission ordering without retaining arbitrary
131
+ caller-supplied FSMSession IDs.
132
+ - ACS-13 admission-owner redesign remains cleanly separated.
133
+
134
+ ### Trade-offs
135
+
136
+ - Workflow temporarily uses a private FSMSession identity reservation because
137
+ its current admission owner is still the future FSMSession ID.
138
+ - Runtime result application now requires explicit state/semantic-ID validation
139
+ rather than relying on a shared mutable continuation container.
@@ -0,0 +1,188 @@
1
+ # ADR-024: EventLoop Single-Writer Agent Runtime State
2
+
3
+ **Status**: Accepted
4
+ **Date**: 2026-08-24
5
+ **Partially supersedes**: [ADR-014](014-unified-persistence-durable-state.md) for live Agent Runtime ownership and `AgentExecutionActivation`
6
+ **Complements**: [ADR-010](010-cooperative-first-concurrency.md), [ADR-012](012-canonical-execution-log-and-context-policy.md), [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)
7
+
8
+ ---
9
+
10
+ ## Context
11
+
12
+ ADR-014 correctly separated durable Persistence from process-local Agent continuation state, but its implementation introduced `AgentExecutionActivation` and `ActivationRegistry` as a shared mutable Runtime container. The Activation was protected by a Mutex and was mutated from EventLoop, OffloadPool workers, and asynchronous completion callbacks.
13
+
14
+ That model prevents one component from being the unambiguous live-state authority. In particular, Agent durable preparation and terminal commit paths could successfully persist a transition and then directly mutate live `AgentExecution`, `AgentRoot`, Journal projection, Provider-call state, or Runtime projection from an OffloadPool worker.
15
+
16
+ CG-03b / ADR-023 established the prerequisite routing foundation: every concrete Agent/Tool/Multi-Agent FSMSession owns a fresh Runtime incarnation identity and asynchronous completion is routed through that session's local EventSink. ACS-11 closes the remaining state-ownership gap.
17
+
18
+ ## Decision
19
+
20
+ ### EventLoop is the single writer of Phronomy-managed live Agent execution state
21
+
22
+ All Phronomy-managed live mutation associated with an active Agent execution is applied on the Runtime EventLoop thread.
23
+
24
+ The Runtime no longer owns an `ActivationRegistry`, and `AgentExecutionActivation` is removed. EventLoop owns a process-local execution directory:
25
+
26
+ ```text
27
+ execution_id
28
+ -> immutable AgentExecutionState value
29
+ agent
30
+ coordinator
31
+ current AgentExecution
32
+ current RuntimeProjection
33
+ base Manifest
34
+ current AgentInvocation
35
+ current owning fsm_session_id
36
+ ```
37
+
38
+ The directory is the mutable authority. Individual entries are immutable values and are replaced atomically on EventLoop rather than mutated in place.
39
+
40
+ External live-owner lookup does not expose this state. `Runtime#__agent_execution_owner` returns only a read-only process-local owner view sufficient to resolve the current Agent/coordinator and execution status.
41
+
42
+ ### AgentInvocation owns FSM-local mutable invocation facts
43
+
44
+ Mutable facts that are meaningful only while one Agent FSMSession progresses belong to `AgentInvocation` and are mutated only from EventLoop-driven FSM handling. These include:
45
+
46
+ - the active Provider Call provenance;
47
+ - uncommitted Provider outcomes;
48
+ - uncommitted Tool/runtime events;
49
+ - application callback failure state;
50
+ - Tool batch and approval-resume state.
51
+
52
+ This is not a replacement Activation. `AgentInvocation` is the FSM context of one logical Agent execution and is not shared as a worker-side mutable authority.
53
+
54
+ ### OffloadPool receives operation-specific snapshots and returns operation-specific results
55
+
56
+ Synchronous Persistence I/O and other long synchronous work remain off EventLoop. Each operation captures the value data it needs before submission. Hash, Array, and String command data is recursively copied/frozen at the Tool authorization worker boundary.
57
+
58
+ A worker command may also carry an explicitly classified Application-owned behavior handle, such as an approval-policy callable. Such a callable is executable behavior, not Phronomy live-state authority. Phronomy does not place live Agent, Tool, ToolInvocation, FSMSession, Runtime, EventLoop, or other Phronomy-managed live domain objects into the callable's command/request data.
59
+
60
+ Application-defined opaque objects embedded in Application-owned context/metadata are not given a complete general value-type protocol by ACS-11. They remain Application-owned and must be safe for the Application's chosen worker usage. General serialization/value-type enforcement for those opaque objects is deferred hardening.
61
+
62
+ The Agent pipeline uses distinct operation shapes for at least:
63
+
64
+ ```text
65
+ InitialPreparationCommand -> InitialPreparationResult
66
+ FollowupPreparationCommand -> FollowupPreparationResult
67
+ ResumeCommitCommand -> ResumeCommitResult
68
+ TerminalCommitCommand -> TerminalOutcome
69
+ ```
70
+
71
+ A worker may perform blocking Persistence I/O and operation-local calculation. It must not call live-state mutation hooks such as EventLoop execution replacement, Agent root replacement, Journal live-view append, or AgentInvocation runtime-fact acknowledgement.
72
+
73
+ After worker completion, a lightweight callback posts the result to EventLoop. EventLoop validates authority and only then applies the committed result to live state.
74
+
75
+ ### Durable commit and live apply are distinct phases
76
+
77
+ Persistence is the last committed durable representation and recovery source. Successful Persistence operations return the resulting immutable durable values to EventLoop; they do not make Persistence the live read authority.
78
+
79
+ The normal path remains:
80
+
81
+ ```text
82
+ EventLoop-owned live snapshot
83
+ -> OffloadPool durable operation
84
+ -> optimistic durable commit
85
+ -> operation result
86
+ -> EventLoop authority validation
87
+ -> EventLoop live apply
88
+ ```
89
+
90
+ Mutable Agent root, execution, and Journal state are not reloaded from Persistence merely to obtain freshness. Existing revision and Agent watermark checks remain the conflict boundary.
91
+
92
+ ### Provider result authority uses FSMSession state and `llm_call_id`
93
+
94
+ A Provider Call receives its semantic `llm_call_id` on EventLoop before transport begins. Provider completion and streaming chunks carry that ID back through the owning FSMSession EventSink.
95
+
96
+ A result is applicable only when the Runtime still recognizes the owning FSMSession incarnation and the AgentInvocation still has the same active `llm_call_id` in the required FSM state.
97
+
98
+ A result for an old Provider Call is consumed as stale and does not advance the FSM. A callback targeting an old FSMSession incarnation is rejected by the session-local routing boundary established by ADR-023.
99
+
100
+ Phronomy does not introduce a generic generation token, generic invocation ID, or Offload operation identity as a second semantic authority.
101
+
102
+ ### Tool worker results use the same direction of ownership
103
+
104
+ Tool authorization and execution continue to settle through explicit FSMSession events. Authorization worker input is captured on EventLoop before submission. Its value data contains Agent identity metadata and Tool description/operation data, not live Agent or Tool objects. Tool authorization behavior (`approval_facts`, `requires_approval`, and Agent approval policy when callable) is captured as explicitly classified Application-owned behavior handles.
105
+
106
+ `ApprovalEvaluationRequest` is therefore a value-only policy input. It exposes `agent_id`, `agent_definition_id`, `agent_definition_version`, execution identity, Tool name/schema, arguments, facts, context, origin, metadata, and default decision; it does not expose live `agent` or `tool` references.
107
+
108
+ Actual Tool execution is a separate behavior boundary: executing the configured Tool is the purpose of that operation. The authorization worker does not need the Tool instance and must not use one as live authorization input.
109
+
110
+ Authorization/execution outcomes are immutable result carriers and are applied to `ToolInvocation` only by EventLoop-driven FSM handling. They carry `tool_invocation_id`; EventLoop-driven Tool FSM handling consumes an outcome as stale when that semantic ID does not match the current ToolInvocation. `tool_invocation_id` remains the semantic Tool-operation identity. FSMSession identity remains Runtime routing identity. The two are not conflated.
111
+
112
+ ### Approval suspension retains the same live owner without Activation
113
+
114
+ Approval suspension retains the same process-local Agent and AgentInvocation. The suspended execution remains present in EventLoop's execution directory, but no active FSMSession owns it while suspended.
115
+
116
+ `Agent::Base.live_for_execution(execution_id)` and `agent.approve_async(...)` resolve the process-local execution owner through EventLoop's read-only owner view. They do not load a replacement Agent/Execution from Persistence.
117
+
118
+ A resume performs its durable approval transition off EventLoop and applies the result on EventLoop before constructing a fresh resume FSMSession incarnation.
119
+
120
+ If no live owner exists, durable rehydration remains a separate capability and `ExecutionRehydrationRequiredError` is raised.
121
+
122
+ ### Application callbacks do not become a worker-side authority
123
+
124
+ Application stream/event callbacks are invoked from EventLoop-owned AgentInvocation event handling. Callback failure is recorded independently of canonical runtime event capture, converted into an explicit FSM failure event, and cannot mutate durable or live execution state from an Offload worker.
125
+
126
+ Approval notification callbacks may execute off EventLoop because they are application work. Their execution does not advance Phronomy-managed live state.
127
+
128
+ ## Required invariants
129
+
130
+ The implementation must preserve all of the following:
131
+
132
+ 1. Journal / Manifest / ContentStore remain the canonical execution/context record authorities defined by ADR-012.
133
+ 2. Persistence remains the durable recovery authority, not the normal live refresh source.
134
+ 3. FSMSession incarnation identity and EventSink routing remain as defined by ADR-023.
135
+ 4. `execution_id` remains the logical Agent execution parent identity defined by ADR-022.
136
+ 5. Provider result authority is checked with current FSMSession/FSM state plus `llm_call_id`.
137
+ 6. Tool result authority remains tied to the current Tool FSMSession and `tool_invocation_id`.
138
+ 7. OffloadPool never waits synchronously for logical EventLoop progress.
139
+ 8. Worker completion callbacks do not fall back to direct live mutation when EventLoop is unavailable.
140
+ 9. Tool authorization command/request value data contains no Phronomy-managed live domain object; explicitly classified Application-owned behavior handles remain permitted.
141
+
142
+ ## Consequences
143
+
144
+ ### Positive
145
+
146
+ - Agent live-state ownership is explicit and mechanically enforceable.
147
+ - Mutex-protected shared Activation state disappears.
148
+ - Worker-side durable I/O can scale independently without becoming a second live-state writer.
149
+ - Late Provider results cannot overwrite the provenance of a newer Provider Call.
150
+ - Approval lookup remains process-local without exposing mutable execution internals.
151
+ - The design provides the state/result foundation required by later recovery, cancellation, and durable-barrier work.
152
+
153
+ ### Trade-offs
154
+
155
+ - Durable commit and EventLoop live apply are separate phases, so code must explicitly model and validate result application.
156
+ - Internal Agent execution coordination uses more typed command/result values than the Activation model.
157
+ - Process loss still loses in-flight Runtime continuation; this decision does not implement rehydration.
158
+
159
+ ## Explicitly deferred work
160
+
161
+ This decision does **not** implement:
162
+
163
+ - ACS-12: same-process Agent admission/exclusion policy;
164
+ - ACS-13: opaque Workflow admission ownership and Workflow terminal-save ordering;
165
+ - ACS-14: cross-process leases/fencing;
166
+ - ACS-15: durable Agent/FSMSession rehydration;
167
+ - ACS-16: cancellation/HITL Task semantic completion;
168
+ - ACS-17: semantic retry and causal durable barriers.
169
+
170
+ Those changes build on this ownership/result model and must not be folded into ACS-11 implicitly.
171
+
172
+ ## Rejected alternatives
173
+
174
+ ### Rename Activation and keep the same shared mutable object
175
+
176
+ Rejected. A renamed mutex-protected container shared by EventLoop and workers preserves the ownership defect.
177
+
178
+ ### Let workers update Agent live state after a successful commit
179
+
180
+ Rejected. Successful durability does not grant a worker live mutation authority. The result must return to EventLoop for apply.
181
+
182
+ ### Reload Agent state from Persistence before every apply
183
+
184
+ Rejected. This would make Persistence the live source of truth and reintroduce implicit refresh/merge semantics rejected by ADR-014.
185
+
186
+ ### Add a generic generation/correlation token
187
+
188
+ Rejected. Result authority is expressed using the current FSM state plus existing purpose-specific semantic identifiers. A new generic token would recreate identity ambiguity already removed by ADR-021 through ADR-023.