use crate::workflow::{WorkflowDefinition, WorkflowEvent, WorkflowRunState};
#[cfg(not(target_arch = "wasm32"))]
use serde::Serialize;
use std::path::{Path, PathBuf};
pub struct WorkflowWorkspace {
root_dir: PathBuf,
runs_dir: PathBuf,
}
impl WorkflowWorkspace {
pub fn new(home_dir: impl Into<PathBuf>) -> Self {
let root_dir = home_dir.into().join(".atomiagent").join("workflows");
let runs_dir = root_dir.join("runs");
Self { root_dir, runs_dir }
}
pub async fn ensure_dirs(&self) -> Result<(), String> {
#[cfg(target_arch = "wasm32")]
{
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
{
tokio::fs::create_dir_all(&self.runs_dir)
.await
.map_err(|e| format!("Failed to create workflow workspace: {e}"))
}
}
pub fn root_dir(&self) -> &Path {
&self.root_dir
}
pub fn run_dir(&self, run_id: &str) -> PathBuf {
self.runs_dir.join(run_id)
}
pub fn task_workspace(&self, run_id: &str, node_id: &str) -> PathBuf {
self.run_dir(run_id).join("tasks").join(node_id)
}
pub fn state_file(&self, run_id: &str) -> PathBuf {
self.run_dir(run_id).join("state.json")
}
pub fn snapshot_file(&self, run_id: &str) -> PathBuf {
self.run_dir(run_id).join("workflow.json")
}
pub fn events_file(&self, run_id: &str) -> PathBuf {
self.run_dir(run_id).join("events.jsonl")
}
pub fn interventions_file(&self, run_id: &str) -> PathBuf {
self.run_dir(run_id).join("interventions.json")
}
pub async fn initialize_run_dir(&self, run_id: &str) -> Result<(), String> {
#[cfg(target_arch = "wasm32")]
{
let _ = run_id;
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
{
let run_dir = self.run_dir(run_id);
tokio::fs::create_dir_all(run_dir.join("tasks"))
.await
.map_err(|e| format!("Failed to create workflow run directory: {e}"))
}
}
pub async fn save_definition(
&self,
run_id: &str,
definition: &WorkflowDefinition,
) -> Result<(), String> {
self.write_json(self.snapshot_file(run_id), definition)
.await
}
pub async fn save_state(&self, run_id: &str, state: &WorkflowRunState) -> Result<(), String> {
self.write_json(self.state_file(run_id), state).await?;
self.write_json(
self.interventions_file(run_id),
&state.pending_interventions,
)
.await
}
pub async fn load_state(&self, run_id: &str) -> Result<WorkflowRunState, String> {
self.read_json(self.state_file(run_id)).await
}
pub async fn load_definition(&self, run_id: &str) -> Result<WorkflowDefinition, String> {
self.read_json(self.snapshot_file(run_id)).await
}
pub async fn append_event(&self, run_id: &str, event: &WorkflowEvent) -> Result<(), String> {
#[cfg(target_arch = "wasm32")]
{
let _ = (run_id, event);
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = self.events_file(run_id);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("Failed to create workflow event directory: {e}"))?;
}
let mut line = serde_json::to_string(event)
.map_err(|e| format!("Failed to serialize workflow event: {e}"))?;
line.push('\n');
use tokio::io::AsyncWriteExt;
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
.map_err(|e| format!("Failed to open workflow event log: {e}"))?;
file.write_all(line.as_bytes())
.await
.map_err(|e| format!("Failed to append workflow event: {e}"))
}
}
pub async fn list_run_ids(&self) -> Result<Vec<String>, String> {
#[cfg(target_arch = "wasm32")]
{
Ok(Vec::new())
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut runs = Vec::new();
let mut entries = match tokio::fs::read_dir(&self.runs_dir).await {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(runs),
Err(err) => return Err(format!("Failed to read workflow runs: {err}")),
};
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| format!("Failed to iterate workflow runs: {e}"))?
{
if entry
.file_type()
.await
.map_err(|e| format!("Failed to inspect workflow run entry: {e}"))?
.is_dir()
{
runs.push(entry.file_name().to_string_lossy().to_string());
}
}
runs.sort();
Ok(runs)
}
}
#[cfg(not(target_arch = "wasm32"))]
async fn write_json<T>(&self, path: PathBuf, value: &T) -> Result<(), String>
where
T: Serialize,
{
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("Failed to create workflow directory: {e}"))?;
}
let raw = serde_json::to_string_pretty(value)
.map_err(|e| format!("Failed to serialize workflow state: {e}"))?;
tokio::fs::write(path, raw)
.await
.map_err(|e| format!("Failed to write workflow state: {e}"))
}
#[cfg(target_arch = "wasm32")]
async fn write_json<T>(&self, _path: PathBuf, _value: &T) -> Result<(), String> {
Ok(())
}
async fn read_json<T>(&self, path: PathBuf) -> Result<T, String>
where
T: serde::de::DeserializeOwned,
{
#[cfg(target_arch = "wasm32")]
{
let _ = path;
Err("Workflow persistence is not available on wasm32.".to_string())
}
#[cfg(not(target_arch = "wasm32"))]
{
let raw = tokio::fs::read_to_string(path)
.await
.map_err(|e| format!("Failed to read workflow file: {e}"))?;
serde_json::from_str(&raw).map_err(|e| format!("Failed to parse workflow file: {e}"))
}
}
}