stakpak-server 0.3.62

Stakpak: Your DevOps AI Agent. Generate infrastructure code, debug Kubernetes, configure CI/CD, automate deployments, without giving an LLM the keys to production.
Documentation
use std::path::{Path, PathBuf};

use stakpak_agent_core::{CheckpointEnvelopeV1, deserialize_checkpoint, serialize_checkpoint};
use uuid::Uuid;

#[derive(Debug, Clone)]
pub struct CheckpointStore {
    root: PathBuf,
}

impl CheckpointStore {
    pub fn new(root: impl Into<PathBuf>) -> Self {
        let root = root.into();
        let _ = std::fs::create_dir_all(&root);
        Self { root }
    }

    pub fn default_local() -> Self {
        let root = std::env::var("HOME")
            .map(|home| {
                PathBuf::from(home)
                    .join(".stakpak")
                    .join("server")
                    .join("checkpoints")
            })
            .unwrap_or_else(|_| PathBuf::from(".stakpak").join("server").join("checkpoints"));

        Self::new(root)
    }

    pub fn root(&self) -> &Path {
        &self.root
    }

    pub async fn load_latest(
        &self,
        session_id: Uuid,
    ) -> Result<Option<CheckpointEnvelopeV1>, String> {
        let latest_path = self.latest_path(session_id);

        let payload = match tokio::fs::read(&latest_path).await {
            Ok(payload) => payload,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(error) => {
                return Err(format!(
                    "Failed to read checkpoint envelope from {}: {}",
                    latest_path.display(),
                    error
                ));
            }
        };

        deserialize_checkpoint(&payload)
            .map(Some)
            .map_err(|error| format!("Failed to deserialize checkpoint envelope: {error}"))
    }

    pub async fn save_latest(
        &self,
        session_id: Uuid,
        envelope: &CheckpointEnvelopeV1,
    ) -> Result<(), String> {
        let latest_path = self.latest_path(session_id);
        if let Some(parent) = latest_path.parent() {
            tokio::fs::create_dir_all(parent).await.map_err(|error| {
                format!(
                    "Failed to create checkpoint directory {}: {}",
                    parent.display(),
                    error
                )
            })?;
        }

        let payload = serialize_checkpoint(envelope)
            .map_err(|error| format!("Failed to serialize checkpoint envelope: {error}"))?;

        let temp_path = latest_path.with_extension("tmp");

        tokio::fs::write(&temp_path, payload)
            .await
            .map_err(|error| {
                format!(
                    "Failed to write temporary checkpoint envelope {}: {}",
                    temp_path.display(),
                    error
                )
            })?;

        tokio::fs::rename(&temp_path, &latest_path)
            .await
            .map_err(|error| {
                format!(
                    "Failed to finalize checkpoint envelope {}: {}",
                    latest_path.display(),
                    error
                )
            })?;

        Ok(())
    }

    fn latest_path(&self, session_id: Uuid) -> PathBuf {
        self.root
            .join(session_id.to_string())
            .join("latest.checkpoint")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use stakai::{Message, Role};

    #[tokio::test]
    async fn save_and_load_checkpoint_envelope_roundtrip() {
        let root = std::env::temp_dir().join(format!("stakpak-checkpoint-test-{}", Uuid::new_v4()));
        let store = CheckpointStore::new(root);
        let session_id = Uuid::new_v4();
        let run_id = Uuid::new_v4();

        let envelope = CheckpointEnvelopeV1::new(
            Some(run_id),
            vec![Message::new(Role::User, "hello")],
            json!({"turn": 1}),
        );

        let save = store.save_latest(session_id, &envelope).await;
        assert!(save.is_ok());

        let loaded = store.load_latest(session_id).await;
        assert!(loaded.is_ok());

        let Some(loaded_envelope) = loaded.ok().flatten() else {
            panic!("expected checkpoint envelope");
        };

        assert_eq!(loaded_envelope.run_id, Some(run_id));
        assert_eq!(
            loaded_envelope
                .messages
                .first()
                .and_then(stakai::Message::text),
            Some("hello".to_string())
        );
    }

    #[tokio::test]
    async fn load_latest_migrates_legacy_messages_array_payload() {
        let root = std::env::temp_dir().join(format!("stakpak-checkpoint-test-{}", Uuid::new_v4()));
        let store = CheckpointStore::new(root);
        let session_id = Uuid::new_v4();

        let legacy_payload = json!([
            {
                "role": "user",
                "content": "legacy"
            }
        ]);

        let latest_path = store
            .root()
            .join(session_id.to_string())
            .join("latest.checkpoint");

        if let Some(parent) = latest_path.parent() {
            let create_dir = tokio::fs::create_dir_all(parent).await;
            assert!(create_dir.is_ok());
        }

        let write_result = tokio::fs::write(&latest_path, legacy_payload.to_string()).await;
        assert!(write_result.is_ok());

        let loaded = store.load_latest(session_id).await;
        assert!(loaded.is_ok());

        let Some(loaded_envelope) = loaded.ok().flatten() else {
            panic!("expected migrated checkpoint envelope");
        };

        assert_eq!(
            loaded_envelope
                .messages
                .first()
                .and_then(stakai::Message::text),
            Some("legacy".to_string())
        );
    }

    #[tokio::test]
    async fn load_latest_returns_none_for_missing_session() {
        let root = std::env::temp_dir().join(format!("stakpak-checkpoint-test-{}", Uuid::new_v4()));
        let store = CheckpointStore::new(root);

        let loaded = store.load_latest(Uuid::new_v4()).await;
        assert!(loaded.is_ok());
        assert!(loaded.ok().flatten().is_none());
    }
}