vtcode-core 0.136.1

Core library for VT Code - a Rust-based terminal coding agent
use super::*;

pub fn normalize_whitespace(text: &str) -> String {
    text.split_whitespace().fold(String::new(), |mut acc, s| {
        if !acc.is_empty() {
            acc.push(' ');
        }
        acc.push_str(s);
        acc
    })
}

pub fn truncate_for_fact(text: &str, max_chars: usize) -> String {
    let trimmed = text.trim();
    if trimmed.chars().count() <= max_chars {
        return trimmed.to_string();
    }
    format!("{}...", trimmed.chars().take(max_chars.saturating_sub(3)).collect::<String>())
}

pub fn maybe_extract_tool_fact(message: &Message) -> Option<GroundedFactRecord> {
    if message.role != MessageRole::Tool {
        return None;
    }
    let tool_name = message.origin_tool.as_deref().unwrap_or("tool");
    let text = message.content.as_text();
    let raw = text.trim();
    if raw.is_empty() {
        return None;
    }

    let candidate = serde_json::from_str::<serde_json::Value>(raw)
        .ok()
        .and_then(|value| {
            if value.get("error").is_some() || value.get("success") == Some(&serde_json::Value::Bool(false)) {
                return None;
            }
            for key in ["summary", "message", "result", "output", "stdout"] {
                if let Some(v) = value.get(key) {
                    if let Some(text) = v.as_str() {
                        let normalized = normalize_whitespace(text);
                        if !normalized.is_empty() {
                            return Some(normalized);
                        }
                    } else if !v.is_null() {
                        let normalized = normalize_whitespace(&v.to_string());
                        if !normalized.is_empty() {
                            return Some(normalized);
                        }
                    }
                }
            }
            let compact = normalize_whitespace(&value.to_string());
            (!compact.is_empty()).then_some(compact)
        })
        .or_else(|| {
            let lowered = raw.to_ascii_lowercase();
            if lowered.contains("error")
                || lowered.contains("failed")
                || lowered.contains("denied")
                || lowered.contains("timeout")
            {
                return None;
            }
            Some(normalize_whitespace(raw))
        })?;

    Some(GroundedFactRecord {
        fact: truncate_for_fact(&candidate, 180),
        source: format!("tool:{tool_name}"),
    })
}

pub fn maybe_extract_user_fact(message: &Message) -> Option<GroundedFactRecord> {
    if message.role != MessageRole::User {
        return None;
    }
    let text = normalize_whitespace(message.content.as_text().as_ref());
    if text.is_empty() {
        return None;
    }
    let candidate_text = strip_user_memory_candidate_prefixes(&text);
    let (candidate_text, looks_authored_note) = strip_user_memory_note_marker(candidate_text)
        .map(|fact| (fact, true))
        .unwrap_or((candidate_text, false));
    let looks_durable_self_fact = SELF_FACT_PREFIXES
        .iter()
        .any(|p| candidate_text.to_ascii_lowercase().starts_with(*p));
    (looks_authored_note || looks_durable_self_fact).then(|| GroundedFactRecord {
        fact: truncate_for_fact(candidate_text, 180),
        source: "user_assertion".to_string(),
    })
}

fn strip_user_memory_candidate_prefixes(text: &str) -> &str {
    let mut trimmed = text.trim();
    loop {
        let lowered = trimmed.to_ascii_lowercase();
        let Some(prefix) = STRIP_PREFIXES.iter().find(|p| lowered.starts_with(**p)) else {
            return trimmed;
        };
        trimmed = trimmed
            .get(prefix.len()..)
            .unwrap_or("")
            .trim_start_matches([',', ':', '-', ' '])
            .trim_start();
    }
}

fn strip_user_memory_note_marker(text: &str) -> Option<&str> {
    let lowered = text.to_ascii_lowercase();
    CLEANUP_NOTE_PREFIXES.iter().find_map(|prefix| {
        lowered.starts_with(prefix).then(|| {
            text.get(prefix.len()..)
                .unwrap_or("")
                .trim_start_matches([',', ':', '-', ' '])
                .trim_start()
        })
    })
}

pub fn dedup_latest_facts(history: &[Message], limit: usize) -> Vec<GroundedFactRecord> {
    let mut facts = Vec::new();
    for message in history {
        if let Some(fact) = maybe_extract_tool_fact(message).or_else(|| maybe_extract_user_fact(message)) {
            let normalized = normalize_whitespace(&fact.fact).to_ascii_lowercase();
            if let Some(existing_idx) = facts.iter().position(|entry: &GroundedFactRecord| {
                normalize_whitespace(&entry.fact).to_ascii_lowercase() == normalized
            }) {
                facts.remove(existing_idx);
            }
            facts.push(fact);
        }
    }

    let keep_from = facts.len().saturating_sub(limit);
    facts.into_iter().skip(keep_from).collect()
}