lean-ctx 3.5.18

Context Runtime for AI Agents with CCP. 63 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing + diaries, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24 AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use serde::Serialize;
use std::path::{Path, PathBuf};

const MAX_LADDER_STEPS: usize = 12;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DegradationVerdictV1 {
    Ok,
    Warn,
    Throttle,
    Block,
}

#[derive(Debug, Clone, Serialize)]
pub struct PressureSnapshotV1 {
    pub utilization_pct: u8,
    pub remaining_tokens: usize,
    pub action: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct DegradationDecisionV1 {
    pub verdict: DegradationVerdictV1,
    pub enforced: bool,
    pub throttle_ms: Option<u64>,
    pub reason_code: String,
    pub reason: String,
    pub ladder: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct DegradationPolicyV1 {
    pub schema_version: u32,
    pub created_at: String,
    pub role: String,
    pub profile: String,
    pub tool: String,
    pub budgets: crate::core::budget_tracker::BudgetSnapshot,
    pub slo: crate::core::slo::SloSnapshot,
    pub pressure: PressureSnapshotV1,
    pub decision: DegradationDecisionV1,
}

pub fn evaluate_v1_for_tool(tool: &str, created_at_override: Option<&str>) -> DegradationPolicyV1 {
    let created_at = created_at_override.map_or_else(
        || chrono::Utc::now().to_rfc3339(),
        std::string::ToString::to_string,
    );

    let role = crate::core::roles::active_role_name();
    let profile_name = crate::core::profiles::active_profile_name();
    let profile = crate::core::profiles::active_profile();
    let enforce = profile.degradation.enforce_effective();
    let throttle_ms = profile.degradation.throttle_ms_effective();

    let budgets = crate::core::budget_tracker::BudgetTracker::global().check();
    let slo = crate::core::slo::evaluate_quiet();
    let pressure = crate::core::context_ledger::ContextLedger::load().pressure();

    let pressure_snapshot = PressureSnapshotV1 {
        utilization_pct: (pressure.utilization * 100.0).min(254.0) as u8,
        remaining_tokens: pressure.remaining_tokens,
        action: format!("{:?}", pressure.recommendation),
    };

    let decision = decide(&budgets, &slo, &pressure, enforce, throttle_ms);

    DegradationPolicyV1 {
        schema_version: crate::core::contracts::DEGRADATION_POLICY_V1_SCHEMA_VERSION,
        created_at,
        role,
        profile: profile_name,
        tool: tool.to_string(),
        budgets,
        slo,
        pressure: pressure_snapshot,
        decision,
    }
}

pub fn write_project_degradation_policy(
    project_root: &Path,
    policy: &DegradationPolicyV1,
    filename: Option<&str>,
) -> Result<PathBuf, String> {
    let proofs_dir = project_root.join(".lean-ctx").join("proofs");
    std::fs::create_dir_all(&proofs_dir).map_err(|e| e.to_string())?;

    let ts = chrono::Utc::now().format("%Y-%m-%d_%H%M%S");
    let name = filename.map_or_else(
        || format!("degradation-policy-v1_{ts}.json"),
        std::string::ToString::to_string,
    );
    let path = proofs_dir.join(name);

    let json = serde_json::to_string_pretty(policy).map_err(|e| e.to_string())?;
    let json = crate::core::redaction::redact_text(&json);
    crate::config_io::write_atomic(&path, &json)?;
    Ok(path)
}

fn decide(
    budgets: &crate::core::budget_tracker::BudgetSnapshot,
    slo: &crate::core::slo::SloSnapshot,
    pressure: &crate::core::context_ledger::ContextPressure,
    enforce: bool,
    throttle_ms: u64,
) -> DegradationDecisionV1 {
    use crate::core::budget_tracker::BudgetLevel;
    use crate::core::slo::SloAction;

    let mut ladder: Vec<String> = Vec::new();

    if *budgets.worst_level() == BudgetLevel::Exhausted {
        ladder.push("block".to_string());
        return DegradationDecisionV1 {
            verdict: DegradationVerdictV1::Block,
            enforced: true,
            throttle_ms: None,
            reason_code: "budget_exhausted".to_string(),
            reason: format!("budget_exhausted: {}", budgets.format_compact()),
            ladder,
        };
    }

    if slo.should_block() {
        ladder.push("reduce_scope".to_string());
        ladder.push("switch_mode=signatures".to_string());
        ladder.push("output_density=terse".to_string());
        ladder.push("block".to_string());
        return DegradationDecisionV1 {
            verdict: if enforce {
                DegradationVerdictV1::Block
            } else {
                DegradationVerdictV1::Warn
            },
            enforced: enforce,
            throttle_ms: None,
            reason_code: "slo_block".to_string(),
            reason: format!(
                "slo_block: worst_action={:?} violations={}",
                slo.worst_action,
                slo.violations
                    .iter()
                    .map(|v| v.name.as_str())
                    .collect::<Vec<_>>()
                    .join(",")
            ),
            ladder,
        };
    }

    if slo.should_throttle() {
        ladder.push("reduce_scope".to_string());
        ladder.push("switch_mode=map|signatures".to_string());
        ladder.push(format!("throttle_ms={throttle_ms}"));
        return DegradationDecisionV1 {
            verdict: if enforce {
                DegradationVerdictV1::Throttle
            } else {
                DegradationVerdictV1::Warn
            },
            enforced: enforce,
            throttle_ms: if enforce { Some(throttle_ms) } else { None },
            reason_code: "slo_throttle".to_string(),
            reason: format!(
                "slo_throttle: worst_action={:?} violations={}",
                slo.worst_action,
                slo.violations
                    .iter()
                    .map(|v| v.name.as_str())
                    .collect::<Vec<_>>()
                    .join(",")
            ),
            ladder,
        };
    }

    let budget_warn = *budgets.worst_level() == BudgetLevel::Warning;
    let pressure_warn = matches!(
        pressure.recommendation,
        crate::core::context_ledger::PressureAction::SuggestCompression
            | crate::core::context_ledger::PressureAction::ForceCompression
            | crate::core::context_ledger::PressureAction::EvictLeastRelevant
    );

    if budget_warn || pressure_warn || matches!(slo.worst_action, Some(SloAction::Warn)) {
        ladder.push("reduce_scope".to_string());
        ladder.push("switch_mode=signatures".to_string());
        ladder.push("output_density=compact|terse".to_string());
        ladder.truncate(MAX_LADDER_STEPS);
        return DegradationDecisionV1 {
            verdict: DegradationVerdictV1::Warn,
            enforced: false,
            throttle_ms: None,
            reason_code: "warn_only".to_string(),
            reason: "warn_only".to_string(),
            ladder,
        };
    }

    DegradationDecisionV1 {
        verdict: DegradationVerdictV1::Ok,
        enforced: false,
        throttle_ms: None,
        reason_code: "ok".to_string(),
        reason: "ok".to_string(),
        ladder: Vec::new(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::budget_tracker::{BudgetLevel, CostStatus, DimensionStatus};

    fn budget_snapshot(level: BudgetLevel) -> crate::core::budget_tracker::BudgetSnapshot {
        crate::core::budget_tracker::BudgetSnapshot {
            role: "coder".to_string(),
            tokens: DimensionStatus {
                used: 10,
                limit: 10,
                percent: 100,
                level: level.clone(),
            },
            shell: DimensionStatus {
                used: 0,
                limit: 0,
                percent: 0,
                level: BudgetLevel::Ok,
            },
            cost: CostStatus {
                used_usd: 0.0,
                limit_usd: 0.0,
                percent: 0,
                level,
            },
        }
    }

    #[test]
    fn budget_exhausted_blocks_even_when_enforce_false() {
        let b = budget_snapshot(BudgetLevel::Exhausted);
        let slo = crate::core::slo::SloSnapshot {
            slos: vec![],
            violations: vec![],
            worst_action: None,
        };
        let pressure = crate::core::context_ledger::ContextPressure {
            utilization: 0.1,
            remaining_tokens: 1000,
            entries_count: 0,
            recommendation: crate::core::context_ledger::PressureAction::NoAction,
        };
        let d = decide(&b, &slo, &pressure, false, 250);
        assert_eq!(d.verdict, DegradationVerdictV1::Block);
    }
}