enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Inbox Message Types
//!
//! @see docs/TECHNICAL/31-MID-EXECUTION-GUIDANCE.md Section 3

use crate::kernel::ExecutionId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Inbox message - wraps all message types
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InboxMessage {
    /// Control message (pause/resume/cancel)
    Control(ControlMessage),
    /// User or system guidance
    Guidance(GuidanceMessage),
    /// Evidence update from discovery or external source
    Evidence(EvidenceUpdate),
    /// Agent-to-Agent message
    A2a(A2aMessage),
}

impl InboxMessage {
    /// Get the message type for logging/audit
    pub fn message_type(&self) -> InboxMessageType {
        match self {
            InboxMessage::Control(_) => InboxMessageType::Control,
            InboxMessage::Guidance(_) => InboxMessageType::Guidance,
            InboxMessage::Evidence(_) => InboxMessageType::Evidence,
            InboxMessage::A2a(_) => InboxMessageType::A2a,
        }
    }

    /// Get the message ID
    pub fn id(&self) -> &str {
        match self {
            InboxMessage::Control(m) => &m.id,
            InboxMessage::Guidance(m) => &m.id,
            InboxMessage::Evidence(m) => &m.id,
            InboxMessage::A2a(m) => &m.id,
        }
    }

    /// Get the execution ID this message is for
    pub fn execution_id(&self) -> &ExecutionId {
        match self {
            InboxMessage::Control(m) => &m.execution_id,
            InboxMessage::Guidance(m) => &m.execution_id,
            InboxMessage::Evidence(m) => &m.execution_id,
            InboxMessage::A2a(m) => &m.execution_id,
        }
    }

    /// Get the priority for sorting (lower = higher priority)
    ///
    /// INV-INBOX-002: Control messages have highest priority
    pub fn priority_order(&self) -> u8 {
        match self {
            InboxMessage::Control(_) => 0, // Highest priority
            InboxMessage::Evidence(e) if e.impact == EvidenceImpact::ContradictsPlan => 1,
            InboxMessage::Evidence(_) => 2,
            InboxMessage::Guidance(g) if g.priority == GuidancePriority::High => 3,
            InboxMessage::Guidance(_) => 4,
            InboxMessage::A2a(_) => 5, // Lowest priority
        }
    }

    /// Check if this is a control message
    pub fn is_control(&self) -> bool {
        matches!(self, InboxMessage::Control(_))
    }
}

/// Message type for logging
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InboxMessageType {
    Control,
    Guidance,
    Evidence,
    A2a,
}

// =============================================================================
// Control Messages
// =============================================================================

/// Control message - pause/resume/cancel execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlMessage {
    /// Unique message ID
    pub id: String,
    /// Target execution
    pub execution_id: ExecutionId,
    /// Control action
    pub action: ControlAction,
    /// Reason for the action
    pub reason: Option<String>,
    /// Actor who initiated (user_id, system, agent_id)
    pub actor: String,
    /// When the message was created
    pub created_at: DateTime<Utc>,
}

impl ControlMessage {
    /// Create a new control message
    pub fn new(execution_id: ExecutionId, action: ControlAction, actor: impl Into<String>) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            execution_id,
            action,
            reason: None,
            actor: actor.into(),
            created_at: Utc::now(),
        }
    }

    /// Add a reason
    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
        self.reason = Some(reason.into());
        self
    }
}

/// Control action
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ControlAction {
    /// Pause execution
    Pause,
    /// Resume paused execution
    Resume,
    /// Cancel execution
    Cancel,
    /// Create a checkpoint
    Checkpoint,
    /// Compact context (free memory)
    Compact,
}

// =============================================================================
// Guidance Messages
// =============================================================================

/// Guidance message - user or system guidance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuidanceMessage {
    /// Unique message ID
    pub id: String,
    /// Target execution
    pub execution_id: ExecutionId,
    /// Source of guidance
    pub from: GuidanceSource,
    /// Guidance content
    pub content: String,
    /// Additional context
    pub context: Option<serde_json::Value>,
    /// Priority level
    pub priority: GuidancePriority,
    /// When the message was created
    pub created_at: DateTime<Utc>,
}

impl GuidanceMessage {
    /// Create a new guidance message from user
    pub fn from_user(execution_id: ExecutionId, content: impl Into<String>) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            execution_id,
            from: GuidanceSource::User,
            content: content.into(),
            context: None,
            priority: GuidancePriority::Medium,
            created_at: Utc::now(),
        }
    }

    /// Set priority
    pub fn with_priority(mut self, priority: GuidancePriority) -> Self {
        self.priority = priority;
        self
    }

    /// Add context
    pub fn with_context(mut self, context: serde_json::Value) -> Self {
        self.context = Some(context);
        self
    }
}

