use crate::opslog::{OpEntry, OpRow, Origin};
use std::path::Path;
pub const YOG_MONITOR: &str = "yog-monitor";
pub const STEP: &str = "monitor";
const REASON_MAX: usize = 400;
const ABSENT: &str = "-";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Check {
pub workspace: String,
pub agent: String,
pub verdict: super::Verdict,
pub sha: String,
pub reason: String,
pub model: String,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
}
pub fn entry(ts: String, check: &Check) -> OpEntry {
OpEntry {
ts,
argv: vec![
YOG_MONITOR.to_owned(),
check.verdict.token().to_owned(),
check.agent.clone(),
check.sha.clone(),
check.model.clone(),
count(check.input_tokens),
count(check.output_tokens),
],
cwd: check.workspace.clone(),
exit: 0,
stdout: clip(&check.reason),
stderr: String::new(),
origin: Origin::World,
}
}
pub fn failure(ts: String, workspace: &Path, agent: &str, why: &str) -> OpEntry {
OpEntry::step_failure(
ts,
STEP,
crate::nav::ws_key(workspace),
format!("{agent}: {why}"),
Origin::World,
)
}
fn read(argv: &[&str], cwd: &str, stdout: &str) -> Option<Check> {
let &[head, verdict, agent, sha, model, input, output] = argv else {
return None;
};
if head != YOG_MONITOR {
return None;
}
Some(Check {
workspace: cwd.to_owned(),
agent: agent.to_owned(),
verdict: super::Verdict::parse(verdict)?,
sha: sha.to_owned(),
reason: stdout.to_owned(),
model: model.to_owned(),
input_tokens: tokens(input),
output_tokens: tokens(output),
})
}
pub fn of_entries(entries: &[OpEntry]) -> Vec<Check> {
entries
.iter()
.filter_map(|e| {
let argv: Vec<&str> = e.argv.iter().map(String::as_str).collect();
read(&argv, &e.cwd, &e.stdout)
})
.collect()
}
pub fn of_rows(rows: &[OpRow]) -> Vec<Check> {
rows.iter()
.filter_map(|r| read(&r.argv.split(' ').collect::<Vec<_>>(), &r.cwd, &r.stdout))
.collect()
}
pub fn latest(checks: &[Check], workspace: &str, agent: &str) -> Option<Check> {
checks
.iter()
.rev()
.find(|c| c.workspace == workspace && c.agent == agent)
.cloned()
}
pub fn worst(checks: &[Check], workspace: &str, agents: &[String]) -> Option<Check> {
agents
.iter()
.filter_map(|a| latest(checks, workspace, a))
.max_by_key(|c| c.verdict)
}
fn count(n: Option<u64>) -> String {
n.map_or_else(|| ABSENT.to_owned(), |n| n.to_string())
}
fn tokens(token: &str) -> Option<u64> {
token.parse().ok()
}
pub(super) fn clip(reason: &str) -> String {
let flat = reason.replace(['\n', '\r'], " ");
if flat.chars().count() > REASON_MAX {
flat.chars().take(REASON_MAX).collect()
} else {
flat
}
}
#[cfg(test)]
mod tests;