phronomy 0.21.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 +20 -0
  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 +91 -50
  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 +150 -67
  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 -104
  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
@@ -0,0 +1,172 @@
1
+ # Architecture Decision Records
2
+
3
+ This directory contains Phronomy Architecture Decision Records (ADRs).
4
+
5
+ The repository-wide architecture authority model is defined by
6
+ [`017-design-authority-and-adr-governance`](017-design-authority-and-adr-governance.md).
7
+ This index is the canonical navigation surface for ADR identity, status, and
8
+ supersession relationships.
9
+
10
+ ## Canonical ADR identity
11
+
12
+ The **canonical decision key** is the ADR filename basename, for example:
13
+
14
+ ```text
15
+ 012-canonical-execution-log-and-context-policy
16
+ ```
17
+
18
+ The three-digit numeric prefix is an ordering/display number. It is not, by
19
+ itself, a globally unique historical identifier because this repository
20
+ already contains two legacy ADRs numbered `011`.
21
+
22
+ Therefore:
23
+
24
+ - new or modified normative material SHOULD link to the ADR file or use its
25
+ full canonical decision key;
26
+ - the bare label `ADR-011` is ambiguous and MUST NOT be used to identify one
27
+ of the two legacy `011` decisions;
28
+ - existing historical text is not rewritten merely to normalize old
29
+ references.
30
+
31
+ ## Status and authority
32
+
33
+ ADR status has architecture meaning:
34
+
35
+ - **Accepted** — normative architecture intent unless explicitly superseded.
36
+ - **Amended** — normative architecture intent including its recorded
37
+ amendments.
38
+ - **Superseded** — retained as decision history; non-normative for the
39
+ superseded scope.
40
+ - **Proposed** — not normative until accepted.
41
+
42
+ An Accepted ADR can temporarily disagree with implementation reality. That is
43
+ an **architecture inconsistency**, not permission to silently treat whichever
44
+ artifact is newer as authoritative. The inconsistency must be tracked and
45
+ resolved explicitly.
46
+
47
+ ## ADR index
48
+
49
+ | Canonical decision key | Status | Normative now? | Supersession / note |
50
+ |---|---|---:|---|
51
+ | [`001-rubyllm-as-provider-layer`](001-rubyllm-as-provider-layer.md) | Superseded | No | Superseded by [`027-llm-adapter-provider-boundary`](027-llm-adapter-provider-boundary.md); historical RubyLLM adoption rationale retained. |
52
+ | [`002-workflow-context-immutability`](002-workflow-context-immutability.md) | Amended | Yes | `WorkflowContext#merge` remains new-instance semantics; direct generated field writers are EventLoop-owned guarded mutation APIs. |
53
+ | [`003-event-loop-singleton`](003-event-loop-singleton.md) | Accepted | Yes | Current until explicitly superseded/refined. |
54
+ | [`004-invoke-timeout-is-not-cancellation`](004-invoke-timeout-is-not-cancellation.md) | Superseded | No | Superseded by [`011-delegate-transport-policy-to-adapters`](011-delegate-transport-policy-to-adapters.md). |
55
+ | [`005-static-knowledge-class-level-cache`](005-static-knowledge-class-level-cache.md) | Superseded | No | Superseded by [`013-journal-backed-knowledge-as-context-candidates`](013-journal-backed-knowledge-as-context-candidates.md). |
56
+ | [`006-no-built-in-guardrails`](006-no-built-in-guardrails.md) | Superseded | No | Superseded by [`019-filter-contract-and-security-boundaries`](019-filter-contract-and-security-boundaries.md); historical minimal-built-in/Guardrail rationale retained. |
57
+ | [`007-mcp-is-beta-stability`](007-mcp-is-beta-stability.md) | Accepted | Yes | Current. |
58
+ | [`008-orchestrator-uses-os-threads`](008-orchestrator-uses-os-threads.md) | Superseded | No | Superseded by [`010-cooperative-first-concurrency`](010-cooperative-first-concurrency.md). |
59
+ | [`009-state-store-abstraction`](009-state-store-abstraction.md) | Superseded | No | Superseded by [`014-unified-persistence-durable-state`](014-unified-persistence-durable-state.md). |
60
+ | [`010-cooperative-first-concurrency`](010-cooperative-first-concurrency.md) | Accepted | Yes | Current until explicitly refined/superseded. |
61
+ | [`011-build-context-as-single-llm-input-authority`](011-build-context-as-single-llm-input-authority.md) | Superseded | No | Superseded by [`012-canonical-execution-log-and-context-policy`](012-canonical-execution-log-and-context-policy.md). |
62
+ | [`011-delegate-transport-policy-to-adapters`](011-delegate-transport-policy-to-adapters.md) | Accepted | Yes | Legacy duplicate numeric prefix; use the full canonical key. |
63
+ | [`012-canonical-execution-log-and-context-policy`](012-canonical-execution-log-and-context-policy.md) | Accepted | Yes | Current Journal / Manifest / Context authority. |
64
+ | [`013-journal-backed-knowledge-as-context-candidates`](013-journal-backed-knowledge-as-context-candidates.md) | Accepted | Yes | Current persistent Knowledge authority. |
65
+ | [`014-unified-persistence-durable-state`](014-unified-persistence-durable-state.md) | Accepted | Yes | Durable-backend and live-owner/no-reload intent remains current; live Agent execution mutation is refined by ADR-024, same-process Agent identity/admission ownership by ADR-025, and same-process Workflow admission/terminal-barrier ordering by ADR-026. Workflow identity terminology is superseded by ADR-020, generic `InvocationContext` / Agent correlation semantics by ADR-021, and concrete FSMSession/Agent-Tool routing identity by ADR-023. |
66
+ | [`015-tool-public-facade-and-rbs-boundary`](015-tool-public-facade-and-rbs-boundary.md) | Accepted | Yes | Current Tool façade / extension-SPI / RBS boundary. |
67
+ | [`016-semantic-multi-agent-handoff`](016-semantic-multi-agent-handoff.md) | Accepted | Yes | Current semantic Handoff intent; runtime/context-transfer reconciliation is implemented and reflected in current architecture documentation. |
68
+ | [`017-design-authority-and-adr-governance`](017-design-authority-and-adr-governance.md) | Accepted | Yes | Repository-wide architecture authority and ADR governance. |
69
+ | [`018-durability-guarantees-and-failure-model`](018-durability-guarantees-and-failure-model.md) | Accepted | Yes | Repository-wide durability/concurrency/external-effect guarantee vocabulary and F0-F4/X0 failure model. |
70
+ | [`019-filter-contract-and-security-boundaries`](019-filter-contract-and-security-boundaries.md) | Accepted | Yes | Current Filter transform/block and bounded PromptInjectionFilter/isolation boundaries; the follow-up review adds no fourth Context Filter call site and places semantic Context trust in Application ContextPolicy. |
71
+ | [`020-canonical-workflow-instance-identity`](020-canonical-workflow-instance-identity.md) | Accepted | Yes | Canonical logical/durable Workflow identity and CG-01 clean-break migration. |
72
+ | [`021-generic-agent-invocation-identity-removal`](021-generic-agent-invocation-identity-removal.md) | Accepted | Yes | Removes generic Agent/InvocationContext identity and canonical Journal `correlation_id`; CG-02 is closed, with targeted legacy durable-key read compatibility and no eager rewrite. |
73
+ | [`022-agent-execution-parent-identity-and-runtime-routing-boundary`](022-agent-execution-parent-identity-and-runtime-routing-boundary.md) | Accepted | Yes | Canonicalizes Agent-owned Tool/approval logical parent as `execution_id`; CG-03a is reconciled, ADR-023 supplies incarnation routing, and ADR-024 supplies EventLoop result/live-state authority. |
74
+ | [`023-fsm-session-incarnation-identity-and-routing`](023-fsm-session-incarnation-identity-and-routing.md) | Accepted | Yes | FSMSession-owned incarnation identity, session-local Agent/Tool/Multi-Agent routing, and stale-target drop remain current. Its transitional Workflow identity-reservation/admission bridge is superseded by ADR-026; Agent result authority is completed by ADR-024. |
75
+ | [`024-event-loop-single-writer-agent-runtime`](024-event-loop-single-writer-agent-runtime.md) | Accepted | Yes | EventLoop is the single writer of Phronomy-managed live Agent execution state; removes Activation/ActivationRegistry and defines operation-specific Offload result application with current FSM + semantic-ID authority. |
76
+ | [`025-process-local-agent-ownership-and-runtime-admission`](025-process-local-agent-ownership-and-runtime-admission.md) | Accepted | Yes | One mutable live Agent owner per `agent_id` per Runtime; EventLoop is the primary same-process top-level admission authority while Persistence admission remains durable defense. |
77
+ | [`026-workflow-runtime-admission-and-durable-terminal-barrier`](026-workflow-runtime-admission-and-durable-terminal-barrier.md) | Accepted | Yes | EventLoop-owned opaque Workflow admission owner, admission-before-hydration ordering, and FSMSession-integrated durable terminal save barrier with fail-closed uncertain outcomes. |
78
+ | [`027-llm-adapter-provider-boundary`](027-llm-adapter-provider-boundary.md) | Accepted | Yes | Phronomy-owned Provider-call extension boundary; RubyLLM is the default adapter/integration while current input materialization remains RubyLLM-specific. |
79
+
80
+ ## Legacy duplicate `011`
81
+
82
+ The repository intentionally preserves both legacy files:
83
+
84
+ ```text
85
+ 011-build-context-as-single-llm-input-authority
86
+ 011-delegate-transport-policy-to-adapters
87
+ ```
88
+
89
+ They are different decisions. The first is superseded by ADR-012; the second
90
+ remains Accepted.
91
+
92
+ They MUST NOT be silently renumbered. Their filenames are stable historical
93
+ decision keys. New decisions may not introduce another duplicate numeric
94
+ prefix.
95
+
96
+ ## Allocating a new ADR identifier
97
+
98
+ For a new ADR:
99
+
100
+ 1. inspect all `NNN-*.md` files in this directory;
101
+ 2. allocate `max(existing numeric prefix) + 1`;
102
+ 3. format the number with three decimal digits;
103
+ 4. never fill an old gap and never reuse an existing prefix;
104
+ 5. if two unmerged changes select the same number, the change merged later
105
+ rebases and renumbers **only its new, not-yet-historical ADR**;
106
+ 6. add the ADR to this index in the same change.
107
+
108
+ Existing ADRs are not renumbered merely because a numbering defect is later
109
+ discovered. `016-semantic-multi-agent-handoff` is therefore preserved as-is.
110
+ Under this rule the Design Authority decision is ADR-017.
111
+
112
+ ## Superseding or amending a decision
113
+
114
+ Do not rewrite historical rationale into a fictional current history.
115
+
116
+ When superseding an ADR:
117
+
118
+ - add a new decision describing the new architecture;
119
+ - update the old ADR's status/supersession note only as needed;
120
+ - preserve the old rationale;
121
+ - update this index in the same change;
122
+ - use canonical decision keys/links where a numeric label would be ambiguous.
123
+
124
+ Amendments are appropriate only when the original decision remains the same
125
+ decision and the amendment can be understood without erasing the historical
126
+ rationale. Materially different architecture should normally be a new ADR.
127
+
128
+ ## Architecture inconsistency process
129
+
130
+ Architecture intent, public contract, and implementation reality are separate
131
+ authority domains. If they disagree, do not resolve the conflict by recency.
132
+
133
+ An unresolved architecture inconsistency must be recorded in the active
134
+ repository work item (normally a GitHub issue or PR) with at least:
135
+
136
+ ```text
137
+ Concern
138
+ Normative authority
139
+ Implementation / contract reality
140
+ Conflict
141
+ Resolution dependency
142
+ Status: OPEN | DEFERRED | RESOLVING | RESOLVED
143
+ Final resolution
144
+ ```
145
+
146
+ Existing Workstream carry-forward (`CF-*`) and architecture-inconsistency
147
+ (`AI-*`) registers are discovery/planning evidence, not normative architecture.
148
+ During the current reconciliation program their unresolved contents are
149
+ consolidated into named Architecture Change Sets (ACS) and Compatibility Gates
150
+ (CG). A repository change that resolves such an item should identify the
151
+ corresponding ACS/CG in its PR/commit rationale.
152
+
153
+ A legacy carry-forward proposition is closed only by one of:
154
+
155
+ - adoption into current source/public contract/current documentation;
156
+ - an Accepted ADR;
157
+ - explicit rejection/non-carry disposition; or
158
+ - an open repository work item when the resolution is intentionally deferred.
159
+
160
+ Working registers must not be copied wholesale into current architecture
161
+ documentation as if they were normative decisions.
162
+
163
+ ## Relationship to explanatory architecture documentation
164
+
165
+ ADRs record normative decisions. Explanatory architecture documents describe
166
+ the current reconciled system but do not supersede ADRs merely by being newer.
167
+
168
+ Current explanatory architecture starts at
169
+ [`docs/architecture.md`](../architecture.md). Non-current design snapshots are
170
+ segregated under `docs/archive/design/` and are non-normative. Explanatory
171
+ architecture documents describe the reconciled current system but do not
172
+ supersede ADRs merely by being newer.
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
+ | **Semantic Multi-Agent Handoff** — `MultiAgent::Handoff` transfers active responsibility from an explicit Source Agent to a Target Agent, projects policy-bounded Context with provenance, keeps the Target active across user turns within the same Runtime/main-Agent lifetime, and does not claim durable continuation across Runtime reset | 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 |
@@ -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,6 +160,26 @@ task = agent.invoke_async("Hello")
142
160
  result = task.wait_result
