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
data/docs/features.md CHANGED
@@ -18,9 +18,9 @@ for production deployments.
18
18
  | Feature | Stability |
19
19
  |---|---|
20
20
  | **Workflow** — Stateful, branching workflows with `wait_state` and explicit events | Stable |
21
- | **Agent** — Stateful ReAct-style agents with stable `agent_id`, persistence-backed execution state, canonical history, guardrails, and conversation context | Stable |
21
+ | **Agent** — Stateful ReAct-style agents with stable `agent_id`, one mutable live owner per Runtime, persistence-backed execution state, canonical history, and conversation context | Stable |
22
22
  | **Tool authoring façade** — `Phronomy::Tool::Base` is the public authoring name for the existing Capability base class; the legacy namespace remains compatible | Beta |
23
- | **Unified Persistence** — One durable backend abstraction for Agent state and Workflow `workflow_states`; live Agent/Workflow state remains owned by the active instance/session between durable commits; custom backends implement the documented Backend SPI and repository/transaction semantics | Beta |
23
+ | **Unified Persistence** — One durable backend abstraction for Agent state and Workflow `workflow_states`; live Agent/Workflow state remains owned by Runtime/session authority between durable commits; custom backends implement the documented Backend SPI and repository/transaction semantics | Beta |
24
24
  | **LLMAdapter SPI** — `Phronomy::LLMAdapter::Base#complete` / `#stream` define the Beta call-adapter extension boundary; Phronomy owns async/offload wrapping | Beta |
25
25
  | **Before-Large-Language-Model (LLM) Input Hook** — Three-tier per-call LLM input customization via `before_llm_input` and `LLMInputPatch` | Stable |
26
26
  | **Context Management** — Journal + Context Policy + per-LLM-call Manifest with token-budget-aware selection and protocol-safe Tool Call / Tool message dependencies | Stable |
@@ -28,9 +28,18 @@ for production deployments.
28
28
  | **`PromptInjectionFilter`** — Built-in pattern-based prompt-injection filter | Beta |
29
29
  | **Capability redaction/result-size controls** — `redact_params` and `max_result_size` | Beta |
30
30
  | **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
31
- | **Tracing** — Pluggable span-based observability | Stable |
31
+ | **Tracing** — Pluggable logical-operation span observability with bounded automatic coverage | Stable |
32
32
  | **Error Taxonomy** — Provider errors translated to Phronomy transport/authentication/rate-limit/context errors | Beta |
33
33
 
34
+ Agent definition lineage is separate from Agent instance identity. A named
35
+ concrete Agent may declare `agent_definition version: N`; when `id:` is omitted,
36
+ its fully-qualified Ruby class name is the stable `agent_definition_id`.
37
+ Applications may keep a lineage independent of Ruby constant naming with an
38
+ explicit `id:`. Concrete subclasses declare their own definition revision
39
+ rather than implicitly inheriting the parent revision. The Stable
40
+ `before_llm_input` context exposes the semantic revision as
41
+ `agent_definition_version`.
42
+
34
43
  ## Knowledge and integration
35
44
 
36
45
  | Feature | Stability |
@@ -47,10 +56,12 @@ for production deployments.
47
56
 
48
57
  | Feature | Stability |
49
58
  |---|---|
50
- | **EventLoop** — Runtime-owned event-driven execution core shared by Agent, ToolInvocation, Workflow, and MultiAgent sessions | Beta |
51
- | **Workflow durable admission** — Durable `thread_id` ownership is held by a Runtime-only `fsm_session_id` from load through terminal save | Beta |
59
+ | **EventLoop** — Runtime-owned event-driven execution core shared by Agent, ToolInvocation, Workflow, and MultiAgent sessions; single writer for Phronomy-managed live Agent execution state | Beta |
60
+ | **Agent execution result authority** — Session-local routing plus current FSM state and purpose-specific semantic IDs (`llm_call_id`, `tool_invocation_id`) reject stale async results | Beta |
61
+ | **Agent live ownership/admission** — Runtime owns one mutable live Agent per `agent_id`; EventLoop rejects competing same-Agent top-level executions while suspension retains the logical execution slot; Persistence admission remains durable defense | Beta |
62
+ | **Workflow durable admission** — Same-process admission is keyed by durable `workflow_instance_id` from pre-hydration admission through the durable terminal/halted save barrier; admission-owner representation is Runtime-internal | Beta |
52
63
  | **`invoke` / `invoke_async`** — Blocking and non-blocking Agent/Workflow entry points | Stable |
