Skip to main content

lean_ctx/core/context_kernel/
bridge.rs

1//! Runtime integration helpers for the Context Control Kernel.
2
3use 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/// Result of kernel gating: what to add and what to suppress.
9#[derive(Debug, Clone)]
10pub struct KernelVerdict {
11    /// Cross-store context to supplement (hard-capped, never exceeds budget).
12    pub supplement: Option<String>,
13    /// Content identifiers that are already in context and should not be resent.
14    pub suppress: Vec<String>,
15    /// Tokens consumed by the kernel supplement.
16    pub budget_used: usize,
17}
18/// Result of kernel enrichment for compose integration.
19#[derive(Debug, Clone)]
20pub struct KernelEnrichment {
21    /// The selection plan that produced the injected blocks.
22    pub plan: ContextPlanV1,
23    /// Human-readable blocks suitable for compose output injection.
24    pub blocks: String,
25    /// Gate decision accompanying the backward-compatible blocks.
26    pub verdict: KernelVerdict,
27    /// Kernel policy mode used while producing this enrichment.
28    pub enforced_mode: KernelMode,
29}
30/// Check whether `path` should be suppressed as already delivered.
31/// Returns `false` until the orchestrator exposes recent-delivery state.
32pub fn kernel_gate(_path: &str, _project_root: &str) -> bool {
33    false
34}
35/// Enrich a compose response with kernel-selected context.
36///
37/// Returns cross-store context that the compose pipeline misses, or `None`.
38pub 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
178/// Emit a plan-created event on the OclaBus.
179pub 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
195/// Emit a receipt-recorded event on the OclaBus.
196pub 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
209/// Update the bandit-learned FieldWeights based on a receipt outcome.
210///
211/// Accepted outcomes reinforce the balanced arm, rejected outcomes penalize
212/// the aggressive arm, and partial outcomes inform the conservative arm.
213pub 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
231/// Format a plan as a compact human-readable summary.
232pub 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}