//! lab: benchmark the same task on several agents (issue #62). use super::*; use crate::cli::LabArgs; use crate::lab::{self, LabReport}; use anyhow::Result; pub fn run(app: &App, args: &LabArgs) -> Result { if args.list { let tasks = lab::list_tasks(app); if app.json() { let names: Vec = tasks.iter().map(|p| p.display().to_string()).collect(); crate::output::print_json(&names); return Ok(0); } if tasks.is_empty() { app.log.info(&format!( "no tasks in {} — write a task.yaml (name, args, timeout) or run 'lab --agents a,b --task hello'", lab::dir(app).display() )); return Ok(0); } let mut table = crate::output::Table::new(vec!["TASK"]); for p in tasks { table.row(vec![p.display().to_string()]); } print!("{}", table.render()); return Ok(0); } let path = match args.task.as_deref() { Some(p) => lab::resolve_task(app, p), None => anyhow::bail!("--task is required"), }; if !path.exists() { anyhow::bail!( "task not found: {} — write it or use 'lab --list' to see the tasks of {}", path.display(), lab::dir(app).display() ); } let task = lab::load_task(&path)?; let agents = lab::split_agents(args.agents.as_deref().unwrap_or("")); if agents.is_empty() { anyhow::bail!( "--agents expects at least one agent name, got '{}'", args.agents.as_deref().unwrap_or("") ); } app.log.info(&format!( "lab '{}' — {} agent(s), {}", task.name, agents.len(), if args.parallel { "parallèle" } else { "séquentiel" } )); let report = lab::run_benchmark(app, &agents, &task, args.parallel, args.timeout)?; if app.json() { crate::output::print_json(&report); } else { print_report(app, &report); } // Exit code: 0 when every run succeeded, 1 otherwise (CI-friendly). Ok(if report.runs.iter().all(|r| r.status == "ok") { 0 } else { 1 }) } fn print_report(app: &App, report: &LabReport) { let mut table = crate::output::Table::new(vec![ "AGENT", "STATUS", "EXIT", "DURATION", "COST", "OUTPUT", ]); for r in &report.runs { table.row(vec![ r.agent.clone(), r.status.clone(), r.exit_code .map(|c| c.to_string()) .unwrap_or_else(|| "-".into()), format!("{:.2}s", r.duration_ms as f64 / 1000.0), r.cost_usd .map(|c| format!("${c:.4}")) .unwrap_or_else(|| "-".into()), crate::output::truncate(r.output.lines().next().unwrap_or(""), 40), ]); } print!("{}", table.render()); let s = &report.summary; let mut line = format!( "{} run(s) — {} ok, {} failed, {} timeout", s.runs, s.ok, s.failed, s.timeout ); if let Some(f) = &s.fastest { line.push_str(&format!(" — fastest: {f}")); } if let Some(c) = &s.cheapest { line.push_str(&format!(" — cheapest: {c}")); } app.log.info(&line); }