Skip to main content

enact_core/inbox/
message.rs

1//! Inbox Message Types
2//!
3//! @see docs/TECHNICAL/31-MID-EXECUTION-GUIDANCE.md Section 3
4
5use crate::kernel::ExecutionId;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Inbox message - wraps all message types
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum InboxMessage {
13    /// Control message (pause/resume/cancel)
14    Control(ControlMessage),
15    /// User or system guidance
16    Guidance(GuidanceMessage),
17    /// Evidence update from discovery or external source
18    Evidence(EvidenceUpdate),
19    /// Agent-to-Agent message
20    A2a(A2aMessage),
21}
22
23impl InboxMessage {
24    /// Get the message type for logging/audit
25    pub fn message_type(&self) -> InboxMessageType {
26        match self {
27            InboxMessage::Control(_) => InboxMessageType::Control,
28            InboxMessage::Guidance(_) => InboxMessageType::Guidance,
29            InboxMessage::Evidence(_) => InboxMessageType::Evidence,
30            InboxMessage::A2a(_) => InboxMessageType::A2a,
31        }
32    }
33
34    /// Get the message ID
35    pub fn id(&self) -> &str {
36        match self {
37            InboxMessage::Control(m) => &m.id,
38            InboxMessage::Guidance(m) => &m.id,
39            InboxMessage::Evidence(m) => &m.id,
40            InboxMessage::A2a(m) => &m.id,
41        }
42    }
43
44    /// Get the execution ID this message is for
45    pub fn execution_id(&self) -> &ExecutionId {
46        match self {
47            InboxMessage::Control(m) => &m.execution_id,
48            InboxMessage::Guidance(m) => &m.execution_id,
49            InboxMessage::Evidence(m) => &m.execution_id,
50            InboxMessage::A2a(m) => &m.execution_id,
51        }
52    }
53
54    /// Get the priority for sorting (lower = higher priority)
55    ///
56    /// INV-INBOX-002: Control messages have highest priority
57    pub fn priority_order(&self) -> u8 {
58        match self {
59            InboxMessage::Control(_) => 0, // Highest priority
60            InboxMessage::Evidence(e) if e.impact == EvidenceImpact::ContradictsPlan => 1,
61            InboxMessage::Evidence(_) => 2,
62            InboxMessage::Guidance(g) if g.priority == GuidancePriority::High => 3,
63            InboxMessage::Guidance(_) => 4,
64            InboxMessage::A2a(_) => 5, // Lowest priority
65        }
66    }
67
68    /// Check if this is a control message
69    pub fn is_control(&self) -> bool {
70        matches!(self, InboxMessage::Control(_))
71    }
72}
73
74/// Message type for logging
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum InboxMessageType {
78    Control,
79    Guidance,
80    Evidence,
81    A2a,
82}
83
84// =============================================================================
85// Control Messages
86// =============================================================================
87
88/// Control message - pause/resume/cancel execution
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ControlMessage {
91    /// Unique message ID
92    pub id: String,
93    /// Target execution
94    pub execution_id: ExecutionId,
95    /// Control action
96    pub action: ControlAction,
97    /// Reason for the action
98    pub reason: Option<String>,
99    /// Actor who initiated (user_id, system, agent_id)
100    pub actor: String,
101    /// When the message was created
102    pub created_at: DateTime<Utc>,
103}
104
105impl ControlMessage {
106    /// Create a new control message
107    pub fn new(execution_id: ExecutionId, action: ControlAction, actor: impl Into<String>) -> Self {
108        Self {
109            id: uuid::Uuid::new_v4().to_string(),
110            execution_id,
111            action,
112            reason: None,
113            actor: actor.into(),
114            created_at: Utc::now(),
115        }
116    }
117
118    /// Add a reason
119    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
120        self.reason = Some(reason.into());
121        self
122    }
123}
124
125/// Control action
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum ControlAction {
129    /// Pause execution
130    Pause,
131    /// Resume paused execution
132    Resume,
133    /// Cancel execution
134    Cancel,
135    /// Create a checkpoint
136    Checkpoint,
137    /// Compact context (free memory)
138    Compact,
139}
140
141// =============================================================================
142// Guidance Messages
143// =============================================================================
144
145/// Guidance message - user or system guidance
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct GuidanceMessage {
148    /// Unique message ID
149    pub id: String,
150    /// Target execution
151    pub execution_id: ExecutionId,
152    /// Source of guidance
153    pub from: GuidanceSource,
154    /// Guidance content
155    pub content: String,
156    /// Additional context
157    pub context: Option<serde_json::Value>,
158    /// Priority level
159    pub priority: GuidancePriority,
160    /// When the message was created
161    pub created_at: DateTime<Utc>,
162}
163
164impl GuidanceMessage {
165    /// Create a new guidance message from user
166    pub fn from_user(execution_id: ExecutionId, content: impl Into<String>) -> Self {
167        Self {
168            id: uuid::Uuid::new_v4().to_string(),
169            execution_id,
170            from: GuidanceSource::User,
171            content: content.into(),
172            context: None,
173            priority: GuidancePriority::Medium,
174            created_at: Utc::now(),
175        }
176    }
177
178    /// Set priority
179    pub fn with_priority(mut self, priority: GuidancePriority) -> Self {
180        self.priority = priority;
181        self
182    }
183
184    /// Add context
185    pub fn with_context(mut self, context: serde_json::Value) -> Self {
186        self.context = Some(context);
187        self
188    }
189}
190
191/// Source of guidance
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum GuidanceSource {
195    User,
196    System,
197    Agent,
198}
199
200/// Guidance priority
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum GuidancePriority {
204    Low,
205    Medium,
206    High,
207}
208
209// =============================================================================
210// Evidence Updates
211// =============================================================================
212
213/// Evidence update - new information that may affect execution
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct EvidenceUpdate {
216    /// Unique message ID
217    pub id: String,
218    /// Target execution
219    pub execution_id: ExecutionId,
220    /// Source of evidence
221    pub source: EvidenceSource,
222    /// The evidence content
223    pub title: String,
224    /// Detailed content
225    pub content: serde_json::Value,
226    /// Confidence score (0.0 - 1.0)
227    pub confidence: Option<f64>,
228    /// Impact classification
229    pub impact: EvidenceImpact,
230    /// When the message was created
231    pub created_at: DateTime<Utc>,
232}
233
234impl EvidenceUpdate {
235    /// Create a new evidence update
236    pub fn new(
237        execution_id: ExecutionId,
238        source: EvidenceSource,
239        title: impl Into<String>,
240        content: serde_json::Value,
241        impact: EvidenceImpact,
242    ) -> Self {
243        Self {
244            id: uuid::Uuid::new_v4().to_string(),
245            execution_id,
246            source,
247            title: title.into(),
248            content,
249            confidence: None,
250            impact,
251            created_at: Utc::now(),
252        }
253    }
254
255    /// Set confidence score
256    pub fn with_confidence(mut self, confidence: f64) -> Self {
257        self.confidence = Some(confidence.clamp(0.0, 1.0));
258        self
259    }
260}
261
262/// Source of evidence
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(rename_all = "snake_case")]
265pub enum EvidenceSource {
266    /// Discovered during execution
267    Discovery,
268    /// From a tool result
269    ToolResult,
270    /// From external source
271    External,
272    /// From memory retrieval
273    Memory,
274}
275
276/// Evidence impact classification
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "snake_case")]
279pub enum EvidenceImpact {
280    /// Just informational, add to context
281    Informational,
282    /// Requires human review
283    RequiresReview,
284    /// Contradicts current plan
285    ContradictsPlan,
286}
287
288// =============================================================================
289// A2A Messages (Agent-to-Agent)
290// =============================================================================
291
292/// Agent-to-Agent message
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct A2aMessage {
295    /// Unique message ID
296    pub id: String,
297    /// Target execution
298    pub execution_id: ExecutionId,
299    /// Source agent ID
300    pub from_agent: String,
301    /// Message type
302    pub message_type: String,
303    /// Message payload
304    pub payload: serde_json::Value,
305    /// When the message was created
306    pub created_at: DateTime<Utc>,
307}
308
309impl A2aMessage {
310    /// Create a new A2A message
311    pub fn new(
312        execution_id: ExecutionId,
313        from_agent: impl Into<String>,
314        message_type: impl Into<String>,
315        payload: serde_json::Value,
316    ) -> Self {
317        Self {
318            id: uuid::Uuid::new_v4().to_string(),
319            execution_id,
320            from_agent: from_agent.into(),
321            message_type: message_type.into(),
322            payload,
323            created_at: Utc::now(),
324        }
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_control_message_priority() {
334        let exec_id = ExecutionId::new();
335        let control = InboxMessage::Control(ControlMessage::new(
336            exec_id.clone(),
337            ControlAction::Pause,
338            "test_user",
339        ));
340
341        assert_eq!(control.priority_order(), 0);
342        assert!(control.is_control());
343    }
344
345    #[test]
346    fn test_evidence_priority_contradicts() {
347        let exec_id = ExecutionId::new();
348        let evidence = InboxMessage::Evidence(EvidenceUpdate::new(
349            exec_id,
350            EvidenceSource::Discovery,
351            "Found conflict",
352            serde_json::json!({}),
353            EvidenceImpact::ContradictsPlan,
354        ));
355
356        assert_eq!(evidence.priority_order(), 1);
357    }
358
359    #[test]
360    fn test_guidance_priority() {
361        let exec_id = ExecutionId::new();
362        let high = InboxMessage::Guidance(
363            GuidanceMessage::from_user(exec_id.clone(), "Focus on EU")
364                .with_priority(GuidancePriority::High),
365        );
366        let low = InboxMessage::Guidance(
367            GuidanceMessage::from_user(exec_id, "Also check this")
368                .with_priority(GuidancePriority::Low),
369        );
370
371        assert_eq!(high.priority_order(), 3);
372        assert_eq!(low.priority_order(), 4);
373    }
374
375    #[test]
376    fn test_message_sorting() {
377        let exec_id = ExecutionId::new();
378        let mut messages = [
379            InboxMessage::Guidance(GuidanceMessage::from_user(exec_id.clone(), "test")),
380            InboxMessage::Control(ControlMessage::new(
381                exec_id.clone(),
382                ControlAction::Pause,
383                "user",
384            )),
385            InboxMessage::Evidence(EvidenceUpdate::new(
386                exec_id,
387                EvidenceSource::Discovery,
388                "Found",
389                serde_json::json!({}),
390                EvidenceImpact::Informational,
391            )),
392        ];
393
394        // Sort by priority (INV-INBOX-002)
395        messages.sort_by_key(|m| m.priority_order());
396
397        // Control should be first
398        assert!(messages[0].is_control());
399        assert!(matches!(messages[1], InboxMessage::Evidence(_)));
400        assert!(matches!(messages[2], InboxMessage::Guidance(_)));
401    }
402}