vv-agent 0.11.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use crate::types::{Message, MessageRole};

use super::{ToolResultArtifactConfig, TOOL_RESULT_COMPACT_MARKER};

pub(crate) fn is_compacted_tool_content(content: &str) -> bool {
    content.starts_with(TOOL_RESULT_COMPACT_MARKER)
}

pub(crate) fn has_recovery_envelope(content: &str) -> bool {
    let Some((_, last_line)) = content.trim_end().rsplit_once('\n') else {
        return false;
    };
    serde_json::from_str::<serde_json::Value>(last_line)
        .ok()
        .and_then(|value| value.get("vv_agent_recovery").cloned())
        .is_some()
}

pub(super) fn kept_tool_message_indices(messages: &[Message], keep_last: usize) -> Vec<usize> {
    if keep_last == 0 {
        return Vec::new();
    }
    messages
        .iter()
        .enumerate()
        .filter_map(|(index, message)| (message.role == MessageRole::Tool).then_some(index))
        .rev()
        .take(keep_last)
        .collect()
}

pub(super) fn should_compact_tool_message(
    message: &Message,
    index: usize,
    keep_indices: &[usize],
    compact_threshold: usize,
) -> bool {
    message.role == MessageRole::Tool
        && !keep_indices.contains(&index)
        && message.content.len() > compact_threshold
        && !is_compacted_tool_content(&message.content)
}

pub(crate) fn build_compacted_tool_content(
    content: &str,
    artifact_path: &str,
    tool_name: &str,
    config: &ToolResultArtifactConfig,
) -> String {
    let excerpt_source = content_without_recovery_envelope(content);
    let head = take_chars(excerpt_source, config.excerpt_head);
    let tail = take_tail_chars(excerpt_source, config.excerpt_tail);
    let mut excerpt_parts = Vec::new();
    if !head.is_empty() {
        excerpt_parts.push(head.clone());
    }
    if !tail.is_empty() && tail != head {
        if !excerpt_parts.is_empty() {
            excerpt_parts.push("...<snip>...".to_string());
        }
        excerpt_parts.push(tail);
    }
    let excerpt = excerpt_parts.join("\n").trim().to_string();
    format!(
        "{TOOL_RESULT_COMPACT_MARKER}\ntool_name: {tool_name}\nartifact_path: {artifact_path}\nretrieval_hint: use read_file on artifact_path if needed\nexcerpt:\n{excerpt}\n</Tool Result Compact>"
    )
}

pub(super) fn content_without_recovery_envelope(content: &str) -> &str {
    let trimmed = content.trim_end();
    let Some((prefix, _)) = trimmed.rsplit_once('\n') else {
        return content;
    };
    if has_recovery_envelope(content) {
        prefix
    } else {
        content
    }
}

fn take_chars(content: &str, count: usize) -> String {
    content.chars().take(count).collect()
}

fn take_tail_chars(content: &str, count: usize) -> String {
    if count == 0 {
        return String::new();
    }
    let chars = content.chars().collect::<Vec<_>>();
    let start = chars.len().saturating_sub(count);
    chars[start..].iter().collect()
}

#[cfg(test)]
mod tests {
    use super::build_compacted_tool_content;
    use crate::memory::artifacts::ToolResultArtifactConfig;

    #[test]
    fn compact_marker_trims_excerpt_and_omits_trailing_recovery_envelope() {
        let recovery = r#"{"vv_agent_recovery":{"artifact":{"path":"hidden"},"truncated":true}}"#;
        let content = format!(" \n useful result \n{recovery}\n \t");
        let marker = build_compacted_tool_content(
            &content,
            ".vv-agent/artifacts/run/call.txt",
            "web_search",
            &ToolResultArtifactConfig {
                excerpt_head: 200,
                excerpt_tail: 200,
                ..ToolResultArtifactConfig::default()
            },
        );

        assert!(marker.contains("excerpt:\nuseful result\n</Tool Result Compact>"));
        assert!(!marker.contains("vv_agent_recovery"));
        assert!(!marker.contains("\"path\":\"hidden\""));
    }
}