lean-ctx 3.1.3

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::path::Path;

use crate::core::cache::SessionCache;
use crate::core::signatures;
use crate::core::tokens::count_tokens;
use crate::tools::CrpMode;

struct FileCandidate {
    path: String,
    score: f64,
    tokens_full: usize,
    tokens_map: usize,
    tokens_sig: usize,
}

pub fn handle(
    cache: &mut SessionCache,
    paths: &[String],
    budget: usize,
    crp_mode: CrpMode,
) -> String {
    if paths.is_empty() {
        return "No files specified.".to_string();
    }

    let mut candidates: Vec<FileCandidate> = Vec::new();

    for path in paths {
        let content = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(_) => continue,
        };

        let ext = Path::new(path)
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");
        let tokens_full = count_tokens(&content);
        let sigs = signatures::extract_signatures(&content, ext);
        let sig_text: String = sigs
            .iter()
            .map(|s| s.to_compact())
            .collect::<Vec<_>>()
            .join("\n");
        let tokens_sig = count_tokens(&sig_text);

        let map_text = format_map(&content, ext, &sigs);
        let tokens_map = count_tokens(&map_text);

        let score = compute_relevance_score(path, &content);

        candidates.push(FileCandidate {
            path: path.clone(),
            score,
            tokens_full,
            tokens_map,
            tokens_sig,
        });
    }

    candidates.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let mut used_tokens = 0usize;
    let mut selections: Vec<(String, String)> = Vec::new();

    for candidate in &candidates {
        if used_tokens >= budget {
            break;
        }

        let remaining = budget - used_tokens;
        let (mode, cost) = select_best_fit(candidate, remaining);

        if cost > remaining {
            let sig_cost = candidate.tokens_sig;
            if sig_cost <= remaining {
                selections.push((candidate.path.clone(), "signatures".to_string()));
                used_tokens += sig_cost;
            }
            continue;
        }

        selections.push((candidate.path.clone(), mode));
        used_tokens += cost;
    }

    let mut output_parts = Vec::new();
    output_parts.push(format!(
        "ctx_fill: {budget} token budget, {} files analyzed, {} selected",
        candidates.len(),
        selections.len()
    ));
    output_parts.push(String::new());

    for (path, mode) in &selections {
        let result = crate::tools::ctx_read::handle(cache, path, mode, crp_mode);
        output_parts.push(result);
        output_parts.push("---".to_string());
    }

    let skipped = candidates.len() - selections.len();
    if skipped > 0 {
        output_parts.push(format!("{skipped} files skipped (budget exhausted)"));
    }
    output_parts.push(format!("\nUsed: {used_tokens}/{budget} tokens"));

    output_parts.join("\n")
}

fn select_best_fit(candidate: &FileCandidate, remaining: usize) -> (String, usize) {
    if candidate.tokens_full <= remaining {
        return ("full".to_string(), candidate.tokens_full);
    }
    if candidate.tokens_map <= remaining {
        return ("map".to_string(), candidate.tokens_map);
    }
    if candidate.tokens_sig <= remaining {
        return ("signatures".to_string(), candidate.tokens_sig);
    }
    ("signatures".to_string(), candidate.tokens_sig)
}

fn compute_relevance_score(path: &str, content: &str) -> f64 {
    let mut score = 1.0;

    let name = Path::new(path)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("");
    if name.contains("test") || name.contains("spec") {
        score *= 0.5;
    }
    if name.contains("config") || name.contains("types") || name.contains("schema") {
        score *= 1.3;
    }
    if name == "mod.rs" || name == "index.ts" || name == "index.js" || name == "__init__.py" {
        score *= 1.5;
    }

    let ext = Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");
    if matches!(ext, "rs" | "ts" | "py" | "go" | "java") {
        score *= 1.2;
    }

    let lines = content.lines().count();
    if lines > 500 {
        score *= 0.8;
    }
    if lines < 50 {
        score *= 1.1;
    }

    let export_count = content
        .lines()
        .filter(|l| l.contains("pub ") || l.contains("export ") || l.contains("def "))
        .count();
    score *= 1.0 + (export_count as f64 * 0.02).min(0.5);

    score
}

fn format_map(content: &str, ext: &str, sigs: &[crate::core::signatures::Signature]) -> String {
    let deps = crate::core::deps::extract_deps(content, ext);
    let mut parts = Vec::new();
    if !deps.imports.is_empty() {
        parts.push(format!("deps: {}", deps.imports.join(", ")));
    }
    if !deps.exports.is_empty() {
        parts.push(format!("exports: {}", deps.exports.join(", ")));
    }
    let key_sigs: Vec<_> = sigs
        .iter()
        .filter(|s| s.is_exported || s.indent == 0)
        .collect();
    for sig in &key_sigs {
        parts.push(sig.to_compact());
    }
    parts.join("\n")
}