Skip to main content

agent_base/types/
plan_update.rs

1use serde::{Deserialize, Serialize};
2
3/// Lightweight plan step status — display-only, no execution semantics.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum PlanStepStatus {
7    Pending,
8    InProgress,
9    Completed,
10}
11
12/// A single step in a lightweight plan checklist.
13///
14/// Contains only human-readable display text and a status.
15/// Does NOT carry tool names, command payloads, host info, or dependency graphs.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PlanItem {
18    pub step: String,
19    pub status: PlanStepStatus,
20}
21
22/// Arguments for the `update_plan` tool.
23///
24/// This is a lightweight progress-display protocol — the tool broadcasts a
25/// structured snapshot to the UI and does nothing else.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct UpdatePlanArgs {
28    /// A one-sentence summary of the user's goal.
29    /// Optional on subsequent calls — the tool remembers the last objective.
30    /// Example: "安装 Casdoor 身份认证系统"
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub objective: Option<String>,
33    /// Optional explanation of why the plan changed.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub explanation: Option<String>,
36    /// The full plan checklist (replaces any previous display).
37    pub plan: Vec<PlanItem>,
38}
39
40impl UpdatePlanArgs {
41    /// Validate the plan arguments.
42    ///
43    /// Rules:
44    /// - `objective`, if provided, must be non-empty after trimming and at most 200 chars.
45    /// - `plan` must contain 1–50 steps.
46    /// - Each `step` must be non-empty.
47    /// - At most one step may be `InProgress`.
48    pub fn validate(&self) -> Result<(), String> {
49        if let Some(ref objective) = self.objective {
50            let objective_trimmed = objective.trim();
51            if objective_trimmed.is_empty() {
52                return Err("objective must not be empty when provided".to_string());
53            }
54            if objective_trimmed.chars().count() > 200 {
55                return Err(format!(
56                    "objective must be at most 200 characters, got {}",
57                    objective_trimmed.chars().count()
58                ));
59            }
60        }
61        if self.plan.is_empty() {
62            return Err("plan must contain at least one step".to_string());
63        }
64        if self.plan.len() > 50 {
65            return Err(format!(
66                "plan must contain at most 50 steps, got {}",
67                self.plan.len()
68            ));
69        }
70
71        let in_progress_count = self
72            .plan
73            .iter()
74            .filter(|item| item.status == PlanStepStatus::InProgress)
75            .count();
76
77        if in_progress_count > 1 {
78            return Err(format!(
79                "at most one step may be in_progress, found {}",
80                in_progress_count
81            ));
82        }
83
84        for (i, item) in self.plan.iter().enumerate() {
85            if item.step.trim().is_empty() {
86                return Err(format!("step[{}] must not be empty", i));
87            }
88        }
89
90        Ok(())
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_optional_objective_deserialization() {
100        // objective present — should parse fine
101        let json =
102            r#"{"objective": "Install Docker", "plan": [{"step": "Step 1", "status": "pending"}]}"#;
103        let args: UpdatePlanArgs = serde_json::from_str(json).unwrap();
104        assert_eq!(args.objective.as_deref(), Some("Install Docker"));
105        assert!(args.validate().is_ok());
106
107        // objective absent — should parse with None
108        let json = r#"{"plan": [{"step": "Step 1", "status": "pending"}]}"#;
109        let args: UpdatePlanArgs = serde_json::from_str(json).unwrap();
110        assert!(args.objective.is_none());
111        assert!(args.validate().is_ok());
112
113        // objective present but empty — should fail validation
114        let json = r#"{"objective": "  ", "plan": [{"step": "Step 1", "status": "pending"}]}"#;
115        let args: UpdatePlanArgs = serde_json::from_str(json).unwrap();
116        assert!(args.validate().is_err());
117    }
118}