1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct MessageMetadata {
20 timestamp: u64,
22
23 importance_score: f64,
29
30 compression_status: CompressionStatus,
32
33 estimated_tokens: usize,
36
37 source: Option<String>,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
47 intent_id: Option<String>,
48}
49
50impl MessageMetadata {
51 pub fn user_input(timestamp: u64, estimated_tokens: usize) -> Self {
53 Self {
54 timestamp,
55 importance_score: 0.5,
56 compression_status: CompressionStatus::Uncompressed,
57 estimated_tokens,
58 source: Some("user_input".into()),
59 intent_id: None,
60 }
61 }
62
63 pub fn llm_response(timestamp: u64, estimated_tokens: usize) -> Self {
65 Self {
66 timestamp,
67 importance_score: 0.6,
68 compression_status: CompressionStatus::Uncompressed,
69 estimated_tokens,
70 source: Some("llm_response".into()),
71 intent_id: None,
72 }
73 }
74
75 pub fn tool_result(timestamp: u64, estimated_tokens: usize) -> Self {
77 Self {
78 timestamp,
79 importance_score: 0.4,
80 compression_status: CompressionStatus::Uncompressed,
81 estimated_tokens,
82 source: Some("tool_result".into()),
83 intent_id: None,
84 }
85 }
86
87 pub fn system(timestamp: u64, estimated_tokens: usize) -> Self {
89 Self {
90 timestamp,
91 importance_score: 1.0,
92 compression_status: CompressionStatus::Uncompressed,
93 estimated_tokens,
94 source: Some("system".into()),
95 intent_id: None,
96 }
97 }
98
99 pub fn synthetic(timestamp: u64, estimated_tokens: usize) -> Self {
101 Self {
102 timestamp,
103 importance_score: 0.3,
104 compression_status: CompressionStatus::Uncompressed,
105 estimated_tokens,
106 source: Some("synthetic".into()),
107 intent_id: None,
108 }
109 }
110
111 #[must_use]
113 pub fn with_intent_id(mut self, intent_id: impl Into<String>) -> Self {
114 self.intent_id = Some(intent_id.into());
115 self
116 }
117
118 #[must_use]
120 pub fn intent_id(&self) -> Option<&str> {
121 self.intent_id.as_deref()
122 }
123
124 fn mark_compressed(&mut self, original_tokens: usize, compressed_tokens: usize) {
126 self.compression_status = CompressionStatus::Compressed {
127 original_token_count: original_tokens,
128 summary_token_count: compressed_tokens,
129 };
130 self.estimated_tokens = compressed_tokens;
131 }
132
133 fn mark_summarized(&mut self, original_tokens: usize, summary_tokens: usize) {
135 self.compression_status = CompressionStatus::Summarized {
136 original_token_count: original_tokens,
137 summary_token_count: summary_tokens,
138 };
139 self.estimated_tokens = summary_tokens;
140 }
141
142 fn set_importance(&mut self, score: f64) {
144 self.importance_score = score.clamp(0.0, 1.0);
145 }
146
147 fn original_token_count(&self) -> usize {
150 match self.compression_status {
151 CompressionStatus::Uncompressed => self.estimated_tokens,
152 CompressionStatus::Compressed { original_token_count, .. }
153 | CompressionStatus::Summarized { original_token_count, .. } => original_token_count,
154 CompressionStatus::Dropped => 0,
155 }
156 }
157
158 fn effective_token_count(&self) -> usize {
160 match self.compression_status {
161 CompressionStatus::Uncompressed => self.estimated_tokens,
162 CompressionStatus::Compressed { summary_token_count, .. }
163 | CompressionStatus::Summarized { summary_token_count, .. } => summary_token_count,
164 CompressionStatus::Dropped => 0,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum CompressionStatus {
173 Uncompressed,
175 Compressed {
177 original_token_count: usize,
178 summary_token_count: usize,
179 },
180 Summarized {
182 original_token_count: usize,
183 summary_token_count: usize,
184 },
185 Dropped,
188}
189
190#[allow(clippy::derivable_impls)]
191impl Default for CompressionStatus {
192 fn default() -> Self {
193 Self::Uncompressed
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_create_user_metadata() {
203 let meta = MessageMetadata::user_input(1000, 50);
204 assert_eq!(meta.timestamp, 1000);
205 assert!((meta.importance_score - 0.5).abs() < f64::EPSILON);
206 assert_eq!(meta.compression_status, CompressionStatus::Uncompressed);
207 assert_eq!(meta.estimated_tokens, 50);
208 assert_eq!(meta.source.as_deref(), Some("user_input"));
209 assert_eq!(meta.intent_id(), None);
210 }
211
212 #[test]
213 fn test_create_llm_response_metadata() {
214 let meta = MessageMetadata::llm_response(2000, 150);
215 assert!((meta.importance_score - 0.6).abs() < f64::EPSILON);
216 }
217
218 #[test]
219 fn test_mark_compressed() {
220 let mut meta = MessageMetadata::user_input(1000, 200);
221 meta.mark_compressed(200, 50);
222 assert_eq!(meta.estimated_tokens, 50);
223 assert_eq!(meta.effective_token_count(), 50);
224 assert_eq!(meta.original_token_count(), 200);
225 }
226
227 #[test]
228 fn test_mark_summarized() {
229 let mut meta = MessageMetadata::user_input(1000, 300);
230 meta.mark_summarized(300, 30);
231 assert_eq!(meta.effective_token_count(), 30);
232 assert_eq!(meta.original_token_count(), 300);
233 }
234
235 #[test]
236 fn test_set_importance_clamps() {
237 let mut meta = MessageMetadata::user_input(1000, 50);
238 meta.set_importance(1.5);
239 assert!((meta.importance_score - 1.0).abs() < f64::EPSILON);
240 meta.set_importance(-0.5);
241 assert!((meta.importance_score - 0.0).abs() < f64::EPSILON);
242 }
243
244 #[test]
245 fn test_compression_status_serde_roundtrip() {
246 let status = CompressionStatus::Compressed { original_token_count: 200, summary_token_count: 50 };
247 let json = serde_json::to_string(&status).unwrap();
248 let deserialized: CompressionStatus = serde_json::from_str(&json).unwrap();
249 assert_eq!(status, deserialized);
250 }
251
252 #[test]
253 fn test_intent_id_is_optional_and_serde_compatible() {
254 let meta = MessageMetadata::user_input(1000, 50).with_intent_id("intent-1");
255 let json = serde_json::to_string(&meta).unwrap();
256 let restored: MessageMetadata = serde_json::from_str(&json).unwrap();
257 assert_eq!(restored.intent_id(), Some("intent-1"));
258
259 let legacy = r#"{"timestamp":1000,"importance_score":0.5,"compression_status":"uncompressed","estimated_tokens":50,"source":"user_input"}"#;
260 let restored: MessageMetadata = serde_json::from_str(legacy).unwrap();
261 assert_eq!(restored.intent_id(), None);
262 }
263}