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 last_tool_calls_invalid: false,
75 all_user_inputs: vec!["test".to_string()],
76 is_reasoning_only: false,
77 is_empty_response: false,
78 is_text_only: false,
79 thinking_disabled: false,
80 original_thinking_enabled: true,
81 remaining_turns: 50,
82 };
83
84 assert!(matches!(guard.on_turn(&ctx).await, GuardDecision::Complete));
85 }
86
87 #[tokio::test]
88 async fn test_noop_guard_handles_degenerate_states() {
89 let guard = NoopGuard;
90
91 let ctx = GuardCtx {
93 session_id: SessionId::new(1),
94 turn_count: 1,
95 user_input: "test".to_string(),
96 model_response: "".to_string(),
97 finish_reason: FinishReason::Stop,
98 available_tools: vec![],
99 reasoning_only_strikes: 1,
100 empty_response_strikes: 0,
101 run_has_tool_calls: false,
102 last_tool_calls_invalid: false,
103 all_user_inputs: vec!["test".to_string()],
104 is_reasoning_only: true,
105 is_empty_response: false,
106 is_text_only: false,
107 thinking_disabled: false,
108 original_thinking_enabled: true,
109 remaining_turns: 50,
110 };
111 assert!(matches!(
112 guard.on_turn(&ctx).await,
113 GuardDecision::Fail { .. }
114 ));
115
116 let mut ctx2 = ctx.clone();
118 ctx2.is_reasoning_only = false;
119 ctx2.is_empty_response = true;
120 assert!(matches!(
121 guard.on_turn(&ctx2).await,
122 GuardDecision::Fail { .. }
123 ));
124
125 let mut ctx3 = ctx.clone();
127 ctx3.is_reasoning_only = false;
128 ctx3.is_empty_response = false;
129 ctx3.is_text_only = true;
130 assert!(matches!(
131 guard.on_turn(&ctx3).await,
132 GuardDecision::Complete
133 ));
134
135 let mut ctx4 = ctx.clone();
137 ctx4.is_reasoning_only = false;
138 ctx4.is_text_only = false;
139 assert!(matches!(
140 guard.on_turn(&ctx4).await,
141 GuardDecision::Complete
142 ));
143 }
144}