agent_base/engine/
react_loop_guard.rs1use async_trait::async_trait;
2
3pub use agent_types::{GuardCtx, GuardDecision};
5
6#[async_trait]
11pub trait ReactLoopGuard: Send + Sync {
12 async fn on_turn(&self, ctx: &GuardCtx) -> GuardDecision;
14
15 async fn on_tool_call(&self, _ctx: &GuardCtx) -> GuardDecision {
25 GuardDecision::Complete
26 }
27}
28
29pub struct NoopGuard;
35
36#[async_trait]
37impl ReactLoopGuard for NoopGuard {
38 async fn on_turn(&self, ctx: &GuardCtx) -> GuardDecision {
39 if ctx.is_reasoning_only || ctx.is_empty_response {
40 GuardDecision::Fail {
41 error: if ctx.is_reasoning_only {
42 "model produced only reasoning, no output".to_string()
43 } else {
44 "model returned empty response".to_string()
45 },
46 }
47 } else {
48 GuardDecision::Complete
49 }
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use agent_types::{FinishReason, SessionId};
57
58 #[tokio::test]
59 async fn test_noop_guard_returns_complete() {
60 let guard = NoopGuard;
61 let ctx = GuardCtx {
62 session_id: SessionId {
63 id: 1,
64 external_id: None,
65 },
66 turn_count: 1,
67 user_input: "test".to_string(),
68 model_response: "response".to_string(),
69 finish_reason: FinishReason::Stop,
70 available_tools: vec![],
71 reasoning_only_strikes: 0,
72 empty_response_strikes: 0,
73 run_has_tool_calls: false,
74 all_user_inputs: vec!["test".to_string()],
75 is_reasoning_only: false,
76 is_empty_response: false,
77 is_text_only: false,
78 thinking_disabled: false,
79 original_thinking_enabled: true,
80 remaining_turns: 50,
81 };
82
83 assert!(matches!(guard.on_turn(&ctx).await, GuardDecision::Complete));
84 }
85
86 #[tokio::test]
87 async fn test_noop_guard_handles_degenerate_states() {
88 let guard = NoopGuard;
89
90 let ctx = GuardCtx {
92 session_id: SessionId::new(1),
93 turn_count: 1,
94 user_input: "test".to_string(),
95 model_response: "".to_string(),
96 finish_reason: FinishReason::Stop,
97 available_tools: vec![],
98 reasoning_only_strikes: 1,
99 empty_response_strikes: 0,
100 run_has_tool_calls: false,
101 all_user_inputs: vec!["test".to_string()],
102 is_reasoning_only: true,
103 is_empty_response: false,
104 is_text_only: false,
105 thinking_disabled: false,
106 original_thinking_enabled: true,
107 remaining_turns: 50,
108 };
109 assert!(matches!(
110 guard.on_turn(&ctx).await,
111 GuardDecision::Fail { .. }
112 ));
113
114 let mut ctx2 = ctx.clone();
116 ctx2.is_reasoning_only = false;
117 ctx2.is_empty_response = true;
118 assert!(matches!(
119 guard.on_turn(&ctx2).await,
120 GuardDecision::Fail { .. }
121 ));
122
123 let mut ctx3 = ctx.clone();
125 ctx3.is_reasoning_only = false;
126 ctx3.is_empty_response = false;
127 ctx3.is_text_only = true;
128 assert!(matches!(
129 guard.on_turn(&ctx3).await,
130 GuardDecision::Complete
131 ));
132
133 let mut ctx4 = ctx.clone();
135 ctx4.is_reasoning_only = false;
136 ctx4.is_text_only = false;
137 assert!(matches!(
138 guard.on_turn(&ctx4).await,
139 GuardDecision::Complete
140 ));
141 }
142}