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
@@ -2,68 +2,150 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Accepted for the stateful Agent refactor. This decision supersedes the parts of ADR-011 that made the legacy `build_context`/Assembler path the long-term LLM-input authority.
5
+ Accepted and implemented by the stateful Agent / ACS-04 Context Policy refactor.
6
+ This decision supersedes the legacy `build_context` / Assembler authority model and
7
+ the intermediate descriptor/registry-based Context Policy design.
6
8
 
7
9
  ## Decision
8
10
 
9
- Phronomy records the logical execution facts it observes as an append-only Canonical Complete Execution Log. Context selection, pruning and compaction do not rewrite or delete those raw facts. They decide only which representation is materialized into one LLM Call Manifest.
11
+ Phronomy records the logical execution facts it observes as an append-only Canonical
12
+ Complete Execution Log. Context selection, pruning, ordering, Tool subset selection,
13
+ and compaction do not rewrite or delete those raw facts. They decide only what is
14
+ materialized into one LLM Call Manifest.
10
15
 
11
16
  Journal and Manifest are separate authorities:
12
17
 
13
- - **Journal** is the authoritative record of logical execution facts that Phronomy observed.
14
- - **Manifest** is the authoritative record of the logical input fixed for one particular LLM Call.
15
- - Runtime Projection is derived from the Manifest and must not add semantic content that the Manifest did not select.
18
+ - **Journal** is the authoritative record of logical execution facts observed by Phronomy.
19
+ - **Manifest** is the authoritative record of the logical input fixed for one LLM Call.
20
+ - Runtime Projection is derived from the Manifest and must not add semantic content that
21
+ the Manifest did not select.
16
22
 
17
- The persistent identity axes have narrow responsibilities:
23
+ `agent_id`, `execution_id`, `llm_call_id`, `tool_call_id`, and Journal `sequence` retain
24
+ their narrow identity/provenance meanings. They are not generic Context-selection
25
+ boundaries. Tool protocol dependencies are represented as indivisible conversation
26
+ groups rather than by grouping all records from one execution.
18
27
 
19
- - `agent_id` identifies the owning Agent.
20
- - `execution_id` identifies one AgentExecution. It is provenance, not a Context-selection atom.
21
- - `llm_call_id` identifies one runtime Provider LLM Call. It is allocated before transport starts and correlates that call's outcome. It is provenance, not a semantic-compaction boundary.
22
- - `tool_call_id` links an assistant message's Tool Call with the corresponding Tool execution/message.
23
- - Journal `sequence` is canonical chronology.
28
+ ## Context Policy semantic boundary
24
29
 
25
- No `message_group_id`, import-only source provenance ID, or synthetic imported `execution_id` / `llm_call_id` is introduced.
30
+ One LLM Call is prepared through:
26
31
 
27
- ### Message preservation
32
+ ```text
33
+ Context sources
34
+ -> ContextPolicyInput
35
+ -> ContextPolicy
36
+ -> ContextPlan
37
+ -> ContextAssembler validation/canonicalization
38
+ -> LLMInputManifest
39
+ ```
40
+
41
+ `ContextPolicyInput` exposes four top-level semantic categories:
28
42
 
29
- A logical message that Phronomy receives is not flattened merely to make later Context assembly convenient.
43
+ ```text
44
+ instruction
45
+ knowledge
46
+ tools
47
+ conversation
48
+ ```
30
49
 
31
- - A Provider assistant response is captured as one complete assistant message containing its observable `content` and all Tool Calls.
32
- - An imported assistant message is journaled as one assistant message with the structure supplied by the Import contract.
33
- - A Tool value returned by Phronomy Tool execution is an execution fact (`tool_result`).
34
- - The Tool-role message actually appended to the LLM conversation is a separate logical fact (`tool_message`).
35
- - Imported Tool-role messages are journaled directly as `tool_message` records; Phronomy does not invent a separate raw Tool execution result for an execution it did not observe.
50
+ The values are immutable Phronomy-defined typed items. Conversation is exposed as an
51
+ ordered array of indivisible groups. An ordinary message is a singleton group; an
52
+ assistant Tool Call and its corresponding Tool-role message(s) form one atomic group.
36
53
 
