use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::types::{ChatMessage, Task};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCheckpoint {
pub run_id: String,
pub goal: String,
pub workspace: String,
pub task_plan: Vec<Task>,
pub messages: Vec<ChatMessage>,
pub updated_at: String,
}
impl RunCheckpoint {
pub fn new(
goal: String,
workspace: String,
task_plan: Vec<Task>,
messages: Vec<ChatMessage>,
) -> Self {
Self {
run_id: uuid::Uuid::new_v4().to_string(),
goal,
workspace,
task_plan,
messages,
updated_at: chrono::Utc::now().to_rfc3339(),
}
}
pub fn update(&mut self, task_plan: Vec<Task>, messages: Vec<ChatMessage>) {
self.task_plan = task_plan;
self.messages = messages;
self.updated_at = chrono::Utc::now().to_rfc3339();
}
}
const CHECKPOINT_DIR: &str = "run_checkpoints";
const CHECKPOINT_FILE: &str = "latest.json";
pub fn save_checkpoint(chat_root: &Path, checkpoint: &RunCheckpoint) -> Result<()> {
let dir = chat_root.join(CHECKPOINT_DIR);
skilllite_fs::create_dir_all(&dir)?;
let path = dir.join(CHECKPOINT_FILE);
let content = serde_json::to_string_pretty(checkpoint)?;
skilllite_fs::atomic_write(&path, &content)?;
tracing::debug!("Run checkpoint saved to {}", path.display());
Ok(())
}
pub fn load_checkpoint(chat_root: &Path) -> Result<Option<RunCheckpoint>> {
let path = chat_root.join(CHECKPOINT_DIR).join(CHECKPOINT_FILE);
if !path.exists() {
return Ok(None);
}
let content = skilllite_fs::read_file(&path)?;
let checkpoint: RunCheckpoint = serde_json::from_str(&content)?;
Ok(Some(checkpoint))
}
pub fn build_resume_message(checkpoint: &RunCheckpoint) -> String {
let completed: Vec<String> = checkpoint
.task_plan
.iter()
.filter(|t| t.completed)
.map(|t| format!(" - [完成] {}", t.description))
.collect();
let remaining: Vec<String> = checkpoint
.task_plan
.iter()
.filter(|t| !t.completed)
.map(|t| format!(" - [待做] {}", t.description))
.collect();
let msg = format!(
"[断点续跑] 继续执行以下目标:\n\n{}\n\n已完成:\n{}\n\n待完成:\n{}\n\n请从下一个待完成任务继续执行。",
checkpoint.goal,
if completed.is_empty() {
" (无)".to_string()
} else {
completed.join("\n")
},
if remaining.is_empty() {
" (无 — 请确认目标是否已完成)".to_string()
} else {
remaining.join("\n")
}
);
msg
}