use std::collections::HashSet;
use crate::api::routes::agent::guard_registry::requested_exact_bullet_count;
use crate::api::routes::agent::guard_registry::{Guard, GuardContext, GuardId, GuardVerdict};
use crate::api::routes::agent::intent_registry::Intent;
pub(in crate::api::routes::agent) struct EmptyResponseGuard;
impl Guard for EmptyResponseGuard {
fn id(&self) -> GuardId {
GuardId::EmptyResponse
}
fn is_relevant(&self, _ctx: &GuardContext) -> bool {
true
}
fn evaluate(&self, content: &str, _ctx: &GuardContext) -> GuardVerdict {
if content.trim().is_empty() {
tracing::warn!("guard[EmptyResponse]: response is blank, requesting retry");
GuardVerdict::RetryRequested {
reason: "response was empty — no useful content for the user".into(),
}
} else {
GuardVerdict::Pass
}
}
}
pub(in crate::api::routes::agent) struct OutputContractGuard;
impl Guard for OutputContractGuard {
fn id(&self) -> GuardId {
GuardId::OutputContract
}
fn is_relevant(&self, ctx: &GuardContext) -> bool {
requested_exact_bullet_count(ctx.user_prompt).is_some()
}
fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
let Some(expected_bullets) = requested_exact_bullet_count(ctx.user_prompt) else {
return GuardVerdict::Pass;
};
let analysis = analyze_bullet_contract(content);
if analysis.bullet_count == expected_bullets && !analysis.has_non_bullet_content {
return GuardVerdict::Pass;
}
tracing::warn!(
expected_bullets,
actual_bullets = analysis.bullet_count,
has_non_bullet_content = analysis.has_non_bullet_content,
"guard[OutputContract]: output shape violated exact bullet contract"
);
GuardVerdict::RetryRequested {
reason: format!(
"response violated exact bullet contract: expected exactly {expected_bullets} bullet points and no extra framing"
),
}
}
}
#[derive(Debug, Clone, Copy)]
struct BulletContractAnalysis {
bullet_count: usize,
has_non_bullet_content: bool,
}
fn is_bullet_line(trimmed: &str) -> bool {
trimmed.starts_with("- ")
|| trimmed.starts_with("* ")
|| trimmed.starts_with("• ")
|| trimmed.starts_with("-\t")
|| trimmed.starts_with("*\t")
|| trimmed.starts_with("•\t")
}
fn analyze_bullet_contract(content: &str) -> BulletContractAnalysis {
let mut bullet_count = 0usize;
let mut has_non_bullet_content = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let is_bullet = is_bullet_line(trimmed);
if is_bullet {
bullet_count += 1;
} else {
has_non_bullet_content = true;
}
}
BulletContractAnalysis {
bullet_count,
has_non_bullet_content,
}
}
pub(in crate::api::routes::agent) struct NonRepetitionGuard;
impl Guard for NonRepetitionGuard {
fn id(&self) -> GuardId {
GuardId::NonRepetition
}
fn is_relevant(&self, ctx: &GuardContext) -> bool {
ctx.previous_assistant.is_some()
}
fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
if let Some(prev) = ctx.previous_assistant
&& looks_repetitive(content, prev)
&& user_requests_fresh_delta(ctx.user_prompt)
{
return GuardVerdict::Rewritten(
"No verified delta since my last report. Name the exact check you want \
and I will run it now."
.into(),
);
}
if let Some(echo) = (!ctx.prior_assistant_messages.is_empty())
.then(|| find_self_echo_across_history(content, ctx.prior_assistant_messages))
.flatten()
{
tracing::warn!(
echo_len = echo.len(),
"guard[NonRepetition]: self-echo from earlier turn detected"
);
return GuardVerdict::RetryRequested {
reason: format!(
"Your response reuses prose from an earlier turn verbatim: \"{}\". \
Generate fresh content — do not recycle your own earlier descriptions.",
if echo.len() > 60 {
format!("{}...", &echo[..57])
} else {
echo
}
),
};
}
GuardVerdict::Pass
}
}
fn find_self_echo_across_history(output: &str, prior_messages: &[String]) -> Option<String> {
let output_lower = output.to_ascii_lowercase();
let output_words: Vec<&str> = output_lower.split_whitespace().collect();
let min_words = 10;
if output_words.len() < min_words {
return None;
}
for prior in prior_messages {
let prior_lower = prior.to_ascii_lowercase();
for start in 0..=(output_words.len() - min_words) {
let phrase = output_words[start..start + min_words].join(" ");
if prior_lower.contains(&phrase) {
let mut end = start + min_words;
while end < output_words.len() {
let extended = output_words[start..=end].join(" ");
if !prior_lower.contains(&extended) {
break;
}
end += 1;
}
return Some(output_words[start..end].join(" "));
}
}
}
None
}
fn user_requests_fresh_delta(user_prompt: &str) -> bool {
let lower = user_prompt.to_ascii_lowercase();
const MARKERS: &[&str] = &[
"status",
"update",
"what changed",
"anything changed",
"fresh check",
"check again",
"still",
"latest",
"current",
"sitrep",
];
MARKERS.iter().any(|m| lower.contains(m))
}
fn repeat_tokens(text: &str) -> HashSet<String> {
text.to_ascii_lowercase()
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|tok| tok.len() >= 3)
.map(|tok| tok.to_string())
.collect()
}
fn common_prefix_ratio(a: &str, b: &str) -> f64 {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let max_len = a_chars.len().max(b_chars.len());
if max_len == 0 {
return 0.0;
}
let shared = a_chars
.iter()
.zip(b_chars.iter())
.take_while(|(ac, bc)| ac == bc)
.count();
shared as f64 / max_len as f64
}
fn looks_repetitive(current: &str, previous: &str) -> bool {
let cur = current.trim();
let prev = previous.trim();
if cur.is_empty() || prev.is_empty() {
return false;
}
if cur.eq_ignore_ascii_case(prev) {
return true;
}
if cur.len() < 80 || prev.len() < 80 {
return false;
}
let a = repeat_tokens(cur);
let b = repeat_tokens(prev);
if a.is_empty() || b.is_empty() {
return false;
}
let overlap = a.intersection(&b).count() as f64;
let denom = a.len().max(b.len()) as f64;
let overlap_ratio = overlap / denom;
let prefix_ratio = common_prefix_ratio(&cur.to_ascii_lowercase(), &prev.to_ascii_lowercase());
overlap_ratio >= 0.86 || (overlap_ratio >= 0.72 && prefix_ratio >= 0.55)
}
pub(in crate::api::routes::agent) struct LowValueParrotingGuard;
impl Guard for LowValueParrotingGuard {
fn id(&self) -> GuardId {
GuardId::LowValueParroting
}
fn is_relevant(&self, ctx: &GuardContext) -> bool {
ctx.tool_results.is_empty() && !ctx.has_intent(Intent::Execution)
}
fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
if is_low_value_response(content, ctx.intents)
|| is_parroting_user_prompt(ctx.user_prompt, content)
{
tracing::warn!("guard[LowValueParroting]: low-value or parroting response detected");
GuardVerdict::RetryRequested {
reason: "low-value placeholder or parroting response".into(),
}
} else {
GuardVerdict::Pass
}
}
}
fn is_low_value_response(response: &str, intents: &[Intent]) -> bool {
if intents.contains(&Intent::Acknowledgement) {
return false;
}
let trimmed = response.trim();
if trimmed.is_empty() {
return true;
}
let lower = trimmed.to_ascii_lowercase();
if lower == "ready"
|| lower == "on it"
|| lower == "working on that now"
|| lower == "working on that now."
|| lower == "i await your insights"
|| lower == "i await your insights."
{
return true;
}
const NOISE_MARKERS: &[&str] = &[
"ready",
"i await your insights",
"acknowledged. working on that now.",
"acknowledged. working on that now",
];
let lines = trimmed
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.collect::<Vec<_>>();
if !lines.is_empty()
&& lines.iter().all(|line| {
let low = line.to_ascii_lowercase();
NOISE_MARKERS.iter().any(|m| low == *m)
})
{
return true;
}
false
}
fn prompt_allows_echo(user_prompt: &str) -> bool {
let lower = user_prompt.to_ascii_lowercase();
const MARKERS: &[&str] = &[
"repeat",
"echo",
"quote",
"verbatim",
"paraphrase",
"summarize what i said",
"summarize my message",
];
MARKERS.iter().any(|m| lower.contains(m))
}
fn is_parroting_user_prompt(user_prompt: &str, response: &str) -> bool {
if prompt_allows_echo(user_prompt) {
return false;
}
let u = user_prompt.trim();
let r = response.trim();
if u.is_empty() || r.is_empty() {
return false;
}
let u_lower = u.to_ascii_lowercase();
let r_lower = r.to_ascii_lowercase();
if r_lower == u_lower {
return true;
}
let ut = repeat_tokens(&u_lower);
let rt = repeat_tokens(&r_lower);
if ut.is_empty() || rt.is_empty() {
return false;
}
let overlap = ut.intersection(&rt).count() as f64;
let overlap_vs_prompt = overlap / ut.len() as f64;
let prefix_ratio = common_prefix_ratio(&u_lower, &r_lower);
let length_ratio = (r.len() as f64 / u.len().max(1) as f64).clamp(0.0, 10.0);
overlap_vs_prompt >= 0.88 && prefix_ratio >= 0.55 && length_ratio <= 1.35
}
pub(in crate::api::routes::agent) struct UserEchoGuard;
impl Guard for UserEchoGuard {
fn id(&self) -> GuardId {
GuardId::UserEcho
}
fn is_relevant(&self, _ctx: &GuardContext) -> bool {
true
}
fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict {
if let Some(echo) = find_user_echo(ctx.user_prompt, content, 8) {
tracing::warn!(
echo_len = echo.len(),
"guard[UserEcho]: verbatim echo of user's words detected"
);
GuardVerdict::RetryRequested {
reason: format!(
"Response echoed the user's own words verbatim: \"{}\". \
React to the sentiment in your own words instead of quoting the user.",
if echo.len() > 60 {
format!("{}...", &echo[..57])
} else {
echo
}
),
}
} else {
GuardVerdict::Pass
}
}
}
fn find_user_echo(user_input: &str, output: &str, min_words: usize) -> Option<String> {
let user_words: Vec<&str> = user_input.split_whitespace().collect();
let output_lower = output.to_ascii_lowercase();
if user_words.len() < min_words {
return None;
}
for window_size in (min_words..=user_words.len()).rev() {
for start in 0..=(user_words.len() - window_size) {
let phrase = user_words[start..start + window_size]
.join(" ")
.to_ascii_lowercase();
if output_lower.contains(&phrase) {
return Some(phrase);
}
}
}
None
}