Skip to main content

yoagent_state/
store.rs

1use crate::{ArtifactRef, Event, EventId, ForkId, ForkSnapshot, GraphSnapshot, StateError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6
7#[async_trait]
8pub trait EventStore: Send + Sync {
9    async fn append(&self, events: Vec<Event>) -> Result<Vec<EventId>, StateError>;
10    async fn scan(&self) -> Result<Vec<Event>, StateError>;
11    async fn scan_after(&self, event_id: Option<EventId>) -> Result<Vec<Event>, StateError>;
12}
13
14#[async_trait]
15pub trait SnapshotStore: Send + Sync {
16    async fn save_snapshot(&self, snapshot: GraphSnapshot) -> Result<(), StateError>;
17    async fn load_snapshot(&self) -> Result<Option<GraphSnapshot>, StateError>;
18}
19
20#[async_trait]
21pub trait ForkStore: Send + Sync {
22    async fn save_fork(&self, fork: ForkSnapshot) -> Result<(), StateError>;
23    async fn load_fork(&self, fork_id: ForkId) -> Result<Option<ForkSnapshot>, StateError>;
24}
25
26#[async_trait]
27pub trait IndexStore: Send + Sync {
28    async fn put_index(&self, key: String, value: serde_json::Value) -> Result<(), StateError>;
29    async fn get_index(&self, key: String) -> Result<Option<serde_json::Value>, StateError>;
30}
31
32#[async_trait]
33pub trait ArtifactStore: Send + Sync {
34    async fn put_artifact(
35        &self,
36        artifact: ArtifactRef,
37        bytes: Vec<u8>,
38    ) -> Result<ArtifactRef, StateError>;
39    async fn get_artifact(&self, artifact: ArtifactRef) -> Result<Option<Vec<u8>>, StateError>;
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43struct IndexRecord {
44    key: String,
45    value: serde_json::Value,
46}
47
48#[derive(Debug, Clone, Default)]
49pub struct MemoryEventStore {
50    events: Arc<Mutex<Vec<Event>>>,
51}
52
53impl MemoryEventStore {
54    pub fn new() -> Self {
55        Self::default()
56    }
57}
58
59#[async_trait]
60impl EventStore for MemoryEventStore {
61    async fn append(&self, events: Vec<Event>) -> Result<Vec<EventId>, StateError> {
62        let ids = events.iter().map(|event| event.id.clone()).collect();
63        self.events
64            .lock()
65            .map_err(|err| StateError::Store(err.to_string()))?
66            .extend(events);
67        Ok(ids)
68    }
69
70    async fn scan(&self) -> Result<Vec<Event>, StateError> {
71        Ok(self
72            .events
73            .lock()
74            .map_err(|err| StateError::Store(err.to_string()))?
75            .clone())
76    }
77
78    async fn scan_after(&self, event_id: Option<EventId>) -> Result<Vec<Event>, StateError> {
79        scan_after_events(self.scan().await?, event_id)
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct JsonlEventStore {
85    path: PathBuf,
86    lock: Arc<tokio::sync::Mutex<()>>,
87}
88
89impl JsonlEventStore {
90    pub fn new(path: impl Into<PathBuf>) -> Self {
91        Self {
92            path: path.into(),
93            lock: Arc::new(tokio::sync::Mutex::new(())),
94        }
95    }
96
97    pub fn path(&self) -> &Path {
98        &self.path
99    }
100
101    fn snapshot_path(&self) -> PathBuf {
102        self.path.with_extension("snapshot.json")
103    }
104
105    fn index_path(&self) -> PathBuf {
106        self.path.with_extension("index.jsonl")
107    }
108
109    fn fork_path(&self, fork_id: &ForkId) -> PathBuf {
110        self.path
111            .parent()
112            .unwrap_or_else(|| Path::new("."))
113            .join("forks")
114            .join(format!("{}.json", fork_id.0))
115    }
116}
117
118#[async_trait]
119impl EventStore for JsonlEventStore {
120    async fn append(&self, events: Vec<Event>) -> Result<Vec<EventId>, StateError> {
121        let _guard = self.lock.lock().await;
122        if let Some(parent) = self.path.parent() {
123            tokio::fs::create_dir_all(parent).await?;
124        }
125
126        let mut existing = if self.path.exists() {
127            tokio::fs::read_to_string(&self.path).await?
128        } else {
129            String::new()
130        };
131        let ids = events.iter().map(|event| event.id.clone()).collect();
132        for event in events {
133            existing.push_str(&serde_json::to_string(&event)?);
134            existing.push('\n');
135        }
136        tokio::fs::write(&self.path, existing).await?;
137        Ok(ids)
138    }
139
140    async fn scan(&self) -> Result<Vec<Event>, StateError> {
141        if !self.path.exists() {
142            return Ok(Vec::new());
143        }
144
145        let raw = tokio::fs::read_to_string(&self.path).await?;
146        raw.lines()
147            .filter(|line| !line.trim().is_empty())
148            .map(|line| serde_json::from_str(line).map_err(StateError::from))
149            .collect()
150    }
151
152    async fn scan_after(&self, event_id: Option<EventId>) -> Result<Vec<Event>, StateError> {
153        scan_after_events(self.scan().await?, event_id)
154    }
155}
156
157#[async_trait]
158impl SnapshotStore for JsonlEventStore {
159    async fn save_snapshot(&self, snapshot: GraphSnapshot) -> Result<(), StateError> {
160        let path = self.snapshot_path();
161        if let Some(parent) = path.parent() {
162            tokio::fs::create_dir_all(parent).await?;
163        }
164        tokio::fs::write(path, serde_json::to_vec_pretty(&snapshot)?).await?;
165        Ok(())
166    }
167
168    async fn load_snapshot(&self) -> Result<Option<GraphSnapshot>, StateError> {
169        let path = self.snapshot_path();
170        if !path.exists() {
171            return Ok(None);
172        }
173        Ok(Some(serde_json::from_slice(&tokio::fs::read(path).await?)?))
174    }
175}
176
177#[async_trait]
178impl ForkStore for JsonlEventStore {
179    async fn save_fork(&self, fork: ForkSnapshot) -> Result<(), StateError> {
180        let path = self.fork_path(&fork.id);
181        if let Some(parent) = path.parent() {
182            tokio::fs::create_dir_all(parent).await?;
183        }
184        tokio::fs::write(path, serde_json::to_vec_pretty(&fork)?).await?;
185        Ok(())
186    }
187
188    async fn load_fork(&self, fork_id: ForkId) -> Result<Option<ForkSnapshot>, StateError> {
189        let path = self.fork_path(&fork_id);
190        if !path.exists() {
191            return Ok(None);
192        }
193        Ok(Some(serde_json::from_slice(&tokio::fs::read(path).await?)?))
194    }
195}
196
197#[async_trait]
198impl IndexStore for JsonlEventStore {
199    async fn put_index(&self, key: String, value: serde_json::Value) -> Result<(), StateError> {
200        let path = self.index_path();
201        if let Some(parent) = path.parent() {
202            tokio::fs::create_dir_all(parent).await?;
203        }
204        let mut existing = if path.exists() {
205            tokio::fs::read_to_string(&path).await?
206        } else {
207            String::new()
208        };
209        existing.push_str(&serde_json::to_string(&IndexRecord { key, value })?);
210        existing.push('\n');
211        tokio::fs::write(path, existing).await?;
212        Ok(())
213    }
214
215    async fn get_index(&self, key: String) -> Result<Option<serde_json::Value>, StateError> {
216        let path = self.index_path();
217        if !path.exists() {
218            return Ok(None);
219        }
220        let mut found = None;
221        for line in tokio::fs::read_to_string(path).await?.lines() {
222            if line.trim().is_empty() {
223                continue;
224            }
225            let record: IndexRecord = serde_json::from_str(line)?;
226            if record.key == key {
227                found = Some(record.value);
228            }
229        }
230        Ok(found)
231    }
232}
233
234pub(crate) fn scan_after_events(
235    events: Vec<Event>,
236    event_id: Option<EventId>,
237) -> Result<Vec<Event>, StateError> {
238    let Some(event_id) = event_id else {
239        return Ok(events);
240    };
241
242    let position = events
243        .iter()
244        .position(|event| event.id == event_id)
245        .ok_or(StateError::EventNotFound(event_id))?;
246    Ok(events.into_iter().skip(position + 1).collect())
247}