53
- | **Agent async events** — `invoke_async(..., on_event:)` and `stream_async(..., on_event:)`; streaming additionally emits `:token` | Beta |
64
+ | **Agent async events** — one Runtime-only `on_event` listener is bound at Agent `new` / `create` / `load`; invoke/stream operations publish through that listener and streaming additionally emits `:token` | Beta |
54
65
  | **`stream` / `stream_async`** — Event callbacks execute on EventLoop and must return quickly | Beta |
55
66
  | **`stream_callback_error_policy`** — Terminal event callback error policy (`:report` / `:fail_task`) | Beta |
56
67
  | **Task completion contract** — `Task` is the common caller-facing completion handle for EventLoop/FSMSession lifecycles and OffloadPool work | Beta |
@@ -58,7 +69,7 @@ for production deployments.
58
69
  | **CancellationToken** — Cooperative cancellation with explicit `cancel!`, lazy monotonic deadlines, and callback registration | Experimental |
59
70
  | **Tool `execution_mode`** — `:cooperative` for short EventLoop-safe work; `:offloaded` for synchronous work that must stay off EventLoop | Experimental |
60
71
  | **OffloadPool sizing** — `offload_pool_size` / `offload_queue_size`; named pools available for application-owned isolation | Beta |
61
- | **InvocationContext** — Explicit correlation/cancellation/deadline context for Agent and Workflow invocations | Beta |
72
+ | **InvocationContext** — Explicit cancellation/deadline/policy plus observability-correlation context for Agent and Workflow invocations | Beta |
62
73
  | **Metrics** — OffloadPool active/queue/abandoned metrics plus EventLoop queue/lag metrics | Beta |
63
74
  | **Runtime lifecycle** — Runtime-owned EventLoop and terminal `Runtime#shutdown` | Beta |
64
75
 
@@ -67,13 +78,18 @@ for production deployments.
67
78
  | Feature | Stability |
68
79
  |---|---|
69
80
  | **Workflow asynchronous pattern** — Start async work, return immediately, and continue through `Workflow#signal` | Beta |
70
- | **Multi-agent** — Agent-as-Tool and hub-and-spoke handoff routing | Beta |
81
+ | **Durable Agent Handoff** — `Agent::Handoff` and `Agent::HandoffRunner` persist responsibility, immutable Context and exact Target reservation in one Persistence domain; current graph wiring is required for continuation | Beta |
71
82
  | **GeneratorVerifier** — Generator-Verifier loop with injectable prompts/parsers | Beta |
72
83
  | **`Phronomy::MultiAgent::Orchestrator`** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
73
84
  | **`Phronomy::MultiAgent::TeamCoordinator`** — LLM coordinator with stateful worker Agents | Beta |
74
85
  | **SharedState** — Peer-agent shared-state coordination | Experimental |
