Skip to main content

cortex_core/
trace.rs

1//! Causal chains of events.
2//!
3//! A [`Trace`] is the canonical container for a sequence of related events
4//! (e.g. all events in one agent invocation). The shape mirrors BUILD_SPEC §9.2.
5//!
6//! Lifecycle:
7//!
8//! - [`TraceStatus::Open`] — accepting new events; not yet sealed.
9//! - [`TraceStatus::Closed`] — sealed; the chain integrity check has passed
10//!   and `closed_at` is set. Closed traces are immutable.
11//! - [`TraceStatus::Quarantined`] — closed but flagged: integrity check
12//!   failed, an unresolved contradiction was attached, or an explicit
13//!   operator action quarantined it. Reflection MUST skip quarantined
14//!   traces unless explicitly asked.
15
16use chrono::{DateTime, Utc};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use crate::ids::{EventId, TraceId};
21
22/// Lifecycle state of a [`Trace`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "snake_case")]
25pub enum TraceStatus {
26    /// Accepting new events; not yet sealed.
27    Open,
28    /// Sealed; integrity verified; immutable.
29    Closed,
30    /// Sealed but flagged (integrity failure, unresolved contradiction, or
31    /// explicit operator action).
32    Quarantined,
33}
34
35/// A causal chain of events.
36///
37/// Field order matches BUILD_SPEC §9.2 wire order; do not reorder without a
38/// schema bump.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40pub struct Trace {
41    /// Stable identifier.
42    pub id: TraceId,
43    /// Schema version this row was written under.
44    pub schema_version: u16,
45    /// When the trace was opened.
46    pub opened_at: DateTime<Utc>,
47    /// When the trace was sealed (`Closed` or `Quarantined`); `None` while
48    /// `Open`.
49    pub closed_at: Option<DateTime<Utc>>,
50    /// Ordered event IDs in this trace. Ordering is the canonical
51    /// ledger ordering (event-hash chain), not wall-clock.
52    pub event_ids: Vec<EventId>,
53    /// Free-form trace type tag (e.g. `agent_run`, `manual_session`,
54    /// `replay`). Conventions live with the producer.
55    pub trace_type: String,
56    /// Lifecycle status.
57    pub status: TraceStatus,
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::SCHEMA_VERSION;
64    use chrono::TimeZone;
65
66    fn fixture_trace() -> Trace {
67        Trace {
68            id: "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap(),
69            schema_version: SCHEMA_VERSION,
70            opened_at: Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 0).unwrap(),
71            closed_at: Some(Utc.with_ymd_and_hms(2026, 1, 1, 12, 5, 0).unwrap()),
72            event_ids: vec![
73                "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".parse().unwrap(),
74                "evt_01ARZ3NDEKTSV4RRFFQ69G5FAW".parse().unwrap(),
75            ],
76            trace_type: "agent_run".into(),
77            status: TraceStatus::Closed,
78        }
79    }
80
81    #[test]
82    fn trace_serde_round_trip() {
83        let t = fixture_trace();
84        let j = serde_json::to_value(&t).expect("serialize");
85        let back: Trace = serde_json::from_value(j.clone()).expect("deserialize");
86        assert_eq!(t, back);
87
88        let obj = j.as_object().expect("trace serializes as object");
89        for k in [
90            "id",
91            "schema_version",
92            "opened_at",
93            "closed_at",
94            "event_ids",
95            "trace_type",
96            "status",
97        ] {
98            assert!(obj.contains_key(k), "trace JSON missing field `{k}`");
99        }
100        assert_eq!(obj["status"], serde_json::json!("closed"));
101    }
102
103    #[test]
104    fn trace_status_wire_strings() {
105        assert_eq!(
106            serde_json::to_value(TraceStatus::Open).unwrap(),
107            serde_json::json!("open")
108        );
109        assert_eq!(
110            serde_json::to_value(TraceStatus::Closed).unwrap(),
111            serde_json::json!("closed")
112        );
113        assert_eq!(
114            serde_json::to_value(TraceStatus::Quarantined).unwrap(),
115            serde_json::json!("quarantined")
116        );
117    }
118
119    #[test]
120    fn open_trace_round_trips_with_null_closed_at() {
121        let mut t = fixture_trace();
122        t.status = TraceStatus::Open;
123        t.closed_at = None;
124        let j = serde_json::to_value(&t).unwrap();
125        assert_eq!(j["closed_at"], serde_json::Value::Null);
126        let back: Trace = serde_json::from_value(j).unwrap();
127        assert_eq!(t, back);
128    }
129}