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};
31
32/// The three abstraction levels in the compression spectrum.
33///
34/// Higher variants are cheaper to retrieve (fewer tokens) but represent a higher
35/// abstraction over raw episodic experience.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
37pub enum CompressionLevel {
38    /// Raw episodic messages — full fidelity, high token cost.
39    Episodic,
40    /// Abstracted procedural knowledge (how-to, tool patterns).
41    Procedural,
42    /// Stable declarative facts and reference material.
43    Declarative,
44}
45
46impl CompressionLevel {
47    /// A relative token-cost factor for budgeting purposes.
48    ///
49    /// `Episodic = 1.0` (baseline), `Procedural = 0.6`, `Declarative = 0.3`.
50    #[must_use]
51    pub fn cost_factor(self) -> f32 {
52        match self {
53            Self::Episodic => 1.0,
54            Self::Procedural => 0.6,
55            Self::Declarative => 0.3,
56        }
57    }
58}
59
60/// Token-budget-aware tier selector for context assembly.
61///
62/// Maps a `remaining_ratio` (0.0 = budget exhausted, 1.0 = budget fully available) to
63/// the subset of [`CompressionLevel`]s to include in the context recall step.
64///
65/// # Examples
66///
67/// ```
68/// use zeph_memory::compression::{RetrievalPolicy, CompressionLevel};
69///
70/// let policy = RetrievalPolicy::default();
71/// // Full budget → all three tiers.
72/// let levels = policy.select(0.80);
73/// assert!(levels.contains(&CompressionLevel::Episodic));
74/// assert!(levels.contains(&CompressionLevel::Declarative));
75///
76/// // Low budget → episodic only.
77/// let levels = policy.select(0.10);
78/// assert_eq!(levels, &[CompressionLevel::Episodic]);
79/// ```
80#[derive(Debug, Clone, Copy)]
81pub struct RetrievalPolicy {
82    /// Below this ratio only `Episodic` recall is attempted. Default: `0.20`.
83    pub low_budget_ratio: f32,
84    /// Below this ratio `Episodic + Procedural` recall is attempted. Default: `0.50`.
85    pub mid_budget_ratio: f32,
86}
87
88impl Default for RetrievalPolicy {
89    fn default() -> Self {
90        Self {
91            low_budget_ratio: 0.20,
92            mid_budget_ratio: 0.50,
93        }
94    }
95}
96
97impl RetrievalPolicy {
98    /// Select which compression levels should be included for `remaining_ratio`.
99    ///
100    /// | `remaining_ratio` | Levels returned |
101    /// |-------------------|-----------------|
102    /// | `< low_budget_ratio` | `[Episodic]` |
103    /// | `< mid_budget_ratio` | `[Episodic, Procedural]` |
104    /// | `≥ mid_budget_ratio` | `[Episodic, Procedural, Declarative]` |
105    #[tracing::instrument(name = "memory.compression.select", skip_all, fields(remaining_ratio))]
106    pub fn select(&self, remaining_ratio: f32) -> &'static [CompressionLevel] {
107        if remaining_ratio < self.low_budget_ratio {
108            &[CompressionLevel::Episodic]
109        } else if remaining_ratio < self.mid_budget_ratio {
110            &[CompressionLevel::Episodic, CompressionLevel::Procedural]
111        } else {
112            &[
113                CompressionLevel::Episodic,
114                CompressionLevel::Procedural,
115                CompressionLevel::Declarative,
116            ]
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn compression_level_cost_factors() {
127        assert!((CompressionLevel::Episodic.cost_factor() - 1.0).abs() < 1e-6);
128        assert!((CompressionLevel::Procedural.cost_factor() - 0.6).abs() < 1e-6);
129        assert!((CompressionLevel::Declarative.cost_factor() - 0.3).abs() < 1e-6);
130    }
131
132    #[test]
133    fn retrieval_policy_full_budget() {
134        let policy = RetrievalPolicy::default();
135        let levels = policy.select(0.80);
136        assert!(levels.contains(&CompressionLevel::Episodic));
137        assert!(levels.contains(&CompressionLevel::Procedural));
138        assert!(levels.contains(&CompressionLevel::Declarative));
139    }
140
141    #[test]
142    fn retrieval_policy_mid_budget() {
143        let policy = RetrievalPolicy::default();
144        let levels = policy.select(0.35);
145        assert!(levels.contains(&CompressionLevel::Episodic));
146        assert!(levels.contains(&CompressionLevel::Procedural));
147        assert!(!levels.contains(&CompressionLevel::Declarative));
148    }
149
150    #[test]
151    fn retrieval_policy_low_budget() {
152        let policy = RetrievalPolicy::default();
153        let levels = policy.select(0.10);
154        assert_eq!(levels, &[CompressionLevel::Episodic]);
155    }
156
157    #[test]
158    fn retrieval_policy_boundary_at_low() {
159        let policy = RetrievalPolicy::default();
160        // Exactly at low_budget_ratio — mid tier.
161        let levels = policy.select(0.20);
162        assert!(levels.contains(&CompressionLevel::Procedural));
163    }
164
165    #[test]
166    fn retrieval_policy_boundary_at_mid() {
167        let policy = RetrievalPolicy::default();
168        // Exactly at mid_budget_ratio — all tiers.
169        let levels = policy.select(0.50);
170        assert!(levels.contains(&CompressionLevel::Declarative));
171    }
172}