75
- | **Human-in-the-loop approval** — Suspension and approval/resume of Tool requests on the same live Agent/Activation owner | Beta |
76
- | **`tool_approval_policy`** — Application-defined allow/approve/reject policy | Beta |
86
+ | **Human-in-the-loop approval** — durable suspension publishes `:approval_required` through the Agent listener; `approve` / `approve_async` resumes the same logical `execution_id` with a fresh FSMSession incarnation | Beta |
87
+ | **`tool_approval_policy`** — Application-defined allow/approve/reject policy using a value-only `ApprovalEvaluationRequest` without live Agent/Tool references | Beta |
88
+
89
+ For Handoff, Application `HandoffPolicy` controls what may cross the Source/Target
90
+ boundary. Transferred Handoff Context is immutable request-scoped material rather
91
+ than automatic Target Journal/Knowledge adoption. Target Context Policy still
92
+ selects what enters each individual Target LLM call.
77
93
 
78
94
  ## Public API boundary
79
95
 
@@ -100,12 +116,16 @@ extension contracts are deliberate exceptions to the ordinary
100
116
  application-facing interpretation of `@api public`: they are compatibility
101
117
  contracts for implementers. Extension implementations must not depend on Runtime
102
118
  private execution objects such as EventLoop/FSMSession/OffloadPool operation
103
- records.
119
+ records or EventLoop Agent execution entries.
104
120
 
105
121
  `Phronomy::StateStore` is no longer a public backend abstraction. Workflow
106
122
  durability is provided through `Phronomy::Persistence#workflow_states`; see the
107
123
  0.19 migration guide when upgrading code that used `state_store:`.
108
124
 
125
+ For current architecture/authority boundaries, start at
126
+ [Architecture](architecture.md). Archived historical design material is not a
127
+ current compatibility contract.
128
+
109
129
  ## Advanced and internal APIs
110
130
 
111
131
  | Feature | Stability |
@@ -120,3 +140,37 @@ durability is provided through `Phronomy::Persistence#workflow_states`; see the
120
140
 
121
141
  For runtime ownership and the distinction between public lifecycle APIs and
122
142
  private execution machinery, see [Runtime and concurrency](runtime-and-concurrency.md).
143
+
144
+ ### Durable semantic coordination (V2 revision 2)
145
+
146
+ Static `Orchestrator.subagent` Tools invoked inside a parent AgentExecution reserve
147
+ child Agent/execution identities before dispatch and reuse retained outcomes.
148
+ Direct `dispatch_parallel` / `fan_out` calls remain Runtime-only convenience APIs.
149
+ `Orchestrator#resume(execution_id)` continues retained parent coordination.
150
+
151
+ `TeamCoordinator` requires `team_definition id:, version:` and provides
152
+ `create`, `load`, `get`, `resume`, `executions`, `result` and scoped `cancel`.
153
+ Team tasks, worker assignments, exact child identities and aggregate outcomes
154
+ are durable facts. Workers still execute sequentially through the existing Agent
155
+ engine. `schedule` and `aggregate` must be pure and replay-safe; aggregate returns
156
+ canonical JSON data. Default assignment results have string keys and error data
157
+ contains class/message strings. Arbitrary Ruby objects are not durable results.
158
+
159
+ `Persistence#execution_result` / `team_execution_result` read exact results;
160
+ `list_executions` / `list_team_executions` discover retained active and terminal
161
+ runs by known owner ID. Reads do not load Runtime owners or deliver callbacks.
162
+ The cursor is an exclusive lexical execution ID and limit must be positive.
163
+ Retention and request correlation remain Application/backend policy.
164
+
165
+ Guarantee: confirmed outcomes are reused, unfinished work keeps its semantic ID,
166
+ and external outcome uncertainty uses Agent Recovery. For F1/F4 this is
167
+ conditional on a conforming retained Persistence domain and compatible current
168
+ wiring. InMemory provides transaction/CAS semantics but is not durable disk storage.
169
+ Callbacks/streams/Tasks are Runtime observations; there is no restart-spanning
170
+ notification outbox, ACK, synthetic fan-out execution or external exactly-once
171
+ claim. See [the migration](migrations/durable-semantic-coordination-v2.md).
172
+
173
+ `Persistence#handoff_result(source_execution_id)` also resolves the exact Handoff
174
+ turn read-only, including a current absent-but-reserved Target, without graph or
175
+ Agent hydration. Purging an idle main Agent removes its Handoff anchor; pending
176
+ coordination rejects purge rather than discarding its reservation.
@@ -100,7 +100,7 @@ result = agent.invoke("What is my name?")
100
100
  puts result[:output]