37
- The Journal therefore does not need to infer or reconstruct a source message boundary that Phronomy already observed. Context Policy can inspect Tool Call IDs contained in an assistant message and form protocol-safe selection units with the corresponding Tool messages.
54
+ `ContextPlan` uses the same four categories. Items omitted from the Plan are omitted from
55
+ that LLM Call. Plan ordering expresses Policy ordering within the Framework-owned
56
+ structural layout. ContextAssembler validates required material, group integrity, Tool
57
+ configuration, and the final token budget before it stores the Manifest.
38
58
 
39
- ### Import boundary
59
+ A custom ContextPolicy is ordinary Ruby strategy code. It may select, omit, order,
60
+ compact, retrieve, or otherwise compute its Plan. Phronomy does not expose a public
61
+ Pipeline/Selector/UnitBuilder composition DSL as the Context Policy SPI.
40
62
 
41
- The application supplying imported history is responsible for satisfying Phronomy's Import contract. Phronomy interprets valid input according to that contract and rejects only data that is invalid under the contract, such as unsupported roles, missing Tool Call IDs, orphan/duplicate Tool results, unresolved Tool calls, or malformed message structure.
63
+ ## Agent binding and lifetime
42
64
 
43
- External resource acquisition is not a `ContextImporter` responsibility. Files, URLs and similar resources are obtained and interpreted by the Application or Tool that owns that capability; Phronomy journals the logical content/results it actually receives. `ContextImporter` therefore does not introduce RubyLLM-specific attachment handling or an attachment-specific reject path.
65
+ A ContextPolicy is Application code/runtime wiring, not durable Agent state.
44
66
 
45
- Phronomy must not reject an otherwise valid input merely because an internal flattened representation would lose information. In particular, two separately supplied assistant messages remain two separate Journal messages.
67
+ An Application binds a **ContextPolicy instance** on the Agent class:
46
68
 
47
- ### Manifest boundary
69
+ ```ruby
70
+ SEARCH_POLICY = SearchContextPolicy.new(vector_store: VECTOR_STORE)
48
71
 
49
- The Manifest fixes what one LLM Call will actually receive after Context Policy selection. A historical raw Tool return value and the Tool message produced from it are not interchangeable: the raw result belongs to the execution log, while the message selected for an LLM Call belongs to the Manifest input path.
72
+ class ResearchAgent < Phronomy::Agent::Base
73
+ context_policy SEARCH_POLICY
74
+ end
75
+ ```
50
76
 
51
- One Provider response is captured as a Phronomy-owned `ProviderCallOutcome` before Agent-owned Tool execution starts. The canonical assistant-message record is produced from that outcome, not from Application callback delivery.
77
+ If no Policy is bound, the built-in Default instance is used. There is no Policy override
78
+ on Agent instance creation/loading or on `invoke` / `stream` calls. A Policy instance may
79
+ be shared by multiple Agent classes; concurrency safety of a shared Policy and its runtime
80
+ dependencies is the Application's responsibility.
52
81
 
53
- Context selection is expressed separately through `ContextCandidate`, dependency-aware `ContextSelectionUnit`, `ContextRequest`, `ContextPolicy`, validated `ContextPlan`, and final token-budget validation. An assistant message containing Tool Calls and the corresponding Tool messages form an atomic protocol unit. Ordinary messages in the same `execution_id` remain independently selectable.
82
+ Phronomy does not persist or reconstruct a Policy instance and defines no
83
+ `ContextPolicyDescriptor`, Policy registry, serialized Policy config, or Policy version
84
+ contract. Recovery hydrates finalized `LLMInputManifest` values directly. Future Context
85
+ preparation after Recovery uses the ContextPolicy supplied by the currently loaded
86
+ Application code.
54
87
 
