Skip to main content

agent_diva_core/soul/
mod.rs

1//! Soul state helpers.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6
7/// Runtime state for soul/bootstrap lifecycle.
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct SoulState {
10    /// Timestamp when bootstrap was first seeded.
11    pub bootstrap_seeded_at: Option<DateTime<Utc>>,
12    /// Timestamp when bootstrap was marked as completed.
13    pub bootstrap_completed_at: Option<DateTime<Utc>>,
14}
15
16/// Small persistence helper for soul lifecycle state.
17pub struct SoulStateStore {
18    path: PathBuf,
19}
20
21impl SoulStateStore {
22    /// Create a state store under `<workspace>/.agent-diva/soul-state.json`.
23    pub fn new(workspace: impl AsRef<Path>) -> Self {
24        let path = workspace
25            .as_ref()
26            .join(".agent-diva")
27            .join("soul-state.json");
28        Self { path }
29    }
30
31    /// Read state from disk. Missing file returns defaults.
32    pub fn load(&self) -> std::io::Result<SoulState> {
33        if !self.path.exists() {
34            return Ok(SoulState::default());
35        }
36
37        let raw = std::fs::read_to_string(&self.path)?;
38        let state: SoulState = serde_json::from_str(&raw).unwrap_or_default();
39        Ok(state)
40    }
41
42    /// Persist state to disk.
43    pub fn save(&self, state: &SoulState) -> std::io::Result<()> {
44        if let Some(parent) = self.path.parent() {
45            std::fs::create_dir_all(parent)?;
46        }
47        let content = serde_json::to_string_pretty(state)?;
48        std::fs::write(&self.path, content)
49    }
50
51    /// Returns true if bootstrap completion was already recorded.
52    pub fn is_bootstrap_completed(&self) -> bool {
53        self.load()
54            .ok()
55            .and_then(|state| state.bootstrap_completed_at)
56            .is_some()
57    }
58
59    /// Mark bootstrap as seeded once.
60    pub fn mark_bootstrap_seeded(&self) -> std::io::Result<()> {
61        let mut state = self.load().unwrap_or_default();
62        if state.bootstrap_seeded_at.is_none() {
63            state.bootstrap_seeded_at = Some(Utc::now());
64            self.save(&state)?;
65        }
66        Ok(())
67    }
68
69    /// Mark bootstrap as completed.
70    pub fn mark_bootstrap_completed(&self) -> std::io::Result<()> {
71        let mut state = self.load().unwrap_or_default();
72        if state.bootstrap_seeded_at.is_none() {
73            state.bootstrap_seeded_at = Some(Utc::now());
74        }
75        if state.bootstrap_completed_at.is_none() {
76            state.bootstrap_completed_at = Some(Utc::now());
77            self.save(&state)?;
78        }
79        Ok(())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_load_missing_returns_default() {
89        let temp = tempfile::tempdir().unwrap();
90        let store = SoulStateStore::new(temp.path());
91        let state = store.load().unwrap();
92        assert!(state.bootstrap_seeded_at.is_none());
93        assert!(state.bootstrap_completed_at.is_none());
94    }
95
96    #[test]
97    fn test_mark_bootstrap_seeded_writes_once() {
98        let temp = tempfile::tempdir().unwrap();
99        let store = SoulStateStore::new(temp.path());
100
101        store.mark_bootstrap_seeded().unwrap();
102        let first = store.load().unwrap();
103        assert!(first.bootstrap_seeded_at.is_some());
104
105        store.mark_bootstrap_seeded().unwrap();
106        let second = store.load().unwrap();
107        assert_eq!(first.bootstrap_seeded_at, second.bootstrap_seeded_at);
108    }
109
110    #[test]
111    fn test_is_bootstrap_completed() {
112        let temp = tempfile::tempdir().unwrap();
113        let store = SoulStateStore::new(temp.path());
114        assert!(!store.is_bootstrap_completed());
115
116        let mut state = SoulState::default();
117        state.bootstrap_completed_at = Some(Utc::now());
118        store.save(&state).unwrap();
119        assert!(store.is_bootstrap_completed());
120    }
121
122    #[test]
123    fn test_mark_bootstrap_completed_sets_completed_at() {
124        let temp = tempfile::tempdir().unwrap();
125        let store = SoulStateStore::new(temp.path());
126        store.mark_bootstrap_completed().unwrap();
127        let state = store.load().unwrap();
128        assert!(state.bootstrap_seeded_at.is_some());
129        assert!(state.bootstrap_completed_at.is_some());
130    }
131}