use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::types::{FinishReason, SessionId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GuardAction {
Continue(String),
Done,
Fail(String),
}
#[derive(Debug, Clone)]
pub struct GuardCtx {
pub session_id: SessionId,
pub turn_count: u32,
pub user_input: String,
pub model_response: String,
pub finish_reason: FinishReason,
pub available_tools: Vec<String>,
pub reasoning_only_strikes: usize,
pub empty_response_strikes: usize,
pub run_has_tool_calls: bool,
}
#[async_trait]
pub trait ReactLoopGuard: Send + Sync {
async fn on_reasoning_only(&self, ctx: &GuardCtx) -> GuardAction;
async fn on_empty_response(&self, ctx: &GuardCtx) -> GuardAction;
async fn on_text_only(&self, ctx: &GuardCtx) -> GuardAction;
}
pub struct NoopGuard;
#[async_trait]
impl ReactLoopGuard for NoopGuard {
async fn on_reasoning_only(&self, _ctx: &GuardCtx) -> GuardAction {
GuardAction::Done
}
async fn on_empty_response(&self, _ctx: &GuardCtx) -> GuardAction {
GuardAction::Done
}
async fn on_text_only(&self, _ctx: &GuardCtx) -> GuardAction {
GuardAction::Done
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_noop_guard_always_done() {
let guard = NoopGuard;
let ctx = GuardCtx {
session_id: SessionId {
id: 1,
external_id: None,
},
turn_count: 1,
user_input: "test".to_string(),
model_response: "response".to_string(),
finish_reason: FinishReason::Stop,
available_tools: vec![],
reasoning_only_strikes: 0,
empty_response_strikes: 0,
run_has_tool_calls: false,
};
assert!(matches!(
guard.on_reasoning_only(&ctx).await,
GuardAction::Done
));
assert!(matches!(
guard.on_empty_response(&ctx).await,
GuardAction::Done
));
assert!(matches!(guard.on_text_only(&ctx).await, GuardAction::Done));
}
}