lean-ctx 3.6.2

Context Runtime for AI Agents with CCP. 51 MCP tools, 10 read modes, 60+ 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 chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::core::knowledge::{ConsolidatedInsight, KnowledgeFact, ProjectPattern};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PackageContent {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub knowledge: Option<KnowledgeLayer>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub graph: Option<GraphLayer>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub session: Option<SessionLayer>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub patterns: Option<PatternsLayer>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gotchas: Option<GotchasLayer>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeLayer {
    pub facts: Vec<KnowledgeFact>,
    pub patterns: Vec<ProjectPattern>,
    pub insights: Vec<ConsolidatedInsight>,
    pub exported_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphLayer {
    pub nodes: Vec<GraphNodeExport>,
    pub edges: Vec<GraphEdgeExport>,
    pub exported_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNodeExport {
    pub kind: String,
    pub name: String,
    pub file_path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub line_start: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub line_end: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdgeExport {
    pub source_path: String,
    pub source_name: String,
    pub target_path: String,
    pub target_name: String,
    pub kind: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionLayer {
    pub task_description: Option<String>,
    pub findings: Vec<SessionFinding>,
    pub decisions: Vec<SessionDecision>,
    pub next_steps: Vec<String>,
    pub files_touched: Vec<String>,
    pub exported_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionFinding {
    pub summary: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub line: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionDecision {
    pub summary: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rationale: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternsLayer {
    pub patterns: Vec<ProjectPattern>,
    pub exported_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GotchasLayer {
    pub gotchas: Vec<GotchaExport>,
    pub exported_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GotchaExport {
    pub id: String,
    pub category: String,
    pub severity: String,
    pub trigger: String,
    pub resolution: String,
    #[serde(default)]
    pub file_patterns: Vec<String>,
    pub confidence: f32,
}

impl PackageContent {
    pub fn active_layer_count(&self) -> usize {
        let mut n = 0;
        if self.knowledge.is_some() {
            n += 1;
        }
        if self.graph.is_some() {
            n += 1;
        }
        if self.session.is_some() {
            n += 1;
        }
        if self.patterns.is_some() {
            n += 1;
        }
        if self.gotchas.is_some() {
            n += 1;
        }
        n
    }

    pub fn is_empty(&self) -> bool {
        self.active_layer_count() == 0
    }

    pub fn estimated_token_count(&self) -> usize {
        let json = serde_json::to_string(self).unwrap_or_default();
        json.len() / 4
    }
}