vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::collections::BTreeMap;

use serde_json::Value;
use sha2::{Digest, Sha256};

pub fn hash_system_prompt_sections(sections: &[Value]) -> String {
    let normalized = sections
        .iter()
        .filter_map(normalize_section)
        .collect::<Vec<_>>();
    if normalized.is_empty() {
        return String::new();
    }
    hash_json(&normalized)
}

pub fn hash_tool_payload(tools: &[Value]) -> String {
    if tools.is_empty() {
        return String::new();
    }
    hash_json(tools)
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct CacheBreakTracker {
    last_system_hash: String,
    last_tool_hash: String,
    total_requests: usize,
    cache_breaks: usize,
    break_reasons: Vec<String>,
}

impl CacheBreakTracker {
    pub fn check(&mut self, system_hash: String, tool_hash: String) -> Vec<String> {
        let mut reasons = Vec::new();
        if !self.last_system_hash.is_empty() && system_hash != self.last_system_hash {
            reasons.push("system_prompt_changed".to_string());
        }
        if !self.last_tool_hash.is_empty() && tool_hash != self.last_tool_hash {
            reasons.push("tool_schemas_changed".to_string());
        }

        self.last_system_hash = system_hash;
        self.last_tool_hash = tool_hash;
        self.total_requests += 1;
        if !reasons.is_empty() {
            self.cache_breaks += 1;
            self.break_reasons.extend(reasons.clone());
        }
        reasons
    }

    pub fn total_requests(&self) -> usize {
        self.total_requests
    }

    pub fn cache_breaks(&self) -> usize {
        self.cache_breaks
    }

    pub fn break_reasons(&self) -> Vec<String> {
        self.break_reasons.clone()
    }

    pub fn cache_hit_rate(&self) -> f64 {
        if self.total_requests == 0 {
            return 1.0;
        }
        1.0 - (self.cache_breaks as f64 / self.total_requests as f64)
    }
}

fn normalize_section(section: &Value) -> Option<BTreeMap<String, Value>> {
    let object = section.as_object()?;
    let text = object
        .get("text")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .trim();
    if text.is_empty() {
        return None;
    }
    Some(BTreeMap::from([
        (
            "id".to_string(),
            Value::String(
                object
                    .get("id")
                    .and_then(Value::as_str)
                    .unwrap_or_default()
                    .trim()
                    .to_string(),
            ),
        ),
        ("text".to_string(), Value::String(text.to_string())),
        (
            "stable".to_string(),
            Value::Bool(
                object
                    .get("stable")
                    .and_then(Value::as_bool)
                    .unwrap_or(true),
            ),
        ),
    ]))
}

fn hash_json<T: serde::Serialize + ?Sized>(value: &T) -> String {
    let value = serde_json::to_value(value).unwrap_or(Value::Null);
    let payload = stable_sorted_json(&value);
    let digest = Sha256::digest(payload.as_bytes());
    hex_lower(&digest)
}

fn stable_sorted_json(value: &Value) -> String {
    match value {
        Value::Null => "null".to_string(),
        Value::Bool(value) => {
            if *value {
                "true".to_string()
            } else {
                "false".to_string()
            }
        }
        Value::Number(value) => value.to_string(),
        Value::String(value) => serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string()),
        Value::Array(items) => format!(
            "[{}]",
            items
                .iter()
                .map(stable_sorted_json)
                .collect::<Vec<_>>()
                .join(", ")
        ),
        Value::Object(object) => {
            let mut entries = object.iter().collect::<Vec<_>>();
            entries.sort_by(|left, right| left.0.cmp(right.0));
            format!(
                "{{{}}}",
                entries
                    .into_iter()
                    .map(|(key, value)| format!(
                        "{}: {}",
                        serde_json::to_string(key).unwrap_or_else(|_| "\"\"".to_string()),
                        stable_sorted_json(value)
                    ))
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        }
    }
}

fn hex_lower(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        output.push(HEX[(byte >> 4) as usize] as char);
        output.push(HEX[(byte & 0x0f) as usize] as char);
    }
    output
}