Skip to main content

t_minus/
types.rs

1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use uuid::Uuid;
5
6/// Unique identifier for an agent in the system.
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct AgentId(pub String);
9
10impl fmt::Display for AgentId {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        write!(f, "{}", self.0)
13    }
14}
15
16impl From<&str> for AgentId {
17    fn from(s: &str) -> Self {
18        AgentId(s.to_string())
19    }
20}
21
22/// The kind of event being coordinated.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub enum EventKind {
25    Meeting,
26    Checkpoint,
27    Review,
28    Deploy,
29    Custom(String),
30}
31
32impl fmt::Display for EventKind {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            EventKind::Meeting => write!(f, "meeting"),
36            EventKind::Checkpoint => write!(f, "checkpoint"),
37            EventKind::Review => write!(f, "review"),
38            EventKind::Deploy => write!(f, "deploy"),
39            EventKind::Custom(s) => write!(f, "custom:{}", s),
40        }
41    }
42}
43
44impl std::str::FromStr for EventKind {
45    type Err = String;
46    fn from_str(s: &str) -> Result<Self, Self::Err> {
47        match s.to_lowercase().as_str() {
48            "meeting" => Ok(EventKind::Meeting),
49            "checkpoint" => Ok(EventKind::Checkpoint),
50            "review" => Ok(EventKind::Review),
51            "deploy" => Ok(EventKind::Deploy),
52            other => Ok(EventKind::Custom(other.to_string())),
53        }
54    }
55}
56
57/// An agent's response to an event invitation.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum ResponseStatus {
60    Pending,
61    Confirmed,
62    Deferred(Duration),
63    Missed,
64}
65
66impl fmt::Display for ResponseStatus {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            ResponseStatus::Pending => write!(f, "pending"),
70            ResponseStatus::Confirmed => write!(f, "confirmed"),
71            ResponseStatus::Deferred(d) => write!(f, "deferred:{}s", d.num_seconds()),
72            ResponseStatus::Missed => write!(f, "missed"),
73        }
74    }
75}
76
77/// A T-minus countdown event for agent coordination.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TMinusEvent {
80    pub id: Uuid,
81    pub kind: EventKind,
82    pub scheduled_at: DateTime<Utc>,
83    pub t_minus: Duration,
84    pub organizer: AgentId,
85    pub attendees: Vec<(AgentId, ResponseStatus)>,
86    pub quorum: usize,
87    pub payload: serde_json::Value,
88}
89
90impl TMinusEvent {
91    /// The effective fire time: scheduled_at minus t_minus.
92    pub fn fire_time(&self) -> DateTime<Utc> {
93        self.scheduled_at - self.t_minus
94    }
95
96    /// Whether the event has reached quorum (enough confirmations).
97    pub fn has_quorum(&self) -> bool {
98        self.confirmed_count() >= self.quorum
99    }
100
101    /// Count of confirmed attendees.
102    pub fn confirmed_count(&self) -> usize {
103        self.attendees
104            .iter()
105            .filter(|(_, s)| matches!(s, ResponseStatus::Confirmed))
106            .count()
107    }
108
109    /// Count of pending attendees.
110    pub fn pending_count(&self) -> usize {
111        self.attendees
112            .iter()
113            .filter(|(_, s)| matches!(s, ResponseStatus::Pending))
114            .count()
115    }
116
117    /// Whether the event has fired (fire time has passed and quorum met).
118    pub fn is_fired(&self, now: DateTime<Utc>) -> bool {
119        now >= self.fire_time() && self.has_quorum()
120    }
121
122    /// Whether the event has been missed (fire time passed without quorum).
123    pub fn is_missed(&self, now: DateTime<Utc>) -> bool {
124        now >= self.fire_time() && !self.has_quorum()
125    }
126
127    /// Time remaining until fire time.
128    pub fn time_remaining(&self, now: DateTime<Utc>) -> Duration {
129        self.fire_time() - now
130    }
131}
132
133/// A campaign is a sequence of events with dependencies.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Campaign {
136    pub id: Uuid,
137    pub name: String,
138    pub events: Vec<Uuid>,
139    pub dependencies: Vec<(Uuid, Uuid)>,
140}
141
142impl Campaign {
143    pub fn new(name: String) -> Self {
144        Campaign {
145            id: Uuid::new_v4(),
146            name,
147            events: Vec::new(),
148            dependencies: Vec::new(),
149        }
150    }
151
152    /// Topological sort of events based on dependencies.
153    /// Returns Ok(sorted) or Err(cycle) if a dependency cycle exists.
154    pub fn execution_order(&self) -> Result<Vec<Uuid>, Vec<Uuid>> {
155        let mut in_degree: std::collections::HashMap<Uuid, usize> = std::collections::HashMap::new();
156        let mut adj: std::collections::HashMap<Uuid, Vec<Uuid>> = std::collections::HashMap::new();
157
158        for id in &self.events {
159            in_degree.insert(*id, 0);
160            adj.insert(*id, Vec::new());
161        }
162
163        for (from, to) in &self.dependencies {
164            adj.get_mut(from).unwrap().push(*to);
165            *in_degree.entry(*to).or_insert(0) += 1;
166        }
167
168        let mut queue: std::collections::VecDeque<Uuid> = std::collections::VecDeque::new();
169        for (id, &deg) in &in_degree {
170            if deg == 0 {
171                queue.push_back(*id);
172            }
173        }
174
175        let mut sorted = Vec::new();
176        while let Some(id) = queue.pop_front() {
177            sorted.push(id);
178            if let Some(neighbors) = adj.get(&id) {
179                for &next in neighbors {
180                    let deg = in_degree.get_mut(&next).unwrap();
181                    *deg -= 1;
182                    if *deg == 0 {
183                        queue.push_back(next);
184                    }
185                }
186            }
187        }
188
189        if sorted.len() == self.events.len() {
190            Ok(sorted)
191        } else {
192            // Return events in the cycle (those not in sorted)
193            let cycle: Vec<Uuid> = self.events.iter().filter(|e| !sorted.contains(e)).copied().collect();
194            Err(cycle)
195        }
196    }
197}
198
199/// Result of a tick operation — events that were fired or missed.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct TickResult {
202    pub fired: Vec<Uuid>,
203    pub missed: Vec<Uuid>,
204}
205
206/// Errors that can occur in the engine.
207#[derive(Debug, thiserror::Error)]
208pub enum TMinusError {
209    #[error("event not found: {0}")]
210    EventNotFound(Uuid),
211    #[error("campaign not found: {0}")]
212    CampaignNotFound(Uuid),
213    #[error("agent {0} not an attendee of event {1}")]
214    NotAttendee(AgentId, Uuid),
215    #[error("dependency cycle detected: {0:?}")]
216    DependencyCycle(Vec<Uuid>),
217    #[error("database error: {0}")]
218    Database(#[from] rusqlite::Error),
219    #[error("serde error: {0}")]
220    Serde(#[from] serde_json::Error),
221    #[error("invalid input: {0}")]
222    InvalidInput(String),
223}