Skip to main content

lean_ctx/core/value_gate/
report.rs

1use super::{ValueAssessment, cpao};
2use crate::core::savings_ledger::roi_report;
3use serde::Serialize;
4
5#[derive(Debug, Clone, Default, Serialize)]
6pub struct ValueReport {
7    pub total_tasks: usize,
8    pub accepted_rate: f64,
9    pub cpao_micros: Option<u64>,
10    pub etpao_tokens: Option<u64>,
11    pub total_cost_micros: u64,
12    pub savings_usd: f64,
13    pub tasks: Vec<ValueAssessment>,
14}
15
16pub fn build(tasks: Vec<ValueAssessment>) -> ValueReport {
17    let total_cost_micros = tasks.iter().map(|task| task.cost_micros).sum();
18    let total_tokens = tasks.iter().map(|task| task.total_tokens).sum();
19    let costs = tasks
20        .iter()
21        .map(|task| task.cost_micros)
22        .collect::<Vec<_>>();
23    let accepted = tasks
24        .iter()
25        .map(|task| task.outcome_accepted)
26        .collect::<Vec<_>>();
27    let accepted_count = accepted.iter().filter(|&&ok| ok).count() as u64;
28    ValueReport {
29        total_tasks: tasks.len(),
30        accepted_rate: if tasks.is_empty() {
31            0.0
32        } else {
33            accepted_count as f64 / tasks.len() as f64
34        },
35        cpao_micros: cpao::cost_per_accepted_outcome(&costs, &accepted),
36        etpao_tokens: cpao::etpao(total_tokens, accepted_count),
37        total_cost_micros,
38        savings_usd: roi_report(crate::core::agent_identity::current_agent_id()).saved_usd,
39        tasks,
40    }
41}
42
43pub fn table(report: &ValueReport) -> String {
44    let mut output = format!(
45        "Value Report\nTasks: {}  Accepted: {:.1}%  CPAO: {}  ETPAO: {}  Cost: ${:.4}  Savings: ${:.2}\n\n{:<12} {:>10} {:<9} {:>10} {:<18}\n{}\n",
46        report.total_tasks,
47        report.accepted_rate * 100.0,
48        money(report.cpao_micros),
49        tokens(report.etpao_tokens),
50        report.total_cost_micros as f64 / 1_000_000.0,
51        report.savings_usd,
52        "Task",
53        "Cost",
54        "Outcome",
55        "CPAO",
56        "Model",
57        "-".repeat(66)
58    );
59    for task in &report.tasks {
60        output.push_str(&format!(
61            "{:<12} {:>10} {:<9} {:>10} {:<18}\n",
62            short(&task.task_id),
63            money(Some(task.cost_micros)),
64            outcome(task),
65            money(task.cpao_micros),
66            task.model
67        ));
68    }
69    output
70}
71
72pub fn markdown(report: &ValueReport) -> String {
73    let mut output = format!(
74        "# 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",
75        report.total_tasks,
76        report.accepted_rate * 100.0,
77        money(report.cpao_micros),
78        tokens(report.etpao_tokens),
79        report.total_cost_micros as f64 / 1_000_000.0,
80        report.savings_usd
81    );
82    for task in &report.tasks {
83        output.push_str(&format!(
84            "| {} | {} | {} | {} | {} |\n",
85            short(&task.task_id),
86            money(Some(task.cost_micros)),
87            outcome(task),
88            money(task.cpao_micros),
89            task.model
90        ));
91    }
92    output
93}
94
95pub fn json(report: &ValueReport) -> String {
96    serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
97}
98fn outcome(task: &ValueAssessment) -> &str {
99    if task.outcome_accepted {
100        "accepted"
101    } else {
102        "rejected"
103    }
104}
105fn short(id: &str) -> &str {
106    id.get(..12).unwrap_or(id)
107}
108fn money(value: Option<u64>) -> String {
109    value.map_or_else(
110        || "n/a".to_string(),
111        |value| format!("${:.4}", value as f64 / 1_000_000.0),
112    )
113}
114fn tokens(value: Option<u64>) -> String {
115    value.map_or_else(|| "n/a".to_string(), |value| value.to_string())
116}