lean-ctx 3.3.7

Context Runtime for AI Agents with CCP. 46 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 crate::core::gain::GainEngine;

pub fn handle(
    action: &str,
    period: Option<&str>,
    model: Option<&str>,
    limit: Option<usize>,
) -> String {
    let engine = GainEngine::load();
    let lim = limit.unwrap_or(10).clamp(1, 50);
    let env_model = std::env::var("LEAN_CTX_MODEL")
        .or_else(|_| std::env::var("LCTX_MODEL"))
        .ok();
    let model = model.or(env_model.as_deref());

    match action {
        "status" | "report" | "" => format_summary(&engine, model),
        "score" => format_score(&engine, model),
        "tasks" => format_tasks(&engine),
        "heatmap" => format_heatmap(&engine, lim),
        "agents" => format_agents(&engine, lim),
        "cost" => crate::core::a2a::cost_attribution::format_cost_report(&engine.costs, lim),
        "wrapped" => {
            let p = period.unwrap_or("all");
            let r = crate::core::wrapped::WrappedReport::generate(p);
            r.format_ascii()
        }
        "json" => format_json(&engine, model, lim),
        _ => format!(
            "Unknown action '{action}'. Available: status, report, score, cost, tasks, heatmap, wrapped, agents, json"
        ),
    }
}

fn format_summary(engine: &GainEngine, model: Option<&str>) -> String {
    let s = engine.summary(model);
    let saved = format_tokens(s.tokens_saved);
    let input = format_tokens(s.input_tokens);
    let out = format_tokens(s.output_tokens);
    let avoided = format_usd(s.avoided_usd);
    let spend = format_usd(s.tool_spend_usd);
    let roi = s
        .roi
        .map(|r| format!("{r:.2}x"))
        .unwrap_or_else(|| "n/a".to_string());
    let trend = match s.score.trend {
        crate::core::gain::gain_score::Trend::Rising => "rising",
        crate::core::gain::gain_score::Trend::Stable => "stable",
        crate::core::gain::gain_score::Trend::Declining => "declining",
    };

    format!(
        "lean-ctx gain\n\
         ────────────\n\
         Score: {total}/100  (compression {comp}, cost {cost}, quality {qual}, consistency {cons})  trend={trend}\n\
         Tokens: {input} in → {out} out  | saved {saved}  ({rate:.1}%)\n\
         Gain: {avoided} avoided  | tool spend {spend}  | ROI {roi}\n\
         Pricing: model={model_key} ({match_kind:?}) in=${in_m:.2}/M out=${out_m:.2}/M\n",
        total = s.score.total,
        comp = s.score.compression,
        cost = s.score.cost_efficiency,
        qual = s.score.quality,
        cons = s.score.consistency,
        rate = s.gain_rate_pct,
        model_key = s.model.model_key,
        match_kind = s.model.match_kind,
        in_m = s.model.cost.input_per_m,
        out_m = s.model.cost.output_per_m,
    )
}

fn format_score(engine: &GainEngine, model: Option<&str>) -> String {
    let s = engine.summary(model);
    format!(
        "Gain Score: {}/100\n\
         ──────────────────\n\
         Compression:     {}/100\n\
         Cost efficiency: {}/100\n\
         Quality:         {}/100\n\
         Consistency:     {}/100\n\
         Trend:           {:?}\n",
        s.score.total,
        s.score.compression,
        s.score.cost_efficiency,
        s.score.quality,
        s.score.consistency,
        s.score.trend
    )
}

fn format_tasks(engine: &GainEngine) -> String {
    let rows = engine.task_breakdown();
    if rows.is_empty() {
        return "No task data yet.".to_string();
    }
    let mut lines = Vec::new();
    lines.push("Task Breakdown (gain-first):".to_string());
    lines.push(String::new());
    for r in rows.iter().take(13) {
        lines.push(format!(
            "  {:<14}  saved {:>8} tok  cmds {:>5}  tools {:>5}  tool spend {}",
            r.category.label(),
            format_tokens(r.tokens_saved),
            r.commands,
            r.tool_calls,
            format_usd(r.tool_spend_usd)
        ));
    }
    lines.join("\n")
}

fn format_heatmap(engine: &GainEngine, limit: usize) -> String {
    let rows = engine.heatmap_gains(limit);
    if rows.is_empty() {
        return "No heatmap data recorded yet.".to_string();
    }
    let mut lines = Vec::new();
    lines.push(format!("Heatmap (top {limit} files by tokens saved):"));
    for (i, r) in rows.iter().enumerate() {
        lines.push(format!(
            "  {}. {}{} tok saved, {} accesses, {:.0}% compression",
            i + 1,
            r.path,
            format_tokens(r.tokens_saved),
            r.access_count,
            r.compression_pct
        ));
    }
    lines.join("\n")
}

fn format_agents(engine: &GainEngine, limit: usize) -> String {
    let top = engine.costs.top_agents(limit);
    if top.is_empty() {
        return "No agent cost data recorded yet.".to_string();
    }
    let mut lines = Vec::new();
    lines.push(format!("Top Agents by tool spend (top {limit}):"));
    for (i, a) in top.iter().enumerate() {
        lines.push(format!(
            "  {}. {} ({}) — {} calls, {} in + {} out tok, {}{}",
            i + 1,
            a.agent_id,
            a.agent_type,
            a.total_calls,
            format_tokens(a.total_input_tokens),
            format_tokens(a.total_output_tokens),
            format_usd(a.cost_usd),
            a.model_key
                .as_deref()
                .map(|m| format!(" [{m}]"))
                .unwrap_or_default()
        ));
    }
    lines.join("\n")
}

fn format_json(engine: &GainEngine, model: Option<&str>, limit: usize) -> String {
    #[derive(serde::Serialize)]
    struct Payload {
        summary: crate::core::gain::GainSummary,
        tasks: Vec<crate::core::gain::TaskGainRow>,
        heatmap: Vec<crate::core::gain::FileGainRow>,
    }
    let payload = Payload {
        summary: engine.summary(model),
        tasks: engine.task_breakdown(),
        heatmap: engine.heatmap_gains(limit),
    };
    serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())
}

fn format_tokens(tokens: u64) -> String {
    if tokens >= 1_000_000 {
        format!("{:.1}M", tokens as f64 / 1_000_000.0)
    } else if tokens >= 1_000 {
        format!("{:.1}K", tokens as f64 / 1_000.0)
    } else {
        format!("{tokens}")
    }
}

fn format_usd(amount: f64) -> String {
    if amount >= 0.01 {
        format!("${amount:.2}")
    } else {
        format!("${amount:.3}")
    }
}