Skip to main content

enact_core/kernel/artifact/
metadata.rs

1//! Artifact Metadata - Structured information about stored artifacts
2
3use crate::kernel::ids::{ArtifactId, ExecutionId, StepId};
4use serde::{Deserialize, Serialize};
5
6// =============================================================================
7// Artifact Types
8// =============================================================================
9
10/// Type of artifact content
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ArtifactType {
14    /// Plain text output
15    Text,
16    /// Generated code
17    Code,
18    /// Structured JSON data
19    Json,
20    /// Image file (PNG, JPEG, etc.)
21    Image,
22    /// PDF document
23    Pdf,
24    /// Search/RAG results
25    SearchResults,
26    /// Tool execution output
27    ToolOutput,
28    /// LLM thought/reasoning
29    Thought,
30    /// Execution plan
31    Plan,
32    /// Error details
33    Error,
34    /// Screenshot
35    Screenshot,
36    /// Audio file
37    Audio,
38    /// Video file
39    Video,
40    /// Binary data
41    Binary,
42}
43
44impl ArtifactType {
45    /// Get the default content type (MIME type) for this artifact type
46    pub fn default_content_type(&self) -> &'static str {
47        match self {
48            Self::Text => "text/plain",
49            Self::Code => "text/plain",
50            Self::Json => "application/json",
51            Self::Image => "image/png",
52            Self::Pdf => "application/pdf",
53            Self::SearchResults => "application/json",
54            Self::ToolOutput => "application/json",
55            Self::Thought => "text/plain",
56            Self::Plan => "application/json",
57            Self::Error => "application/json",
58            Self::Screenshot => "image/png",
59            Self::Audio => "audio/wav",
60            Self::Video => "video/mp4",
61            Self::Binary => "application/octet-stream",
62        }
63    }
64}
65
66impl std::fmt::Display for ArtifactType {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Text => write!(f, "text"),
70            Self::Code => write!(f, "code"),
71            Self::Json => write!(f, "json"),
72            Self::Image => write!(f, "image"),
73            Self::Pdf => write!(f, "pdf"),
74            Self::SearchResults => write!(f, "search_results"),
75            Self::ToolOutput => write!(f, "tool_output"),
76            Self::Thought => write!(f, "thought"),
77            Self::Plan => write!(f, "plan"),
78            Self::Error => write!(f, "error"),
79            Self::Screenshot => write!(f, "screenshot"),
80            Self::Audio => write!(f, "audio"),
81            Self::Video => write!(f, "video"),
82            Self::Binary => write!(f, "binary"),
83        }
84    }
85}
86
87// =============================================================================
88// Compression Types
89// =============================================================================
90
91/// Compression algorithm used for artifact storage
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
93#[serde(rename_all = "snake_case")]
94pub enum CompressionType {
95    /// No compression
96    None,
97    /// Zstandard compression (default for local storage)
98    #[default]
99    Zstd,
100    /// Gzip compression
101    Gzip,
102    /// LZ4 compression
103    Lz4,
104}
105
106impl CompressionType {
107    /// Get file extension for this compression type
108    pub fn extension(&self) -> &'static str {
109        match self {
110            Self::None => "",
111            Self::Zstd => ".zst",
112            Self::Gzip => ".gz",
113            Self::Lz4 => ".lz4",
114        }
115    }
116}
117
118// =============================================================================
119// Artifact Metadata
120// =============================================================================
121
122/// Metadata about a stored artifact
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ArtifactMetadata {
125    /// Unique artifact ID
126    pub artifact_id: ArtifactId,
127
128    /// Execution that produced this artifact
129    pub execution_id: ExecutionId,
130
131    /// Step that produced this artifact
132    pub step_id: StepId,
133
134    /// Name of the artifact
135    pub name: String,
136
137    /// Type of artifact
138    pub artifact_type: ArtifactType,
139
140    /// Content type (MIME type)
141    pub content_type: String,
142
143    /// Original (uncompressed) size in bytes
144    pub original_size: u64,
145
146    /// Compressed size in bytes
147    pub compressed_size: u64,
148
149    /// Compression algorithm used
150    pub compression: CompressionType,
151
152    /// Content hash (SHA-256) for integrity verification
153    pub content_hash: Option<String>,
154
155    /// Storage URI (for external storage backends)
156    pub storage_uri: Option<String>,
157
158    /// Creation timestamp (Unix milliseconds)
159    pub created_at: i64,
160
161    /// Last access timestamp (Unix milliseconds)
162    pub last_accessed_at: Option<i64>,
163
164    /// Additional metadata
165    pub metadata: Option<serde_json::Value>,
166}
167
168impl ArtifactMetadata {
169    /// Create new artifact metadata
170    pub fn new(
171        artifact_id: ArtifactId,
172        execution_id: ExecutionId,
173        step_id: StepId,
174        name: impl Into<String>,
175        artifact_type: ArtifactType,
176    ) -> Self {
177        Self {
178            artifact_id,
179            execution_id,
180            step_id,
181            name: name.into(),
182            artifact_type,
183            content_type: artifact_type.default_content_type().to_string(),
184            original_size: 0,
185            compressed_size: 0,
186            compression: CompressionType::None,
187            content_hash: None,
188            storage_uri: None,
189            created_at: chrono::Utc::now().timestamp_millis(),
190            last_accessed_at: None,
191            metadata: None,
192        }
193    }
194
195    /// Set content type
196    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
197        self.content_type = content_type.into();
198        self
199    }
200
201    /// Set original size
202    pub fn with_original_size(mut self, size: u64) -> Self {
203        self.original_size = size;
204        self
205    }
206
207    /// Set compressed size
208    pub fn with_compressed_size(mut self, size: u64) -> Self {
209        self.compressed_size = size;
210        self
211    }
212
213    /// Set compression type
214    pub fn with_compression(mut self, compression: CompressionType) -> Self {
215        self.compression = compression;
216        self
217    }
218
219    /// Set content hash
220    pub fn with_content_hash(mut self, hash: impl Into<String>) -> Self {
221        self.content_hash = Some(hash.into());
222        self
223    }
224
225    /// Set storage URI
226    pub fn with_storage_uri(mut self, uri: impl Into<String>) -> Self {
227        self.storage_uri = Some(uri.into());
228        self
229    }
230
231    /// Set metadata
232    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
233        self.metadata = Some(metadata);
234        self
235    }
236
237    /// Get compression ratio (compressed / original)
238    pub fn compression_ratio(&self) -> f64 {
239        if self.original_size == 0 {
240            1.0
241        } else {
242            self.compressed_size as f64 / self.original_size as f64
243        }
244    }
245
246    /// Get space savings percentage
247    pub fn space_savings_percent(&self) -> f64 {
248        (1.0 - self.compression_ratio()) * 100.0
249    }
250}
251
252// =============================================================================
253// Tests
254// =============================================================================
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_artifact_type_content_types() {
262        assert_eq!(ArtifactType::Text.default_content_type(), "text/plain");
263        assert_eq!(
264            ArtifactType::Json.default_content_type(),
265            "application/json"
266        );
267        assert_eq!(ArtifactType::Image.default_content_type(), "image/png");
268        assert_eq!(ArtifactType::Pdf.default_content_type(), "application/pdf");
269    }
270
271    #[test]
272    fn test_compression_ratio() {
273        let metadata = ArtifactMetadata::new(
274            ArtifactId::new(),
275            ExecutionId::new(),
276            StepId::new(),
277            "test.txt",
278            ArtifactType::Text,
279        )
280        .with_original_size(1000)
281        .with_compressed_size(300);
282
283        assert!((metadata.compression_ratio() - 0.3).abs() < 0.01);
284        assert!((metadata.space_savings_percent() - 70.0).abs() < 0.1);
285    }
286}