55
- ## RubyLLM boundary
88
+ ## Policy-generated material
56
89
 
57
- Agent-owned Tool execution requires RubyLLM's additive callback contract introduced in RubyLLM 1.15. Phronomy therefore requires `ruby_llm >= 1.15, < 2`.
90
+ A Policy may create new instruction, knowledge, or conversation items through the small
91
+ protected helper/factory API on `ContextPolicy`. Such material is an ordinary current-call
92
+ Plan item; there is no separate `DerivedContentSpec` collection.
58
93
 
59
- RubyLLM 1.15 adds the complete assistant message to `Chat#messages` before `before_tool_call` callbacks run. Phronomy captures the immutable Provider outcome at that boundary and raises `ToolCallIntercepted` only as an internal control transfer so approval, suspension, parallel dispatch and durable state remain Phronomy-owned.
94
+ Phronomy assigns current-call identity, estimates tokens, freezes/canonicalizes the value,
95
+ and stores content when needed by the finalized Manifest. The Application owns the
96
+ semantic transformation and any internal source mapping/provenance it requires. Merely
97
+ using a generated item in a Manifest does not promote it to a Journal fact or reusable
98
+ future Context candidate.
99
+
100
+ The ACS-04 Tool category is selection-only: a Policy may choose a subset of the effective
101
+ Agent Tool definitions but may not invent a runtime Tool implementation from schema-only
102
+ data.
103
+
104
+ ## Default Context Policy
105
+
106
+ The built-in Default is deterministic and model-free:
107
+
108
+ - retain effective instructions in stable order;
109
+ - retain the effective Agent Tool configuration;
110
+ - retain required/current conversation and choose a contiguous recent optional history;
111
+ - choose Knowledge in stable order, skipping an oversized item and continuing with later
112
+ items that fit;
113
+ - allocate variable remainder approximately 60% to conversation and 40% to Knowledge,
114
+ allowing unused share to be reused by the other category;
115
+ - perform no automatic compaction, embedding search, reranking, or additional LLM Call.
116
+
117
+ If required/fixed Context cannot fit, preparation fails rather than silently deleting it.
118
+
119
+ ## Execution / Persistence boundary
120
+
121
+ ContextPolicy executes synchronously on an OffloadPool worker, never on the Runtime
122
+ EventLoop. No Phronomy Persistence transaction spans `ContextPolicy#call`.
123
+
124
+ The required preparation shape is:
125
+
126
+ ```text
127
+ capture authoritative local/durable snapshot
128
+ -> build immutable ContextPolicyInput
129
+ -> ContextPolicy#call outside Persistence transaction
130
+ -> revalidate durable base/revision/lineage
131
+ -> short commit transaction
132
+ validate Plan
133
+ canonicalize selected/generated content
134
+ final budget validation
135
+ store LLMInputManifest
136
+ save execution state
137
+ ```
138
+
139
+ A stale Policy result is rejected by the final revision/watermark precondition. Phronomy
140
+ does not automatically retry or fall back to another Policy after Policy failure.
60
141
 
61
142
  ## Consequences
62
143
 
63
- - Canonical execution history is independent of the current Context budget or policy.
64
- - Import and runtime histories converge on the same canonical assistant/tool-message model without synthetic grouping identity.
65
- - Old optional working history may be excluded from a follow-up Manifest without being deleted.
66
- - Raw Tool results remain available as execution facts even when the corresponding Tool message is omitted from a later Manifest.
67
- - Tool protocol dependencies are validated independently from semantic selection policy.
68
- - Context Policy can later introduce deterministic derived/compacted records without replacing their raw sources.
69
- - Public custom Context Policy APIs, transaction-boundary restructuring, deterministic compaction, Manifest v2/tool subsets, and legacy Assembler removal remain later phases.
144
+ - Agent/Workflow durability is independent of ContextPolicy durability.
145
+ - The Manifest, not the historical Policy implementation, is the Recovery authority for a
146
+ finalized Provider input.
147
+ - Application Context strategies remain ordinary reusable Ruby objects with ordinary DI.
148
+ - Framework-internal protocol grouping/validation may use private helpers, but those
149
+ helpers are not the Application Policy API.
150
+ - Replaced descriptor/registry/request/derived-content and selector-pipeline abstractions
151
+ are removed rather than retained as compatibility aliases.
@@ -1,8 +1,15 @@
1
1
  # ADR 014: Unified Persistence for Durable State
