use crate::trace_analyzer::HarnessTraceSummary;
use crate::{EvalMetric, task::EvalCategory};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct TaskReport {
task_id: String,
pub(crate) category: String,
pub(crate) metric: EvalMetric,
}
#[derive(Debug, Clone, Serialize)]
pub struct SuiteReport {
pub(crate) suite_id: String,
pub(crate) suite_name: String,
pub(crate) task_reports: Vec<TaskReport>,
pub(crate) aggregate: EvalMetric,
pub(crate) capability_metrics: EvalMetric,
pub(crate) regression_metrics: EvalMetric,
pub(crate) cost_usd: Option<f64>,
pub(crate) unpriced_runs: u32,
pub(crate) duration_secs: f64,
pub(crate) trace_summary: Option<HarnessTraceSummary>,
pub(crate) mean_cost_per_attempt: Option<f64>,
pub(crate) cost_per_solve: Option<f64>,
pub(crate) mean_tokens_per_attempt: Option<f64>,
pub(crate) mean_turns_per_attempt: Option<f64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct EvalReport {
pub(crate) generated_at: String,
pub(crate) suites: Vec<SuiteReport>,
}
impl EvalReport {
pub fn new(generated_at: impl Into<String>, suites: Vec<SuiteReport>) -> Self {
Self { generated_at: generated_at.into(), suites }
}
pub fn to_markdown(&self) -> String {
let mut out = String::new();
out.push_str("# Eval Report\n\n");
for s in &self.suites {
out.push_str(&format!("## {}\n\n", s.suite_name));
out.push_str(&format!(
"- Aggregate: pass@k={:.1}%; pass^k={:.1}%\n",
s.aggregate.pass_at_k * 100.0,
s.aggregate.pass_power_k * 100.0
));
if let Some(cost) = s.cost_usd {
out.push_str(&format!("- Cost (known): ${cost:.6}; unpriced attempts: {}\n", s.unpriced_runs));
} else if s.unpriced_runs > 0 {
out.push_str(&format!("- Cost (known): unavailable; unpriced attempts: {}\n", s.unpriced_runs));
}
if s.mean_cost_per_attempt.is_some()
|| s.cost_per_solve.is_some()
|| s.mean_tokens_per_attempt.is_some()
|| s.mean_turns_per_attempt.is_some()
{
let mut parts = Vec::new();
if let Some(cost_per_solve) = s.cost_per_solve {
parts.push(format!("cost/solve ${cost_per_solve:.4}"));
}
if let Some(mean_cost) = s.mean_cost_per_attempt {
parts.push(format!("${mean_cost:.4} per priced attempt"));
}
if let Some(mean_tokens) = s.mean_tokens_per_attempt {
parts.push(format!("{mean_tokens:.0} tokens"));
}
if let Some(mean_turns) = s.mean_turns_per_attempt {
parts.push(format!("{mean_turns:.1} turns"));
}
out.push_str(&format!("- Efficiency: {}\n", parts.join(" · ")));
}
if let Some(trace) = &s.trace_summary {
if let Some(mean_ms) = trace.latency.mean_ms {
out.push_str(&format!(
"- Trace: {} turns, {} tool calls, mean latency {:.1} ms\n",
trace.turns, trace.tool_calls, mean_ms
));
} else {
out.push_str(&format!("- Trace: {} turns, {} tool calls\n", trace.turns, trace.tool_calls));
}
}
out.push_str("| Task | Category | pass@k | pass^k | passed/total |\n");
out.push_str("|------|----------|--------|--------|-------------|\n");
for t in &s.task_reports {
out.push_str(&format!(
"| {} | {} | {:.1}% | {:.1}% | {}/{} |\n",
t.task_id,
t.category.as_str(),
t.metric.pass_at_k * 100.0,
t.metric.pass_power_k * 100.0,
t.metric.passed_runs,
t.metric.total_runs
));
}
out.push('\n');
}
out
}
}
pub fn build_task_report(task_id: &str, _name: &str, category: EvalCategory, metric: EvalMetric) -> TaskReport {
TaskReport {
task_id: task_id.into(),
category: category.label().into(),
metric,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct CostEfficiency {
pub mean_cost_per_attempt: Option<f64>,
pub cost_per_solve: Option<f64>,
pub mean_tokens_per_attempt: Option<f64>,
pub mean_turns_per_attempt: Option<f64>,
}
pub fn priced_cost(cost_usd: Option<f64>) -> Option<f64> {
cost_usd.filter(|value| value.is_finite() && *value >= 0.0)
}
impl CostEfficiency {
pub fn from_runs<'a>(results: impl IntoIterator<Item = &'a crate::task::EvalRunResult>) -> Self {
let mut total_cost = 0.0_f64;
let mut known_cost_runs = 0_u32;
let mut unpriced_runs = 0_u32;
let mut passed_runs = 0_u32;
let mut token_sum = 0.0_f64;
let mut token_count = 0_u32;
let mut turn_sum = 0.0_f64;
let mut turn_count = 0_u32;
for result in results {
if result.outcome == crate::task::RunOutcome::Pass {
passed_runs = passed_runs.saturating_add(1);
}
match priced_cost(result.cost_usd) {
Some(cost) => {
total_cost += cost;
known_cost_runs = known_cost_runs.saturating_add(1);
}
None => {
unpriced_runs = unpriced_runs.saturating_add(1);
}
}
if let Some(summary) = &result.trace_summary {
let gross = (summary
.token_usage
.input_tokens
.saturating_add(summary.token_usage.output_tokens)) as f64;
token_sum += gross;
token_count = token_count.saturating_add(1);
turn_sum += summary.turns as f64;
turn_count = turn_count.saturating_add(1);
}
}
let mean_cost_per_attempt = (known_cost_runs > 0).then(|| total_cost / f64::from(known_cost_runs));
let cost_per_solve =
(unpriced_runs == 0 && passed_runs > 0 && known_cost_runs > 0).then(|| total_cost / f64::from(passed_runs));
let mean_tokens_per_attempt = (token_count > 0).then(|| token_sum / f64::from(token_count));
let mean_turns_per_attempt = (turn_count > 0).then(|| turn_sum / f64::from(turn_count));
Self {
mean_cost_per_attempt,
cost_per_solve,
mean_tokens_per_attempt,
mean_turns_per_attempt,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metric::EvalMetric;
use crate::task::EvalCategory;
#[test]
fn to_markdown_renders_tasks_and_aggregate() {
let report = EvalReport {
generated_at: "2026-01-01".into(),
suites: vec![SuiteReport {
suite_id: "s1".into(),
suite_name: "demo".into(),
task_reports: vec![TaskReport {
task_id: "t1".into(),
category: "Capability".into(),
metric: EvalMetric {
pass_at_k: 0.5,
pass_power_k: 0.25,
pass_all_k: 0.0,
k: 1,
total_runs: 2,
passed_runs: 1,
task_id: "t1".into(),
},
}],
aggregate: EvalMetric {
pass_at_k: 0.5,
pass_power_k: 0.25,
pass_all_k: 0.0,
k: 1,
total_runs: 2,
passed_runs: 1,
task_id: "aggregate".into(),
},
capability_metrics: EvalMetric {
pass_at_k: 0.5,
pass_power_k: 0.25,
pass_all_k: 0.0,
k: 1,
total_runs: 2,
passed_runs: 1,
task_id: "cap".into(),
},
regression_metrics: EvalMetric {
pass_at_k: 0.0,
pass_power_k: 0.0,
pass_all_k: 0.0,
k: 0,
total_runs: 0,
passed_runs: 0,
task_id: "reg".into(),
},
cost_usd: Some(0.0),
unpriced_runs: 0,
duration_secs: 0.0,
trace_summary: None,
mean_cost_per_attempt: None,
cost_per_solve: None,
mean_tokens_per_attempt: None,
mean_turns_per_attempt: None,
}],
};
let md = report.to_markdown();
assert!(md.contains("# Eval Report"));
assert!(md.contains("demo"));
assert!(md.contains("t1"));
assert!(md.contains("Capability"));
assert!(md.contains("pass^k"));
}
#[test]
fn build_task_report_maps_category() {
let tr = build_task_report(
"t1",
"name",
EvalCategory::Regression,
EvalMetric {
pass_at_k: 1.0,
pass_power_k: 1.0,
pass_all_k: 1.0,
k: 1,
total_runs: 1,
passed_runs: 1,
task_id: "t1".into(),
},
);
assert_eq!(tr.task_id, "t1");
assert_eq!(tr.category, "Regression");
}
fn run(outcome: crate::task::RunOutcome, cost: Option<f64>, turns: u64, tokens: u64) -> crate::task::EvalRunResult {
use crate::trace_analyzer::{HarnessTraceSummary, TokenUsage};
crate::task::EvalRunResult {
task_id: "t1".into(),
outcome,
error_message: None,
duration_secs: 1.0,
attempt: 1,
cost_usd: cost,
transcript_path: None,
trace_summary: Some(HarnessTraceSummary {
turns,
token_usage: TokenUsage {
input_tokens: tokens / 2,
output_tokens: tokens - tokens / 2,
..TokenUsage::default()
},
..HarnessTraceSummary::default()
}),
}
}
#[test]
fn cost_efficiency_uses_total_cost_over_passed_attempts() {
let results = vec![
run(crate::task::RunOutcome::Pass, Some(0.02), 10, 1_000),
run(crate::task::RunOutcome::Fail, Some(0.04), 20, 3_000),
];
let eff = CostEfficiency::from_runs(&results);
assert_eq!(eff.cost_per_solve, Some(0.06));
assert_eq!(eff.mean_cost_per_attempt, Some(0.03));
assert_eq!(eff.mean_tokens_per_attempt, Some(2_000.0));
assert_eq!(eff.mean_turns_per_attempt, Some(15.0));
}
#[test]
fn cost_per_solve_is_none_when_any_attempt_is_unpriced() {
let results = vec![
run(crate::task::RunOutcome::Pass, Some(0.02), 5, 100),
run(crate::task::RunOutcome::Pass, None, 5, 100),
];
let eff = CostEfficiency::from_runs(&results);
assert_eq!(eff.cost_per_solve, None);
assert_eq!(eff.mean_cost_per_attempt, Some(0.02));
}
#[test]
fn cost_per_solve_is_none_when_nothing_passed() {
let results = vec![run(crate::task::RunOutcome::Fail, Some(0.10), 3, 50)];
let eff = CostEfficiency::from_runs(&results);
assert_eq!(eff.cost_per_solve, None);
assert_eq!(eff.mean_cost_per_attempt, Some(0.10));
}
#[test]
fn token_and_turn_means_are_none_without_traces() {
let mut result = run(crate::task::RunOutcome::Pass, Some(0.01), 1, 1);
result.trace_summary = None;
let eff = CostEfficiency::from_runs(std::slice::from_ref(&result));
assert_eq!(eff.mean_tokens_per_attempt, None);
assert_eq!(eff.mean_turns_per_attempt, None);
}
#[test]
fn markdown_renders_efficiency_line() {
let report = EvalReport {
generated_at: "2026-01-01".into(),
suites: vec![SuiteReport {
suite_id: "s1".into(),
suite_name: "demo".into(),
task_reports: Vec::new(),
aggregate: EvalMetric {
pass_at_k: 1.0,
pass_power_k: 1.0,
pass_all_k: 1.0,
k: 1,
total_runs: 1,
passed_runs: 1,
task_id: "aggregate".into(),
},
capability_metrics: EvalMetric {
pass_at_k: 1.0,
pass_power_k: 1.0,
pass_all_k: 1.0,
k: 1,
total_runs: 1,
passed_runs: 1,
task_id: "cap".into(),
},
regression_metrics: EvalMetric {
pass_at_k: 0.0,
pass_power_k: 0.0,
pass_all_k: 0.0,
k: 0,
total_runs: 0,
passed_runs: 0,
task_id: "reg".into(),
},
cost_usd: Some(0.05),
unpriced_runs: 0,
duration_secs: 1.0,
trace_summary: None,
mean_cost_per_attempt: Some(0.05),
cost_per_solve: Some(0.05),
mean_tokens_per_attempt: Some(12_000.0),
mean_turns_per_attempt: Some(8.5),
}],
};
let md = report.to_markdown();
assert!(
md.contains("- Efficiency: cost/solve $0.0500 · $0.0500 per priced attempt · 12000 tokens · 8.5 turns"),
"missing efficiency line: {md}"
);
}
}