enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Artifact Metadata - Structured information about stored artifacts

use crate::kernel::ids::{ArtifactId, ExecutionId, StepId};
use serde::{Deserialize, Serialize};

// =============================================================================
// Artifact Types
// =============================================================================

/// Type of artifact content
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactType {
    /// Plain text output
    Text,
    /// Generated code
    Code,
    /// Structured JSON data
    Json,
    /// Image file (PNG, JPEG, etc.)
    Image,
    /// PDF document
    Pdf,
    /// Search/RAG results
    SearchResults,
    /// Tool execution output
    ToolOutput,
    /// LLM thought/reasoning
    Thought,
    /// Execution plan
    Plan,
    /// Error details
    Error,
    /// Screenshot
    Screenshot,
    /// Audio file
    Audio,
    /// Video file
    Video,
    /// Binary data
    Binary,
}

impl ArtifactType {
    /// Get the default content type (MIME type) for this artifact type
    pub fn default_content_type(&self) -> &'static str {
        match self {
            Self::Text => "text/plain",
            Self::Code => "text/plain",
            Self::Json => "application/json",
            Self::Image => "image/png",
            Self::Pdf => "application/pdf",
            Self::SearchResults => "application/json",
            Self::ToolOutput => "application/json",
            Self::Thought => "text/plain",
            Self::Plan => "application/json",
            Self::Error => "application/json",
            Self::Screenshot => "image/png",
            Self::Audio => "audio/wav",
            Self::Video => "video/mp4",
            Self::Binary => "application/octet-stream",
        }
    }
}

impl std::fmt::Display for ArtifactType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Text => write!(f, "text"),
            Self::Code => write!(f, "code"),
            Self::Json => write!(f, "json"),
            Self::Image => write!(f, "image"),
            Self::Pdf => write!(f, "pdf"),
            Self::SearchResults => write!(f, "search_results"),
            Self::ToolOutput => write!(f, "tool_output"),
            Self::Thought => write!(f, "thought"),
            Self::Plan => write!(f, "plan"),
            Self::Error => write!(f, "error"),
            Self::Screenshot => write!(f, "screenshot"),
            Self::Audio => write!(f, "audio"),
            Self::Video => write!(f, "video"),
            Self::Binary => write!(f, "binary"),
        }
    }
}

// =============================================================================
// Compression Types
// =============================================================================

/// Compression algorithm used for artifact storage
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CompressionType {
    /// No compression
    None,
    /// Zstandard compression (default for local storage)
    #[default]
    Zstd,
    /// Gzip compression
    Gzip,
    /// LZ4 compression
    Lz4,
}

impl CompressionType {
    /// Get file extension for this compression type
    pub fn extension(&self) -> &'static str {
        match self {
            Self::None => "",
            Self::Zstd => ".zst",
            Self::Gzip => ".gz",
            Self::Lz4 => ".lz4",
        }
    }
}

// =============================================================================
// Artifact Metadata
// =============================================================================

/// Metadata about a stored artifact
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactMetadata {
    /// Unique artifact ID
    pub artifact_id: ArtifactId,

    /// Execution that produced this artifact
    pub execution_id: ExecutionId,

    /// Step that produced this artifact
    pub step_id: StepId,

    /// Name of the artifact
    pub name: String,

    /// Type of artifact
    pub artifact_type: ArtifactType,

    /// Content type (MIME type)
    pub content_type: String,

    /// Original (uncompressed) size in bytes
    pub original_size: u64,

    /// Compressed size in bytes
    pub compressed_size: u64,

    /// Compression algorithm used
    pub compression: CompressionType,

    /// Content hash (SHA-256) for integrity verification
    pub content_hash: Option<String>,

    /// Storage URI (for external storage backends)
    pub storage_uri: Option<String>,

    /// Creation timestamp (Unix milliseconds)
    pub created_at: i64,

    /// Last access timestamp (Unix milliseconds)
    pub last_accessed_at: Option<i64>,

    /// Additional metadata
    pub metadata: Option<serde_json::Value>,
}

impl ArtifactMetadata {
    /// Create new artifact metadata
    pub fn new(
        artifact_id: ArtifactId,
        execution_id: ExecutionId,
        step_id: StepId,
        name: impl Into<String>,
        artifact_type: ArtifactType,
    ) -> Self {
        Self {
            artifact_id,
            execution_id,
            step_id,
            name: name.into(),
            artifact_type,
            content_type: artifact_type.default_content_type().to_string(),
            original_size: 0,
            compressed_size: 0,
            compression: CompressionType::None,
            content_hash: None,
            storage_uri: None,
            created_at: chrono::Utc::now().timestamp_millis(),
            last_accessed_at: None,
            metadata: None,
        }
    }

    /// Set content type
    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
        self.content_type = content_type.into();
        self
    }

    /// Set original size
    pub fn with_original_size(mut self, size: u64) -> Self {
        self.original_size = size;
        self
    }

    /// Set compressed size
    pub fn with_compressed_size(mut self, size: u64) -> Self {
        self.compressed_size = size;
        self
    }

    /// Set compression type
    pub fn with_compression(mut self, compression: CompressionType) -> Self {
        self.compression = compression;
        self
    }

    /// Set content hash
    pub fn with_content_hash(mut self, hash: impl Into<String>) -> Self {
        self.content_hash = Some(hash.into());
        self
    }

    /// Set storage URI
    pub fn with_storage_uri(mut self, uri: impl Into<String>) -> Self {
        self.storage_uri = Some(uri.into());
        self
    }

    /// Set metadata
    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
        self.metadata = Some(metadata);
        self
    }

    /// Get compression ratio (compressed / original)
    pub fn compression_ratio(&self) -> f64 {
        if self.original_size == 0 {
            1.0
        } else {
            self.compressed_size as f64 / self.original_size as f64
        }
    }

    /// Get space savings percentage
    pub fn space_savings_percent(&self) -> f64 {
        (1.0 - self.compression_ratio()) * 100.0
    }
}

// =============================================================================
// Tests
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_artifact_type_content_types() {
        assert_eq!(ArtifactType::Text.default_content_type(), "text/plain");
        assert_eq!(
            ArtifactType::Json.default_content_type(),
            "application/json"
        );
        assert_eq!(ArtifactType::Image.default_content_type(), "image/png");
        assert_eq!(ArtifactType::Pdf.default_content_type(), "application/pdf");
    }

    #[test]
    fn test_compression_ratio() {
        let metadata = ArtifactMetadata::new(
            ArtifactId::new(),
            ExecutionId::new(),
            StepId::new(),
            "test.txt",
            ArtifactType::Text,
        )
        .with_original_size(1000)
        .with_compressed_size(300);

        assert!((metadata.compression_ratio() - 0.3).abs() < 0.01);
        assert!((metadata.space_savings_percent() - 70.0).abs() < 0.1);
    }
}