2
2
 
3
- **Status**: Accepted
4
- **Date**: 2026-08-14
3
+ **Status**: Accepted
4
+ **Date**: 2026-08-14
5
5
  **Supersedes**: ADR-009
6
+ **Partially superseded by**:
7
+ - [ADR-020](020-canonical-workflow-instance-identity.md) for Workflow identity terminology only
8
+ - [ADR-021](021-generic-agent-invocation-identity-removal.md) for `InvocationContext` generic session/correlation semantics and Agent-side generic invocation identity
9
+ - [ADR-023](023-fsm-session-incarnation-identity-and-routing.md) for concrete FSMSession incarnation identity and Agent/Tool Runtime routing
10
+ - [ADR-024](024-event-loop-single-writer-agent-runtime.md) for live Agent Runtime ownership, EventLoop single-writer apply, and `AgentExecutionActivation` removal
11
+ - [ADR-025](025-process-local-agent-ownership-and-runtime-admission.md) for one-live-Agent-per-`agent_id` process ownership and same-process top-level execution admission
12
+ - [ADR-026](026-workflow-runtime-admission-and-durable-terminal-barrier.md) for same-process Workflow admission ownership and durable terminal-barrier ordering
6
13
 
7
14
  ---
8
15
 
@@ -0,0 +1,112 @@
1
+ # ADR-016: Semantic Multi-Agent Handoff and Runtime-local Ownership
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ The previous Multi-Agent Handoff implementation encoded routing through a
10
+ special Tool name and sentinel Tool result. That mechanism could identify a
11
+ Target Agent, but it conflated control-plane routing with ordinary Tool execution
12
+ and did not define sufficient Context-transfer, provenance, ownership, or
13
+ next-turn semantics.
14
+
15
+ Handoff is also semantically different from Agent-as-Tool delegation. Delegation
16
+ returns control to the parent coordinator. Handoff transfers active
17
+ responsibility to another Agent.
18
+
19
+ ADR-012 remains authoritative for the canonical execution log and per-call
20
+ Context Policy/Manifest pipeline. ADR-013 remains authoritative for persistent
21
+ Journal-backed Knowledge. This ADR defines the Agent-boundary transfer semantics
22
+ without replacing either decision.
23
+
24
+ ## Decision
25
+
26
+ ### 1. Handoff is an explicit Source-to-Target semantic edge
27
+
28
+ `Phronomy::MultiAgent::Handoff` binds concrete live `source_agent` and
29
+ `target_agent` instances plus a `HandoffPolicy` and description.
30
+
31
+ Generated Tool names are private transport encoding. They are not Handoff
32
+ identity, durable routing identity, or public semantic contract.
33
+
34
+ ### 2. Handoff is control-plane transfer, not an ordinary Tool effect
35
+
36
+ The LLM-facing capability is intercepted before `ToolInvocation` creation. A
37
+ valid Handoff produces a typed private `HandoffRequest` and a Source execution
38
+ terminal outcome of `:handed_off`.
39
+
40
+ No sentinel Tool result is generated. One Provider outcome may not mix Handoff
41
+ with ordinary Tool Calls or contain multiple Handoff requests.
42
+
43
+ ### 3. Handoff Policy owns the Agent-boundary transfer decision
44
+
45
+ The initial transfer categories are `current_request`, `history`, `knowledge`,
46
+ and `tool_exchanges`. Application policy classifies each as `required`,
47
+ `forbidden`, or `selectable`, with an include/exclude default for selectable
48
+ categories.
49
+
50
+ Required and forbidden decisions cannot be overridden by the Source Agent.
51
+ Source selection is bounded to the effective Source Context represented by the
52
+ current Manifest.
53
+
54
+ ### 4. Context dependency grouping is preserved across the Handoff boundary
55
+
56
+ Context Policy uses typed `ContextPolicyInput` conversation groups rather than
57
+ the removed `Selection::Unit` SPI. Assistant Tool Calls and their corresponding
58
+ Tool-role messages are one indivisible conversation group. When the selected
59
+ Plan is realized, the Manifest records the Framework conversation-group identity
60
+ and classifies canonical Tool exchanges as `tool_exchanges` for Handoff.
61
+
62
+ `HandoffProjection` groups current-format Manifest segments by that conversation
63
+ group identity. It may still read legacy `selection_unit_id` metadata from a
64
+ finalized pre-ACS-04 Manifest, but new Context assembly does not recreate
65
+ `Selection::Unit`. `Selection::Candidate` and `Selection::Constraint` remain
66
+ internal Context-input normalization details only.
67
+
68
+ Handoff Policy answers what may cross the Agent boundary. Target Context Policy
69
+ remains the final authority for what enters each Target LLM call. Transferred
70
+ Handoff Context is therefore converted to selectable Target Context candidates
71
+ before Target Context Policy runs.
72
+
73
+ ### 5. Handoff Context is immutable, reference-only transferred material
74
+
75
+ Selected Source content is materialized into an immutable private
76
+ `HandoffContext`; Target execution does not depend on later dereferencing mutable
77
+ Source state. Source and Target may use different Persistence adapters.
78
+
79
+ Transferred material is not automatically adopted into Target Journal or
80
+ persistent Knowledge. Provenance records the original Agent/record/execution/LLM
81
+ call/Tool call where available, plus a multi-hop transfer path.
82
+
83
+ ### 6. Active responsibility is Runtime-local
84
+
85
+ One `main_agent` instance anchors one Multi-Agent coordination lifetime. The
86
+ Runtime/EventLoop is the sole mutation authority for `active_agent` and active
87
+ Handoff Context. The current Target remains active across user turns and across
88
+ Runner-facade recreation while the same main Agent instance and Runtime live.
89
+
90
+ Concurrent turns for the same coordination lifetime are rejected by Runtime
91
+ admission rather than racing active-Agent transitions.
92
+
93
+ This coordination state is not durably rehydrated. Runtime/process reset starts
94
+ again at `main_agent`; historical `execution_handed_off` audit facts do not imply
95
+ restored active-Agent ownership.
96
+
97
+ ### 7. Public Runner cutover is a clean break
98
+
99
+ The public coordinator is `Phronomy::MultiAgent::Runner.new(main_agent:,
100
+ handoffs:)`. `Phronomy::Agent::Runner`, `agents:`, `routes:`, and the old Agent-owned
101
+ Handoff Tool registry are removed without compatibility aliases.
102
+
103
+ ## Consequences
104
+
105
+ Applications gain explicit Handoff semantics, bounded multi-hop continuation,
106
+ Context transfer with provenance, and predictable next-turn ownership.
107
+
108
+ The framework has a stronger separation between control-plane operations and
109
+ ordinary Tools, but applications migrating from the old Runner must update their
110
+ configuration code. Active-Agent continuation remains intentionally process-local
111
+ until a future decision defines a durable coordination identity and rehydration
112
+ contract.
@@ -0,0 +1,200 @@
1
+ # ADR-017: Design Authority and ADR Governance
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Date
8
+
9
+ 2026-08-23
10
+
11
+ ## Context
12
+
13
+ Phronomy contains multiple artifact classes that answer different questions:
14
+
15
+ - ADRs record architecture intent;
16
+ - source/runtime behavior records implementation reality;
17
+ - public API documentation, `@api` classification, compatibility/contract
18
+ tests and runtime behavior collectively define public/extension contracts;
19
+ - RBS represents an already-established typed contract;
20
+ - ordinary tests provide regression evidence;
21
+ - historical and archived design documents preserve context but may describe
22
+ architecture that is no longer current.
23
+
24
+ Treating those artifacts as one universal precedence list creates two failure
25
+ modes: stale design can be reimplemented because it looks authoritative, or an
26
+ accidental implementation change can silently redefine architecture because
27
+ it is newer.
28
+
29
+ The repository also contains two historical ADR files with numeric prefix
30
+ `011`. Numeric labels alone therefore cannot provide an unambiguous identity
31
+ for every existing decision.
32
+
33
+ ## Decision
34
+
35
+ ### 1. Authority is question-specific
36
+
37
+ Phronomy does **not** define one universal precedence order for every artifact.
38
+
39
+ - Accepted and non-superseded ADRs are the normative authority for
40
+ architecture decisions.
41
+ - Source code and runtime behavior are the authority for current
42
+ implementation reality. They do not silently amend architecture intent.
43
+ - Public APIs and extension SPIs are composite contracts established by
44
+ runtime behavior, `@api` classification, formal API documentation and
45
+ explicit compatibility/contract tests.
46
+ - RBS represents a contract that already exists. RBS does not create new
47
+ public semantics or make an internal API public.
48
+ - Ordinary implementation tests are regression evidence. A test is an
49
+ architecture/contract authority only when the repository explicitly treats
50
+ it as an architecture guard or compatibility/contract test.
51
+ - Historical, Archived and Superseded artifacts are non-normative for current
52
+ architecture.
53
+
54
+ ### 2. Inconsistency is explicit
55
+
56
+ When normative architecture, public contract and implementation reality
57
+ disagree, no artifact wins automatically because it is newer.
58
+
59
+ The discrepancy is an architecture inconsistency. It must be recorded,
60
+ reviewed and resolved explicitly by changing the appropriate architecture
61
+ decision, contract, implementation, or combination of them.
62
+
63
+ The required tracking fields and lifecycle are defined in
64
+ [`docs/decisions/README.md`](README.md).
65
+
66
+ ### 3. ADR canonical identity is the filename basename
67
+
68
+ The canonical key for an ADR is its filename basename:
69
+
70
+ ```text
71
+ NNN-kebab-case-slug
72
+ ```
73
+
74
+ The numeric prefix is an ordering/display field. Because the repository
75
+ contains two legacy `011` decisions, a bare numeric label is not a globally
76
+ unique historical key.
77
+
78
+ The two legacy `011` ADRs remain in place and are distinguished by:
79
+
80
+ ```text
81
+ 011-build-context-as-single-llm-input-authority
82
+ 011-delegate-transport-policy-to-adapters
83
+ ```
84
+
85
+ New or modified normative references to either decision must use the full
86
+ canonical key or an explicit file link.
87
+
88
+ ### 4. Existing decision history is not renumbered
89
+
90
+ Existing ADR files are not silently renumbered to repair historical numbering
91
+ defects. Renumbering changes references and obscures the actual decision
92
+ history.
93
+
94
+ `016-semantic-multi-agent-handoff` is preserved under its existing key even
95
+ though it was created before this governance rule was canonicalized.
96
+
97
+ ### 5. New ADR numbers are monotonic and unique
98
+
99
+ A new ADR receives:
100
+
101
+ ```text
102
+ max(existing numeric prefix) + 1
103
+ ```
104
+
105
+ at the point the new decision is prepared for merge.
106
+
107
+ New ADRs:
108
+
109
+ - use a three-digit numeric prefix;
110
+ - do not fill gaps;
111
+ - do not reuse any existing prefix;
112
+ - update the decision index in the same change.
113
+
114
+ Concurrent unmerged ADRs that select the same number are resolved by rebasing:
115
+ only the not-yet-historical ADR that merges later is renumbered.
116
+
117
+ This decision follows the rule it establishes: `016` already existed and `017`
118
+ was unused, so this Design Authority decision is ADR-017.
119
+
120
+ ### 6. Supersession preserves history
121
+
122
+ A superseding decision is normally a new ADR.
123
+
124
+ The superseded ADR remains in the repository with its historical rationale.
125
+ Its status/supersession relation and the decision index are updated so the
126
+ current authority is unambiguous.
127
+
128
+ A materially different architecture is not retroactively edited into an old
129
+ Accepted ADR merely to make repository text appear internally consistent.
130
+
131
+ ### 7. Working review registers are planning evidence
132
+
133
+ Carry-forward (`CF-*`), architecture-inconsistency (`AI-*`), Architecture
134
+ Change Set (ACS), and Compatibility Gate (CG) artifacts used during the
135
+ architecture reconciliation program are not themselves normative ADRs.
136
+
137
+ They identify work and evidence. Their output becomes repository authority
138
+ only through the appropriate Accepted ADR, public contract, source/runtime
139
+ implementation, architecture guard, or current explanatory documentation.
140
+
141
+ Unresolved items that survive a working review must be represented by an
142
+ active repository work item or an explicit deferred dependency; they must not
143
+ disappear merely because a working document is retired.
144
+
145
+ ### 8. Explanatory architecture documentation is separate
146
+
147
+ Current explanatory architecture documentation will be organized under a
148
+ dedicated canonical entry as part of the documentation-lifecycle migration.
149
+
150
+ Those documents explain the reconciled architecture. They do not replace the
151
+ ADR system as the authority for architecture decisions.
152
+
153
+ ## Repository contract
154
+
155
+ Contributors making architecture-sensitive changes must:
156
+
157
+ 1. consult [`docs/decisions/README.md`](README.md);
158
+ 2. identify relevant Accepted/non-superseded ADRs by canonical key;
159
+ 3. distinguish architecture intent from implementation reality and public
160
+ contract evidence;
161
+ 4. record unresolved inconsistencies rather than choosing a winner by recency;
162
+ 5. update ADR status/index relationships when accepting, amending or
163
+ superseding a decision;
164
+ 6. preserve the public-API composite contract and RBS non-authority rule.
165
+
166
+ `CONTRIBUTING.md` carries the developer-facing form of this contract.
167
+
168
+ ## Consequences
169
+
170
+ ### Positive
171
+
172
+ - Current architecture intent has an explicit repository authority model.
173
+ - Duplicate historical ADR number `011` no longer makes decision references
174
+ ambiguous.
175
+ - New ADR identifiers cannot silently collide.
176
+ - Source drift does not silently become architecture.
177
+ - Stale ADR text does not silently override implementation reality; conflicts
178
+ are visible and reviewable.
179
+ - RBS and ordinary tests cannot accidentally become architecture-definition
180
+ mechanisms.
181
+ - Historical decision rationale is preserved.
182
+
183
+ ### Trade-offs
184
+
185
+ - Architecture-sensitive changes must update an index and sometimes track an
186
+ explicit inconsistency.
187
+ - Existing bare references to the legacy numeric label `ADR-011` can remain
188
+ ambiguous in historical material until that material is otherwise touched.
189
+ - Accepted ADRs with known inconsistencies remain visible as normative intent
190
+ until an explicit successor is accepted; the inconsistency process is
191
+ therefore required rather than optional.
192
+
193
+ ## Non-goals
194
+
195
+ This ADR does not:
196
+
197
+ - reorganize `spec/design/` into the final current/archive documentation tree;
198
+ - resolve domain-specific architecture inconsistencies;
199
+ - supersede ADR-010, ADR-012, ADR-013, ADR-014, ADR-015 or ADR-016;
200
+ - change Runtime behavior, public APIs, persistence formats, or durable data.