agent_base/engine/
max_turns_nudge.rs1use async_trait::async_trait;
2
3use crate::types::AgentResult;
4
5use super::middleware::{Middleware, PreLlmCtx};
6
7#[derive(Clone, Debug)]
9pub struct MaxTurnsNudgeConfig {
10 pub threshold: u32,
13 pub message: String,
15}
16
17impl Default for MaxTurnsNudgeConfig {
18 fn default() -> Self {
19 Self {
20 threshold: 3,
21 message: "You are approaching the maximum number of turns. \
22 Please wrap up your current work and provide a final answer. \
23 Summarize what you've accomplished and any remaining tasks."
24 .to_string(),
25 }
26 }
27}
28
29pub struct MaxTurnsNudgeMiddleware {
52 config: MaxTurnsNudgeConfig,
53}
54
55impl MaxTurnsNudgeMiddleware {
56 pub fn new(config: MaxTurnsNudgeConfig) -> Self {
57 Self { config }
58 }
59}
60
61#[async_trait]
62impl Middleware for MaxTurnsNudgeMiddleware {
63 async fn on_pre_llm(&self, ctx: &mut PreLlmCtx) -> AgentResult<()> {
64 if self.config.threshold == 0 {
65 return Ok(());
66 }
67
68 let remaining_turns = ctx.max_turns.saturating_sub(ctx.turn_count);
69 if remaining_turns <= self.config.threshold {
70 tracing::info!(
71 session_id = ctx.session_id.id,
72 turn = ctx.turn_count,
73 remaining_turns,
74 max_turns = ctx.max_turns,
75 "max turns nudge: injecting nudge message"
76 );
77 ctx.messages.push(crate::types::ChatMessage::User {
78 content: self.config.message.clone(),
79 images: Vec::new(),
80 ephemeral: false,
81 });
82 }
83
84 Ok(())
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::types::SessionId;
92
93 fn make_ctx(turn_count: u32, max_turns: u32) -> PreLlmCtx {
94 PreLlmCtx {
95 session_id: SessionId::new(1),
96 messages: vec![],
97 tools: vec![],
98 emit_fn: None,
99 turn_count,
100 max_turns,
101 }
102 }
103
104 #[tokio::test]
105 async fn test_nudge_not_injected_when_far_from_limit() {
106 let mw = MaxTurnsNudgeMiddleware::new(MaxTurnsNudgeConfig {
107 threshold: 3,
108 message: "nudge".to_string(),
109 });
110 let mut ctx = make_ctx(1, 10);
111 mw.on_pre_llm(&mut ctx).await.unwrap();
112 assert!(ctx.messages.is_empty());
113 }
114
115 #[tokio::test]
116 async fn test_nudge_injected_at_threshold() {
117 let mw = MaxTurnsNudgeMiddleware::new(MaxTurnsNudgeConfig {
118 threshold: 3,
119 message: "nudge".to_string(),
120 });
121 let mut ctx = make_ctx(8, 10); mw.on_pre_llm(&mut ctx).await.unwrap();
123 assert_eq!(ctx.messages.len(), 1);
124 match &ctx.messages[0] {
125 crate::types::ChatMessage::User { content, .. } => assert_eq!(content, "nudge"),
126 _ => panic!("Expected User message"),
127 }
128 }
129
130 #[tokio::test]
131 async fn test_nudge_injected_at_exact_threshold() {
132 let mw = MaxTurnsNudgeMiddleware::new(MaxTurnsNudgeConfig {
133 threshold: 3,
134 message: "nudge".to_string(),
135 });
136 let mut ctx = make_ctx(7, 10); mw.on_pre_llm(&mut ctx).await.unwrap();
138 assert_eq!(ctx.messages.len(), 1);
139 }
140
141 #[tokio::test]
142 async fn test_nudge_disabled_when_threshold_zero() {
143 let mw = MaxTurnsNudgeMiddleware::new(MaxTurnsNudgeConfig {
144 threshold: 0,
145 message: "nudge".to_string(),
146 });
147 let mut ctx = make_ctx(10, 10); mw.on_pre_llm(&mut ctx).await.unwrap();
149 assert!(ctx.messages.is_empty());
150 }
151}