use crate::error::{SwarmError, SwarmResult};
use crate::phase::TokenUsage;
use crate::types::{ContextVariables, Message};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const CURRENT_CHECKPOINT_VERSION: u32 = 1;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CheckpointData {
pub messages: Vec<Message>,
pub context_variables: ContextVariables,
pub current_agent: String,
pub iteration: u32,
pub token_usage: TokenUsage,
}
impl CheckpointData {
pub fn new(
messages: Vec<Message>,
context_variables: ContextVariables,
current_agent: impl Into<String>,
iteration: u32,
token_usage: TokenUsage,
) -> Self {
Self {
messages,
context_variables,
current_agent: current_agent.into(),
iteration,
token_usage,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CheckpointEnvelope {
pub version: u32,
pub session_id: String,
pub created_at: DateTime<Utc>,
pub payload: CheckpointData,
}
impl CheckpointEnvelope {
pub fn new(session_id: impl Into<String>, payload: CheckpointData) -> Self {
Self {
version: CURRENT_CHECKPOINT_VERSION,
session_id: session_id.into(),
created_at: Utc::now(),
payload,
}
}
pub fn is_compatible(&self) -> bool {
self.version == CURRENT_CHECKPOINT_VERSION
}
pub fn validate(&self) -> SwarmResult<()> {
if self.session_id.trim().is_empty() {
return Err(SwarmError::ValidationError(
"CheckpointEnvelope session_id cannot be empty".to_string(),
));
}
if !self.is_compatible() {
return Err(SwarmError::Other(format!(
"Checkpoint version {} is incompatible with current version {}; \
manual migration required",
self.version, CURRENT_CHECKPOINT_VERSION
)));
}
if self.payload.current_agent.trim().is_empty() {
return Err(SwarmError::ValidationError(
"CheckpointData current_agent cannot be empty".to_string(),
));
}
Ok(())
}
pub fn to_json(&self) -> SwarmResult<String> {
serde_json::to_string(self).map_err(|e| {
SwarmError::SerializationError(format!("checkpoint serialization failed: {}", e))
})
}
pub fn from_json(s: &str) -> SwarmResult<Self> {
serde_json::from_str(s).map_err(|e| {
SwarmError::DeserializationError(format!("checkpoint deserialization failed: {}", e))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::MessageRole;
fn sample_payload() -> CheckpointData {
CheckpointData::new(
vec![Message::new(MessageRole::User, Some("hello".to_string()), None, None).unwrap()],
ContextVariables::new(),
"test-agent",
3,
TokenUsage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
)
}
#[test]
fn test_roundtrip() {
let env = CheckpointEnvelope::new("session-1", sample_payload());
let json = env.to_json().unwrap();
let restored = CheckpointEnvelope::from_json(&json).unwrap();
assert_eq!(restored.session_id, "session-1");
assert_eq!(restored.payload.iteration, 3);
assert!(restored.is_compatible());
}
#[test]
fn test_validate_empty_session() {
let env = CheckpointEnvelope::new("", sample_payload());
assert!(env.validate().is_err());
}
#[test]
fn test_validate_version_mismatch() {
let mut env = CheckpointEnvelope::new("s1", sample_payload());
env.version = 999;
assert!(env.validate().is_err());
}
#[test]
fn test_validate_ok() {
let env = CheckpointEnvelope::new("session-1", sample_payload());
assert!(env.validate().is_ok());
}
}