Skip to main content

skilllite_agent/
planning_guard.rs

1//! Lightweight guards around task planning results.
2
3use crate::types::Task;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6enum FallbackPlanKind {
7    InspectOnly,
8    ChangeAndVerify,
9}
10
11#[derive(Debug, Clone)]
12pub struct EmptyPlanGuardResult {
13    pub reason: &'static str,
14    pub fallback_tasks: Vec<Task>,
15}
16
17fn task(id: u32, description: &str, tool_hint: Option<&str>) -> Task {
18    Task {
19        id,
20        description: description.to_string(),
21        tool_hint: tool_hint.map(|s| s.to_string()),
22        completed: false,
23    }
24}
25
26fn classify_fallback_plan(user_message: &str) -> Option<FallbackPlanKind> {
27    let lower = user_message.to_lowercase();
28    let path_signals = [
29        "crates/",
30        "src/",
31        "docs/",
32        ".rs",
33        ".md",
34        ".json",
35        ".toml",
36        ".yaml",
37        ".yml",
38        ".meta.json",
39        "执行路径",
40        "task_planner",
41        "agent_loop",
42        "executionfeedback",
43        "feedbacksignal",
44        "rules_used",
45        "first_success_rate",
46        "avg_replans",
47        "user_correction_rate",
48        "rule id",
49    ];
50    let action_signals = [
51        "修改", "更新", "补", "修复", "接入", "接线", "增加", "添加", "整理", "写入", "输出",
52        "导出", "实现", "make ", "update ", "wire ", "add ", "fix ", "edit ",
53    ];
54    let verification_signals = [
55        "测试",
56        "单测",
57        "校验",
58        "验证",
59        "核对",
60        "确保",
61        "检查",
62        "确认",
63        "status",
64        "metrics",
65        "replay",
66        "evolution",
67        "benchmark",
68        "eval",
69        "verify",
70        "test ",
71    ];
72
73    let has_path_signal = path_signals.iter().any(|s| lower.contains(s));
74    let has_action_signal = action_signals.iter().any(|s| lower.contains(s));
75    let has_verification_signal = verification_signals.iter().any(|s| lower.contains(s));
76
77    if has_action_signal && (has_path_signal || has_verification_signal) {
78        Some(FallbackPlanKind::ChangeAndVerify)
79    } else if has_path_signal && has_verification_signal {
80        Some(FallbackPlanKind::InspectOnly)
81    } else {
82        None
83    }
84}
85
86fn build_fallback_tasks(kind: FallbackPlanKind) -> Vec<Task> {
87    match kind {
88        FallbackPlanKind::InspectOnly => vec![
89            task(
90                1,
91                "Use read_file to inspect the relevant files or implementation details first.",
92                Some("file_read"),
93            ),
94            task(
95                2,
96                "Analyze whether the current implementation satisfies the user's request.",
97                Some("analysis"),
98            ),
99        ],
100        FallbackPlanKind::ChangeAndVerify => vec![
101            task(
102                1,
103                "Use read_file to inspect the relevant files and confirm the current implementation.",
104                Some("file_read"),
105            ),
106            task(
107                2,
108                "Use file_edit to make the required code or content changes.",
109                Some("file_edit"),
110            ),
111            task(
112                3,
113                "Run a focused verification step or analyze the updated result to confirm correctness.",
114                Some("command"),
115            ),
116        ],
117    }
118}
119
120/// Reject empty plans for requests that clearly need file/code/test work.
121pub fn guard_empty_plan(user_message: &str) -> Option<EmptyPlanGuardResult> {
122    let kind = classify_fallback_plan(user_message)?;
123    Some(EmptyPlanGuardResult {
124        reason: "empty plan rejected by centralized planning guard",
125        fallback_tasks: build_fallback_tasks(kind),
126    })
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_guard_empty_plan_rejects_code_change_requests() {
135        let guard = guard_empty_plan(
136            "在 crates/skilllite-agent/src/task_planner.rs 里补一个单测并验证 rules_used",
137        )
138        .unwrap();
139        assert_eq!(guard.fallback_tasks.len(), 3);
140        assert_eq!(
141            guard.fallback_tasks[0].tool_hint.as_deref(),
142            Some("file_read")
143        );
144        assert_eq!(
145            guard.fallback_tasks[1].tool_hint.as_deref(),
146            Some("file_edit")
147        );
148    }
149
150    #[test]
151    fn test_guard_empty_plan_allows_pure_text_requests() {
152        assert!(guard_empty_plan("帮我解释一下什么是 first_success_rate").is_none());
153    }
154}