Skip to main content

agent_base/engine/
react_loop_guard.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4use crate::types::{FinishReason, SessionId};
5
6/// Guard decision result
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum GuardAction {
9    /// Continue loop, inject nudge message
10    Continue(String),
11    /// End loop, return done
12    Done,
13    /// End loop, return failure
14    Fail(String),
15}
16
17/// Guard context information
18#[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    // RunState information
27    pub reasoning_only_strikes: usize,
28    pub empty_response_strikes: usize,
29    pub run_has_tool_calls: bool,
30}
31
32/// React Loop Guard trait
33///
34/// Each method corresponds to an abnormal branch, returns GuardAction to decide next step.
35/// Implementors can:
36/// - Use Focus for intelligent judgment
37/// - Use counters + thresholds
38/// - Pass through or intercept directly
39#[async_trait]
40pub trait ReactLoopGuard: Send + Sync {
41    /// Model returns only reasoning, no text/tool call
42    async fn on_reasoning_only(&self, ctx: &GuardCtx) -> GuardAction;
43
44    /// Model returns empty (no text, no reasoning, no tool call)
45    async fn on_empty_response(&self, ctx: &GuardCtx) -> GuardAction;
46
47    /// Model returns text-only (no tool call)
48    ///
49    /// Note: This method is called after middleware.
50    /// If middleware has set follow_up_message, this branch won't be entered.
51    async fn on_text_only(&self, ctx: &GuardCtx) -> GuardAction;
52}
53
54/// No-op guard for backward compatibility
55pub 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}