1use super::enforce::{KernelMode, enforce_plan, resolve_mode};
4use super::orchestrator::ContextKernel;
5use super::policy::ContextPolicy;
6use super::types::{ContextPlanV1, ContextReceiptV1, PlanEntry, ReceiptOutcome, RetrievalContext};
7
8#[derive(Debug, Clone)]
10pub struct KernelVerdict {
11 pub supplement: Option<String>,
13 pub suppress: Vec<String>,
15 pub budget_used: usize,
17}
18#[derive(Debug, Clone)]
20pub struct KernelEnrichment {
21 pub plan: ContextPlanV1,
23 pub blocks: String,
25 pub verdict: KernelVerdict,
27 pub enforced_mode: KernelMode,
29}
30pub fn kernel_gate(_path: &str, _project_root: &str) -> bool {
33 false
34}
35pub fn kernel_enrich(
39 task: &str,
40 project_root: &str,
41 budget_tokens: usize,
42) -> Option<KernelEnrichment> {
43 let capped_budget = budget_tokens.min(150);
44 let kernel = ContextKernel::for_project(project_root);
45 let ctx = RetrievalContext {
46 query: task.to_owned(),
47 task: Some(task.to_owned()),
48 project_root: project_root.to_owned(),
49 budget: crate::core::context_field::TokenBudget {
50 total: capped_budget,
51 used: 0,
52 },
53 max_candidates: 20,
54 };
55 let mode = resolve_mode(project_root);
56 let plan = enforce_plan_for_mode(kernel.plan(&ctx), &ContextPolicy::default(), mode);
57 let enrichments: Vec<&PlanEntry> = plan
58 .selected
59 .iter()
60 .filter(|entry| entry.provider != "context.ledger")
61 .collect();
62
63 let blocks = format_enrichment_blocks(&enrichments);
64 enrichment_from_plan(plan, blocks, capped_budget, mode)
65}
66
67fn enforce_plan_for_mode(
68 plan: ContextPlanV1,
69 policy: &ContextPolicy,
70 mode: KernelMode,
71) -> ContextPlanV1 {
72 if mode != KernelMode::Enforce {
73 return plan;
74 }
75
76 let result = enforce_plan(&plan, policy, mode);
77 if !result.blocked.is_empty() {
78 tracing::debug!(
79 blocked = result.blocked.len(),
80 "kernel enforce: plan entries blocked by policy"
81 );
82 }
83
84 ContextPlanV1 {
85 selected: result.allowed,
86 ..plan
87 }
88}
89
90fn enrichment_from_plan(
91 plan: ContextPlanV1,
92 blocks: String,
93 budget: usize,
94 enforced_mode: KernelMode,
95) -> Option<KernelEnrichment> {
96 let verdict = verdict_from_blocks(blocks, budget);
97 let blocks = verdict.supplement.clone()?;
98 Some(KernelEnrichment {
99 plan,
100 blocks,
101 verdict,
102 enforced_mode,
103 })
104}
105
106fn verdict_from_blocks(blocks: String, budget: usize) -> KernelVerdict {
107 let blocks = truncate_to_token_budget(blocks, budget);
108 let supplement = (!blocks.is_empty()).then(|| blocks.clone());
109 let budget_used = supplement
110 .as_deref()
111 .map_or(0, crate::core::tokens::count_tokens);
112 KernelVerdict {
113 supplement,
114 suppress: Vec::new(),
115 budget_used,
116 }
117}
118
119fn truncate_to_token_budget(mut text: String, budget: usize) -> String {
120 if crate::core::tokens::count_tokens(&text) <= budget {
121 return text;
122 }
123 let mut low = 0;
124 let mut high = text.len();
125 while low < high {
126 let middle = low + (high - low).div_ceil(2);
127 let boundary = text.floor_char_boundary(middle);
128 if crate::core::tokens::count_tokens(&text[..boundary]) <= budget {
129 low = middle;
130 } else {
131 high = middle - 1;
132 }
133 }
134 text.truncate(text.floor_char_boundary(low));
135 text
136}
137
138fn format_enrichment_blocks(entries: &[&PlanEntry]) -> String {
139 let mut out = String::new();
140 append_provider_block(&mut out, entries, "knowledge.facts", "Relevant Knowledge");
141 append_provider_block(&mut out, entries, "memory.episodic", "Relevant Episodes");
142 append_provider_block(
143 &mut out,
144 entries,
145 "memory.procedural",
146 "Relevant Procedures",
147 );
148 append_provider_block(&mut out, entries, "session.state", "Relevant Session State");
149 out
150}
151
152fn append_provider_block(
153 output: &mut String,
154 entries: &[&PlanEntry],
155 provider: &str,
156 heading: &str,
157) {
158 let mut found = false;
159 for entry in entries
160 .iter()
161 .copied()
162 .filter(|entry| entry.provider == provider)
163 {
164 if !found {
165 output.push_str("\n## ");
166 output.push_str(heading);
167 output.push('\n');
168 found = true;
169 }
170 output.push_str("- ");
171 output.push_str(&entry.reason);
172 output.push_str(" (phi=");
173 output.push_str(&format!("{:.2}", entry.phi));
174 output.push_str(")\n");
175 }
176}
177
178pub fn emit_plan_event(plan: &ContextPlanV1) {
180 use crate::core::ocla_bus::{self, OclaEvent};
181
182 ocla_bus::emit(OclaEvent::AgentChainEvent {
183 agent_id: format!("kernel:{}", plan.plan_id),
184 action: format!(
185 "plan_created:selected={},excluded={},budget={}/{}",
186 plan.selected.len(),
187 plan.excluded.len(),
188 plan.budget.used_tokens,
189 plan.budget.total_tokens,
190 ),
191 parent_agent: None,
192 });
193}
194
195pub fn emit_receipt_event(receipt: &ContextReceiptV1) {
197 use crate::core::ocla_bus::{self, OclaEvent};
198
199 ocla_bus::emit(OclaEvent::AgentChainEvent {
200 agent_id: format!("kernel:{}", receipt.receipt_id),
201 action: format!(
202 "receipt_recorded:tokens={},outcome={:?}",
203 receipt.delivered_tokens, receipt.outcome,
204 ),
205 parent_agent: Some(receipt.plan_id.clone()),
206 });
207}
208
209pub fn apply_feedback(receipt: &ContextReceiptV1) {
214 use crate::core::context_field::{FieldWeights, set_active_weights};
215
216 let arm_name = match receipt.outcome {
217 ReceiptOutcome::Accepted => "balanced",
218 ReceiptOutcome::Rejected => "aggressive",
219 ReceiptOutcome::Partial => "conservative",
220 ReceiptOutcome::Unknown => return,
221 };
222 let mut bandit = crate::core::bandit::ThresholdBandit::default();
223 bandit.update(arm_name, receipt.outcome == ReceiptOutcome::Accepted);
224
225 let best_idx = bandit.best_arm_idx_by_mean();
226 if let Some(best_arm) = bandit.arms.get(best_idx) {
227 set_active_weights(FieldWeights::from_arm(best_arm));
228 }
229}
230
231pub fn format_plan_summary(plan: &ContextPlanV1) -> String {
233 let mut out = String::new();
234 let plan_prefix = &plan.plan_id[..plan.plan_id.len().min(8)];
235 out.push_str(&format!(
236 "[kernel] plan={plan_prefix} intent=\"{}\" budget={}/{}\n",
237 plan.intent, plan.budget.used_tokens, plan.budget.total_tokens,
238 ));
239 out.push_str(&format!(
240 " selected={} excluded={} deferred={}\n",
241 plan.selected.len(),
242 plan.excluded.len(),
243 plan.deferred.len(),
244 ));
245
246 let mut providers: Vec<_> = plan.provider_stats.iter().collect();
247 providers.sort_unstable_by_key(|(k, _)| *k);
248 for (provider, stat) in providers {
249 out.push_str(&format!(
250 " {provider}: {}/{} candidates, {} tokens\n",
251 stat.candidates_selected, stat.candidates_offered, stat.tokens_used,
252 ));
253 }
254 out
255}
256
257#[cfg(test)]
258mod tests {
259 use std::collections::HashMap;
260
261 use super::super::enforce::KernelMode;
262 use super::super::policy::ContextPolicy;
263 use super::super::types::PlanBudget;
264 use super::{
265 ContextPlanV1, PlanEntry, enforce_plan_for_mode, enrichment_from_plan,
266 format_enrichment_blocks, kernel_gate, verdict_from_blocks,
267 };
268
269 fn plan(selected: Vec<PlanEntry>) -> ContextPlanV1 {
270 ContextPlanV1 {
271 plan_id: "plan".to_owned(),
272 intent: "test".to_owned(),
273 budget: PlanBudget {
274 total_tokens: 150,
275 used_tokens: 0,
276 remaining_tokens: 150,
277 },
278 selected,
279 excluded: Vec::new(),
280 deferred: Vec::new(),
281 provider_stats: HashMap::new(),
282 }
283 }
284
285 fn entry(reason: &str) -> PlanEntry {
286 PlanEntry {
287 object_id: "fact".to_owned(),
288 provider: "knowledge.facts".to_owned(),
289 view: "summary".to_owned(),
290 tokens: 1,
291 phi: 0.8,
292 reason: reason.to_owned(),
293 }
294 }
295
296 #[test]
297 fn budget_capped_at_150() {
298 let item = entry(&"token ".repeat(1_000));
299 let blocks = format_enrichment_blocks(&[&item]);
300 let enrichment = enrichment_from_plan(plan(vec![item]), blocks, 150, KernelMode::Shadow)
301 .expect("long enrichment should be truncated, not removed");
302 assert!(enrichment.verdict.budget_used <= 150);
303 }
304 #[test]
305 fn empty_supplement_when_no_candidates() {
306 let verdict = verdict_from_blocks(String::new(), 150);
307 assert!(verdict.supplement.is_none());
308 assert_eq!(verdict.budget_used, 0);
309 }
310 #[test]
311 fn verdict_has_correct_budget_used() {
312 let item = entry("Known constraint");
313 let blocks = format_enrichment_blocks(&[&item]);
314 let enrichment = enrichment_from_plan(plan(vec![item]), blocks, 150, KernelMode::Shadow)
315 .expect("entry should produce enrichment");
316 assert_eq!(
317 enrichment.verdict.budget_used,
318 crate::core::tokens::count_tokens(enrichment.verdict.supplement.as_deref().unwrap())
319 );
320 }
321 #[test]
322 fn kernel_gate_returns_false_by_default() {
323 assert!(!kernel_gate("src/lib.rs", "/project"));
324 }
325 #[test]
326 fn backward_compat_enrichment_still_works() {
327 let item = entry("Known constraint");
328 let blocks = format_enrichment_blocks(&[&item]);
329 let enrichment = enrichment_from_plan(plan(vec![item]), blocks, 150, KernelMode::Shadow)
330 .expect("entry should produce enrichment");
331 assert_eq!(enrichment.blocks, enrichment.verdict.supplement.unwrap());
332 assert_eq!(enrichment.enforced_mode, KernelMode::Shadow);
333 }
334
335 #[test]
336 fn test_enforce_mode_blocks_policy_violations() {
337 let mut blocked = entry("blocked");
338 blocked.provider = "excluded.provider".to_owned();
339 let policy = ContextPolicy {
340 blocked_sources: vec![blocked.provider.clone()],
341 ..ContextPolicy::default()
342 };
343
344 let enforced = enforce_plan_for_mode(
345 plan(vec![entry("allowed"), blocked]),
346 &policy,
347 KernelMode::Enforce,
348 );
349
350 assert_eq!(enforced.selected.len(), 1);
351 assert_eq!(enforced.selected[0].reason, "allowed");
352 }
353
354 #[test]
355 fn test_shadow_mode_allows_all_entries() {
356 let mut blocked = entry("blocked");
357 blocked.provider = "excluded.provider".to_owned();
358 let policy = ContextPolicy {
359 blocked_sources: vec![blocked.provider.clone()],
360 ..ContextPolicy::default()
361 };
362
363 let shadow = enforce_plan_for_mode(
364 plan(vec![entry("allowed"), blocked]),
365 &policy,
366 KernelMode::Shadow,
367 );
368
369 assert_eq!(shadow.selected.len(), 2);
370 }
371}