netsky-core 0.2.0

netsky core: agent model, prompt loader, spawner, config
Documentation
use serde::{Deserialize, Serialize};

use crate::envelope::Envelope;

pub const KIND_TEXT: &str = "text";
pub const RESTART_KIND_REQUEST: &str = "restart-request";
pub const RESTART_KIND_ACK: &str = "restart-ack";
pub const RESTART_KIND_CONFIRM: &str = "restart-confirm";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestartRequest {
    pub handoff_path: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestartAck {
    pub handoff_path: String,
    pub verified: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestartConfirm {
    pub handoff_path: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RestartMessage {
    Request(RestartRequest),
    Ack(RestartAck),
    Confirm(RestartConfirm),
}

impl RestartMessage {
    pub fn kind(&self) -> &'static str {
        match self {
            Self::Request(_) => RESTART_KIND_REQUEST,
            Self::Ack(_) => RESTART_KIND_ACK,
            Self::Confirm(_) => RESTART_KIND_CONFIRM,
        }
    }

    pub fn handoff_path(&self) -> &str {
        match self {
            Self::Request(v) => &v.handoff_path,
            Self::Ack(v) => &v.handoff_path,
            Self::Confirm(v) => &v.handoff_path,
        }
    }

    pub fn to_text(&self) -> Result<String, serde_json::Error> {
        match self {
            Self::Request(v) => serde_json::to_string(v),
            Self::Ack(v) => serde_json::to_string(v),
            Self::Confirm(v) => serde_json::to_string(v),
        }
    }

    pub fn from_envelope(env: &Envelope) -> Result<Option<Self>, String> {
        match env.kind_or_text() {
            RESTART_KIND_REQUEST => serde_json::from_str::<RestartRequest>(&env.text)
                .map(Self::Request)
                .map(Some)
                .map_err(|e| format!("invalid restart-request payload: {e}")),
            RESTART_KIND_ACK => serde_json::from_str::<RestartAck>(&env.text)
                .map(Self::Ack)
                .map(Some)
                .map_err(|e| format!("invalid restart-ack payload: {e}")),
            RESTART_KIND_CONFIRM => serde_json::from_str::<RestartConfirm>(&env.text)
                .map(Self::Confirm)
                .map(Some)
                .map_err(|e| format!("invalid restart-confirm payload: {e}")),
            _ => Ok(None),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_restart_request_from_envelope() {
        let mut env = Envelope::new(
            "agent0",
            r#"{"handoff_path":"/tmp/handoff.txt"}"#,
            "2026-04-19T20:44:47Z",
        );
        env.kind = Some(RESTART_KIND_REQUEST.to_string());
        let msg = RestartMessage::from_envelope(&env).unwrap();
        assert_eq!(
            msg,
            Some(RestartMessage::Request(RestartRequest {
                handoff_path: "/tmp/handoff.txt".to_string(),
            }))
        );
    }

    #[test]
    fn ignores_non_restart_envelope_kinds() {
        let env = Envelope::new("agent0", "hello", "2026-04-19T20:44:47Z");
        assert_eq!(RestartMessage::from_envelope(&env).unwrap(), None);
    }

    #[test]
    fn rejects_invalid_restart_payload() {
        let mut env = Envelope::new("agent0", "not-json", "2026-04-19T20:44:47Z");
        env.kind = Some(RESTART_KIND_ACK.to_string());
        let err = RestartMessage::from_envelope(&env).unwrap_err();
        assert!(err.contains("invalid restart-ack payload"));
    }
}