143
161
  ```
144
162
 
163
+ Public Agent events belong to the live Agent Runtime incarnation. Register
164
+ the listener when the Agent is materialized, not on each invocation:
165
+
166
+ ```ruby
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")
176
+ ```
177
+
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.
182
+
145
183
  `Phronomy::Task` is the common caller-facing completion handle for asynchronous
146
184
  Phronomy work. Logical lifecycle progress is driven by EventLoop/FSMSession;
147
185
  synchronous work that must execute away from EventLoop is submitted to
@@ -150,46 +188,52 @@ OffloadPool. Both paths expose completion as a `Task`.
150
188
  `Task#wait_result` is for an external caller. Do not block EventLoop waiting for
151
189
  a Task that can only complete through that same EventLoop.
152
190
 
153
- Streaming follows the same split:
191
+ Streaming uses the same Agent-incarnation listener:
154
192
 
155
193
  ```ruby
156
- agent.stream("Explain the design") do |event|
157
- puts event.payload if event.type == :token
158
- end
159
- ```
160
-
161
- ```ruby
162
- task = agent.stream_async(
163
- "Explain the design",
164
- on_event: ->(event) { puts event.payload if event.type == :token }
194
+ agent = ResearchAgent.new(
195
+ on_event: ->(event) {
196
+ puts event.payload if event.type == :token
197
+ }
165
198
  )
199
+
200
+ agent.stream("Explain the design")
201
+ task = agent.stream_async("Explain another design")
166
202
  ```