101
101
  ```
102
102
 
103
- Load the same Agent again when the same Persistence backend is available:
103
+ Resolve the same logical Agent when the same Persistence backend is available:
104
104
 
105
105
  ```ruby
106
106
  agent = ResearchAgent.load(
@@ -111,8 +111,24 @@ agent = ResearchAgent.load(
111
111
  agent.invoke("Continue our previous discussion.")
112
112
  ```
113
113
 
114
- `load` is the hydration boundary. While that Agent instance is live, the
115
- instance and its active `AgentExecutionActivation` own the current logical state.
114
+ A Runtime has at most one mutable live Agent object for an `agent_id`. If the
115
+ Agent is already live, repeated `load` returns that exact Ruby object and does
116
+ not reload Persistence. If it is not live, `load` hydrates the durable Agent
117
+ once. Loading an ID that does not exist durably raises
118
+ `Persistence::NotFoundError`.
119
+
120
+ Use `get` when only a process-local lookup is wanted:
121
+
122
+ ```ruby
123
+ live = ResearchAgent.get("research-session-42")
124
+ ```
125
+
126
+ `get` never reads Persistence and returns `nil` when this Runtime has no live
127
+ owner. `new` and `create` are creation operations, not get-or-load operations;
128
+ they raise `AgentAlreadyExistsError` if the requested identity already exists.
129
+
130
+ While the Agent is live, it owns its current AgentRoot/Journal view and
131
+ Runtime/EventLoop owns process-local identity/admission/execution authority.
116
132
  Phronomy persists snapshots at defined durability boundaries but does not reload
117
133
  mutable Agent/Execution/Journal state before every LLM or Tool step. A conflicting
118
134
  external durable write is surfaced as `Persistence::ConflictError` rather than
@@ -127,8 +143,10 @@ agent.clear_knowledge!
127
143
  agent.reset_context!
128
144
  ```
129
145
 
130
- `purge!` is different: it permanently removes the Agent and persisted execution
131
- history from the configured Persistence backend.
146
+ `purge!` is different: it explicitly destroys the logical Agent and removes its
147
+ persisted execution history from the configured Persistence backend. After a
148
+ successful purge the old Ruby object is permanently unusable; the textual
149
+ `agent_id` may then be used to create a new logical Agent.
132
150
 
133
151
  ## Sync and async Agent APIs
134
152
 
@@ -142,18 +160,25 @@ task = agent.invoke_async("Hello")
142
160
  result = task.wait_result
143
161
  ```
144
162
 
145
- Both `invoke` and `invoke_async` can receive public Agent events through either
146
- an `on_event:` listener or a block. A block is convenient when the listener is
147
- local to the call:
163
+ Public Agent events belong to the live Agent Runtime incarnation. Register
164
+ the listener when the Agent is materialized, not on each invocation:
148
165
 
149
166
  ```ruby
150
- task = agent.invoke_async("Hello") do |event|
151
- puts event.payload[:output] if event.type == :done
152
- end
167
+ agent = ResearchAgent.load(
168
+ "research-session-42",
169
+ persistence: persistence,
170
+ on_event: ->(event) {
171
+ puts event.payload[:output] if event.type == :done
172
+ }
173
+ )
174
+
175
+ task = agent.invoke_async("Hello")
153
176
  ```
154
177
 
155
- Use `on_event:` when the listener already exists as a callable. Do not provide
156
- both `on_event:` and a block to the same invocation.
178
+ `new`, `create`, and `load` also accept an equivalent listener block.
179
+ Supplying both `on_event:` and a construction block is an error. If `load`
180
+ resolves an already-live same-process Agent, supplying any new listener/block
181
+ is also an error; the existing binding is immutable for that Runtime incarnation.
157
182
 
158
183
  `Phronomy::Task` is the common caller-facing completion handle for asynchronous
159
184
  Phronomy work. Logical lifecycle progress is driven by EventLoop/FSMSession;
@@ -163,45 +188,52 @@ OffloadPool. Both paths expose completion as a `Task`.
163
188
  `Task#wait_result` is for an external caller. Do not block EventLoop waiting for
164
189
  a Task that can only complete through that same EventLoop.
165
190
 
166
- Streaming follows the same split:
191
+ Streaming uses the same Agent-incarnation listener:
167
192
 
168
193
  ```ruby
169
- agent.stream("Explain the design") do |event|
170
- puts event.payload if event.type == :token
171
- end
172
- ```
194
+ agent = ResearchAgent.new(
195
+ on_event: ->(event) {
196
+ puts event.payload if event.type == :token
197
+ }
198
+ )
173
199
 
174
- ```ruby
175
- task = agent.stream_async("Explain the design") do |event|
176
- puts event.payload if event.type == :token
177
- end
200
+ agent.stream("Explain the design")
201
+ task = agent.stream_async("Explain another design")
178
202
  ```
179
203
 
180
- Agent event callbacks execute on EventLoop and therefore should return quickly.
204
+ `stream` / `stream_async` require an Agent event listener because event
205
+ delivery is their public streaming channel. Agent event callbacks execute on
206
+ EventLoop and therefore should return quickly.
181
207
 
182
208
  ## Human-in-the-loop approval
183
209
 
184
- A Tool requiring approval can suspend an Agent invocation. Resume it with the
185
- approval request identifier returned by the suspension result.
186
-
187
- At a top-level synchronous boundary:
210
+ A Tool requiring approval suspends the durable logical execution without
211
+ settling the original execution Task. Approval notification is delivered
212
+ through the same Agent listener as `:approval_required`:
188
213
 
189
214
  ```ruby
190
- result = agent.invoke("Perform the requested protected action")
215
+ agent = nil
216
+ agent = ResearchAgent.new(
217
+ on_event: ->(event) {
218
+ next unless event.type == :approval_required
219
+
220
+ request = event.payload.fetch(:request)
221
+ agent.approve_async(
222
+ request.execution_id,
223
+ approval_request_id: request.id,
224
+ approved: true
225
+ )
226
+ }
227
+ )
191
228
 
192
- if result[:suspended]
193
- request = result[:approval_request]
194
- result = agent.approve(
195
- result[:execution_id],
196
- approval_request_id: request.id,
197
- approved: true
198
- )
199
- end
229
+ result = agent.invoke("Perform the requested protected action")
200
230
  ```
201
231
 
202
- From an EventLoop callback, use `approve_async` rather than blocking EventLoop.
203
- Approval resume continues the same live Agent instance, Activation, and
204
- AgentInvocation; it is not an Agent reload boundary.
232
+ Use `approve_async` inside the EventLoop callback; synchronous `approve` is
233
+ for an external thread that already has the durable approval identifiers.
234
+ Approval resume continues the same live Agent instance and AgentInvocation;
235
+ Runtime resolves that process-local owner through EventLoop rather than through a
236
+ shared Activation object. It is not an Agent reload boundary.
205
237
 
206
238
  ## Workflow basics
207
239
 
@@ -237,16 +269,16 @@ workflow = Phronomy::Workflow.define(
237
269
  transition from: :finalize, to: :__finish__
238
270
  end
239
271
 
240
- state = workflow.invoke({draft: ""}, config: {thread_id: "doc-1"})
272
+ state = workflow.invoke({draft: ""}, config: {workflow_instance_id: "doc-1"})
241
273
  final = workflow.send_event(state: state, event: :approve)
242
274
  puts final.approved
243
275
  ```
244
276
 
245
- `Persistence#workflow_states` is the durable Workflow repository. `thread_id`
277
+ `Persistence#workflow_states` is the durable Workflow repository. `workflow_instance_id`
246
278
  identifies the durable Workflow state and remains stable across resume. Each
247
279
  concrete Runtime execution receives a separate internal `fsm_session_id`; the
248
280
  application-level `session_id` remains ordinary caller/tracing metadata. Phronomy
249
- holds owner-aware admission for `thread_id` from durable load through terminal
281
+ holds owner-aware admission for `workflow_instance_id` from durable load through terminal
250
282
  save so another local invocation cannot start from a stale snapshot while the
251
283
  current owner is still committing.
252
284
 
@@ -275,7 +307,7 @@ class AnswerContext
275
307
 
276
308
  field :question, type: :replace, default: ""
277
309
  field :answer, type: :replace, default: nil
278
- field :thread_id, type: :replace, default: nil
310
+ # workflow_instance_id is framework-owned metadata; do not declare it as a field.
279
311
  end
280
312
 
281
313
  class ResearchAgent < Phronomy::Agent::Base
@@ -293,13 +325,13 @@ workflow = Phronomy::Workflow.define(AnswerContext) do
293
325
  state :done
294
326
 
295
327
  entry :asking, ->(ctx) {
296
- thread_id = ctx.thread_id
328
+ workflow_instance_id = ctx.workflow_instance_id
297
329
 
298
330
  my_agent.invoke_async(ctx.question) do |event|
299
331
  next unless event.type == :done
300
332
 
301
333
  workflow.signal(
302
- thread_id: thread_id,
334
+ workflow_instance_id: workflow_instance_id,
303
335
  event: :answer_ready,
304
336
  payload: {answer: event.payload[:output]}
305
337
  )
@@ -98,8 +98,14 @@ A live Agent instance now remains the logical owner of its mutable state after
98
98
  hydration. Phronomy no longer re-loads mutable AgentRoot/AgentExecution/Journal
99
99
  state at each LLM boundary just to obtain freshness.
100
100
 
101
- Approval resume continues the same live Agent/Activation and is performed through
102
- the Agent instance APIs:
101
+ Current Runtime releases keep one process-local mutable Agent owner per `agent_id`
102
+ in a Runtime-owned registry. Repeated `load` returns that exact owner without
103
+ reloading durable state, and `get(agent_id)` performs only a process-local lookup.
104
+ EventLoop separately owns same-Agent top-level execution admission and live
105
+ execution progression; Persistence admission remains a durable integrity defense.
106
+
107
+ Approval resume continues the same process-local live Agent owner and is performed
108
+ through the Agent instance APIs rather than an Activation object:
103
109
 
104
110
  ```ruby
105
111
  agent.approve(...)
@@ -125,11 +131,12 @@ If the concrete Agent class is not known, use:
125
131
  agent = Phronomy::Agent::Base.live_for_execution(execution_id)
126
132
  ```
127
133
 
128
- `live_for_execution` resolves the Runtime-local Activation and returns its
129
- existing Agent instance. It does not accept `persistence:` and does not load a new
130
- Agent or Execution from Persistence. If the live Activation has been lost (for
131
- example after process restart), it raises `ExecutionRehydrationRequiredError`
132
- until durable execution rehydration is implemented.
134
+ `live_for_execution` resolves the Runtime/EventLoop process-local owner and
135
+ returns its existing Agent instance. It does not accept `persistence:` and does
136
+ not load a new Agent or Execution from Persistence. If that live owner has been
137
+ lost (for example after process restart), it raises
138
+ `ExecutionRehydrationRequiredError` until durable execution rehydration is
139
+ implemented.
133
140
 
134
141
  `execution_id` is not an authorization token. HTTP/API applications must still
135
142
  verify that the caller is allowed to act on the resolved Agent and approval