Skip to main content

leviath_core/
cache.rs

1//! Cache hint types for prompt caching across providers.
2//!
3//! Context regions are assembled in order of volatility (most stable first).
4//! Cache breakpoints are inserted at region boundaries. Providers translate
5//! these breakpoints into their native caching APIs.
6
7use serde::{Deserialize, Serialize};
8
9/// Cache hint for a region or message boundary.
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub enum CacheHint {
12    /// Always cache - content never changes (pinned, system, tools, compact history).
13    Always,
14    /// Cache until content hash changes (compacting regions between compaction events).
15    UntilChanged,
16    /// Same cacheability as [`CacheHint::UntilChanged`], and additionally marks
17    /// the start of the most recently changed stretch of that tier.
18    ///
19    /// A provider that spends one cache breakpoint per run of same-hint blocks
20    /// sees the change of hint as a run boundary, so the blocks ahead of the
21    /// mutation end up in a cache entry of their own instead of sharing one
22    /// with the block that just changed. Nothing else about the block differs:
23    /// assembly sorts it to the same position as `UntilChanged`, and its text
24    /// is untouched.
25    RecentlyChanged,
26    /// Cache the stable prefix of a sliding window.
27    /// `stable_fraction` is 0.0..1.0 (default 0.75 = oldest 75% of messages are stable).
28    SlidingPrefix {
29        /// How much of the window counts as stable, in `0.0..1.0`. The oldest
30        /// that fraction is cached; the newest tail is not, because it is what
31        /// changes every turn and would invalidate the whole prefix with it.
32        stable_fraction: f32,
33    },
34    /// Never cache (temporary, clearable, new messages).
35    Never,
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn cache_hint_always_equality() {
44        assert_eq!(CacheHint::Always, CacheHint::Always);
45        assert_ne!(CacheHint::Always, CacheHint::Never);
46    }
47
48    #[test]
49    fn cache_hint_never_equality() {
50        assert_eq!(CacheHint::Never, CacheHint::Never);
51        assert_ne!(CacheHint::Never, CacheHint::UntilChanged);
52    }
53
54    #[test]
55    fn cache_hint_until_changed_equality() {
56        assert_eq!(CacheHint::UntilChanged, CacheHint::UntilChanged);
57    }
58
59    #[test]
60    fn cache_hint_recently_changed_is_distinct_from_until_changed() {
61        assert_eq!(CacheHint::RecentlyChanged, CacheHint::RecentlyChanged);
62        assert_ne!(CacheHint::RecentlyChanged, CacheHint::UntilChanged);
63        let dbg = format!("{:?}", CacheHint::RecentlyChanged);
64        assert!(dbg.contains("RecentlyChanged"));
65    }
66
67    #[test]
68    fn cache_hint_sliding_prefix_equality() {
69        let a = CacheHint::SlidingPrefix {
70            stable_fraction: 0.75,
71        };
72        let b = CacheHint::SlidingPrefix {
73            stable_fraction: 0.75,
74        };
75        assert_eq!(a, b);
76
77        let c = CacheHint::SlidingPrefix {
78            stable_fraction: 0.5,
79        };
80        assert_ne!(a, c);
81    }
82
83    #[test]
84    fn cache_hint_clone() {
85        let hint = CacheHint::SlidingPrefix {
86            stable_fraction: 0.8,
87        };
88        let cloned = hint;
89        assert_eq!(
90            cloned,
91            CacheHint::SlidingPrefix {
92                stable_fraction: 0.8
93            }
94        );
95    }
96
97    #[test]
98    fn cache_hint_debug() {
99        let hint = CacheHint::Always;
100        let dbg = format!("{:?}", hint);
101        assert!(dbg.contains("Always"));
102    }
103
104    #[test]
105    fn cache_hint_serde_roundtrip() {
106        let hints = vec![
107            CacheHint::Always,
108            CacheHint::UntilChanged,
109            CacheHint::RecentlyChanged,
110            CacheHint::SlidingPrefix {
111                stable_fraction: 0.75,
112            },
113            CacheHint::Never,
114        ];
115        for hint in hints {
116            let json = serde_json::to_string(&hint).unwrap();
117            let parsed: CacheHint = serde_json::from_str(&json).unwrap();
118            assert_eq!(hint, parsed);
119        }
120    }
121}