use std::sync::Arc;
use awaken_runtime_contract::contract::lifecycle::StopConditionSpec;
use regex::Regex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopDecision {
Continue,
Stop { code: String, detail: String },
}
#[derive(Debug, Clone)]
pub struct StopPolicyStats {
pub step_count: u32,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub elapsed_ms: u64,
pub consecutive_errors: u32,
pub last_tool_names: Vec<String>,
pub last_response_text: String,
pub recent_response_texts: Vec<String>,
}
pub trait StopPolicy: Send + Sync + 'static {
fn id(&self) -> &str;
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision;
}
pub struct MaxRoundsPolicy {
pub max: usize,
}
impl MaxRoundsPolicy {
pub fn new(max: usize) -> Self {
Self { max }
}
}
impl StopPolicy for MaxRoundsPolicy {
fn id(&self) -> &str {
"max_rounds"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.max == 0 {
return StopDecision::Continue;
}
if stats.step_count as usize >= self.max {
StopDecision::Stop {
code: "max_rounds".into(),
detail: format!("reached {} rounds", self.max),
}
} else {
StopDecision::Continue
}
}
}
pub struct TokenBudgetPolicy {
pub max_total: u64,
}
impl TokenBudgetPolicy {
pub fn new(max_total: u64) -> Self {
Self { max_total }
}
}
impl StopPolicy for TokenBudgetPolicy {
fn id(&self) -> &str {
"token_budget"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.max_total == 0 {
return StopDecision::Continue;
}
let total = stats.total_input_tokens + stats.total_output_tokens;
if total > self.max_total {
StopDecision::Stop {
code: "token_budget".into(),
detail: format!("token usage {} exceeds budget {}", total, self.max_total),
}
} else {
StopDecision::Continue
}
}
}
pub struct TimeoutPolicy {
pub max_ms: u64,
}
impl TimeoutPolicy {
pub fn new(max_ms: u64) -> Self {
Self { max_ms }
}
}
impl StopPolicy for TimeoutPolicy {
fn id(&self) -> &str {
"timeout"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.max_ms == 0 {
return StopDecision::Continue;
}
if stats.elapsed_ms > self.max_ms {
StopDecision::Stop {
code: "timeout".into(),
detail: format!(
"elapsed {}ms exceeds limit {}ms",
stats.elapsed_ms, self.max_ms
),
}
} else {
StopDecision::Continue
}
}
}
pub struct ConsecutiveErrorsPolicy {
pub max: u32,
}
impl ConsecutiveErrorsPolicy {
pub fn new(max: u32) -> Self {
Self { max }
}
}
impl StopPolicy for ConsecutiveErrorsPolicy {
fn id(&self) -> &str {
"consecutive_errors"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.max == 0 {
return StopDecision::Continue;
}
if stats.consecutive_errors >= self.max {
StopDecision::Stop {
code: "consecutive_errors".into(),
detail: format!(
"{} consecutive inference errors (limit {})",
stats.consecutive_errors, self.max
),
}
} else {
StopDecision::Continue
}
}
}
pub struct StopOnToolPolicy {
tool_name: String,
}
impl StopOnToolPolicy {
pub fn new(tool_name: impl Into<String>) -> Self {
Self {
tool_name: tool_name.into(),
}
}
}
impl StopPolicy for StopOnToolPolicy {
fn id(&self) -> &str {
"stop_on_tool"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.tool_name.is_empty() {
return StopDecision::Continue;
}
if stats
.last_tool_names
.iter()
.any(|name| name == &self.tool_name)
{
StopDecision::Stop {
code: "stop_on_tool".into(),
detail: format!("tool {} was requested before execution", self.tool_name),
}
} else {
StopDecision::Continue
}
}
}
pub struct ContentMatchPolicy {
pattern: String,
regex: Result<Regex, String>,
}
impl ContentMatchPolicy {
pub fn new(pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
let regex = Regex::new(&pattern).map_err(|error| error.to_string());
Self { pattern, regex }
}
}
impl StopPolicy for ContentMatchPolicy {
fn id(&self) -> &str {
"content_match"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.pattern.is_empty() {
return StopDecision::Continue;
}
let regex = match &self.regex {
Ok(regex) => regex,
Err(error) => {
return StopDecision::Stop {
code: "content_match_invalid_regex".into(),
detail: format!(
"content_match stop condition has invalid regex {}: {error}",
self.pattern
),
};
}
};
if regex.is_match(&stats.last_response_text) {
StopDecision::Stop {
code: "content_match".into(),
detail: format!("response matched stop regex {}", self.pattern),
}
} else {
StopDecision::Continue
}
}
}
pub struct LoopDetectionPolicy {
window: usize,
}
impl LoopDetectionPolicy {
pub fn new(window: usize) -> Self {
Self { window }
}
}
impl StopPolicy for LoopDetectionPolicy {
fn id(&self) -> &str {
"loop_detection"
}
fn evaluate(&self, stats: &StopPolicyStats) -> StopDecision {
if self.window < 2 || stats.recent_response_texts.len() < self.window {
return StopDecision::Continue;
}
let start = stats.recent_response_texts.len() - self.window;
let window = &stats.recent_response_texts[start..];
let Some(first) = window.first() else {
return StopDecision::Continue;
};
if first.is_empty() {
return StopDecision::Continue;
}
if window.iter().all(|item| item == first) {
StopDecision::Stop {
code: "loop_detection".into(),
detail: format!("same response repeated for {} steps", self.window),
}
} else {
StopDecision::Continue
}
}
}
pub fn policies_from_specs(specs: &[StopConditionSpec]) -> Vec<Arc<dyn StopPolicy>> {
specs
.iter()
.map(|spec| -> Arc<dyn StopPolicy> {
match spec {
StopConditionSpec::MaxRounds { rounds } => Arc::new(MaxRoundsPolicy::new(*rounds)),
StopConditionSpec::Timeout { seconds } => {
Arc::new(TimeoutPolicy::new(seconds.saturating_mul(1000)))
}
StopConditionSpec::TokenBudget { max_total } => {
Arc::new(TokenBudgetPolicy::new(*max_total as u64))
}
StopConditionSpec::ConsecutiveErrors { max } => {
Arc::new(ConsecutiveErrorsPolicy::new(*max as u32))
}
StopConditionSpec::StopOnTool { tool_name } => {
Arc::new(StopOnToolPolicy::new(tool_name.clone()))
}
StopConditionSpec::ContentMatch { pattern } => {
Arc::new(ContentMatchPolicy::new(pattern.clone()))
}
StopConditionSpec::LoopDetection { window } => {
Arc::new(LoopDetectionPolicy::new(*window))
}
}
})
.collect()
}