use async_trait::async_trait;
pub use agent_types::{GuardCtx, GuardDecision};
#[async_trait]
pub trait ReactLoopGuard: Send + Sync {
async fn on_turn(&self, ctx: &GuardCtx) -> GuardDecision;
async fn on_tool_call(&self, _ctx: &GuardCtx) -> GuardDecision {
GuardDecision::Complete
}
}
pub struct NoopGuard;
#[async_trait]
impl ReactLoopGuard for NoopGuard {
async fn on_turn(&self, ctx: &GuardCtx) -> GuardDecision {
if ctx.is_reasoning_only || ctx.is_empty_response {
GuardDecision::Fail {
error: if ctx.is_reasoning_only {
"model produced only reasoning, no output".to_string()
} else {
"model returned empty response".to_string()
},
}
} else {
GuardDecision::Complete
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use agent_types::{FinishReason, SessionId};
#[tokio::test]
async fn test_noop_guard_returns_complete() {
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,
all_user_inputs: vec!["test".to_string()],
is_reasoning_only: false,
is_empty_response: false,
is_text_only: false,
thinking_disabled: false,
original_thinking_enabled: true,
remaining_turns: 50,
};
assert!(matches!(guard.on_turn(&ctx).await, GuardDecision::Complete));
}
#[tokio::test]
async fn test_noop_guard_handles_degenerate_states() {
let guard = NoopGuard;
let ctx = GuardCtx {
session_id: SessionId::new(1),
turn_count: 1,
user_input: "test".to_string(),
model_response: "".to_string(),
finish_reason: FinishReason::Stop,
available_tools: vec![],
reasoning_only_strikes: 1,
empty_response_strikes: 0,
run_has_tool_calls: false,
all_user_inputs: vec!["test".to_string()],
is_reasoning_only: true,
is_empty_response: false,
is_text_only: false,
thinking_disabled: false,
original_thinking_enabled: true,
remaining_turns: 50,
};
assert!(matches!(
guard.on_turn(&ctx).await,
GuardDecision::Fail { .. }
));
let mut ctx2 = ctx.clone();
ctx2.is_reasoning_only = false;
ctx2.is_empty_response = true;
assert!(matches!(
guard.on_turn(&ctx2).await,
GuardDecision::Fail { .. }
));
let mut ctx3 = ctx.clone();
ctx3.is_reasoning_only = false;
ctx3.is_empty_response = false;
ctx3.is_text_only = true;
assert!(matches!(
guard.on_turn(&ctx3).await,
GuardDecision::Complete
));
let mut ctx4 = ctx.clone();
ctx4.is_reasoning_only = false;
ctx4.is_text_only = false;
assert!(matches!(
guard.on_turn(&ctx4).await,
GuardDecision::Complete
));
}
}