use crate::llm_markers::INTENT_GATE_MARKER;
#[cfg(test)]
fn is_pseudo_tool_line(line: &str) -> bool {
let lower = line.trim().to_ascii_lowercase();
lower.starts_with("[tool_use:")
|| lower.starts_with("[tool_call:")
|| lower.starts_with("[function_call:")
|| lower.starts_with("[functioncall:")
}
#[cfg(test)]
fn is_tool_name_like(name: &str) -> bool {
if name.is_empty() {
return false;
}
let lower = name.to_ascii_lowercase();
matches!(
lower.as_str(),
"terminal"
| "browser"
| "web_search"
| "web_fetch"
| "system_info"
| "remember_fact"
| "manage_config"
| "send_file"
| "spawn_agent"
| "cli_agent"
| "manage_cli_agents"
| "health_probe"
| "manage_skills"
| "use_skill"
| "skill_resources"
| "manage_people"
| "manage_api"
| "http_request"
| "manage_http_auth"
| "manage_oauth"
| "read_channel_history"
) || lower.starts_with("mcp__")
|| lower.contains("__")
}
#[cfg(test)]
fn parse_name_field(line: &str) -> Option<String> {
let trimmed = line.trim();
let (key, value) = trimmed.split_once(':')?;
if !key.trim().eq_ignore_ascii_case("name") {
return None;
}
let name = value.trim();
if name.is_empty() || name.contains(' ') {
return None;
}
Some(name.to_string())
}
pub(super) fn looks_like_deferred_action_response(text: &str) -> bool {
let lower = text.trim().to_ascii_lowercase();
if has_action_promise(&lower) {
return true;
}
lower.contains("[consultation]")
|| lower.contains(&INTENT_GATE_MARKER.to_ascii_lowercase())
|| lower.contains("[tool_use:")
|| lower.contains("[tool_call:")
}
pub(super) fn has_action_promise(text: &str) -> bool {
let normalized = text.replace(['\u{2018}', '\u{2019}', '`', '\u{02BC}'], "'");
const KNOWLEDGE_ONLY_VERBS: &[&str] = &[
"explain",
"describe",
"summarize",
"clarify",
"elaborate",
"outline",
"note",
"mention",
"address",
"highlight",
"tell",
"share",
"say",
"answer",
"provide",
"be",
"give",
"offer",
"know",
"rephrase",
"restate",
"recall",
"confirm",
"remember",
"think",
"point",
"help",
];
let words: Vec<String> = normalized
.split_whitespace()
.map(|w| {
w.trim_matches(|c: char| c.is_ascii_punctuation() && c != '\'')
.to_lowercase()
})
.filter(|w| !w.is_empty())
.collect();
for i in 0..words.len() {
let verb_idx = if words[i] == "i'll" {
Some(i + 1)
} else if words[i] == "i" && words.get(i + 1).is_some_and(|w| w == "will") {
Some(i + 2)
} else if words[i] == "let" && words.get(i + 1).is_some_and(|w| w == "me") {
Some(i + 2)
} else if words[i] == "shall" && words.get(i + 1).is_some_and(|w| w == "i") {
Some(i + 2)
} else if words[i] == "would"
&& words.get(i + 1).is_some_and(|w| w == "you")
&& words.get(i + 2).is_some_and(|w| w == "like")
&& words.get(i + 3).is_some_and(|w| w == "me")
&& words.get(i + 4).is_some_and(|w| w == "to")
{
Some(i + 5)
} else {
None
};
if let Some(vi) = verb_idx {
if let Some(verb) = words.get(vi) {
if !KNOWLEDGE_ONLY_VERBS.contains(&verb.as_str()) {
return true;
}
}
}
}
false
}
pub(super) fn is_substantive_text_response(text: &str, min_len: usize) -> bool {
let trimmed = text.trim();
if trimmed.len() < min_len {
return false;
}
let substantive_lines: Vec<&str> = trimmed
.lines()
.filter(|line| {
let l = line.trim();
if l.is_empty() {
return false;
}
!has_action_promise(&l.to_ascii_lowercase())
})
.collect();
let substantive_text: String = substantive_lines.join(" ");
let substantive_len = substantive_text.trim().len();
substantive_len >= min_len
}
pub(super) fn looks_like_multi_part_request(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
let numbered_items = {
let re = regex::Regex::new(r"(?:^|\s)(?:\d+[.)]\s|[a-e][.)]\s|step\s+\d)").unwrap();
re.find_iter(&lower).count()
};
if numbered_items >= 2 {
return true;
}
let explanation_words = [
"explain why",
"explain how",
"tell me why",
"describe how",
"show me",
"what did you",
"summarize what",
"thorough review",
"find all",
"list all",
"review it",
"review the",
"audit",
];
let has_explanation_request = explanation_words.iter().any(|w| lower.contains(w));
let compound_signals = [
"also ",
"then ",
"after that",
"additionally",
"finally ",
"and then",
"before ",
"as well",
];
let compound_count = compound_signals
.iter()
.filter(|s| lower.contains(*s))
.count();
has_explanation_request || compound_count >= 2
}
#[cfg(test)]
pub(super) fn sanitize_response_analysis(analysis: &str) -> String {
let lines: Vec<&str> = analysis.lines().collect();
let has_pseudo_tool_block = lines.iter().any(|line| is_pseudo_tool_line(line));
let mut cleaned: Vec<String> = Vec::with_capacity(lines.len());
let mut i = 0usize;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
let lower = trimmed.to_ascii_lowercase();
if lower == "arguments:" {
let mut j = i + 1;
let mut block_has_tool_signature = false;
while j < lines.len() {
let next = lines[j].trim();
if next.is_empty() {
break;
}
if let Some(name) = parse_name_field(next) {
if is_tool_name_like(&name) {
block_has_tool_signature = true;
}
}
let next_lower = next.to_ascii_lowercase();
if next_lower.starts_with("cmd:")
|| next_lower.starts_with("command:")
|| next_lower.starts_with("args:")
|| next_lower.starts_with("arguments:")
{
block_has_tool_signature = true;
}
j += 1;
}
if block_has_tool_signature {
i = j;
continue;
}
}
if is_pseudo_tool_line(line) {
i += 1;
continue;
}
let replaced = line.replace(crate::llm_markers::TEXT_ONLY_RESPONSE_MARKER, "");
let trimmed_replaced = replaced.trim();
let lower_replaced = trimmed_replaced.to_ascii_lowercase();
if lower_replaced == "[consultation]" {
i += 1;
continue;
}
if lower_replaced.starts_with(&INTENT_GATE_MARKER.to_ascii_lowercase()) {
i += 1;
continue;
}
if lower_replaced.starts_with("[important:")
&& (lower_replaced.contains("consultation")
|| (lower_replaced.contains("you are being consulted")
&& lower_replaced.contains("respond with text only")))
{
i += 1;
continue;
}
if lower_replaced.contains("text only")
&& (lower_replaced.contains("no tools")
|| lower_replaced.contains("no function calls")
|| lower_replaced.contains("tool_use")
|| lower_replaced.contains("functioncall"))
{
i += 1;
continue;
}
if lower_replaced.starts_with("end your response with")
|| lower_replaced.starts_with("end with one line")
|| lower_replaced == "guidelines:"
|| lower_replaced.starts_with("- complexity:")
|| lower_replaced.starts_with("- only include schedule")
|| lower_replaced.starts_with("- domains is optional")
{
i += 1;
continue;
}
if has_pseudo_tool_block
&& (lower_replaced.starts_with("cmd:")
|| lower_replaced.starts_with("command:")
|| lower_replaced.starts_with("args:")
|| lower_replaced.starts_with("arguments:")
|| parse_name_field(trimmed_replaced)
.as_deref()
.is_some_and(is_tool_name_like))
{
i += 1;
continue;
}
if trimmed_replaced.is_empty() {
if cleaned.last().is_some_and(|prev| prev.is_empty()) {
i += 1;
continue;
}
cleaned.push(String::new());
} else {
cleaned.push(replaced.trim_end().to_string());
}
i += 1;
}
cleaned.join("\n").trim().to_string()
}