/// Source of guidance
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuidanceSource {
    User,
    System,
    Agent,
}

/// Guidance priority
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuidancePriority {
    Low,
    Medium,
    High,
}

// =============================================================================
// Evidence Updates
// =============================================================================

/// Evidence update - new information that may affect execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceUpdate {
    /// Unique message ID
    pub id: String,
    /// Target execution
    pub execution_id: ExecutionId,
    /// Source of evidence
    pub source: EvidenceSource,
    /// The evidence content
    pub title: String,
    /// Detailed content
    pub content: serde_json::Value,
    /// Confidence score (0.0 - 1.0)
    pub confidence: Option<f64>,
    /// Impact classification
    pub impact: EvidenceImpact,
    /// When the message was created
    pub created_at: DateTime<Utc>,
}

impl EvidenceUpdate {
    /// Create a new evidence update
    pub fn new(
        execution_id: ExecutionId,
        source: EvidenceSource,
        title: impl Into<String>,
        content: serde_json::Value,
        impact: EvidenceImpact,
    ) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            execution_id,
            source,
            title: title.into(),
            content,
            confidence: None,
            impact,
            created_at: Utc::now(),
        }
    }

    /// Set confidence score
    pub fn with_confidence(mut self, confidence: f64) -> Self {
        self.confidence = Some(confidence.clamp(0.0, 1.0));
        self
    }
}

/// Source of evidence
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceSource {
    /// Discovered during execution
    Discovery,
    /// From a tool result
    ToolResult,
    /// From external source
    External,
    /// From memory retrieval
    Memory,
}

/// Evidence impact classification
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceImpact {
    /// Just informational, add to context
    Informational,
    /// Requires human review
    RequiresReview,
    /// Contradicts current plan
    ContradictsPlan,
}

// =============================================================================
// A2A Messages (Agent-to-Agent)
// =============================================================================

/// Agent-to-Agent message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aMessage {
    /// Unique message ID
    pub id: String,
    /// Target execution
    pub execution_id: ExecutionId,
    /// Source agent ID
    pub from_agent: String,
    /// Message type
    pub message_type: String,
    /// Message payload
    pub payload: serde_json::Value,
    /// When the message was created
    pub created_at: DateTime<Utc>,
}

impl A2aMessage {
    /// Create a new A2A message
    pub fn new(
        execution_id: ExecutionId,
        from_agent: impl Into<String>,
        message_type: impl Into<String>,
        payload: serde_json::Value,
    ) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            execution_id,
            from_agent: from_agent.into(),
            message_type: message_type.into(),
            payload,
            created_at: Utc::now(),
        }
    }
}

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

    #[test]
    fn test_control_message_priority() {
        let exec_id = ExecutionId::new();
        let control = InboxMessage::Control(ControlMessage::new(
            exec_id.clone(),
            ControlAction::Pause,
            "test_user",
        ));

        assert_eq!(control.priority_order(), 0);
        assert!(control.is_control());
    }

    #[test]
    fn test_evidence_priority_contradicts() {
        let exec_id = ExecutionId::new();
        let evidence = InboxMessage::Evidence(EvidenceUpdate::new(
            exec_id,
            EvidenceSource::Discovery,
            "Found conflict",
            serde_json::json!({}),
            EvidenceImpact::ContradictsPlan,
        ));

        assert_eq!(evidence.priority_order(), 1);
    }

    #[test]
    fn test_guidance_priority() {
        let exec_id = ExecutionId::new();
        let high = InboxMessage::Guidance(
            GuidanceMessage::from_user(exec_id.clone(), "Focus on EU")
                .with_priority(GuidancePriority::High),
        );
        let low = InboxMessage::Guidance(
            GuidanceMessage::from_user(exec_id, "Also check this")
                .with_priority(GuidancePriority::Low),
        );

        assert_eq!(high.priority_order(), 3);
        assert_eq!(low.priority_order(), 4);
    }

    #[test]
    fn test_message_sorting() {
        let exec_id = ExecutionId::new();
        let mut messages = [
            InboxMessage::Guidance(GuidanceMessage::from_user(exec_id.clone(), "test")),
            InboxMessage::Control(ControlMessage::new(
                exec_id.clone(),
                ControlAction::Pause,
                "user",
            )),
            InboxMessage::Evidence(EvidenceUpdate::new(
                exec_id,
                EvidenceSource::Discovery,
                "Found",
                serde_json::json!({}),
                EvidenceImpact::Informational,
            )),
        ];

        // Sort by priority (INV-INBOX-002)
        messages.sort_by_key(|m| m.priority_order());

        // Control should be first
        assert!(messages[0].is_control());
        assert!(matches!(messages[1], InboxMessage::Evidence(_)));
        assert!(matches!(messages[2], InboxMessage::Guidance(_)));
    }
}