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}