use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::agent::AgentName;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RunId(String);
impl RunId {
#[must_use]
pub fn generate() -> Self {
Self(format!("run_{}", Ulid::new()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn minted_at(&self) -> Option<Timestamp> {
let ulid: Ulid = self.0.strip_prefix("run_")?.parse().ok()?;
Timestamp::from_millisecond(i64::try_from(ulid.timestamp_ms()).ok()?).ok()
}
}
impl From<&str> for RunId {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
impl std::fmt::Display for RunId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct FlightId(String);
impl FlightId {
#[must_use]
pub fn generate() -> Self {
Self(format!("flt_{}", Ulid::new()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for FlightId {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
impl std::fmt::Display for FlightId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct ItineraryId(String);
impl ItineraryId {
#[must_use]
pub fn generate() -> Self {
Self(format!("itn_{}", Ulid::new()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Origin {
Human,
Agent(AgentName),
}
impl Origin {
#[must_use]
pub fn agent(&self) -> Option<&AgentName> {
match self {
Self::Human => None,
Self::Agent(name) => Some(name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Flight {
pub id: FlightId,
pub itinerary: ItineraryId,
pub from: Origin,
pub to: AgentName,
pub body: String,
pub hops_remaining: u32,
pub sent_at: Timestamp,
}
impl Flight {
#[must_use]
pub fn new(
itinerary: ItineraryId,
from: Origin,
to: AgentName,
body: impl Into<String>,
hops_remaining: u32,
) -> Self {
Self {
id: FlightId::generate(),
itinerary,
from,
to,
body: body.into(),
hops_remaining,
sent_at: Timestamp::now(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifiers_are_unique_and_prefixed() {
let first = FlightId::generate();
let second = FlightId::generate();
assert_ne!(first, second);
assert!(first.as_str().starts_with("flt_"));
assert!(ItineraryId::generate().as_str().starts_with("itn_"));
}
#[test]
fn a_run_id_says_when_it_was_minted() {
let before = Timestamp::now();
let run = RunId::generate();
let after = Timestamp::now();
let minted = run.minted_at().expect("a generated id decodes");
assert!(
minted.as_millisecond() >= before.as_millisecond() - 1
&& minted <= after + jiff::Span::new().milliseconds(1),
"minted {minted} is outside {before}..{after}"
);
}
#[test]
fn a_real_run_id_decodes_to_when_that_run_happened() {
let minted = RunId::from("run_01M31S7S94MCCD56RC8S6TFQ4Y")
.minted_at()
.expect("decodes");
let day = minted.to_string();
assert!(day.starts_with("2026-09-21"), "decoded to {day}");
}
#[test]
fn an_id_not_minted_here_has_no_time_rather_than_a_wrong_one() {
assert!(RunId::from("my-notes").minted_at().is_none());
assert!(RunId::from("run_not-a-ulid").minted_at().is_none());
assert!(
RunId::from("itn_01M31S7S94MCCD56RC8S6TFQ4Y")
.minted_at()
.is_none()
);
}
#[test]
fn human_origin_has_no_agent() {
assert!(Origin::Human.agent().is_none());
assert_eq!(
Origin::Agent("planner".into()).agent(),
Some(&AgentName::from("planner"))
);
}
}