Skip to main content

vv_agent/prompt/
cache_tracker.rs

1use std::collections::BTreeMap;
2
3use serde_json::Value;
4use sha2::{Digest, Sha256};
5
6pub fn hash_system_prompt_sections(sections: &[Value]) -> String {
7    let normalized = sections
8        .iter()
9        .filter_map(normalize_section)
10        .collect::<Vec<_>>();
11    if normalized.is_empty() {
12        return String::new();
13    }
14    hash_json(&normalized)
15}
16
17pub fn hash_tool_payload(tools: &[Value]) -> String {
18    if tools.is_empty() {
19        return String::new();
20    }
21    hash_json(tools)
22}
23
24#[derive(Debug, Clone, Default, PartialEq)]
25pub struct CacheBreakTracker {
26    last_system_hash: String,
27    last_tool_hash: String,
28    total_requests: usize,
29    cache_breaks: usize,
30    break_reasons: Vec<String>,
31}
32
33impl CacheBreakTracker {
34    pub fn check(&mut self, system_hash: String, tool_hash: String) -> Vec<String> {
35        let mut reasons = Vec::new();
36        if !self.last_system_hash.is_empty() && system_hash != self.last_system_hash {
37            reasons.push("system_prompt_changed".to_string());
38        }
39        if !self.last_tool_hash.is_empty() && tool_hash != self.last_tool_hash {
40            reasons.push("tool_schemas_changed".to_string());
41        }
42
43        self.last_system_hash = system_hash;
44        self.last_tool_hash = tool_hash;
45        self.total_requests += 1;
46        if !reasons.is_empty() {
47            self.cache_breaks += 1;
48            self.break_reasons.extend(reasons.clone());
49        }
50        reasons
51    }
52
53    pub fn total_requests(&self) -> usize {
54        self.total_requests
55    }
56
57    pub fn cache_breaks(&self) -> usize {
58        self.cache_breaks
59    }
60
61    pub fn break_reasons(&self) -> Vec<String> {
62        self.break_reasons.clone()
63    }
64
65    pub fn cache_hit_rate(&self) -> f64 {
66        if self.total_requests == 0 {
67            return 1.0;
68        }
69        1.0 - (self.cache_breaks as f64 / self.total_requests as f64)
70    }
71}
72
73fn normalize_section(section: &Value) -> Option<BTreeMap<String, Value>> {
74    let object = section.as_object()?;
75    let text = object
76        .get("text")
77        .and_then(Value::as_str)
78        .unwrap_or_default()
79        .trim();
80    if text.is_empty() {
81        return None;
82    }
83    Some(BTreeMap::from([
84        (
85            "id".to_string(),
86            Value::String(
87                object
88                    .get("id")
89                    .and_then(Value::as_str)
90                    .unwrap_or_default()
91                    .trim()
92                    .to_string(),
93            ),
94        ),
95        ("text".to_string(), Value::String(text.to_string())),
96        (
97            "stable".to_string(),
98            Value::Bool(
99                object
100                    .get("stable")
101                    .and_then(Value::as_bool)
102                    .unwrap_or(true),
103            ),
104        ),
105    ]))
106}
107
108fn hash_json<T: serde::Serialize + ?Sized>(value: &T) -> String {
109    let value = serde_json::to_value(value).unwrap_or(Value::Null);
110    let payload = stable_sorted_json(&value);
111    let digest = Sha256::digest(payload.as_bytes());
112    hex_lower(&digest)
113}
114
115fn stable_sorted_json(value: &Value) -> String {
116    match value {
117        Value::Null => "null".to_string(),
118        Value::Bool(value) => {
119            if *value {
120                "true".to_string()
121            } else {
122                "false".to_string()
123            }
124        }
125        Value::Number(value) => value.to_string(),
126        Value::String(value) => serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string()),
127        Value::Array(items) => format!(
128            "[{}]",
129            items
130                .iter()
131                .map(stable_sorted_json)
132                .collect::<Vec<_>>()
133                .join(", ")
134        ),
135        Value::Object(object) => {
136            let mut entries = object.iter().collect::<Vec<_>>();
137            entries.sort_by(|left, right| left.0.cmp(right.0));
138            format!(
139                "{{{}}}",
140                entries
141                    .into_iter()
142                    .map(|(key, value)| format!(
143                        "{}: {}",
144                        serde_json::to_string(key).unwrap_or_else(|_| "\"\"".to_string()),
145                        stable_sorted_json(value)
146                    ))
147                    .collect::<Vec<_>>()
148                    .join(", ")
149            )
150        }
151    }
152}
153
154fn hex_lower(bytes: &[u8]) -> String {
155    const HEX: &[u8; 16] = b"0123456789abcdef";
156    let mut output = String::with_capacity(bytes.len() * 2);
157    for byte in bytes {
158        output.push(HEX[(byte >> 4) as usize] as char);
159        output.push(HEX[(byte & 0x0f) as usize] as char);
160    }
161    output
162}