167
203
 
168
- Streaming 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.
169
207
 
170
208
  ## Human-in-the-loop approval
171
209
 
172
- A Tool requiring approval can suspend an Agent invocation. Resume it with the
173
- approval request identifier returned by the suspension result.
174
-
175
- 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`:
176
213
 
177
214
  ```ruby
178
- 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
+ )
179
228
 
180
- if result[:suspended]
181
- request = result[:approval_request]
182
- result = agent.approve(
183
- result[:execution_id],
184
- approval_request_id: request.id,
185
- approved: true
186
- )
187
- end
229
+ result = agent.invoke("Perform the requested protected action")
188
230
  ```
189
231
 
190
- From an EventLoop callback, use `approve_async` rather than blocking EventLoop.
191
- Approval resume continues the same live Agent instance, Activation, and
192
- 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.
193
237
 
194
238
  ## Workflow basics
195
239
 
@@ -225,16 +269,16 @@ workflow = Phronomy::Workflow.define(
225
269
  transition from: :finalize, to: :__finish__
226
270
  end
227
271
 
228
- state = workflow.invoke({draft: ""}, config: {thread_id: "doc-1"})
272
+ state = workflow.invoke({draft: ""}, config: {workflow_instance_id: "doc-1"})
229
273
  final = workflow.send_event(state: state, event: :approve)
230
274
  puts final.approved
231
275
  ```
