use argonauts_core::agent_goal::{
AgentGoal, CliGoal, GoalCheck, GoalKeeper, GoalLine, GoalState, GoalTry,
};
use argonauts_core::display_names::builtin_cli_name;
use argonauts_core::events::RunEvent;
use argonauts_core::events::goal::GoalStatus;
use argonauts_core::goal::KeptBy;
use argonauts_core::goal::report::{
CheckReport, CliGoalReport, GoalReport, LineReport, LineState, TryReport,
};
use argonauts_core::model_catalog::display_name_from_id;
use argonauts_core::names::ModelRef;
use argonauts_core::views::{AgentRow, PhaseRow, WorkflowRow};
use argonauts_crew::goal::report::reports;
use time::OffsetDateTime;
const CHECKER: &str = "Argonauts Code";
pub(crate) fn agent_goals(events: &[RunEvent], phases: &[PhaseRow]) -> Vec<AgentGoal> {
let mut goals = goals_at(events, OffsetDateTime::now_utc());
for goal in &mut goals {
let mut rows = phases
.iter()
.flat_map(|phase| &phase.rows)
.filter_map(|row| match row {
WorkflowRow::Agent(agent) if agent.session == goal.session => Some(agent),
_ => None,
});
if let (Some(row), None) = (rows.next(), rows.next()) {
goal.label.clone_from(&row.label);
}
}
goals
}
pub(crate) fn goal_of(events: &[RunEvent], row: &AgentRow) -> Option<Box<AgentGoal>> {
let mut goal = goals_at(events, OffsetDateTime::now_utc())
.into_iter()
.find(|goal| goal.session == row.session)?;
goal.label.clone_from(&row.label);
Some(Box::new(goal))
}
fn goals_at(events: &[RunEvent], now: OffsetDateTime) -> Vec<AgentGoal> {
reports(events)
.into_iter()
.filter_map(|report| agent_goal(report, now))
.collect()
}
fn agent_goal(report: GoalReport, now: OffsetDateTime) -> Option<AgentGoal> {
let session = report.session.clone()?;
let set_ms_ago = report
.set_at
.and_then(|set| u64::try_from((now - set).whole_milliseconds()).ok());
Some(AgentGoal {
session,
role: report.role().map(str::to_owned),
counting: report.counting().to_owned(),
kept_by: match (report.kept.by, &report.kept.cli) {
(KeptBy::Cli, Some(cli)) => GoalKeeper::Cli { name: cli.clone() },
(KeptBy::Cli, None) => GoalKeeper::Cli {
name: cli_of(report.worker.as_ref()),
},
(KeptBy::Argonauts, _) => GoalKeeper::Argonauts,
},
kept_note: report.kept.note,
checked_by: CHECKER.to_owned(),
state: match report.status {
GoalStatus::Pursuing => GoalState::Running,
GoalStatus::Met => GoalState::Met,
GoalStatus::Paused => GoalState::Paused,
GoalStatus::Stuck | GoalStatus::OverLimit => GoalState::Stuck,
GoalStatus::Cleared => GoalState::Cleared,
},
stopped_because: report.ended_because,
set_ms_ago,
lines: report.lines.into_iter().map(line).collect(),
tries: report.tries.into_iter().map(a_try).collect(),
max_tries: report.max_rounds,
work_ms: report.work_ms,
tokens: report.tokens,
checks: report.checks.into_iter().map(check).collect(),
checking: report.checking,
cli_says: report.cli_says.map(cli_says),
label: report.of,
task: report.task,
})
}
fn cli_of(worker: Option<&ModelRef>) -> String {
match worker {
Some(worker) => {
builtin_cli_name(&worker.agent).map_or_else(|| worker.agent.clone(), str::to_owned)
}
None => "the CLI".to_owned(),
}
}
fn line(line: LineReport) -> GoalLine {
GoalLine {
text: line.text,
met: match line.state {
LineState::Unchecked => None,
LineState::Met => Some(true),
LineState::NotMet => Some(false),
},
checked_by: line.checked_by,
met_on_try: line.met_on_try,
failing: Some(line.still_fails).filter(|fails| !fails.trim().is_empty()),
}
}
fn a_try(a_try: TryReport) -> GoalTry {
GoalTry {
number: a_try.n,
did: if a_try.changed_approach {
format!("{} · a different approach", a_try.what)
} else {
a_try.what
},
changed: a_try.changed,
failing: a_try.still_failing.map(|failing| failing.count),
elapsed_ms: a_try.ms,
tokens: a_try.tokens,
running: a_try.running,
}
}
fn check(check: CheckReport) -> GoalCheck {
let mut result = check.summary;
match &check.agent {
Some(judge) => result.push_str(&format!(" · {}", judge_name(judge))),
None if check.ms > 0 => result.push_str(&format!(" · {}", took(check.ms))),
None => {}
}
GoalCheck {
name: check.name,
passed: Some(check.passed),
result,
detail: check.output,
}
}
fn judge_name(judge: &ModelRef) -> String {
match judge.model.as_deref().map(str::trim) {
Some(model) if !model.is_empty() => display_name_from_id(model),
_ => cli_of(Some(judge)),
}
}
fn took(ms: u64) -> String {
let tenths = (ms + 50) / 100;
if tenths < 600 {
return format!("{}.{}s", tenths / 10, tenths % 10);
}
let seconds = ms / 1000;
format!("{}m{:02}s", seconds / 60, seconds % 60)
}
fn cli_says(says: CliGoalReport) -> CliGoal {
CliGoal {
cli: says.cli,
state: says.status,
note: Some(says.note).filter(|note| !note.trim().is_empty()),
turns: says.turns,
}
}
#[cfg(test)]
mod tests;