luft_core/contract/
cache.rs1use crate::contract::ids::PhaseId;
4use blake3::Hasher;
5use unicode_normalization::UnicodeNormalization;
6
7const SEP: u8 = 0;
12
13fn normalize_prompt(prompt: &str) -> String {
17 prompt
18 .nfc()
19 .collect::<String>()
20 .replace("\r\n", "\n")
21 .replace('\r', "\n")
22 .split_whitespace()
23 .collect::<Vec<_>>()
24 .join(" ")
25}
26
27pub fn agent_cache_key(
30 backend_id: &str,
31 model: Option<&str>,
32 prompt: &str,
33 phase: PhaseId,
34) -> String {
35 let mut h = Hasher::new();
36 h.update(backend_id.as_bytes());
37 h.update(&[SEP]);
38 h.update(model.unwrap_or("").as_bytes());
39 h.update(&[SEP]);
40 h.update(normalize_prompt(prompt).as_bytes());
41 h.update(&[SEP]);
42 h.update(&phase.to_be_bytes());
46 h.finalize().to_hex().to_string()
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn key_is_stable_and_distinct() {
55 let a = agent_cache_key("opencode", Some("gpt"), "audit file", 1);
56 let b = agent_cache_key("opencode", Some("gpt"), "audit file", 1);
57 assert_eq!(a, b, "same inputs must yield same key");
58
59 assert_ne!(a, agent_cache_key("opencode", Some("gpt"), "audit file", 2));
60 assert_ne!(a, agent_cache_key("codex", Some("gpt"), "audit file", 1));
61 assert_ne!(a, agent_cache_key("opencode", None, "audit file", 1));
62 }
63
64 #[test]
65 fn whitespace_is_normalized() {
66 assert_eq!(
67 agent_cache_key("b", None, " foo\r\nbar ", 0),
68 agent_cache_key("b", None, "foo bar", 0),
69 );
70 }
71}