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
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Benchmark: Manifest-first Context assembly and Default Context Policy.
3
+ # Benchmark: typed ContextPolicyInput and split Context assembly.
4
4
  #
5
5
  # Usage:
6
6
  # ruby benchmark/bench_context_assembler.rb
7
7
  #
8
- # Measures:
9
- # 1. ContextPolicies::Default selection cost for growing canonical candidate sets.
10
- # 2. ContextAssembler#build_initial end-to-end Manifest construction.
11
- #
12
8
  # No provider call is performed.
13
9
 
14
10
  require "benchmark"
@@ -17,66 +13,40 @@ require_relative "../lib/phronomy"
17
13
  module BenchContextAssembler
18
14
  module_function
19
15
 
20
- def candidate(index)
21
- category, role = if (index % 10).zero?
22
- [:knowledge, :user]
23
- elsif index.even?
24
- [:assistant_message, :assistant]
25
- else
26
- [:external_message, :user]
27
- end
28
-
29
- Phronomy::Agent::ContextCandidate.new(
30
- candidate_id: "candidate-#{index}",
31
- source_kind: :journal,
32
- category: category,
33
- role: role,
34
- content_ref: "content-#{index}",
35
- record_id: "record-#{index}",
36
- agent_id: "bench-agent",
37
- execution_id: "execution-#{index / 4}",
38
- llm_call_id: nil,
39
- tool_call_id: nil,
40
- sequence: index,
41
- requirement: :optional,
42
- priority: 0,
43
- metadata: {
44
- "estimated_tokens" => 8,
45
- "source_sequence" => index
46
- }
47
- )
16
+ def provenance
17
+ Phronomy::Agent::ContextPolicyInput::Provenance.new(origin: :journal)
48
18
  end
49
19
 
50
- def parts
51
- {
52
- unit_builder:
53
- Phronomy::Agent::ContextParts::UnitBuilders::DependencyAwareUnitBuilder.new,
54
- required_context_resolver:
55
- Phronomy::Agent::ContextParts::Requirements::RequiredContextResolver.new,
56
- recent_first_selector:
57
- Phronomy::Agent::ContextParts::Selectors::RecentFirstSelector.new,
58
- token_budget_packer:
59
- Phronomy::Agent::ContextParts::Budget::TokenBudgetPacker.new
60
- }.freeze
61
- end
20
+ def policy_input(item_count)
21
+ knowledge = []
22
+ conversation = []
23
+ item_count.times do |index|
24
+ if (index % 10).zero?
25
+ knowledge << Phronomy::Agent::ContextPolicyInput::KnowledgeItem.new(
26
+ id: "knowledge-#{index}", kind: :knowledge, role: :user,
27
+ content: "knowledge #{index}", content_format: :text,
28
+ estimated_tokens: 8, required: false, provenance: provenance, metadata: {}
29
+ )
30
+ else
31
+ conversation << [Phronomy::Agent::ContextPolicyInput::ConversationItem.new(
32
+ id: "message-#{index}", kind: :external_message, role: :user,
33
+ content: "message #{index}", content_format: :text,
34
+ sequence: index, estimated_tokens: 8, required: false,
35
+ provenance: provenance, tool_call_id: nil, tool_call_ids: [],
36
+ delivery: :chat_message, metadata: {}
37
+ )]
38
+ end
39
+ end
62
40
 
