Skip to main content

vex_core/
context.rs

1//! Context packets for VEX agents
2//!
3//! A [`ContextPacket`] is the unit of information passed between agents,
4//! with temporal metadata and cryptographic hashing.
5
6use crate::merkle::Hash;
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use uuid::Uuid;
11
12/// Compression level for context packets
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum CompressionLevel {
15    /// Full fidelity - no compression
16    Full,
17    /// Summary - moderate compression
18    Summary,
19    /// Abstract - high compression, key points only
20    Abstract,
21    /// Minimal - extreme compression
22    Minimal,
23}
24
25impl CompressionLevel {
26    /// Get the numeric compression ratio (0.0 = full, 1.0 = minimal)
27    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/// A context packet - the unit of information in VEX
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ContextPacket {
40    /// Unique identifier for this packet
41    pub id: Uuid,
42    /// The actual content
43    pub content: String,
44    /// When this context was created
45    pub created_at: DateTime<Utc>,
46    /// When this context expires (if applicable)
47    pub expires_at: Option<DateTime<Utc>>,
48    /// Compression level applied
49    pub compression: CompressionLevel,
50    /// SHA-256 hash of the content
51    pub hash: Hash,
52    /// Hash of the parent packet (for chaining)
53    pub parent_hash: Option<Hash>,
54    /// Logit-Merkle trace root (for provenance)
55    pub trace_root: Option<Hash>,
56    /// Source agent ID
57    pub source_agent: Option<Uuid>,
58    /// Importance score (0.0 - 1.0)
59    pub importance: f64,
60}
61
62/// Maximum allowed context content size in bytes (50KB)
63pub const MAX_CONTENT_SIZE: usize = 50 * 1024;
64
65impl ContextPacket {
66    /// Create a new context packet with the given content
67    /// Content is truncated if it exceeds MAX_CONTENT_SIZE
68    pub fn new(content: &str) -> Self {
69        // Truncate content if too large to prevent memory exhaustion
70        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    /// Create a context packet with a TTL (time-to-live)
97    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    /// Compute SHA-256 hash of content
104    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    /// Check if this packet has expired
111    pub fn is_expired(&self) -> bool {
112        self.expires_at.is_some_and(|exp| Utc::now() > exp)
113    }
114
115    /// Get the age of this packet
116    pub fn age(&self) -> Duration {
117        Utc::now().signed_duration_since(self.created_at)
118    }
119
120    /// Create a compressed version of this packet
121    pub fn compress(&self, level: CompressionLevel) -> Self {
122        // In a real implementation, this would use an LLM to summarize
123        // For now, we just truncate based on compression ratio
124        let max_len = ((1.0 - level.ratio()) * self.content.len() as f64) as usize;
125        let compressed_content = if max_len < self.content.len() {
126            format!("{}...", &self.content[..max_len.max(10)])
127        } else {
128            self.content.clone()
129        };
130
131        let mut packet = Self::new(&compressed_content);
132        packet.compression = level;
133        packet.parent_hash = Some(self.hash.clone());
134        packet.source_agent = self.source_agent;
135        packet.importance = self.importance;
136        packet
137    }
138
139    /// Chain this packet to a parent
140    pub fn chain_to(&mut self, parent: &ContextPacket) {
141        self.parent_hash = Some(parent.hash.clone());
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_create_packet() {
151        let packet = ContextPacket::new("Hello, world!");
152        assert_eq!(packet.content, "Hello, world!");
153        assert_eq!(packet.compression, CompressionLevel::Full);
154        assert!(!packet.is_expired());
155    }
156
157    #[test]
158    fn test_packet_with_ttl() {
159        let packet = ContextPacket::with_ttl("Temporary data", Duration::hours(1));
160        assert!(packet.expires_at.is_some());
161        assert!(!packet.is_expired());
162    }
163
164    #[test]
165    fn test_compress_packet() {
166        let packet = ContextPacket::new(
167            "This is a long piece of content that should be compressed when needed.",
168        );
169        let compressed = packet.compress(CompressionLevel::Summary);
170        assert_eq!(compressed.compression, CompressionLevel::Summary);
171        assert!(compressed.content.len() <= packet.content.len());
172    }
173}