1use crate::merkle::Hash;
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum CompressionLevel {
15 Full,
17 Summary,
19 Abstract,
21 Minimal,
23}
24
25impl CompressionLevel {
26 pub fn ratio(&self) -> f64 {
28 match self {
29 Self::Full => 0.0,
30 Self::Summary => 0.3,
31 Self::Abstract => 0.6,
32 Self::Minimal => 0.9,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ContextPacket {
40 pub id: Uuid,
42 pub content: String,
44 pub created_at: DateTime<Utc>,
46 pub expires_at: Option<DateTime<Utc>>,
48 pub compression: CompressionLevel,
50 pub hash: Hash,
52 pub parent_hash: Option<Hash>,
54 pub trace_root: Option<Hash>,
56 pub source_agent: Option<Uuid>,
58 pub importance: f64,
60}
61
62pub const MAX_CONTENT_SIZE: usize = 50 * 1024;
64
65impl ContextPacket {
66 pub fn new(content: &str) -> Self {
69 let content = if content.len() > MAX_CONTENT_SIZE {
71 tracing::warn!(
72 size = content.len(),
73 max = MAX_CONTENT_SIZE,
74 "Context content truncated"
75 );
76 &content[..MAX_CONTENT_SIZE]
77 } else {
78 content
79 };
80
81 let hash = Self::compute_hash(content);
82 Self {
83 id: Uuid::new_v4(),
84 content: content.to_string(),
85 created_at: Utc::now(),
86 expires_at: None,
87 compression: CompressionLevel::Full,
88 hash,
89 parent_hash: None,
90 trace_root: None,
91 source_agent: None,
92 importance: 0.5,
93 }
94 }
95
96 pub fn with_ttl(content: &str, ttl: Duration) -> Self {
98 let mut packet = Self::new(content);
99 packet.expires_at = Some(Utc::now() + ttl);
100 packet
101 }
102
103 pub fn compute_hash(content: &str) -> Hash {
105 let mut hasher = Sha256::new();
106 hasher.update(content.as_bytes());
107 Hash(hasher.finalize().into())
108 }
109
110 pub fn is_expired(&self) -> bool {
112 self.expires_at.is_some_and(|exp| Utc::now() > exp)
113 }
114
115 pub fn age(&self) -> Duration {
117 Utc::now().signed_duration_since(self.created_at)
118 }
119
120 pub fn compress_truncate(&self, level: CompressionLevel) -> Self {
123 let max_len = ((1.0 - level.ratio()) * self.content.len() as f64) as usize;
124 let compressed_content = if max_len < self.content.len() {
125 format!("{}...", &self.content[..max_len.max(10)])
126 } else {
127 self.content.clone()
128 };
129
130 let mut packet = Self::new(&compressed_content);
131 packet.compression = level;
132 packet.parent_hash = Some(self.hash.clone());
133 packet.source_agent = self.source_agent;
134 packet.importance = self.importance;
135 packet
136 }
137
138 pub fn compress(&self, level: CompressionLevel) -> Self {
142 self.compress_truncate(level)
143 }
144
145 pub fn chain_to(&mut self, parent: &ContextPacket) {
147 self.parent_hash = Some(parent.hash.clone());
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_create_packet() {
157 let packet = ContextPacket::new("Hello, world!");
158 assert_eq!(packet.content, "Hello, world!");
159 assert_eq!(packet.compression, CompressionLevel::Full);
160 assert!(!packet.is_expired());
161 }
162
163 #[test]
164 fn test_packet_with_ttl() {
165 let packet = ContextPacket::with_ttl("Temporary data", Duration::hours(1));
166 assert!(packet.expires_at.is_some());
167 assert!(!packet.is_expired());
168 }
169
170 #[test]
171 fn test_compress_packet() {
172 let packet = ContextPacket::new(
173 "This is a long piece of content that should be compressed when needed.",
174 );
175 let compressed = packet.compress(CompressionLevel::Summary);
176 assert_eq!(compressed.compression, CompressionLevel::Summary);
177 assert!(compressed.content.len() <= packet.content.len());
178 }
179}