mod support;
use std::sync::{Arc, Mutex};
use support::*;
use yoagent::extension::*;
use yoagent::provider::mock::MockResponse;
use yoagent::*;
struct TestsReported {
required: bool,
endings: Arc<Mutex<Vec<Ending>>>,
}
struct Ending {
end: RunEnd,
asked: usize,
}
struct TestsReportedRun {
asked: usize,
endings: Arc<Mutex<Vec<Ending>>>,
}
#[async_trait::async_trait]
impl Extension for TestsReported {
fn name(&self) -> &str {
"tests-reported"
}
fn mode(&self) -> ExtensionMode {
if self.required {
ExtensionMode::Required
} else {
ExtensionMode::Advisory
}
}
async fn start_run(&self, _run: &RunContext<'_>) -> Result<Box<dyn RunHooks>, ExtensionError> {
Ok(Box::new(TestsReportedRun {
asked: 0,
endings: self.endings.clone(),
}))
}
}
#[async_trait::async_trait]
impl RunHooks for TestsReportedRun {
async fn on_stop(&mut self, stop: &StopContext<'_>) -> StopDecision {
let Message::Assistant { content, .. } = stop.answer else {
return StopDecision::Accept;
};
if text_of(content).contains("Tests: ") {
StopDecision::Accept
} else {
self.asked += 1;
StopDecision::Continue(
"Run the tests and end with `Tests: passed` or `Tests: failed`.".into(),
)
}
}
async fn finish(&mut self, outcome: &RunOutcome) {
println!(
" finish: {:?} (asked to continue {} time(s))",
outcome.end(),
self.asked
);
self.endings.lock().unwrap().push(Ending {
end: outcome.end().clone(),
asked: self.asked,
});
}
}
async fn demo(title: &str, required: bool, script: Vec<MockResponse>) -> (Ending, String) {
println!("{title}");
let endings = Arc::new(Mutex::new(Vec::new()));
let mut agent = new_agent(script)
.with_system_prompt("You are a coding assistant.")
.with_max_stop_continues(2)
.with_extension(TestsReported {
required,
endings: endings.clone(),
});
let events = run(&mut agent, "Fix the off-by-one in pagination.").await;
let last = last_text(&events);
println!(" last message: {last:?}\n");
let ending = endings.lock().unwrap().pop().expect("finish ran");
(ending, last)
}
#[tokio::main]
async fn main() {
let (ending, last) = demo(
"A model that reports after one reminder:",
false,
vec![
answer("Fixed the off-by-one."),
answer("Fixed the off-by-one. Tests: passed"),
],
)
.await;
check(
ending.end == RunEnd::Completed && ending.asked == 1 && last.ends_with("Tests: passed"),
"the run completed with the second answer, after one continue",
);
let never = || {
vec![
answer("Done."),
answer("Done, really."),
answer("All done."),
]
};
let (ending, last) = demo("An advisory verifier at its cap:", false, never()).await;
check(
ending.end == RunEnd::Completed && ending.asked == 3 && last == "All done.",
"an advisory verifier is asked three times; its third refusal is overridden (with a warning) at the cap",
);
let (ending, _) = demo("A required verifier at its cap:", true, never()).await;
check(
ending.asked == 3
&& matches!(&ending.end, RunEnd::Failed { extension: Some(name), .. } if name == "tests-reported"),
"a required verifier fails the run at the cap, after its third refusal",
);
}