skilllite_agent/
run_checkpoint.rs1use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9use crate::types::{ChatMessage, Task};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RunCheckpoint {
14 pub run_id: String,
15 pub goal: String,
16 pub workspace: String,
17 pub task_plan: Vec<Task>,
18 pub messages: Vec<ChatMessage>,
19 pub updated_at: String,
20}
21
22impl RunCheckpoint {
23 pub fn new(
24 goal: String,
25 workspace: String,
26 task_plan: Vec<Task>,
27 messages: Vec<ChatMessage>,
28 ) -> Self {
29 Self {
30 run_id: uuid::Uuid::new_v4().to_string(),
31 goal,
32 workspace,
33 task_plan,
34 messages,
35 updated_at: chrono::Utc::now().to_rfc3339(),
36 }
37 }
38
39 pub fn update(&mut self, task_plan: Vec<Task>, messages: Vec<ChatMessage>) {
41 self.task_plan = task_plan;
42 self.messages = messages;
43 self.updated_at = chrono::Utc::now().to_rfc3339();
44 }
45}
46
47const CHECKPOINT_DIR: &str = "run_checkpoints";
48const CHECKPOINT_FILE: &str = "latest.json";
49
50pub fn save_checkpoint(chat_root: &Path, checkpoint: &RunCheckpoint) -> Result<()> {
52 let dir = chat_root.join(CHECKPOINT_DIR);
53 skilllite_fs::create_dir_all(&dir)?;
54 let path = dir.join(CHECKPOINT_FILE);
55 let content = serde_json::to_string_pretty(checkpoint)?;
56 skilllite_fs::atomic_write(&path, &content)?;
57 tracing::debug!("Run checkpoint saved to {}", path.display());
58 Ok(())
59}
60
61pub fn load_checkpoint(chat_root: &Path) -> Result<Option<RunCheckpoint>> {
64 let path = chat_root.join(CHECKPOINT_DIR).join(CHECKPOINT_FILE);
65 if !path.exists() {
66 return Ok(None);
67 }
68 let content = skilllite_fs::read_file(&path)?;
69 let checkpoint: RunCheckpoint = serde_json::from_str(&content)?;
70 Ok(Some(checkpoint))
71}
72
73pub fn build_resume_message(checkpoint: &RunCheckpoint) -> String {
75 let completed: Vec<String> = checkpoint
76 .task_plan
77 .iter()
78 .filter(|t| t.completed)
79 .map(|t| format!(" - [完成] {}", t.description))
80 .collect();
81 let remaining: Vec<String> = checkpoint
82 .task_plan
83 .iter()
84 .filter(|t| !t.completed)
85 .map(|t| format!(" - [待做] {}", t.description))
86 .collect();
87
88 let msg = format!(
89 "[断点续跑] 继续执行以下目标:\n\n{}\n\n已完成:\n{}\n\n待完成:\n{}\n\n请从下一个待完成任务继续执行。",
90 checkpoint.goal,
91 if completed.is_empty() {
92 " (无)".to_string()
93 } else {
94 completed.join("\n")
95 },
96 if remaining.is_empty() {
97 " (无 — 请确认目标是否已完成)".to_string()
98 } else {
99 remaining.join("\n")
100 }
101 );
102 msg
103}