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 claims_completed_side_effect(text: &str) -> bool {
let normalized = text
.trim()
.to_ascii_lowercase()
.replace(['\u{2018}', '\u{2019}', '`', '\u{02BC}'], "'");
const PAST_ACTION_VERBS: &[&str] = &[
"deleted",
"removed",
"created",
"wrote",
"written",
"updated",
"edited",
"modified",
"replaced",
"moved",
"renamed",
"executed",
"ran",
"installed",
"saved",
"stored",
"copied",
"combined",
"merged",
"deployed",
"restarted",
"stopped",
"killed",
"downloaded",
"cloned",
"pushed",
"pulled",
"committed",
"cleaned",
"cleared",
"built",
"generated",
"appended",
"uploaded",
"sent",
"added",
];
const FILLER_ADVERBS: &[&str] = &["now", "just", "successfully", "already", "also"];
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();
let is_action_verb = |w: &str| PAST_ACTION_VERBS.contains(&w);
let verb_after = |start: usize| -> bool {
let mut idx = start;
while words
.get(idx)
.is_some_and(|w| FILLER_ADVERBS.contains(&w.as_str()))
{
idx += 1;
}
words.get(idx).is_some_and(|w| is_action_verb(w))
};
for i in 0..words.len() {
let claim = if words[i] == "i've" {
verb_after(i + 1)
} else if words[i] == "i" && words.get(i + 1).is_some_and(|w| w == "have") {
verb_after(i + 2)
} else if words[i] == "i" {
verb_after(i + 1)
} else {
false
};
if claim {
return true;
}
}
false
}
pub(super) fn claims_delegation_started(text: &str) -> bool {
let normalized = text
.trim()
.to_ascii_lowercase()
.replace(['\u{2018}', '\u{2019}', '`', '\u{02BC}'], "'");
let mentions_delegated_agent = [
"specialist agent",
"specialized review agent",
"review agent",
"research agent",
"sub-agent",
"subagent",
"background agent",
]
.iter()
.any(|phrase| normalized.contains(phrase));
if !mentions_delegated_agent {
return false;
}
[
"i've initiated",
"i have initiated",
"i initiated",
"i've started",
"i have started",
"i started",
"i've launched",
"i have launched",
"i launched",
"i've spawned",
"i have spawned",
"i spawned",
"i've delegated",
"i have delegated",
"i delegated",
"is now running",
"is running in the background",
"has been started",
]
.iter()
.any(|phrase| normalized.contains(phrase))
}
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()
}