Skip to main content

zeph_memory/compression/
mod.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Experience compression spectrum (#3305).
5//!
6//! This module implements a three-tier memory retrieval policy and a background
7//! promotion engine that converts recurring episodic patterns into generated SKILL.md files.
8//!
9//! # Tiers
10//!
11//! | Tier | Description |
12//! |------|-------------|
13//! | `Episodic` | Raw conversation snippets, lowest abstraction, highest token cost. |
14//! | `Procedural` | Tool-use patterns and how-to knowledge. |
15//! | `Declarative` | Stable facts and reference knowledge. |
16//!
17//! # Retrieval policy
18//!
19//! [`RetrievalPolicy`] maps the current remaining token-budget ratio to a subset of
20//! tiers. When the budget is ample (> `mid_budget_ratio`) all three tiers are queried;
21//! as the budget narrows, cheaper tiers are preferred.
22//!
23//! # Promotion engine
24//!
25//! [`promotion::PromotionEngine`] scans a window of recent episodic messages for
26//! clustering patterns and promotes qualifying clusters to SKILL.md files.
27
28pub mod promotion;
29
30pub use promotion::{PromotionCandidate, PromotionConfig, PromotionEngine, PromotionInput};
31pub use zeph_common::memory::CompressionLevel;
32
33/// Token-budget-aware tier selector for context assembly.
34///
35/// Maps a `remaining_ratio` (0.0 = budget exhausted, 1.0 = budget fully available) to
36/// the subset of [`CompressionLevel`]s to include in the context recall step.
37///
38/// # Examples
39///
40/// ```
41/// use zeph_memory::compression::{RetrievalPolicy, CompressionLevel};
42///
43/// let policy = RetrievalPolicy::default();
44/// // Full budget → all three tiers.
45/// let levels = policy.select(0.80);
46/// assert!(levels.contains(&CompressionLevel::Episodic));
47/// assert!(levels.contains(&CompressionLevel::Declarative));
48///
49/// // Low budget → episodic only.
50/// let levels = policy.select(0.10);
51/// assert_eq!(levels, &[CompressionLevel::Episodic]);
52/// ```
53#[derive(Debug, Clone, Copy)]
54pub struct RetrievalPolicy {
55    /// Below this ratio only `Episodic` recall is attempted. Default: `0.20`.
56    pub low_budget_ratio: f32,
57    /// Below this ratio `Episodic + Procedural` recall is attempted. Default: `0.50`.
58    pub mid_budget_ratio: f32,
59}
60
61impl Default for RetrievalPolicy {
62    fn default() -> Self {
63        Self {
64            low_budget_ratio: 0.20,
65            mid_budget_ratio: 0.50,
66        }
67    }
68}
69
70impl RetrievalPolicy {
71    /// Select which compression levels should be included for `remaining_ratio`.
72    ///
73    /// | `remaining_ratio` | Levels returned |
74    /// |-------------------|-----------------|
75    /// | `< low_budget_ratio` | `[Episodic]` |
76    /// | `< mid_budget_ratio` | `[Episodic, Procedural]` |
77    /// | `≥ mid_budget_ratio` | `[Episodic, Procedural, Declarative]` |
78    #[tracing::instrument(name = "memory.compression.select", skip_all, fields(remaining_ratio))]
79    pub fn select(&self, remaining_ratio: f32) -> &'static [CompressionLevel] {
80        if remaining_ratio < self.low_budget_ratio {
81            &[CompressionLevel::Episodic]
82        } else if remaining_ratio < self.mid_budget_ratio {
83            &[CompressionLevel::Episodic, CompressionLevel::Procedural]
84        } else {
85            &[
86                CompressionLevel::Episodic,
87                CompressionLevel::Procedural,
88                CompressionLevel::Declarative,
89            ]
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn compression_level_cost_factors() {
100        assert!((CompressionLevel::Episodic.cost_factor() - 1.0).abs() < 1e-6);
101        assert!((CompressionLevel::Procedural.cost_factor() - 0.6).abs() < 1e-6);
102        assert!((CompressionLevel::Declarative.cost_factor() - 0.3).abs() < 1e-6);
103    }
104
105    #[test]
106    fn retrieval_policy_full_budget() {
107        let policy = RetrievalPolicy::default();
108        let levels = policy.select(0.80);
109        assert!(levels.contains(&CompressionLevel::Episodic));
110        assert!(levels.contains(&CompressionLevel::Procedural));
111        assert!(levels.contains(&CompressionLevel::Declarative));
112    }
113
114    #[test]
115    fn retrieval_policy_mid_budget() {
116        let policy = RetrievalPolicy::default();
117        let levels = policy.select(0.35);
118        assert!(levels.contains(&CompressionLevel::Episodic));
119        assert!(levels.contains(&CompressionLevel::Procedural));
120        assert!(!levels.contains(&CompressionLevel::Declarative));
121    }
122
123    #[test]
124    fn retrieval_policy_low_budget() {
125        let policy = RetrievalPolicy::default();
126        let levels = policy.select(0.10);
127        assert_eq!(levels, &[CompressionLevel::Episodic]);
128    }
129
130    #[test]
131    fn retrieval_policy_boundary_at_low() {
132        let policy = RetrievalPolicy::default();
133        // Exactly at low_budget_ratio — mid tier.
134        let levels = policy.select(0.20);
135        assert!(levels.contains(&CompressionLevel::Procedural));
136    }
137
138    #[test]
139    fn retrieval_policy_boundary_at_mid() {
140        let policy = RetrievalPolicy::default();
141        // Exactly at mid_budget_ratio — all tiers.
142        let levels = policy.select(0.50);
143        assert!(levels.contains(&CompressionLevel::Declarative));
144    }
145}