lean-ctx 3.9.18

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, 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::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;

const DEFAULT_CHILD_FRACTION: f64 = 0.5;
const DEFAULT_MINIMUM_BUDGET: u64 = 1_000;
const DEFAULT_MAXIMUM_BUDGET: u64 = 500_000;
const MAX_CASCADE_DEPTH: u32 = 5;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BudgetAllocation {
    pub parent_budget_tokens: u64,
    pub parent_used_tokens: u64,
    pub child_fraction: f64,
    pub minimum_budget: u64,
    pub maximum_budget: u64,
}

impl Default for BudgetAllocation {
    fn default() -> Self {
        Self {
            parent_budget_tokens: 0,
            parent_used_tokens: 0,
            child_fraction: DEFAULT_CHILD_FRACTION,
            minimum_budget: DEFAULT_MINIMUM_BUDGET,
            maximum_budget: DEFAULT_MAXIMUM_BUDGET,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CascadedBudget {
    pub allocated_tokens: u64,
    pub parent_remaining: u64,
    pub depth: u32,
    pub lineage: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CascadeError {
    ZeroAllocation,
    DepthLimitExceeded { depth: u32, maximum_depth: u32 },
    LineageCycle { agent_id: String },
}

impl fmt::Display for CascadeError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ZeroAllocation => {
                formatter.write_str("cascaded budget must be greater than zero")
            }
            Self::DepthLimitExceeded {
                depth,
                maximum_depth,
            } => write!(
                formatter,
                "cascade depth {depth} exceeds maximum depth {maximum_depth}"
            ),
            Self::LineageCycle { agent_id } => {
                write!(
                    formatter,
                    "cascade lineage contains cycle at agent {agent_id}"
                )
            }
        }
    }
}

impl std::error::Error for CascadeError {}

pub fn cascade_budget(allocation: &BudgetAllocation) -> CascadedBudget {
    let parent_remaining = allocation
        .parent_budget_tokens
        .saturating_sub(allocation.parent_used_tokens);
    let fractional_budget = (parent_remaining as f64 * allocation.child_fraction) as u64;
    let allocated_tokens = fractional_budget
        .max(allocation.minimum_budget)
        .min(allocation.maximum_budget);

    CascadedBudget {
        allocated_tokens,
        parent_remaining,
        depth: 0,
        lineage: Vec::new(),
    }
}

pub fn validate_cascade(budget: &CascadedBudget) -> Result<(), CascadeError> {
    if budget.allocated_tokens == 0 {
        return Err(CascadeError::ZeroAllocation);
    }
    if budget.depth > MAX_CASCADE_DEPTH {
        return Err(CascadeError::DepthLimitExceeded {
            depth: budget.depth,
            maximum_depth: MAX_CASCADE_DEPTH,
        });
    }

    let mut seen = HashSet::with_capacity(budget.lineage.len());
    for agent_id in &budget.lineage {
        if !seen.insert(agent_id) {
            return Err(CascadeError::LineageCycle {
                agent_id: agent_id.clone(),
            });
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn normal_cascade_allocates_default_fraction() {
        let allocation = BudgetAllocation {
            parent_budget_tokens: 100_000,
            parent_used_tokens: 20_000,
            ..BudgetAllocation::default()
        };

        let budget = cascade_budget(&allocation);

        assert_eq!(budget.allocated_tokens, 40_000);
        assert_eq!(budget.parent_remaining, 80_000);
        assert_eq!(budget.depth, 0);
        assert!(budget.lineage.is_empty());
        assert_eq!(validate_cascade(&budget), Ok(()));
    }

    #[test]
    fn cascade_saturates_remaining_and_applies_bounds() {
        let exhausted = BudgetAllocation {
            parent_budget_tokens: 10,
            parent_used_tokens: 20,
            minimum_budget: 1_000,
            maximum_budget: 5_000,
            ..BudgetAllocation::default()
        };
        let capped = BudgetAllocation {
            parent_budget_tokens: 2_000_000,
            parent_used_tokens: 0,
            child_fraction: 1.0,
            minimum_budget: 1_000,
            maximum_budget: 500_000,
        };

        assert_eq!(cascade_budget(&exhausted).parent_remaining, 0);
        assert_eq!(cascade_budget(&exhausted).allocated_tokens, 1_000);
        assert_eq!(cascade_budget(&capped).allocated_tokens, 500_000);
    }

    #[test]
    fn validation_rejects_depth_above_limit() {
        let budget = CascadedBudget {
            allocated_tokens: 1_000,
            parent_remaining: 2_000,
            depth: 6,
            lineage: vec!["parent".to_string()],
        };

        assert_eq!(
            validate_cascade(&budget),
            Err(CascadeError::DepthLimitExceeded {
                depth: 6,
                maximum_depth: 5,
            })
        );
    }

    #[test]
    fn validation_rejects_zero_budget() {
        let budget = CascadedBudget {
            allocated_tokens: 0,
            parent_remaining: 0,
            depth: 0,
            lineage: Vec::new(),
        };

        assert_eq!(validate_cascade(&budget), Err(CascadeError::ZeroAllocation));
    }

    #[test]
    fn validation_rejects_lineage_cycle() {
        let budget = CascadedBudget {
            allocated_tokens: 1_000,
            parent_remaining: 2_000,
            depth: 2,
            lineage: vec!["root".to_string(), "child".to_string(), "root".to_string()],
        };

        assert_eq!(
            validate_cascade(&budget),
            Err(CascadeError::LineageCycle {
                agent_id: "root".to_string(),
            })
        );
    }
}