use super::failure_tracker::FailureTracker;
use super::message::LoopMessage;
use super::storm::{StormBreaker, StormReport};
use super::tools::ToolCall;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Ok,
Error,
Timeout,
Denied,
}
impl Outcome {
pub fn classify(is_error: bool, excerpt: &str) -> Self {
if !is_error {
return Outcome::Ok;
}
if crate::agent::tools::is_permission_denial(excerpt) {
Outcome::Denied
} else if excerpt.contains("timed out after") || excerpt.contains("auto-killed after") {
Outcome::Timeout
} else {
Outcome::Error
}
}
}
pub struct LoopGuards {
storm: StormBreaker,
failures: Arc<FailureTracker>,
}
impl LoopGuards {
pub fn new(storm: StormBreaker, failures: Arc<FailureTracker>) -> Self {
Self { storm, failures }
}
pub fn reset_turn(&mut self) {
self.storm.reset();
}
pub fn inspect_calls(&mut self, calls: &[ToolCall]) -> (Vec<ToolCall>, StormReport) {
self.storm.filter_calls(calls)
}
pub fn record_result(&mut self, call: &ToolCall, is_error: bool, excerpt: &str) {
let outcome = Outcome::classify(is_error, excerpt);
self.storm.note_outcome(call, outcome);
self.failures.record(outcome, &call.name, excerpt);
}
pub fn poll_reflection(&self) -> Vec<LoopMessage> {
self.failures.poll_reflection()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_maps_success_error_and_timeout() {
assert_eq!(Outcome::classify(false, "all good"), Outcome::Ok);
assert_eq!(
Outcome::classify(true, "old_string not found"),
Outcome::Error
);
assert_eq!(
Outcome::classify(true, "Command timed out after 120s"),
Outcome::Timeout
);
assert_eq!(
Outcome::classify(true, "background shell auto-killed after 600s timeout"),
Outcome::Timeout
);
}
#[test]
fn timeout_text_on_a_success_is_still_ok() {
assert_eq!(
Outcome::classify(false, "note: a prior run timed out after 5s"),
Outcome::Ok
);
}
#[test]
fn classify_maps_permission_denials_to_denied() {
for text in [
"Permission denied: writes outside project",
"Permission denied by user",
"Permission denied (non-interactive mode)",
"Auto-approval denied by approval_provider: file is outside the project directory",
] {
assert_eq!(Outcome::classify(true, text), Outcome::Denied, "{text}");
}
}
#[test]
fn denial_text_on_a_success_is_still_ok() {
assert_eq!(
Outcome::classify(false, "Permission denied: (quoting a past run)"),
Outcome::Ok
);
}
#[test]
fn a_denial_that_also_timed_out_is_classified_denied() {
assert_eq!(
Outcome::classify(true, "Permission denied: command timed out after 1s"),
Outcome::Denied
);
}
}