agent_base/engine/
react_loop_guard.rs1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4use crate::types::{FinishReason, SessionId};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum GuardAction {
9 Continue(String),
11 Done,
13 Fail(String),
15}
16
17#[derive(Debug, Clone)]
19pub struct GuardCtx {
20 pub session_id: SessionId,
21 pub turn_count: u32,
22 pub user_input: String,
23 pub model_response: String,
24 pub finish_reason: FinishReason,
25 pub available_tools: Vec<String>,
26 pub reasoning_only_strikes: usize,
28 pub empty_response_strikes: usize,
29 pub run_has_tool_calls: bool,
30}
31
32#[async_trait]
40pub trait ReactLoopGuard: Send + Sync {
41 async fn on_reasoning_only(&self, ctx: &GuardCtx) -> GuardAction;
43
44 async fn on_empty_response(&self, ctx: &GuardCtx) -> GuardAction;
46
47 async fn on_text_only(&self, ctx: &GuardCtx) -> GuardAction;
52}
53
54pub struct NoopGuard;
56
57#[async_trait]
58impl ReactLoopGuard for NoopGuard {
59 async fn on_reasoning_only(&self, _ctx: &GuardCtx) -> GuardAction {
60 GuardAction::Done
61 }
62
63 async fn on_empty_response(&self, _ctx: &GuardCtx) -> GuardAction {
64 GuardAction::Done
65 }
66
67 async fn on_text_only(&self, _ctx: &GuardCtx) -> GuardAction {
68 GuardAction::Done
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[tokio::test]
77 async fn test_noop_guard_always_done() {
78 let guard = NoopGuard;
79 let ctx = GuardCtx {
80 session_id: SessionId {
81 id: 1,
82 external_id: None,
83 },
84 turn_count: 1,
85 user_input: "test".to_string(),
86 model_response: "response".to_string(),
87 finish_reason: FinishReason::Stop,
88 available_tools: vec![],
89 reasoning_only_strikes: 0,
90 empty_response_strikes: 0,
91 run_has_tool_calls: false,
92 };
93
94 assert!(matches!(
95 guard.on_reasoning_only(&ctx).await,
96 GuardAction::Done
97 ));
98 assert!(matches!(
99 guard.on_empty_response(&ctx).await,
100 GuardAction::Done
101 ));
102 assert!(matches!(guard.on_text_only(&ctx).await, GuardAction::Done));
103 }
104}