use async_trait::async_trait;
use crate::types::AgentResult;
use super::middleware::{Middleware, PostLlmCtx};
pub struct AutoContinueMiddleware {
prompt: String,
}
impl AutoContinueMiddleware {
pub fn new() -> Self {
Self {
prompt: "Please continue.".to_string(),
}
}
pub fn with_prompt(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
}
}
}
impl Default for AutoContinueMiddleware {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Middleware for AutoContinueMiddleware {
async fn on_post_llm(&self, ctx: &mut PostLlmCtx) -> AgentResult<()> {
if ctx.finish_reason.is_truncated() && ctx.tool_calls.is_empty() {
tracing::info!(
session_id = ctx.session_id.id,
turn = ctx.turn_count,
"text-only response truncated — injecting auto-continue prompt"
);
ctx.follow_up_message = Some(self.prompt.clone());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{FinishReason, SessionId};
fn ctx(finish_reason: FinishReason, tool_calls: Vec<(String, String, String)>) -> PostLlmCtx {
PostLlmCtx {
session_id: SessionId::new(1),
full_text: "partial answer...".to_string(),
is_tool_call: !tool_calls.is_empty(),
tool_calls,
available_tools: vec![],
turn_count: 1,
total_tool_calls: 0,
nudge_count: 0,
turn_tool_calls: 0,
skip_push: false,
follow_up_message: None,
finish_reason,
}
}
#[tokio::test]
async fn triggers_on_truncated_text_only() {
let mw = AutoContinueMiddleware::new();
let mut c = ctx(
FinishReason::Truncated {
reason: Some("max_tokens".into()),
},
vec![],
);
mw.on_post_llm(&mut c).await.unwrap();
assert_eq!(c.follow_up_message, Some("Please continue.".to_string()));
}
#[tokio::test]
async fn triggers_on_truncated_no_reason() {
let mw = AutoContinueMiddleware::new();
let mut c = ctx(FinishReason::Truncated { reason: None }, vec![]);
mw.on_post_llm(&mut c).await.unwrap();
assert_eq!(c.follow_up_message, Some("Please continue.".to_string()));
}
#[tokio::test]
async fn skips_when_tool_calls_present() {
let mw = AutoContinueMiddleware::new();
let mut c = ctx(
FinishReason::Truncated {
reason: Some("length".into()),
},
vec![("id".into(), "shell".into(), "{}".into())],
);
mw.on_post_llm(&mut c).await.unwrap();
assert!(c.follow_up_message.is_none());
}
#[tokio::test]
async fn skips_when_stop() {
let mw = AutoContinueMiddleware::new();
let mut c = ctx(FinishReason::Stop, vec![]);
mw.on_post_llm(&mut c).await.unwrap();
assert!(c.follow_up_message.is_none());
}
#[tokio::test]
async fn skips_when_tool_use() {
let mw = AutoContinueMiddleware::new();
let mut c = ctx(FinishReason::ToolUse, vec![]);
mw.on_post_llm(&mut c).await.unwrap();
assert!(c.follow_up_message.is_none());
}
#[tokio::test]
async fn custom_prompt() {
let mw = AutoContinueMiddleware::with_prompt("Continue please.");
let mut c = ctx(
FinishReason::Truncated {
reason: Some("max_tokens".into()),
},
vec![],
);
mw.on_post_llm(&mut c).await.unwrap();
assert_eq!(c.follow_up_message, Some("Continue please.".to_string()));
}
}