use super::super::task_planner::TaskPlanner;
use super::super::types::*;
#[derive(Debug)]
pub(super) enum ReflectionOutcome {
Continue,
Nudge(String),
Break,
}
pub(super) fn reflect_simple(
assistant_content: &Option<String>,
all_tools_len: usize,
iterations: usize,
no_tool_retries: &mut usize,
max_no_tool_retries: usize,
event_sink: &mut dyn EventSink,
messages: &mut Vec<ChatMessage>,
) -> ReflectionOutcome {
if iterations == 1 && all_tools_len > 0 && *no_tool_retries == 0 {
tracing::info!(
"First iteration produced no tool calls with {} tools available, nudging",
all_tools_len
);
if messages.last().is_some_and(|m| m.role == "assistant") {
messages.pop();
}
*no_tool_retries += 1;
return ReflectionOutcome::Nudge(
"You responded with text but did not call any tools. \
If the task requires action (browsing, file I/O, computation, etc.), \
you MUST call the appropriate tool functions. Do not describe what you \
would do — actually do it by invoking the tools."
.to_string(),
);
}
if let Some(ref content) = assistant_content {
event_sink.on_text(content);
}
*no_tool_retries += 1;
if *no_tool_retries >= max_no_tool_retries || assistant_content.is_some() {
ReflectionOutcome::Break
} else {
ReflectionOutcome::Continue
}
}
pub(super) fn reflect_planning(
assistant_content: &Option<String>,
suppress_stream: bool,
planner: &mut TaskPlanner,
consecutive_no_tool: &mut usize,
max_no_tool_retries: usize,
event_sink: &mut dyn EventSink,
messages: &mut Vec<ChatMessage>,
) -> ReflectionOutcome {
if suppress_stream {
if messages.last().is_some_and(|m| m.role == "assistant") {
messages.pop();
}
tracing::info!(
"Anti-hallucination: silently rejected text-only response \
(no tools executed yet, plan requires tool tasks)"
);
*consecutive_no_tool += 1;
if *consecutive_no_tool >= max_no_tool_retries {
tracing::warn!(
"LLM failed to start execution after {} attempts, stopping",
max_no_tool_retries
);
return ReflectionOutcome::Break;
}
if let Some(nudge) = planner.build_nudge_message() {
return ReflectionOutcome::Nudge(format!(
"CRITICAL: You just described what you would do but did NOT \
actually execute anything. The task plan has been generated — \
now you must EXECUTE each task step by step. Call the required \
tools NOW. Do NOT claim completion before calling `complete_task`, \
and do NOT say the overall job is finished while pending tasks remain.\n\n{}\n\nIf the plan does not fit the goal, you may call update_task_plan to revise it.",
nudge
));
}
return ReflectionOutcome::Break;
}
if planner.all_completed() || planner.is_empty() {
if let Some(ref content) = assistant_content {
event_sink.on_text(content);
}
return ReflectionOutcome::Break;
}
if messages.last().is_some_and(|m| m.role == "assistant") {
messages.pop();
}
tracing::info!("Swallowed no-tool assistant text while tasks are pending");
*consecutive_no_tool += 1;
if *consecutive_no_tool >= max_no_tool_retries {
tracing::warn!(
"LLM failed to make progress after {} attempts, stopping",
max_no_tool_retries
);
return ReflectionOutcome::Break;
}
if let Some(nudge) = planner.build_nudge_message() {
tracing::info!(
"Auto-nudge (attempt {}): pending tasks remain",
*consecutive_no_tool
);
return ReflectionOutcome::Nudge(nudge);
}
ReflectionOutcome::Break
}
#[cfg(test)]
mod tests {
use super::*;
use crate::task_planner::TaskPlanner;
use crate::types::{ChatMessage, SilentEventSink, Task};
fn planner_with_tasks(tasks: Vec<Task>) -> TaskPlanner {
let mut planner = TaskPlanner::new(None, None, None);
planner.task_list = tasks;
planner
}
#[test]
fn test_reflect_simple_first_iteration_with_tools_nudges() {
let mut no_tool_retries = 0;
let mut messages = vec![
ChatMessage::user("Do something"),
ChatMessage::assistant("I will do it"),
];
let mut sink = SilentEventSink;
let content = Some("I will do it".to_string());
let out = reflect_simple(
&content,
5,
1,
&mut no_tool_retries,
3,
&mut sink,
&mut messages,
);
match &out {
ReflectionOutcome::Nudge(s) => {
assert!(s.contains("call the appropriate tool functions"));
assert!(s.contains("Do not describe"));
}
_ => panic!("expected Nudge, got {:?}", out),
}
assert_eq!(no_tool_retries, 1);
assert_eq!(messages.len(), 1);
}
#[test]
fn test_reflect_simple_break_when_has_content() {
let mut no_tool_retries = 1;
let mut messages = vec![];
let mut sink = SilentEventSink;
let content = Some("Here is the result".to_string());
let out = reflect_simple(
&content,
5,
2,
&mut no_tool_retries,
3,
&mut sink,
&mut messages,
);
assert!(matches!(out, ReflectionOutcome::Break));
assert_eq!(no_tool_retries, 2);
}
#[test]
fn test_reflect_simple_break_when_max_retries() {
let mut no_tool_retries = 2;
let mut messages = vec![];
let mut sink = SilentEventSink;
let content = None;
let out = reflect_simple(
&content,
5,
3,
&mut no_tool_retries,
3,
&mut sink,
&mut messages,
);
assert!(matches!(out, ReflectionOutcome::Break));
assert_eq!(no_tool_retries, 3);
}
#[test]
fn test_reflect_planning_all_completed_breaks() {
let mut planner = planner_with_tasks(vec![Task {
id: 1,
description: "Done".to_string(),
tool_hint: None,
completed: true,
}]);
let mut consecutive_no_tool = 0;
let mut messages = vec![];
let mut sink = SilentEventSink;
let content = Some("All done!".to_string());
let out = reflect_planning(
&content,
false,
&mut planner,
&mut consecutive_no_tool,
3,
&mut sink,
&mut messages,
);
assert!(matches!(out, ReflectionOutcome::Break));
}
#[test]
fn test_reflect_planning_pending_tasks_nudges() {
let mut planner = planner_with_tasks(vec![Task {
id: 1,
description: "Generate a page".to_string(),
tool_hint: Some("file_operation".to_string()),
completed: false,
}]);
let mut consecutive_no_tool = 0;
let mut messages = vec![ChatMessage::assistant("I'll summarize...")];
let mut sink = SilentEventSink;
let content = Some("I'll summarize...".to_string());
let out = reflect_planning(
&content,
false,
&mut planner,
&mut consecutive_no_tool,
3,
&mut sink,
&mut messages,
);
match &out {
ReflectionOutcome::Nudge(s) => {
assert!(s.contains("task") || s.contains("Task") || s.contains("pending"));
}
ReflectionOutcome::Break => {}
_ => panic!("expected Nudge or Break, got {:?}", out),
}
assert_eq!(messages.len(), 0);
}
#[test]
fn test_reflect_planning_empty_plan_preserves_response() {
let mut planner = planner_with_tasks(vec![]);
let mut consecutive_no_tool = 0;
let mut messages = vec![ChatMessage::assistant("Here is the answer")];
let mut sink = SilentEventSink;
let content = Some("Here is the answer".to_string());
let out = reflect_planning(
&content,
false,
&mut planner,
&mut consecutive_no_tool,
3,
&mut sink,
&mut messages,
);
assert!(matches!(out, ReflectionOutcome::Break));
assert_eq!(
messages.len(),
1,
"assistant message must NOT be popped for empty plans"
);
}
}