use super::types::{AttemptSummary, DetectionConfig, MetaSignal, StuckRequest};
const UNCERTAINTY_PATTERNS: &[&str] = &[
"I'm not certain",
"I couldn't find definitive",
"This might be",
"I would need",
"Without access to",
"I'm not sure",
"It's unclear",
"I don't have enough information",
];
const STUCK_PATTERNS: &[&str] = &[
"I've tried several approaches",
"I'm not making progress",
"I'm going in circles",
"I need clarification",
"I'm stuck",
"I can't figure out",
"I've exhausted",
];
pub fn detect_meta_signal(output: &str, config: &DetectionConfig) -> Option<MetaSignal> {
if let Some(signal) = parse_explicit_answer(output) {
return Some(signal);
}
if let Some(signal) = parse_explicit_uncertain(output) {
return Some(signal);
}
if let Some(signal) = parse_explicit_stuck(output) {
return Some(signal);
}
if let Some(signal) = parse_explicit_yield(output) {
return Some(signal);
}
if let Some(signal) = parse_explicit_thinking(output) {
return Some(signal);
}
if config.detect_implicit {
if let Some(signal) = detect_stuck_patterns(output) {
return Some(signal);
}
if let Some(signal) = detect_uncertainty_patterns(output) {
return Some(signal);
}
}
None
}
fn parse_explicit_answer(output: &str) -> Option<MetaSignal> {
let start_idx = output.find("<answer")?;
let end_tag = "</answer>";
let end_idx = output.find(end_tag)?;
let tag_content = &output[start_idx..end_idx + end_tag.len()];
let confidence = extract_attribute(tag_content, "confidence")
.and_then(|s| s.parse::<f32>().ok())
.unwrap_or(0.8);
let inner_start = tag_content.find('>')? + 1;
let inner = &tag_content[inner_start..tag_content.len() - end_tag.len()];
let caveats = extract_all_tags(inner, "caveat");
let mut content = inner.to_string();
for caveat in &caveats {
let caveat_tag = format!("<caveat>{caveat}</caveat>");
content = content.replace(&caveat_tag, "");
}
let content = content.trim().to_string();
Some(MetaSignal::Answer {
content,
confidence: confidence.clamp(0.0, 1.0),
caveats,
})
}
fn parse_explicit_uncertain(output: &str) -> Option<MetaSignal> {
let start_tag = "<uncertain>";
let end_tag = "</uncertain>";
let start_idx = output.find(start_tag)?;
let end_idx = output.find(end_tag)?;
let inner = &output[start_idx + start_tag.len()..end_idx];
let partial_answer = extract_tag_content(inner, "partial");
let missing_information = extract_all_tags(inner, "missing");
let would_help = extract_all_tags(inner, "would_help");
Some(MetaSignal::Uncertain {
partial_answer,
missing_information,
would_help,
})
}
fn parse_explicit_stuck(output: &str) -> Option<MetaSignal> {
let start_tag = "<stuck>";
let end_tag = "</stuck>";
let start_idx = output.find(start_tag)?;
let end_idx = output.find(end_tag)?;
let inner = &output[start_idx + start_tag.len()..end_idx];
let hypothesis = extract_tag_content(inner, "hypothesis");
let attempts = extract_all_tags(inner, "attempt")
.into_iter()
.map(|desc| AttemptSummary {
description: desc,
outcome: String::new(),
})
.collect();
let request_text = extract_tag_content(inner, "request");
let request = match request_text.as_deref() {
Some(text) if text.contains("clarif") => {
StuckRequest::Clarification(vec![text.to_string()])
},
Some(text) if text.contains("context") => StuckRequest::MoreContext {
about: text.to_string(),
},
Some(text) if text.contains("tool") => StuckRequest::DifferentTools {
need: vec![text.to_string()],
},
Some(text) => StuckRequest::HumanIntervention {
reason: text.to_string(),
},
None => StuckRequest::HumanIntervention {
reason: "Agent is stuck".to_string(),
},
};
Some(MetaSignal::Stuck {
attempts,
hypothesis,
request,
})
}
fn parse_explicit_yield(output: &str) -> Option<MetaSignal> {
let start_tag = "<yield>";
let end_tag = "</yield>";
let start_idx = output.find(start_tag)?;
let end_idx = output.find(end_tag)?;
let inner = &output[start_idx + start_tag.len()..end_idx];
let partial_progress = extract_tag_content(inner, "partial");
let suggested_expertise = extract_all_tags(inner, "expertise");
Some(MetaSignal::Yield {
partial_progress,
suggested_expertise,
})
}
fn parse_explicit_thinking(output: &str) -> Option<MetaSignal> {
let start_idx = output.find("<thinking")?;
let end_tag = "</thinking>";
let end_idx = output.find(end_tag)?;
let tag_content = &output[start_idx..end_idx + end_tag.len()];
let direction = extract_attribute(tag_content, "direction").unwrap_or_default();
let estimated_steps =
extract_attribute(tag_content, "steps").and_then(|s| s.parse::<u32>().ok());
let direction = if direction.is_empty() {
let inner_start = tag_content.find('>')? + 1;
let inner = &tag_content[inner_start..tag_content.len() - end_tag.len()];
inner.trim().to_string()
} else {
direction
};
if direction.is_empty() {
return None;
}
Some(MetaSignal::Thinking {
direction,
estimated_steps,
})
}
fn detect_uncertainty_patterns(output: &str) -> Option<MetaSignal> {
let lower = output.to_lowercase();
let matched: Vec<&&str> = UNCERTAINTY_PATTERNS
.iter()
.filter(|p| lower.contains(&p.to_lowercase()))
.collect();
if matched.is_empty() {
return None;
}
Some(MetaSignal::Uncertain {
partial_answer: Some(output.to_string()),
missing_information: vec![],
would_help: vec![],
})
}
fn detect_stuck_patterns(output: &str) -> Option<MetaSignal> {
let lower = output.to_lowercase();
let matched: Vec<&&str> = STUCK_PATTERNS
.iter()
.filter(|p| lower.contains(&p.to_lowercase()))
.collect();
if matched.is_empty() {
return None;
}
Some(MetaSignal::Stuck {
attempts: vec![],
hypothesis: None,
request: StuckRequest::HumanIntervention {
reason: "Implicit stuck signal detected".to_string(),
},
})
}
fn extract_attribute(tag: &str, attr: &str) -> Option<String> {
let pattern = format!("{attr}=\"");
let start = tag.find(&pattern)? + pattern.len();
let rest = &tag[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn extract_tag_content(text: &str, tag: &str) -> Option<String> {
let start_tag = format!("<{tag}>");
let end_tag = format!("</{tag}>");
let start = text.find(&start_tag)? + start_tag.len();
let end = text.find(&end_tag)?;
Some(text[start..end].trim().to_string())
}
fn extract_all_tags(text: &str, tag: &str) -> Vec<String> {
let start_tag = format!("<{tag}>");
let end_tag = format!("</{tag}>");
let mut results = Vec::new();
let mut search_from = 0;
while let Some(start) = text[search_from..].find(&start_tag) {
let abs_start = search_from + start + start_tag.len();
if let Some(end) = text[abs_start..].find(&end_tag) {
let content = text[abs_start..abs_start + end].trim().to_string();
results.push(content);
search_from = abs_start + end + end_tag.len();
} else {
break;
}
}
results
}