lean-ctx 3.9.19

Context Runtime for AI Agents with CCP. 79 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%.
use super::{ValueAssessment, cpao};
use crate::core::savings_ledger::roi_report;
use serde::Serialize;

#[derive(Debug, Clone, Default, Serialize)]
pub struct ValueReport {
    pub total_tasks: usize,
    pub accepted_rate: f64,
    pub cpao_micros: Option<u64>,
    pub etpao_tokens: Option<u64>,
    pub total_cost_micros: u64,
    pub savings_usd: f64,
    pub tasks: Vec<ValueAssessment>,
}

pub fn build(tasks: Vec<ValueAssessment>) -> ValueReport {
    let total_cost_micros = tasks.iter().map(|task| task.cost_micros).sum();
    let total_tokens = tasks.iter().map(|task| task.total_tokens).sum();
    let costs = tasks
        .iter()
        .map(|task| task.cost_micros)
        .collect::<Vec<_>>();
    let accepted = tasks
        .iter()
        .map(|task| task.outcome_accepted)
        .collect::<Vec<_>>();
    let accepted_count = accepted.iter().filter(|&&ok| ok).count() as u64;
    ValueReport {
        total_tasks: tasks.len(),
        accepted_rate: if tasks.is_empty() {
            0.0
        } else {
            accepted_count as f64 / tasks.len() as f64
        },
        cpao_micros: cpao::cost_per_accepted_outcome(&costs, &accepted),
        etpao_tokens: cpao::etpao(total_tokens, accepted_count),
        total_cost_micros,
        savings_usd: roi_report(crate::core::agent_identity::current_agent_id()).saved_usd,
        tasks,
    }
}

pub fn table(report: &ValueReport) -> String {
    let mut output = format!(
        "Value Report\nTasks: {}  Accepted: {:.1}%  CPAO: {}  ETPAO: {}  Cost: ${:.4}  Savings: ${:.2}\n\n{:<12} {:>10} {:<9} {:>10} {:<18}\n{}\n",
        report.total_tasks,
        report.accepted_rate * 100.0,
        money(report.cpao_micros),
        tokens(report.etpao_tokens),
        report.total_cost_micros as f64 / 1_000_000.0,
        report.savings_usd,
        "Task",
        "Cost",
        "Outcome",
        "CPAO",
        "Model",
        "-".repeat(66)
    );
    for task in &report.tasks {
        output.push_str(&format!(
            "{:<12} {:>10} {:<9} {:>10} {:<18}\n",
            short(&task.task_id),
            money(Some(task.cost_micros)),
            outcome(task),
            money(task.cpao_micros),
            task.model
        ));
    }
    output
}

pub fn markdown(report: &ValueReport) -> String {
    let mut output = format!(
        "# Value Report\n\n- **Total tasks:** {}\n- **Accepted rate:** {:.1}%\n- **CPAO:** {}\n- **ETPAO:** {}\n- **Total cost:** ${:.4}\n- **Savings:** ${:.2}\n\n| Task | Cost | Outcome | CPAO | Model |\n|---|---:|---|---:|---|\n",
        report.total_tasks,
        report.accepted_rate * 100.0,
        money(report.cpao_micros),
        tokens(report.etpao_tokens),
        report.total_cost_micros as f64 / 1_000_000.0,
        report.savings_usd
    );
    for task in &report.tasks {
        output.push_str(&format!(
            "| {} | {} | {} | {} | {} |\n",
            short(&task.task_id),
            money(Some(task.cost_micros)),
            outcome(task),
            money(task.cpao_micros),
            task.model
        ));
    }
    output
}

pub fn json(report: &ValueReport) -> String {
    serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
}
fn outcome(task: &ValueAssessment) -> &str {
    if task.outcome_accepted {
        "accepted"
    } else {
        "rejected"
    }
}
fn short(id: &str) -> &str {
    id.get(..12).unwrap_or(id)
}
fn money(value: Option<u64>) -> String {
    value.map_or_else(
        || "n/a".to_string(),
        |value| format!("${:.4}", value as f64 / 1_000_000.0),
    )
}
fn tokens(value: Option<u64>) -> String {
    value.map_or_else(|| "n/a".to_string(), |value| value.to_string())
}