agentic_workflow/engine/
store.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::format::{AwfReader, AwfWriter};
5use crate::types::{Workflow, WorkflowError, WorkflowResult};
6
7pub struct WorkflowStore {
9 path: PathBuf,
10 workflows: HashMap<String, Workflow>,
11 dirty: bool,
12 auto_save: bool,
13}
14
15impl WorkflowStore {
16 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 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 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 loop {
54 match reader.read_workflow() {
55 Ok(wf) => {
56 self.workflows.insert(wf.id.clone(), wf);
57 }
58 Err(WorkflowError::IoError(_)) => break, Err(e) => {
60 eprintln!("WorkflowStore: error reading workflow: {}", e);
61 break;
62 }
63 }
64 }
65
66 Ok(())
67 }
68
69 pub fn save(&mut self) -> WorkflowResult<()> {
71 if self.path.as_os_str().is_empty() {
72 return Ok(()); }
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 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 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 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 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 pub fn list(&self) -> Vec<&Workflow> {
125 self.workflows.values().collect()
126 }
127
128 pub fn count(&self) -> usize {
130 self.workflows.len()
131 }
132
133 pub fn is_dirty(&self) -> bool {
135 self.dirty
136 }
137
138 pub fn set_auto_save(&mut self, enabled: bool) {
140 self.auto_save = enabled;
141 }
142
143 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 {
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 {
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 }
213
214 let store = WorkflowStore::open(&path).unwrap();
215 assert_eq!(store.count(), 1);
216 }
217}