cascade-agent 0.1.1

Async LLM agentic engine with tool execution, memory management, and dynamic skills
Documentation
pub mod types;

use crate::error::Result;
use std::path::{Path, PathBuf};
use types::{Plan, PlanData, PlanStatus, StepStatus};

#[derive(Debug)]
pub struct PlanManager {
    plans_dir: PathBuf,
}

impl PlanManager {
    pub fn new(plans_dir: PathBuf) -> Result<Self> {
        std::fs::create_dir_all(&plans_dir)?;
        Ok(Self { plans_dir })
    }

    pub fn create_plan(&self, task_id: &str, title: &str, steps: Vec<String>) -> Result<Plan> {
        let mut plan = Plan::new(task_id, title, steps);
        let filename = format!(
            "{}_{}.toml",
            plan.created_at.format("%Y%m%d_%H%M%S"),
            &plan.id[..8]
        );
        plan.file_path = self.plans_dir.join(filename);
        self.save_plan(&plan)?;
        Ok(plan)
    }

    pub fn save_plan(&self, plan: &Plan) -> Result<()> {
        let data = PlanData::from_plan(plan);
        let content = toml::to_string_pretty(&data)?;
        std::fs::write(&plan.file_path, content)?;
        Ok(())
    }

    pub fn load_plan(&self, file_path: &Path) -> Result<Plan> {
        let content = std::fs::read_to_string(file_path)?;
        let data: PlanData = toml::from_str(&content)?;
        data.to_plan(file_path)
    }

    pub fn list_plans(&self) -> Result<Vec<PathBuf>> {
        let mut plans: Vec<PathBuf> = std::fs::read_dir(&self.plans_dir)?
            .filter_map(|e| e.ok())
            .map(|e| e.path())
            .filter(|p| p.extension().is_some_and(|ext| ext == "toml"))
            .collect();
        plans.sort();
        Ok(plans)
    }

    pub fn update_step(
        &self,
        plan: &mut Plan,
        step_num: usize,
        status: StepStatus,
        result: Option<&str>,
    ) -> Result<()> {
        match status {
            StepStatus::Completed => {
                if !plan.mark_step_completed(step_num, result.map(String::from)) {
                    return Err(crate::error::AgentError::ConfigError(format!(
                        "Step {} not found in plan",
                        step_num
                    )));
                }
            }
            StepStatus::Failed => {
                if !plan.mark_step_failed(step_num, result.unwrap_or_default().to_string()) {
                    return Err(crate::error::AgentError::ConfigError(format!(
                        "Step {} not found in plan",
                        step_num
                    )));
                }
            }
            StepStatus::InProgress => {
                if !plan.mark_step_in_progress(step_num) {
                    return Err(crate::error::AgentError::ConfigError(format!(
                        "Step {} not found in plan",
                        step_num
                    )));
                }
            }
            _ => {
                if let Some(step) = plan.steps.iter_mut().find(|s| s.number == step_num) {
                    step.status = status;
                    step.result = result.map(String::from);
                    plan.updated_at = chrono::Utc::now();
                } else {
                    return Err(crate::error::AgentError::ConfigError(format!(
                        "Step {} not found in plan",
                        step_num
                    )));
                }
            }
        }
        self.save_plan(plan)?;
        Ok(())
    }

    pub fn render_markdown(&self, plan: &Plan) -> String {
        let status_emoji = match &plan.status {
            PlanStatus::Draft => "📝",
            PlanStatus::PendingApproval => "",
            PlanStatus::Approved => "",
            PlanStatus::InProgress => "🔄",
            PlanStatus::Completed => "🏁",
            PlanStatus::Cancelled => "",
        };

        let step_emoji = |s: &StepStatus| match s {
            StepStatus::Pending => "",
            StepStatus::InProgress => "🔄",
            StepStatus::Completed => "",
            StepStatus::Failed => "",
            StepStatus::Skipped => "⏭️",
        };

        let mut md = format!("# {}\n\n", plan.title);

        md.push_str(&format!("<!-- plan_id: {} -->\n", plan.id));
        md.push_str(&format!("<!-- task_id: {} -->\n", plan.task_id));
        md.push_str(&format!("<!-- plan_status: {:?} -->\n", plan.status));
        md.push_str(&format!(
            "<!-- created_at: {} -->\n",
            plan.created_at.to_rfc3339()
        ));
        md.push_str(&format!(
            "<!-- updated_at: {} -->\n",
            plan.updated_at.to_rfc3339()
        ));

        md.push_str(&format!("**Status:** {} {:?}\n", status_emoji, plan.status));
        md.push_str(&format!("**Task ID:** {}\n", plan.task_id));
        md.push_str(&format!("**Plan ID:** {}\n", plan.id));
        md.push_str(&format!(
            "**Created:** {}\n\n",
            plan.created_at.format("%Y-%m-%d %H:%M:%S UTC")
        ));
        md.push_str("---\n\n## Steps\n\n");

        for step in &plan.steps {
            md.push_str(&format!(
                "<!-- step_status_{}: {:?} -->\n",
                step.number, step.status
            ));
            md.push_str(&format!(
                "{} **Step {}:** {}\n",
                step_emoji(&step.status),
                step.number,
                step.description
            ));
            if let Some(result) = &step.result {
                md.push_str(&format!("   - Result: {}\n", result));
            }
            md.push('\n');
        }

        md
    }
}