- #62: am lab --agents a,b --task <f> — séquentiel ou --parallel, capture durée/exit/sortie/coût estimé, rapport comparatif tableau + --json stable, tâches versionnables dans <state>/lab/ (--list), rejouables à l'identique - #75: plugins/ sous state_dir, déclenchés sur on_install/on_start/on_stop/ on_update (App::emit), contrat JSON stdin/stdout, timeout configurable, échec non bloquant, garde anti-récursion AM_PLUGINS_RUNNING, am plugins + am plugins --test <name> (CI), exemples notify + ci-webhook - intégration REPL (parse, complétion, bannière, is_am_command), help, tips, man pages (am-lab.1, am-plugins.1), README, ROADMAP (26/27), v0.5.4 - 334 tests verts (309 + 25) closes #62 closes #75
102 lines
3.2 KiB
Rust
102 lines
3.2 KiB
Rust
//! 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<i32> {
|
|
if args.list {
|
|
let tasks = lab::list_tasks(app);
|
|
if app.json() {
|
|
let names: Vec<String> = 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 <file> 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);
|
|
}
|