63
- def request(candidate_count)
64
- candidates = Array.new(candidate_count) { |i| candidate(i) }
65
- Phronomy::Agent::ContextRequest.new(
66
- agent_id: "bench-agent",
67
- execution_id: "bench-execution",
68
- call_sequence: 2,
69
- call_mode: :complete,
70
- candidates: candidates,
41
+ Phronomy::Agent::ContextPolicyInput.new(
42
+ agent_id: "bench-agent", execution_id: "bench-execution",
43
+ call_sequence: 2, call_mode: :complete,
44
+ instruction: [], knowledge: knowledge, tools: [], conversation: conversation,
71
45
  token_budget: Phronomy::LlmContextWindow::TokenBudget.new(
72
- context_window: [candidate_count * 16, 4_096].max,
46
+ context_window: [item_count * 16, 4_096].max,
73
47
  max_output_tokens: 512
74
48
  ),
75
- model_config: {},
76
- previous_manifest: nil,
77
- required_coverage: [],
78
- parts: parts,
79
- metadata: {"mandatory_token_estimate" => 32}
49
+ model_config: {}, previous_manifest: nil, metadata: {}
80
50
  )
81
51
  end
82
52
 
@@ -124,31 +94,32 @@ module BenchContextAssembler
124
94
  end
125
95
  end
126
96
 
127
- puts "Manifest-first Context benchmark"
97
+ puts "Typed Context Policy benchmark"
128
98
  puts "Ruby #{RUBY_VERSION} on #{RUBY_PLATFORM}"
129
99
  puts "=" * 72
130
100
 
131
- policy = Phronomy::Agent::ContextPolicies::Default.new
132
- policy_requests = [10, 100, 1_000].to_h do |count|
133
- [count, BenchContextAssembler.request(count)]
101
+ policy = Phronomy::Agent::ContextPolicies::Default.instance
102
+ policy_inputs = [10, 100, 1_000].to_h do |count|
103
+ [count, BenchContextAssembler.policy_input(count)]
134
104
  end
135
105
 
136
106
  Benchmark.bm(46) do |x|
137
- policy_requests.each do |count, request|
107
+ policy_inputs.each do |count, policy_input|
138
108
  iterations = (count >= 1_000) ? 200 : 1_000
139
- x.report("DefaultContextPolicy #{count} candidates x#{iterations}") do
140
- iterations.times { policy.call(request) }
109
+ x.report("DefaultContextPolicy #{count} items x#{iterations}") do
110
+ iterations.times { policy.call(policy_input) }
141
111
  end
142
112
  end
143
113
 
144
114
  assembler, root, execution = BenchContextAssembler.assembler_fixture
145
- x.report("ContextAssembler#build_initial x500") do
115
+ x.report("ContextAssembler prepare/finalize x500") do
146
116
  500.times do
147
- assembler.build_initial(
117
+ prepared = assembler.prepare_initial(
148
118
  input: "benchmark input",
149
119
  agent_root: root,
150
120
  execution: execution
151
121
  )
122
+ assembler.finalize(prepared)
152
123
  end
153
124
  end
154
125
  end
@@ -97,7 +97,7 @@ end
97
97
  stub_agent_class = Class.new(Phronomy::Agent::Base) do
98
98
  agent_definition id: "bench-stub", version: 1
99
99
 
100
- define_method(:invoke) do |_input, thread_id: nil, config: {}|
100
+ define_method(:invoke) do |_input, config: {}|
101
101
  {output: "stub", messages: []}
102
102
  end
103
103
 
@@ -112,7 +112,7 @@ stub_agent_class = Class.new(Phronomy::Agent::Base) do
112
112
  end
113
113
  end
114
114
 
115
- orchestrator_class = Class.new(Phronomy::MultiAgent::Orchestrator)
115
+ orchestrator_class = Class.new(Phronomy::MultiAgent::Orchestrator) { agent_definition id: "orchestrator", version: 1 }
116
116
  orchestrator = orchestrator_class.new
117
117
 
118
118
  PARALLEL_ITERATIONS = 200
@@ -0,0 +1,174 @@
1
+ > **CURRENT explanatory architecture**
2
+ >
3
+ > This document describes the reconciled current Phronomy system. Normative
4
+ > architecture decisions remain in the [ADR index](../decisions/README.md);
5
+ > source/runtime behavior remains implementation reality and does not silently
6
+ > amend an ADR.
7
+
8
+ # Agent Context, Identity, and Ownership
9
+
10
+ ## 1. System boundary
11
+
12
+ A stateful Agent separates canonical durable facts, one live Runtime owner, and
13
+ the logical input finalized for each Provider call.
14
+
15
+ ```text
16
+ Application
17
+ |
18
+ live Agent instance -- agent_id --> one Runtime-local mutable owner
19
+ |
20
+ +-- AgentRoot / Journal view
21
+ |
22
+ +-- AgentExecution -- execution_id
23
+ |
24
+ +-- FSMSession incarnation -- fsm_session_id
25
+ |
26
+ +-- Provider Call -- llm_call_id
27
+ |
28
+ +-- ToolInvocation -- tool_invocation_id / tool_call_id
29
+ ```
30
+
31
+ `agent_id` identifies the logical Agent. `execution_id` identifies one logical
32
+ Agent execution. `fsm_session_id` identifies one Runtime execution incarnation.
33
+ Provider and Tool semantic IDs identify their own operations. These identities
34
+ are intentionally distinct.
35
+
36
+ Phronomy does not restore a generic Agent `thread_id`, `session_id`,
37
+ `correlation_id`, or `agent_invocation_id` identity. Application observability
38
+ may use `InvocationContext#task_id` / `#parent_task_id`, but those values are not
39
+ Agent, Workflow, Tool, or Runtime domain identities.
40
+
41
+ Relevant decisions:
42
+ [ADR-021](../decisions/021-generic-agent-invocation-identity-removal.md),
43
+ [ADR-022](../decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md),
44
+ and [ADR-023](../decisions/023-fsm-session-incarnation-identity-and-routing.md).
45
+
46
+ ## 2. Canonical history and one-call input
47
+
48
+ The Agent Journal is append-only canonical execution history. It is not the
49
+ Provider message buffer.
50
+
51
+ ```text
52
+ Journal / registered Knowledge / current-call material
53
+ |
54
+ v
55
+ ContextPolicyInput
56
+ instruction / knowledge / tools / conversation
57
+ |
58
+ v
59
+ ContextPolicy
60
+ |
61
+ v
62
+ ContextPlan
63
+ |
64
+ v
65
+ ContextAssembler
66
+ |
67
+ v
68
+ LLMInputManifest
69
+ |
70
+ v
71
+ RubyLLMMaterializer
72
+ |
73
+ v
74
+ Provider
75
+ ```
76
+
77
+ The finalized `LLMInputManifest` is the logical authority for one Provider Call.
78
+ The Journal remains the canonical history authority. Context selection never
79
+ rewrites old Journal facts.
80
+
81
+ See [Context Management](context-management.md) and
82
+ [ADR-012](../decisions/012-canonical-execution-log-and-context-policy.md).
83
+
84
+ ## 3. Live mutable ownership
85
+
86
+ Within one Runtime/process, one mutable live Agent instance owns one `agent_id`.
87
+ Loading or creating a second mutable live instance for the same `agent_id` is
88
+ rejected.
89
+
90
+ During active execution, EventLoop is the single writer of Phronomy-managed live
91
+ Agent execution state. Blocking Persistence work and other synchronous work that
92
+ must not run on EventLoop are submitted to OffloadPool. Worker results return as
93
+ immutable operation-specific results and are applied only after EventLoop checks
94
+ that the current execution/session/semantic-operation authority still matches.
95
+
96
+ Persistence conflict detection protects durable state from stale writes. It is
97
+ not a substitute for Runtime ownership.
98
+
99
+ See
100
+ [ADR-024](../decisions/024-event-loop-single-writer-agent-runtime.md) and
101
+ [ADR-025](../decisions/025-process-local-agent-ownership-and-runtime-admission.md).
102
+
103
+ ## 4. Same-process and cross-process responsibility
104
+
105
+ Same-process competing top-level Agent execution is rejected by Runtime
106
+ admission. Cross-process competing-execution exclusion is **conditional**, not an
107
+ unconditional Phronomy guarantee.
108
+
109
+ A multi-process deployment must arrange stable routing/partitioning by logical
110
+ identity or use an external coordination mechanism that establishes exclusive
111
+ authority. Optimistic Persistence CAS/revision checks detect stale durable
112
+ transitions; they do not themselves provide distributed exclusion.
113
+
114
+ Phronomy does not currently add a distributed lease/fencing coordinator, live
115
+ Agent migration protocol, or automatic takeover subsystem merely because
116
+ Persistence is durable.
117
+
118
+ See
119
+ [ADR-018](../decisions/018-durability-guarantees-and-failure-model.md).
120
+
121
+ ## 5. Suspension, recovery, and process loss
122
+
123
+ A live approval suspension retains the logical Agent execution slot. A later
124
+ approval resumes the same logical `execution_id` with a new Runtime
125
+ FSMSession incarnation as needed.
126
+
127
+ After Runtime/process loss, process-local objects are gone. `Agent.load` uses
128
+ confirmed durable state to classify unfinished work as:
129
+
130
+ ```text
131
+ resumable
132
+ reconcilable
133
+ resolution_required
134
+ ```
135
+
136
+ Recovery does not restore the old Ruby object graph or old Task callbacks. It
137
+ reconstructs current logical state from durable evidence and continues only
138
+ when authority and outcome certainty permit it.
139
+
140
+ A finalized historical Manifest is reused for the Provider input it represents;
141
+ Recovery does not rerun the historical Context Policy merely to reconstruct that
142
+ Manifest.
143
+
144
+ See [Persistence](persistence.md) for the durability/recovery boundary.
145
+
146
+ ## 6. Multi-Agent responsibility
147
+
148
+ A semantic Handoff changes which live Agent is responsible for continuing the
149
+ interaction. It does not merge Source and Target canonical state.
150
+
151
+ Transferred Handoff Context is immutable request-scoped material. The Target
152
+ Context Policy may select it for a Target LLM Call, but Handoff does not
153
+ automatically append it to Target Journal or persistent Knowledge.
154
+
155
+ Active Target responsibility persists across later Runner turns only within the
156
+ same Runtime/main-Agent coordination lifetime. It is not durably rehydrated
157
+ after Runtime/process reset.
158
+
159
+ See [Multi-Agent Handoff](multi-agent-handoff.md).
160
+
161
+ ## 7. Removed models
162
+
163
+ The following are not current Agent Context authorities:
164
+
165
+ - mutable RubyLLM message history as canonical Agent state;
166
+ - `Memory::ConversationManager`;
167
+ - `LlmContextWindow::Assembler`;
168
+ - `ContextVersionCache`;
169
+ - a Static/Entity/RAG Knowledge source hierarchy;
170
+ - a generic Agent conversation/thread/session identity;
171
+ - cross-Agent Journal sharing as Handoff semantics.
172
+
173
+ See [Removed Agent Context Architecture](removed/agent-context.md) for the
174
+ negative guidance retained from the retired designs.
@@ -0,0 +1,78 @@
1
+ > **CURRENT explanatory architecture**
2
+ >
3
+ > This document describes the reconciled current Phronomy system. Normative
4
+ > architecture decisions remain in the [ADR index](../decisions/README.md);
5
+ > source/runtime behavior remains implementation reality and does not silently
6
+ > amend an ADR.
7
+
8
+ # before_llm_input
9
+
10
+ ## 1. Purpose
11
+
12
+ `before_llm_input` is the supported request-scoped customization boundary before
13
+ a logical LLM input is finalized.
14
+
15
+ It may adjust model configuration and add logical Context candidates without
16
+ mutating Agent Journal state or a RubyLLM message buffer.
17
+
18
+ ## 2. Registration tiers
19
+
20
+ Hooks may be configured globally, on an Agent class, or on one Agent instance.
21
+
22
+ ```text
23
+ global -> class -> instance
24
+ ```
25
+
26
+ Later model-configuration patches take precedence for the same key. Segment
27
+ candidates are accumulated in hook order.
28
+
29
+ ## 3. Input and result
30
+
31
+ Hooks receive immutable `Phronomy::Agent::LLMInputBuildContext` metadata. They do
32
+ not receive a mutable Provider chat/message array.
33
+
34
+ A hook returns `Phronomy::Agent::LLMInputPatch` or `nil`.
35
+
36
+ ```ruby
37
+ Phronomy::Agent::LLMInputPatch.new(
38
+ model_config_patch: {temperature: 0.2},
39
+ segment_candidates: [
40
+ {
41
+ category: :knowledge,
42
+ role: :user,
43
+ content: "request-scoped retrieved context"
44
+ }
45
+ ]
46
+ )
47
+ ```
48
+
49
+ ## 4. Context Policy boundary
50
+
51
+ `segment_candidates` are candidate material, not preselected Manifest segments.
52
+
53
+ They enter the same typed `ContextPolicyInput` path and may be selected, omitted,
54
+ or otherwise handled by Application Policy subject to Framework invariants.
55
+
56
+ Hook candidates are current-call material and are not automatically written to
57
+ Journal or persistent Knowledge.
58
+
59
+ ## 5. Constraints
60
+
61
+ A hook must not:
62
+
63
+ - mutate Agent Journal state;
64
+ - mutate RubyLLM Chat/messages;
65
+ - assume an optional candidate will be selected;
66
+ - forge Framework-reserved Context metadata;
67
+ - bypass ContextPlan validation/canonicalization; or
68
+ - bypass final token-budget validation.
69
+
70
+ ## 6. Trust/security
71
+
72
+ Phronomy does not reinterpret `input_filter` as a universal Filter over hook
73
+ candidates.
74
+
75
+ Application ContextPolicy may inspect hook candidate provenance/metadata and
76
+ apply domain-specific trust/security policy.
77
+
78
+ See [Security Boundaries](security-boundaries.md).
@@ -0,0 +1,232 @@
1
+ > **CURRENT explanatory architecture**
2
+ >
3
+ > This document describes the reconciled current Phronomy system. Normative
4
+ > architecture decisions remain in the [ADR index](../decisions/README.md);
5
+ > source/runtime behavior remains implementation reality and does not silently
6
+ > amend an ADR.
7
+
8
+ # Context Management
9
+
10
+ ## 1. Authority model
11
+
12
+ Phronomy separates canonical execution facts from the logical input of one LLM
13
+ Call.
14
+
15
+ ```text
16
+ Observed execution facts / registered Knowledge / current-call material
17
+ |
18
+ v
19
+ JournalProjection + internal normalization
20
+ |
21
+ v
22
+ ContextPolicyInput
23
+ instruction / knowledge / tools / conversation
24
+ |
25
+ v
26
+ ContextPolicy#call
27
+ |
28
+ v
29
+ ContextPlan
30
+ instruction / knowledge / tools / conversation
31
+ |
32
+ v
33
+ ContextAssembler validation / canonicalization / final budget check
34
+ |
35
+ v
36
+ LLMInputManifest
37
+ |
38
+ v
39
+ RubyLLMMaterializer
40
+ ```
41
+
42
+ - **Journal** is canonical append-only logical execution history.
43
+ - **ContextPolicyInput** is an immutable typed snapshot of material eligible for
44
+ policy consideration.
45
+ - **ContextPlan** expresses the Policy's semantic selection/order/generation.
46
+ - **LLMInputManifest** is canonical logical input for one Provider Call.
47
+ - **RubyLLMMaterializer** realizes that Manifest against runtime Provider/Tool
48
+ wiring; it is not another Context authority.
49
+
50
+ The normative Journal/Manifest split is
51
+ [ADR-012](../decisions/012-canonical-execution-log-and-context-policy.md).
52
+
53
+ ## 2. Public Context Policy SPI
54
+
55
+ Application code supplies an ordinary reusable Ruby strategy object:
56
+
57
+ ```ruby
58
+ class SearchContextPolicy < Phronomy::Agent::ContextPolicy
59
+ def initialize(search:)
60
+ @search = search
61
+ end
62
+
63
+ def call(input)
64
+ plan(
65
+ instruction: input.instruction,
66
+ knowledge: @search.call(input.knowledge),
67
+ tools: input.tools,
68
+ conversation: input.conversation
69
+ )
70
+ end
71
+ end
72
+
73
+ SEARCH_POLICY = SearchContextPolicy.new(search: SEARCH_SERVICE)
74
+
75
+ class ResearchAgent < Phronomy::Agent::Base
76
+ context_policy SEARCH_POLICY
77
+ end
78
+ ```
79
+
80
+ `context_policy` binds a **Policy instance** on the Agent class. Policy
81
+ construction/dependency injection are Application responsibilities. If the same
82
+ instance is shared by multiple Agents/classes, its concurrency safety is also an
83
+ Application responsibility.
84
+
85
+ There are no Policy overrides on Agent create/load/invoke/stream and no durable
86
+ Policy descriptor/registry. A currently loaded Application supplies the current
87
+ Policy code.
88
+
89
+ ## 3. ContextPolicyInput
90
+
91
+ `ContextPolicyInput` has four top-level semantic collections:
92
+
93
+ ```text
94
+ instruction
95
+ knowledge
96
+ tools
97
+ conversation
98
+ ```
99
+
100
+ Instruction, Knowledge, Tool, and Conversation items are typed immutable values.
101
+
102
+ Conversation is an ordered outer Array of immutable non-empty inner Arrays. Each
103
+ inner Array is an indivisible selection group. Ordinary messages are singleton
104
+ groups. An assistant Tool Call and the matching Tool-role message(s) are one
105
+ atomic group and cannot be split by Policy selection.
106
+
107
+ Policy input also carries execution/call/model/budget information needed for
108
+ selection without exposing mutable Runtime state.
109
+
110
+ ## 4. ContextPlan and Framework validation
111
+
112
+ The Plan uses the same four categories. Presence means selected; omission means
113
+ omitted; order within a category is semantically meaningful.
114
+
115
+ The Framework validates that:
116
+
117
+ - selected input items came from the immutable request;
118
+ - Policy-generated items use permitted generated-item rules;
119
+ - required material is retained;
120
+ - input conversation groups are not split, merged, or internally reordered;
121
+ - generated conversation Tool protocol is structurally valid;
122
+ - selected Tools resolve to the effective runtime Tool wiring;
123
+ - Framework-owned metadata cannot be forged by Application content; and
124
+ - the realized canonical input fits the final token budget.
125
+
126
+ Provider/Manifest structural placement remains `ContextAssembler`
127
+ responsibility.
128
+
129
+ ## 5. Policy-generated / compacted material
130
+
131
+ An Application Policy may derive current-call material using the protected
132
+ instruction/knowledge/conversation item helpers. This is also the supported
133
+ shape for Application-defined semantic compaction:
134
+
135
+ ```text
136
+ source optional Context
137
+ |
138
+ Policy omits source item(s) for this call
139
+ +
140
+ Policy creates derived current-call item
141
+ |
142
+ ContextPlan
143
+ ```
144
+
145
+ The original Journal facts are not deleted or rewritten. Generated content is
146
+ current-call material unless the Application explicitly persists equivalent
147
+ Knowledge through a separate supported mutation.
148
+
149
+ Phronomy does not provide a parallel mutable "Memory Compression" subsystem, a
150
+ fifth `derived` Plan collection, automatic summarization, or an extra built-in
151
+ LLM call for the Default Policy.
152
+
153
+ Required material cannot be silently compacted away. If required input cannot
154
+ be accepted/represented within the applicable invariants and budget, Context
155
+ preparation fails.
156
+
157
+ ## 6. Default Policy
158
+
159
+ The built-in Default Policy is deterministic and model-free:
160
+
161
+ - retain instruction material in stable order;
162
+ - retain effective Tool definitions/order;
163
+ - retain required/current conversation;
164
+ - select an optional recent contiguous conversation window at group granularity;
165
+ - select Knowledge in stable order, skipping an oversized optional item and
166
+ continuing;
167
+ - use an approximate 60% conversation / 40% Knowledge split for variable
168
+ remainder and allow unused share to spill to the other category;
169
+ - perform no vector retrieval, embedding/reranking, or automatic compaction.
170
+
171
+ If required/fixed material does not fit, Context preparation fails rather than
172
+ silently dropping it.
173
+
174
+ ## 7. Execution and Persistence boundary
175
+
176
+ `ContextPolicy#call` is synchronous and runs on the Agent preparation Offload
177
+ worker, not on EventLoop. No Phronomy Persistence transaction spans the Policy
178
+ call.
179
+
180
+ Preparation captures an immutable authoritative base, executes Application
181
+ Policy outside the durable transaction, then revalidates Agent/Execution
182
+ revision/watermark before the short commit/finalization path. A stale Policy
183
+ result fails closed rather than overwriting newer durable state.
184
+
185
+ Policy failure is not automatically retried and Phronomy does not silently fall
186
+ back to another Policy.
187
+
188
+ ContextPolicy is **not** one of the Framework's standard automatic tracing span
189
+ types. Policy-internal retrieval/transformation tracing is Application-owned.
190
+ See [Tracing](tracing.md).
191
+
192
+ ## 8. Recovery
193
+
194
+ A finalized `LLMInputManifest` is the Recovery authority for the Provider input
195
+ it represents. Recovery hydrates that Manifest rather than rerunning the
196
+ historical Policy.
197
+
198
+ Future calls use the Policy supplied by the currently loaded Application code.
199
+ Agent/Workflow durability therefore does not depend on Policy object durability.
200
+
201
+ ## 9. Security/trust policy
202
+
203
+ The Framework validates structure/integrity; it does not impose one universal
204
+ semantic trust policy on arbitrary Context.
205
+
206
+ Application-defined `ContextPolicy` is the semantic authority for source-aware
207
+ selection, rejection/failure, redaction/sanitization, retrieval, reranking, or
208
+ derived content needed for a particular LLM Call. Phronomy does not add a fourth
209
+ universal `context_filter` call site.
210
+
211
+ See [Security Boundaries](security-boundaries.md).
212
+
213
+ ## 10. Removed intermediate contracts
214
+
215
+ The following are not current public Context Policy contracts:
216
+
217
+ - `ContextRequest`;
218
+ - `ContextPolicyDescriptor` / `ContextPolicyRegistry`;
219
+ - `DerivedContentSpec`;
220
+ - `ContextPlan#selected_unit_ids`;
221
+ - `ContextPlan#derived_contents`;
222
+ - `ContextPlan#ordering_hints`;
223
+ - `ContextPlan#policy_descriptor`;
224
+ - public `request.parts`;
225
+ - `Selection::Unit` / `Selection::Validator`;
226
+ - `DependencyAwareUnitBuilder`;
227
+ - `RequiredContextResolver`;
228
+ - `RecentFirstSelector`;
229
+ - `TokenBudgetPacker`.
230
+
231
+ Internal `Selection::Candidate` / `Selection::Constraint` normalization used
232
+ while building the typed request does not create an Application DSL.