Skip to main content

agentic_workflow/engine/
store.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::format::{AwfReader, AwfWriter};
5use crate::types::{Workflow, WorkflowError, WorkflowResult};
6
7/// Persistent workflow store backed by .awf files.
8pub struct WorkflowStore {
9    path: PathBuf,
10    workflows: HashMap<String, Workflow>,
11    dirty: bool,
12    auto_save: bool,
13}
14
15impl WorkflowStore {
16    /// Open or create a workflow store at the given path.
17    pub fn open(path: impl AsRef<Path>) -> WorkflowResult<Self> {
18        let path = path.as_ref().to_path_buf();
19        let mut store = Self {
20            path: path.clone(),
21            workflows: HashMap::new(),
22            dirty: false,
23            auto_save: true,
24        };
25
26        if path.exists() {
27            store.load()?;
28            eprintln!("WorkflowStore: loaded {} workflows from {}", store.workflows.len(), path.display());
29        } else {
30            eprintln!("WorkflowStore: created new store at {}", path.display());
31        }
32
33        Ok(store)
34    }
35
36    /// Open an in-memory store (no persistence).
37    pub fn open_memory() -> Self {
38        Self {
39            path: PathBuf::new(),
40            workflows: HashMap::new(),
41            dirty: false,
42            auto_save: false,
43        }
44    }
45
46    /// Load workflows from .awf file.
47    fn load(&mut self) -> WorkflowResult<()> {
48        let file = std::fs::File::open(&self.path)?;
49        let mut reader = AwfReader::new(file);
50        reader.read_header()?;
51
52        // Read all workflow sections
53        loop {
54            match reader.read_workflow() {
55                Ok(wf) => {
56                    self.workflows.insert(wf.id.clone(), wf);
57                }
58                Err(WorkflowError::IoError(_)) => break, // EOF
59                Err(e) => {
60                    eprintln!("WorkflowStore: error reading workflow: {}", e);
61                    break;
62                }
63            }
64        }
65
66        Ok(())
67    }
68
69    /// Save all workflows to .awf file.
70    pub fn save(&mut self) -> WorkflowResult<()> {
71        if self.path.as_os_str().is_empty() {
72            return Ok(()); // Memory-only store
73        }
74
75        let file = std::fs::File::create(&self.path)?;
76        let mut writer = AwfWriter::new(file);
77        writer.write_header()?;
78
79        for wf in self.workflows.values() {
80            writer.write_workflow(wf)?;
81        }
82
83        writer.finish()?;
84        self.dirty = false;
85        eprintln!("WorkflowStore: saved {} workflows to {}", self.workflows.len(), self.path.display());
86        Ok(())
87    }
88
89    /// Auto-save if dirty and auto_save is enabled.
90    fn maybe_auto_save(&mut self) {
91        if self.dirty && self.auto_save {
92            if let Err(e) = self.save() {
93                eprintln!("WorkflowStore: auto-save failed: {}", e);
94            }
95        }
96    }
97
98    /// Add a workflow.
99    pub fn insert(&mut self, workflow: Workflow) -> WorkflowResult<()> {
100        self.workflows.insert(workflow.id.clone(), workflow);
101        self.dirty = true;
102        self.maybe_auto_save();
103        Ok(())
104    }
105
106    /// Get a workflow by ID.
107    pub fn get(&self, id: &str) -> WorkflowResult<&Workflow> {
108        self.workflows
109            .get(id)
110            .ok_or_else(|| WorkflowError::WorkflowNotFound(id.to_string()))
111    }
112
113    /// Remove a workflow.
114    pub fn remove(&mut self, id: &str) -> WorkflowResult<Workflow> {
115        let wf = self.workflows
116            .remove(id)
117            .ok_or_else(|| WorkflowError::WorkflowNotFound(id.to_string()))?;
118        self.dirty = true;
119        self.maybe_auto_save();
120        Ok(wf)
121    }
122
123    /// List all workflows.
124    pub fn list(&self) -> Vec<&Workflow> {
125        self.workflows.values().collect()
126    }
127
128    /// Count of stored workflows.
129    pub fn count(&self) -> usize {
130        self.workflows.len()
131    }
132
133    /// Check if store has unsaved changes.
134    pub fn is_dirty(&self) -> bool {
135        self.dirty
136    }
137
138    /// Set auto-save behavior.
139    pub fn set_auto_save(&mut self, enabled: bool) {
140        self.auto_save = enabled;
141    }
142
143    /// Get the store file path.
144    pub fn path(&self) -> &Path {
145        &self.path
146    }
147}
148
149impl Drop for WorkflowStore {
150    fn drop(&mut self) {
151        if self.dirty && self.auto_save {
152            if let Err(e) = self.save() {
153                eprintln!("WorkflowStore: drop save failed: {}", e);
154            }
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::types::StepNode;
163    use crate::types::StepType;
164
165    #[test]
166    fn test_store_memory_roundtrip() {
167        let mut store = WorkflowStore::open_memory();
168        let wf = Workflow::new("test-wf", "A test");
169        let wfid = wf.id.clone();
170        store.insert(wf).unwrap();
171
172        assert_eq!(store.count(), 1);
173        assert_eq!(store.get(&wfid).unwrap().name, "test-wf");
174        store.remove(&wfid).unwrap();
175        assert_eq!(store.count(), 0);
176    }
177
178    #[test]
179    fn test_store_file_persistence() {
180        let dir = tempfile::tempdir().unwrap();
181        let path = dir.path().join("test.awf");
182
183        // Write
184        {
185            let mut store = WorkflowStore::open(&path).unwrap();
186            store.set_auto_save(false);
187            let mut wf = Workflow::new("persist", "Persistent workflow");
188            wf.add_step(StepNode::new("S1", StepType::Noop));
189            store.insert(wf).unwrap();
190            store.save().unwrap();
191        }
192
193        // Read back
194        {
195            let store = WorkflowStore::open(&path).unwrap();
196            assert_eq!(store.count(), 1);
197            let wf = store.list()[0];
198            assert_eq!(wf.name, "persist");
199            assert_eq!(wf.steps.len(), 1);
200        }
201    }
202
203    #[test]
204    fn test_store_auto_save_on_drop() {
205        let dir = tempfile::tempdir().unwrap();
206        let path = dir.path().join("autosave.awf");
207
208        {
209            let mut store = WorkflowStore::open(&path).unwrap();
210            store.insert(Workflow::new("auto", "Auto-saved")).unwrap();
211            // Drop triggers save
212        }
213
214        let store = WorkflowStore::open(&path).unwrap();
215        assert_eq!(store.count(), 1);
216    }
217}