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}