ras_agent/application/
detect_loop.rs1use ras_llm::ChatMessage;
2
3use crate::domain::loop_detector::ActionLoopDetector;
4
5#[must_use]
6pub fn build_loop_nudge(detector: &ActionLoopDetector) -> Option<ChatMessage> {
7 let mut parts: Vec<String> = Vec::new();
8 if detector.action_loop_detected() {
9 parts.push(
10 "Heads up: the same action repeated several times. Try a different element or strategy."
11 .into(),
12 );
13 }
14 if detector.page_stagnation_detected() {
15 parts.push(
16 "Heads up: the page state has not changed for several steps. Reassess your approach."
17 .into(),
18 );
19 }
20 if parts.is_empty() {
21 return None;
22 }
23 Some(ChatMessage::system(parts.join("\n\n")))
24}
25
26#[must_use]
27pub fn build_budget_warning(step: u32, max_steps: u32) -> Option<ChatMessage> {
28 if max_steps == 0 {
29 return None;
30 }
31 let pct = (step * 100) / max_steps.max(1);
32 if pct < 95 {
33 return None;
34 }
35 Some(ChatMessage::system(format!(
36 "You are at {pct}% of your step budget. Wrap up: call done if you have an answer, or pivot decisively."
37 )))
38}
39
40#[must_use]
46pub fn build_empty_action_nudge(prev_empty: bool) -> Option<ChatMessage> {
47 if !prev_empty {
48 return None;
49 }
50 Some(ChatMessage::system(
51 "Your previous response had NO action — that is not allowed. You MUST emit exactly one \
52 action this step. If the record you were searching for is genuinely not found, call the \
53 `done` action with a not-found result (its `text` set to a JSON object such as \
54 {\"found\": false}). Do NOT put your decision only in next_goal, memory, or the plan.",
55 ))
56}
57
58#[cfg(test)]
59mod tests {
60 use super::build_empty_action_nudge;
61 use ras_llm::ChatMessage;
62
63 #[test]
64 fn no_nudge_when_previous_step_had_an_action() {
65 assert!(build_empty_action_nudge(false).is_none());
66 }
67
68 #[test]
69 fn nudge_after_empty_demands_action_or_done_not_found() {
70 let text = match build_empty_action_nudge(true) {
71 Some(ChatMessage::System(s)) => s.content,
72 _ => String::new(),
73 };
74 assert!(!text.is_empty(), "an empty action must trigger a re-prompt");
75 let lo = text.to_lowercase();
76 assert!(lo.contains("action"), "demands an action");
77 assert!(lo.contains("done"), "offers the done not-found escape");
78 assert!(
79 lo.contains("next_goal") || lo.contains("not found"),
80 "addresses the next_goal stall"
81 );
82 }
83}