232
276
 
233
- `Persistence#workflow_states` is the durable Workflow repository. `thread_id`
277
+ `Persistence#workflow_states` is the durable Workflow repository. `workflow_instance_id`
234
278
  identifies the durable Workflow state and remains stable across resume. Each
235
279
  concrete Runtime execution receives a separate internal `fsm_session_id`; the
236
280
  application-level `session_id` remains ordinary caller/tracing metadata. Phronomy
237
- 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
238
282
  save so another local invocation cannot start from a stale snapshot while the
239
283
  current owner is still committing.
240
284
 
@@ -263,7 +307,7 @@ class AnswerContext
263
307
 
264
308
  field :question, type: :replace, default: ""
265
309
  field :answer, type: :replace, default: nil
266
- field :thread_id, type: :replace, default: nil
310
+ # workflow_instance_id is framework-owned metadata; do not declare it as a field.
267
311
  end
268
312
 
269
313
  class ResearchAgent < Phronomy::Agent::Base
@@ -281,20 +325,17 @@ workflow = Phronomy::Workflow.define(AnswerContext) do
281
325
  state :done
282
326
 
283
327
  entry :asking, ->(ctx) {
284
- thread_id = ctx.thread_id
285
-
286
- my_agent.invoke_async(
287
- ctx.question,
288
- on_event: ->(event) {
289
- next unless event.type == :done
290
-
291
- workflow.signal(
292
- thread_id: thread_id,
293
- event: :answer_ready,
294
- payload: {answer: event.payload[:output]}
295
- )
296
- }
297
- )
328
+ workflow_instance_id = ctx.workflow_instance_id
329
+
330
+ my_agent.invoke_async(ctx.question) do |event|
331
+ next unless event.type == :done
332
+
333
+ workflow.signal(
334
+ workflow_instance_id: workflow_instance_id,
335
+ event: :answer_ready,
336
+ payload: {answer: event.payload[:output]}
337
+ )
338
+ end
298
339
 
299
340
  ctx
300
341
  }
@@ -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