use std::collections::VecDeque;
use crate::contracts::thread::ToolCall;
use crate::contracts::{RunContext, StoppedReason};
pub struct StopPolicyStats<'a> {
pub step: usize,
pub step_tool_call_count: usize,
pub total_tool_call_count: usize,
pub total_input_tokens: usize,
pub total_output_tokens: usize,
pub consecutive_errors: usize,
pub elapsed: std::time::Duration,
pub last_tool_calls: &'a [ToolCall],
pub last_text: &'a str,
pub tool_call_history: &'a VecDeque<Vec<String>>,
}
pub struct StopPolicyInput<'a> {
pub run_ctx: &'a RunContext,
pub stats: StopPolicyStats<'a>,
}
pub trait StopPolicy: Send + Sync {
fn id(&self) -> &str;
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason>;
}
pub struct MaxRounds(pub usize);
impl StopPolicy for MaxRounds {
fn id(&self) -> &str {
"max_rounds"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
if input.stats.step >= self.0 {
Some(StoppedReason::new("max_rounds_reached"))
} else {
None
}
}
}
pub struct Timeout(pub std::time::Duration);
impl StopPolicy for Timeout {
fn id(&self) -> &str {
"timeout"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
if input.stats.elapsed >= self.0 {
Some(StoppedReason::new("timeout_reached"))
} else {
None
}
}
}
pub struct TokenBudget {
pub max_total: usize,
}
impl StopPolicy for TokenBudget {
fn id(&self) -> &str {
"token_budget"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
if self.max_total > 0
&& (input.stats.total_input_tokens + input.stats.total_output_tokens) >= self.max_total
{
Some(StoppedReason::new("token_budget_exceeded"))
} else {
None
}
}
}
pub struct ConsecutiveErrors(pub usize);
impl StopPolicy for ConsecutiveErrors {
fn id(&self) -> &str {
"consecutive_errors"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
if self.0 > 0 && input.stats.consecutive_errors >= self.0 {
Some(StoppedReason::new("consecutive_errors_exceeded"))
} else {
None
}
}
}
pub struct StopOnTool(pub String);
impl StopPolicy for StopOnTool {
fn id(&self) -> &str {
"stop_on_tool"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
for call in input.stats.last_tool_calls {
if call.name == self.0 {
return Some(StoppedReason::with_detail("tool_called", self.0.clone()));
}
}
None
}
}
pub struct ContentMatch(pub String);
impl StopPolicy for ContentMatch {
fn id(&self) -> &str {
"content_match"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
if !self.0.is_empty() && input.stats.last_text.contains(&self.0) {
Some(StoppedReason::with_detail(
"content_matched",
self.0.clone(),
))
} else {
None
}
}
}
pub struct LoopDetection {
pub window: usize,
}
impl StopPolicy for LoopDetection {
fn id(&self) -> &str {
"loop_detection"
}
fn evaluate(&self, input: &StopPolicyInput<'_>) -> Option<StoppedReason> {
let window = self.window.max(2);
let history = input.stats.tool_call_history;
if history.len() < 2 {
return None;
}
let recent: Vec<_> = history.iter().rev().take(window).collect();
for pair in recent.windows(2) {
if pair[0] == pair[1] {
return Some(StoppedReason::new("loop_detected"));
}
}
None
}
}