lean_ctx/core/context_kernel/
activation.rs1use std::collections::HashMap;
4use std::fs;
5use std::io::ErrorKind;
6use std::path::Path;
7
8use super::enforce::KernelMode;
9use super::feedback::FeedbackCollector;
10use super::learning::OutcomeLearner;
11use super::types::{ContextReceiptV1, ReceiptOutcome};
12
13const MAX_SUPPLEMENT_TOKENS: usize = 150;
14
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct ActivationConfig {
18 pub mode: KernelModeConfig,
21 pub outcome_tracking: bool,
23 pub max_supplement_tokens: usize,
25 pub feedback_loop: bool,
27}
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31#[serde(rename_all = "lowercase")]
32pub enum KernelModeConfig {
33 #[default]
35 Shadow,
36 Enforce,
38 Explain,
40}
41
42impl From<KernelModeConfig> for KernelMode {
43 fn from(mode: KernelModeConfig) -> Self {
44 match mode {
45 KernelModeConfig::Shadow => Self::Shadow,
46 KernelModeConfig::Enforce => Self::Enforce,
47 KernelModeConfig::Explain => Self::Explain,
48 }
49 }
50}
51
52#[derive(Debug, Default, serde::Deserialize)]
53struct ConfigFile {
54 kernel: Option<KernelOverrides>,
55}
56
57#[derive(Debug, Default, serde::Deserialize)]
58struct KernelOverrides {
59 mode: Option<KernelModeConfig>,
60 outcome_tracking: Option<bool>,
61 #[serde(alias = "max_supplement")]
62 max_supplement_tokens: Option<usize>,
63 feedback_loop: Option<bool>,
64}
65
66pub fn load_config(project_root: &str) -> ActivationConfig {
71 let mut config = safe_defaults();
72
73 if let Some(path) = crate::core::config::Config::path()
74 && let Some(overrides) = read_kernel_overrides(&path)
75 {
76 apply_overrides(&mut config, &overrides);
77 }
78
79 let local_path = crate::core::config::Config::local_path(project_root);
80 if let Some(overrides) = read_kernel_overrides(&local_path) {
81 apply_overrides(&mut config, &overrides);
82 }
83
84 config
85}
86
87pub fn record_real_outcome(receipt: &ContextReceiptV1, accepted: bool) -> ContextReceiptV1 {
89 let mut recorded = receipt.clone();
90 recorded.outcome = if accepted {
91 ReceiptOutcome::Accepted
92 } else {
93 ReceiptOutcome::Rejected
94 };
95 recorded
96}
97
98pub fn connect_feedback(receipt: &ContextReceiptV1, project_root: &str) {
103 if !matches!(
104 receipt.outcome,
105 ReceiptOutcome::Accepted | ReceiptOutcome::Rejected
106 ) {
107 return;
108 }
109
110 let mut collector = FeedbackCollector::default_for_project(project_root);
111 collector.load_weights();
112
113 let mut weights: HashMap<String, f64> = receipt
114 .feedback_attribution
115 .keys()
116 .map(|provider| (provider.clone(), collector.provider_weight(provider)))
117 .collect();
118 let learner = OutcomeLearner::default_learner();
119 let updates = learner.learn_from_receipt(receipt, &weights);
120 OutcomeLearner::apply_updates(&mut weights, &updates);
121
122 collector.record_outcome(receipt);
123}
124
125pub fn should_supplement(config: &ActivationConfig) -> bool {
127 matches!(
128 config.mode,
129 KernelModeConfig::Shadow | KernelModeConfig::Enforce | KernelModeConfig::Explain
130 )
131}
132
133pub fn supplement_budget(config: &ActivationConfig) -> usize {
135 config.max_supplement_tokens.min(MAX_SUPPLEMENT_TOKENS)
136}
137
138pub fn should_suppress_in_mode(mode: KernelModeConfig) -> bool {
140 matches!(KernelMode::from(mode), KernelMode::Enforce)
141}
142
143fn safe_defaults() -> ActivationConfig {
144 ActivationConfig {
145 mode: KernelModeConfig::Shadow,
146 outcome_tracking: false,
147 max_supplement_tokens: MAX_SUPPLEMENT_TOKENS,
148 feedback_loop: false,
149 }
150}
151
152fn read_kernel_overrides(path: &Path) -> Option<KernelOverrides> {
153 match fs::read_to_string(path) {
154 Ok(raw) => match toml::from_str::<ConfigFile>(&raw) {
155 Ok(config) => config.kernel,
156 Err(error) => {
157 tracing::warn!(path = %path.display(), %error, "invalid kernel configuration");
158 None
159 }
160 },
161 Err(error) if error.kind() == ErrorKind::NotFound => None,
162 Err(error) => {
163 tracing::warn!(path = %path.display(), %error, "unable to read kernel configuration");
164 None
165 }
166 }
167}
168
169fn apply_overrides(config: &mut ActivationConfig, overrides: &KernelOverrides) {
170 if let Some(mode) = overrides.mode {
171 config.mode = mode;
172 }
173 if let Some(enabled) = overrides.outcome_tracking {
174 config.outcome_tracking = enabled;
175 }
176 if let Some(tokens) = overrides.max_supplement_tokens {
177 config.max_supplement_tokens = tokens;
178 }
179 if let Some(enabled) = overrides.feedback_loop {
180 config.feedback_loop = enabled;
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use std::collections::HashMap;
187
188 use super::{
189 ActivationConfig, KernelModeConfig, connect_feedback, load_config, record_real_outcome,
190 should_suppress_in_mode, supplement_budget,
191 };
192 use crate::core::context_kernel::types::{ContextReceiptV1, ReceiptOutcome};
193
194 fn receipt(outcome: ReceiptOutcome) -> ContextReceiptV1 {
195 ContextReceiptV1 {
196 receipt_id: "receipt-1".to_owned(),
197 plan_id: "plan-1".to_owned(),
198 delivered_tokens: 10,
199 cache_hits: 0,
200 cache_misses: 0,
201 outcome,
202 quality_signals: Vec::new(),
203 feedback_attribution: HashMap::new(),
204 }
205 }
206
207 fn config(mode: KernelModeConfig, max_supplement_tokens: usize) -> ActivationConfig {
208 ActivationConfig {
209 mode,
210 outcome_tracking: false,
211 max_supplement_tokens,
212 feedback_loop: false,
213 }
214 }
215
216 #[test]
217 fn default_config_is_shadow() {
218 let loaded = load_config("/path/that/does/not/exist");
219 assert_eq!(loaded.mode, KernelModeConfig::Shadow);
220 assert!(!loaded.outcome_tracking);
221 }
222
223 #[test]
224 fn supplement_budget_capped_at_150() {
225 assert_eq!(
226 supplement_budget(&config(KernelModeConfig::Enforce, 500)),
227 150
228 );
229 }
230
231 #[test]
232 fn real_outcome_sets_rejected() {
233 let original = receipt(ReceiptOutcome::Unknown);
234 let recorded = record_real_outcome(&original, false);
235
236 assert_eq!(recorded.outcome, ReceiptOutcome::Rejected);
237 assert_eq!(original.outcome, ReceiptOutcome::Unknown);
238 }
239
240 #[test]
241 fn shadow_mode_never_suppresses() {
242 assert!(!should_suppress_in_mode(KernelModeConfig::Shadow));
243 }
244
245 #[test]
246 fn enforce_mode_suppresses() {
247 assert!(should_suppress_in_mode(KernelModeConfig::Enforce));
248 }
249
250 #[test]
251 fn connect_feedback_graceful_on_error() {
252 connect_feedback(
253 &receipt(ReceiptOutcome::Rejected),
254 "/path/that/does/not/exist",
255 );
256 }
257}