lean-ctx 3.1.5

Context Runtime for AI Agents with CCP. 42 MCP tools, 10 read modes, 90+ 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 std::collections::HashMap;

pub fn compress(_cmd: &str, output: &str) -> Option<String> {
    let trimmed = output.trim();
    if trimmed.is_empty() {
        return Some("ok".to_string());
    }

    let has_recap = trimmed.contains("PLAY RECAP");
    if has_recap {
        return Some(compress_playbook(trimmed));
    }

    if trimmed.contains("TASK [") {
        return Some(compress_tasks(trimmed));
    }

    Some(compact_lines(trimmed, 15))
}

fn compress_playbook(output: &str) -> String {
    let mut recap_lines = Vec::new();
    let mut in_recap = false;

    for line in output.lines() {
        if line.contains("PLAY RECAP") {
            in_recap = true;
            continue;
        }
        if in_recap {
            let trimmed = line.trim();
            if !trimmed.is_empty() {
                recap_lines.push(trimmed.to_string());
            }
        }
    }

    if recap_lines.is_empty() {
        return compact_lines(output, 15);
    }

    let mut result = String::from("PLAY RECAP:");
    for line in &recap_lines {
        result.push_str(&format!("\n  {line}"));
    }
    result
}

fn compress_tasks(output: &str) -> String {
    let mut tasks: HashMap<String, Vec<String>> = HashMap::new();

    for line in output.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("ok:")
            || trimmed.starts_with("changed:")
            || trimmed.starts_with("failed:")
            || trimmed.starts_with("skipping:")
        {
            let status = trimmed.split(':').next().unwrap_or("?").to_string();
            tasks.entry(status).or_default().push(trimmed.to_string());
        }
    }

    if tasks.is_empty() {
        return compact_lines(output, 15);
    }

    let mut result = Vec::new();
    for (status, items) in &tasks {
        result.push(format!("{status}: {}", items.len()));
    }
    result.join(", ")
}

fn compact_lines(text: &str, max: usize) -> String {
    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() <= max {
        return lines.join("\n");
    }
    format!(
        "{}\n... ({} more lines)",
        lines[..max].join("\n"),
        lines.len() - max
    )
}