use crate::agent::{AgentOutput, SeatState};
use crate::config::{AgentSpec, ResolvedRoles};
use crate::run::{Candidate, FailClass, Handover, RunState};
pub const FIX_NODES: [&str; 4] = ["fix", "gate-fix", "land", "rebase"];
pub fn attempts(
state: &RunState,
roles: &ResolvedRoles,
winner: &Candidate,
) -> Vec<(AgentSpec, String)> {
let own = || {
(
state
.config
.agent(&winner.agent)
.cloned()
.unwrap_or_else(|_| roles.implementers[winner.index].clone()),
format!("impl-{}", winner.label),
)
};
let Some(chain) = roles.fixer.as_deref().filter(|c| !c.is_empty()) else {
return vec![own()];
};
let start = sticky_start(chain, &state.handovers);
chain
.iter()
.enumerate()
.skip(start)
.map(|(i, f)| {
if i == 0 && f.id == winner.agent {
own()
} else {
(f.clone(), "fix".to_owned())
}
})
.collect()
}
fn sticky_start(chain: &[AgentSpec], handovers: &[Handover]) -> usize {
handovers
.iter()
.rev()
.filter(|h| FIX_NODES.contains(&h.node.as_str()))
.find_map(|h| chain.iter().position(|s| s.id == h.to))
.unwrap_or(0)
}
pub fn seat_for(state: &mut RunState, key: &str, agent: &str) -> SeatState {
if let Some(existing) = state.seats.get(key)
&& existing.agent == agent
{
return existing.clone();
}
let fresh = if state.seats.contains_key(key) {
crate::graph::handover_seat(key, agent, state.next_seat_seed())
} else {
SeatState::new(key, agent, state.seed)
};
state.seats.insert(key.to_owned(), fresh.clone());
fresh
}
pub fn failure_of(out: &anyhow::Result<AgentOutput>) -> (FailClass, String) {
match out {
Err(e) => (
FailClass::Other("error".to_owned()),
format!("{e:#}")
.lines()
.next()
.unwrap_or("")
.chars()
.take(160)
.collect(),
),
Ok(o) if o.quota_exhausted() => (FailClass::Quota, "rate limited (quota)".to_owned()),
Ok(o) if o.timed_out => (FailClass::Timeout, "timed out".to_owned()),
Ok(o) => (
FailClass::Other("unusable".to_owned()),
format!("nothing usable (exit {:?})", o.exit_code),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::AgentKind;
use jiff::Timestamp;
use std::collections::BTreeMap;
fn spec(id: &str) -> AgentSpec {
AgentSpec {
id: id.to_owned(),
kind: AgentKind::Command,
model: None,
command: vec!["true".to_owned()],
extra_args: Vec::new(),
env: BTreeMap::new(),
prompt_delivery: None,
}
}
fn handover(node: &str, to: &str) -> Handover {
Handover {
at: Timestamp::now(),
node: node.to_owned(),
seat: "fix".to_owned(),
from: "x".to_owned(),
to: to.to_owned(),
reason: String::new(),
}
}
#[test]
fn the_start_follows_the_last_fixer_handover_on_the_chain() {
let chain = [spec("a"), spec("b"), spec("c")];
assert_eq!(sticky_start(&chain, &[]), 0);
assert_eq!(sticky_start(&chain, &[handover("review", "c")]), 0);
assert_eq!(sticky_start(&chain, &[handover("fix", "z")]), 0);
assert_eq!(
sticky_start(&chain, &[handover("fix", "b"), handover("rebase", "c")]),
2
);
assert_eq!(
sticky_start(&chain, &[handover("fix", "c"), handover("land", "a")]),
0
);
}
}