phronomy 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +200 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +152 -0
  15. data/docs/architecture/persistence.md +175 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +82 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/README.md +172 -0
  45. data/docs/features.md +31 -11
  46. data/docs/getting-started.md +77 -45
  47. data/docs/migrations/0.19.md +14 -7
  48. data/docs/migrations/0.22.md +390 -0
  49. data/docs/persistence-backends.md +88 -38
  50. data/docs/runtime-and-concurrency.md +227 -33
  51. data/examples/README.md +13 -0
  52. data/lib/phronomy/agent/agent_execution.rb +19 -15
  53. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  54. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  55. data/lib/phronomy/agent/agent_root.rb +3 -3
  56. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  57. data/lib/phronomy/agent/async_event_api.rb +145 -72
  58. data/lib/phronomy/agent/base.rb +388 -181
  59. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  60. data/lib/phronomy/agent/context_assembler.rb +437 -178
  61. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  62. data/lib/phronomy/agent/context_plan.rb +18 -13
  63. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  64. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  65. data/lib/phronomy/agent/context_policy.rb +109 -3
  66. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  67. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  68. data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
  69. data/lib/phronomy/agent/journal_record.rb +17 -4
  70. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  71. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  72. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  73. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  74. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  75. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
  76. data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
  77. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
  78. data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
  79. data/lib/phronomy/agent/recovery_support.rb +512 -0
  80. data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
  81. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  82. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  83. data/lib/phronomy/agent/shared_state.rb +38 -1
  84. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  85. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  86. data/lib/phronomy/agent/tool_invocation.rb +336 -102
  87. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  88. data/lib/phronomy/agent.rb +20 -2
  89. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  90. data/lib/phronomy/agent_purged_error.rb +5 -0
  91. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  92. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  93. data/lib/phronomy/engine/event_loop.rb +622 -63
  94. data/lib/phronomy/engine/fsm_session.rb +194 -21
  95. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  96. data/lib/phronomy/engine/runtime.rb +77 -20
  97. data/lib/phronomy/generator_verifier.rb +12 -14
  98. data/lib/phronomy/invocation_context.rb +9 -29
  99. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  100. data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
  101. data/lib/phronomy/multi_agent/coordinator.rb +154 -0
  102. data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
  103. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  104. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  105. data/lib/phronomy/multi_agent/handoff.rb +24 -45
  106. data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
  107. data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
  108. data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
  109. data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
  110. data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
  111. data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
  112. data/lib/phronomy/multi_agent/runner.rb +98 -0
  113. data/lib/phronomy/persistence/durable_codec.rb +646 -0
  114. data/lib/phronomy/persistence/durable_record.rb +117 -0
  115. data/lib/phronomy/persistence/in_memory.rb +210 -134
  116. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  117. data/lib/phronomy/persistence/repository_facades.rb +316 -0
  118. data/lib/phronomy/persistence.rb +81 -41
  119. data/lib/phronomy/recovery.rb +186 -0
  120. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  121. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
  122. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  123. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  124. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  125. data/lib/phronomy/tracing/automatic.rb +176 -0
  126. data/lib/phronomy/tracing/base.rb +11 -2
  127. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  128. data/lib/phronomy/version.rb +1 -1
  129. data/lib/phronomy/workflow.rb +3 -6
  130. data/lib/phronomy/workflow_context.rb +14 -5
  131. data/lib/phronomy/workflow_recovery.rb +123 -0
  132. data/lib/phronomy/workflow_runner.rb +468 -256
  133. data/lib/phronomy.rb +6 -0
  134. data/scripts/api_snapshot.rb +12 -0
  135. data/sig/phronomy/agent.rbs +209 -7
  136. data/sig/phronomy/multi_agent.rbs +39 -0
  137. data/sig/phronomy/persistence.rbs +62 -4
  138. data/sig/phronomy/runtime.rbs +1 -4
  139. data/sig/phronomy/workflow.rbs +2 -2
  140. data/sig/phronomy.rbs +10 -0
  141. metadata +65 -17
  142. data/examples/workflows/agent_event_mapping.rb +0 -101
  143. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  144. data/lib/phronomy/agent/activation_registry.rb +0 -28
  145. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  146. data/lib/phronomy/agent/context_candidate.rb +0 -47
  147. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  148. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  149. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  150. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  151. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  152. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  153. data/lib/phronomy/agent/context_request.rb +0 -35
  154. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  155. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  156. data/lib/phronomy/agent/runner.rb +0 -97
@@ -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,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