Skip to main content

ainl_memory/
trajectory_table.rs

1//! Row-oriented trajectory storage in `ainl_trajectories` (sibling to graph nodes).
2//!
3//! Full step payloads live here so `ainl_graph_nodes` trajectory rows can stay small for recall.
4
5use ainl_contracts::{TrajectoryOutcome, TrajectoryStep};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// One persisted trajectory run: episode-linked, JSON-step payload, optional graph node cross-ref.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct TrajectoryDetailRecord {
12    pub id: Uuid,
13    pub episode_id: Uuid,
14    /// Corresponding `AinlNodeType::Trajectory` node id when also written to the graph.
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub graph_trajectory_node_id: Option<Uuid>,
17    pub agent_id: String,
18    pub session_id: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub project_id: Option<String>,
21    pub recorded_at: i64,
22    pub outcome: TrajectoryOutcome,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub ainl_source_hash: Option<String>,
25    pub duration_ms: u64,
26    pub steps: Vec<TrajectoryStep>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub frame_vars: Option<serde_json::Value>,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub fitness_delta: Option<f32>,
31}
32
33impl TrajectoryDetailRecord {
34    /// Map this DB row to a [`ainl_trajectory::TrajectoryReplayLine`] for JSONL export.
35    #[must_use]
36    pub fn to_replay_line(&self) -> ainl_trajectory::TrajectoryReplayLine {
37        ainl_trajectory::trajectory_replay_line(
38            self.id,
39            self.episode_id,
40            self.graph_trajectory_node_id,
41            &self.agent_id,
42            &self.session_id,
43            self.project_id.as_deref(),
44            self.recorded_at,
45            self.outcome,
46            self.ainl_source_hash.as_deref(),
47            self.duration_ms,
48            self.steps.clone(),
49            self.frame_vars.clone(),
50            self.fitness_delta,
51        )
52    }
53
54    /// One JSON object + newline (JSONL) for tooling / replay files.
55    pub fn to_replay_jsonl(&self) -> Result<String, serde_json::Error> {
56        self.to_replay_line().to_jsonl_string()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use uuid::Uuid;
64
65    #[test]
66    fn detail_record_replay_jsonl_roundtrip() {
67        let r = TrajectoryDetailRecord {
68            id: Uuid::nil(),
69            episode_id: Uuid::nil(),
70            graph_trajectory_node_id: None,
71            agent_id: "a".into(),
72            session_id: "s".into(),
73            project_id: None,
74            recorded_at: 42,
75            outcome: TrajectoryOutcome::PartialSuccess,
76            ainl_source_hash: Some("h1".into()),
77            duration_ms: 7,
78            steps: vec![],
79            frame_vars: None,
80            fitness_delta: None,
81        };
82        let line = r.to_replay_jsonl().unwrap();
83        let rows = ainl_trajectory::parse_jsonl(&line).unwrap();
84        assert_eq!(rows.len(), 1);
85        assert_eq!(rows[0].agent_id, "a");
86        assert_eq!(rows[0].outcome, TrajectoryOutcome::PartialSuccess);
87    }
88}