1use jiff::Timestamp;
4
5use serde::{Deserialize, Serialize};
6use ulid::Ulid;
7
8use crate::agent::AgentName;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
15#[serde(transparent)]
16pub struct RunId(String);
17
18impl RunId {
19 #[must_use]
21 pub fn generate() -> Self {
22 Self(format!("run_{}", Ulid::new()))
23 }
24
25 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30
31 #[must_use]
43 pub fn minted_at(&self) -> Option<Timestamp> {
44 let ulid: Ulid = self.0.strip_prefix("run_")?.parse().ok()?;
45
46 Timestamp::from_millisecond(i64::try_from(ulid.timestamp_ms()).ok()?).ok()
47 }
48}
49
50impl From<&str> for RunId {
51 fn from(value: &str) -> Self {
52 Self(value.to_owned())
53 }
54}
55
56impl std::fmt::Display for RunId {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.write_str(&self.0)
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
64#[serde(transparent)]
65pub struct FlightId(String);
66
67impl FlightId {
68 #[must_use]
70 pub fn generate() -> Self {
71 Self(format!("flt_{}", Ulid::new()))
72 }
73
74 #[must_use]
76 pub fn as_str(&self) -> &str {
77 &self.0
78 }
79}
80
81impl From<&str> for FlightId {
88 fn from(value: &str) -> Self {
89 Self(value.to_owned())
90 }
91}
92
93impl std::fmt::Display for FlightId {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.write_str(&self.0)
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
101#[serde(transparent)]
102pub struct ItineraryId(String);
103
104impl ItineraryId {
105 #[must_use]
107 pub fn generate() -> Self {
108 Self(format!("itn_{}", Ulid::new()))
109 }
110
111 #[must_use]
113 pub fn as_str(&self) -> &str {
114 &self.0
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
123#[serde(rename_all = "snake_case")]
124pub enum Origin {
125 Human,
127 Agent(AgentName),
129}
130
131impl Origin {
132 #[must_use]
134 pub fn agent(&self) -> Option<&AgentName> {
135 match self {
136 Self::Human => None,
137 Self::Agent(name) => Some(name),
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
148pub struct Flight {
149 pub id: FlightId,
151 pub itinerary: ItineraryId,
153 pub from: Origin,
155 pub to: AgentName,
157 pub body: String,
159 pub hops_remaining: u32,
161 pub sent_at: Timestamp,
163}
164
165impl Flight {
166 #[must_use]
168 pub fn new(
169 itinerary: ItineraryId,
170 from: Origin,
171 to: AgentName,
172 body: impl Into<String>,
173 hops_remaining: u32,
174 ) -> Self {
175 Self {
176 id: FlightId::generate(),
177 itinerary,
178 from,
179 to,
180 body: body.into(),
181 hops_remaining,
182 sent_at: Timestamp::now(),
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn identifiers_are_unique_and_prefixed() {
193 let first = FlightId::generate();
194 let second = FlightId::generate();
195
196 assert_ne!(first, second);
197 assert!(first.as_str().starts_with("flt_"));
198 assert!(ItineraryId::generate().as_str().starts_with("itn_"));
199 }
200
201 #[test]
202 fn a_run_id_says_when_it_was_minted() {
203 let before = Timestamp::now();
206 let run = RunId::generate();
207 let after = Timestamp::now();
208
209 let minted = run.minted_at().expect("a generated id decodes");
210
211 assert!(
213 minted.as_millisecond() >= before.as_millisecond() - 1
214 && minted <= after + jiff::Span::new().milliseconds(1),
215 "minted {minted} is outside {before}..{after}"
216 );
217 }
218
219 #[test]
220 fn a_real_run_id_decodes_to_when_that_run_happened() {
221 let minted = RunId::from("run_01M31S7S94MCCD56RC8S6TFQ4Y")
226 .minted_at()
227 .expect("decodes");
228
229 let day = minted.to_string();
230 assert!(day.starts_with("2026-09-21"), "decoded to {day}");
231 }
232
233 #[test]
234 fn an_id_not_minted_here_has_no_time_rather_than_a_wrong_one() {
235 assert!(RunId::from("my-notes").minted_at().is_none());
238 assert!(RunId::from("run_not-a-ulid").minted_at().is_none());
239 assert!(
240 RunId::from("itn_01M31S7S94MCCD56RC8S6TFQ4Y")
241 .minted_at()
242 .is_none()
243 );
244 }
245
246 #[test]
247 fn human_origin_has_no_agent() {
248 assert!(Origin::Human.agent().is_none());
249 assert_eq!(
250 Origin::Agent("planner".into()).agent(),
251 Some(&AgentName::from("planner"))
252 );
253 }
254}