1use 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
120pub 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}