Skip to main content

aster/config/
agents_md_parser.rs

1//! AGENTS.md 解析器
2//!
3//! 解析项目根目录的 AGENTS.md 文件,并注入到系统提示中
4
5use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
6use parking_lot::RwLock;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::SystemTime;
11
12/// 变更回调函数类型
13pub(crate) type ChangeCallback = Box<dyn Fn(String) + Send + Sync>;
14
15/// 变更回调列表类型
16pub(crate) type ChangeCallbackList = Arc<RwLock<Vec<ChangeCallback>>>;
17
18/// AGENTS.md 文件信息
19#[derive(Debug, Clone)]
20pub struct AgentsMdInfo {
21    /// 文件内容
22    pub content: String,
23    /// 文件路径
24    pub path: PathBuf,
25    /// 文件是否存在
26    pub exists: bool,
27    /// 最后修改时间
28    pub last_modified: Option<SystemTime>,
29}
30
31/// AGENTS.md 统计信息
32#[derive(Debug, Clone)]
33pub struct AgentsMdStats {
34    /// 行数
35    pub lines: usize,
36    /// 字符数
37    pub chars: usize,
38    /// 文件大小(字节)
39    pub size: u64,
40}
41
42/// 验证结果
43#[derive(Debug, Clone)]
44pub struct ValidationResult {
45    /// 是否有效
46    pub valid: bool,
47    /// 警告信息
48    pub warnings: Vec<String>,
49}
50
51/// AGENTS.md 解析器
52pub struct AgentsMdParser {
53    /// AGENTS.md 文件路径
54    agents_md_path: PathBuf,
55    /// 文件监听器
56    watcher: RwLock<Option<RecommendedWatcher>>,
57    /// 变更回调
58    change_callbacks: ChangeCallbackList,
59}
60
61impl AgentsMdParser {
62    /// 创建新的解析器
63    pub fn new(working_dir: Option<&Path>) -> Self {
64        let dir = working_dir
65            .map(|p| p.to_path_buf())
66            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
67
68        let agents_md_path = dir.join("AGENTS.md");
69
70        Self {
71            agents_md_path,
72            watcher: RwLock::new(None),
73            change_callbacks: Arc::new(RwLock::new(Vec::new())),
74        }
75    }
76
77    /// 解析 AGENTS.md 文件
78    pub fn parse(&self) -> AgentsMdInfo {
79        if !self.agents_md_path.exists() {
80            return AgentsMdInfo {
81                content: String::new(),
82                path: self.agents_md_path.clone(),
83                exists: false,
84                last_modified: None,
85            };
86        }
87
88        match fs::read_to_string(&self.agents_md_path) {
89            Ok(content) => {
90                let last_modified = fs::metadata(&self.agents_md_path)
91                    .ok()
92                    .and_then(|m| m.modified().ok());
93
94                AgentsMdInfo {
95                    content,
96                    path: self.agents_md_path.clone(),
97                    exists: true,
98                    last_modified,
99                }
100            }
101            Err(e) => {
102                tracing::warn!("读取 AGENTS.md 失败: {}", e);
103                AgentsMdInfo {
104                    content: String::new(),
105                    path: self.agents_md_path.clone(),
106                    exists: false,
107                    last_modified: None,
108                }
109            }
110        }
111    }
112
113    /// 注入到系统提示
114    ///
115    /// 核心功能:将 AGENTS.md 的内容添加到系统提示中
116    pub fn inject_into_system_prompt(&self, base_prompt: &str) -> String {
117        let info = self.parse();
118
119        if !info.exists || info.content.trim().is_empty() {
120            return base_prompt.to_string();
121        }
122
123        format!(
124            r#"{}
125
126# agentsMd
127Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
128
129Contents of {} (project instructions, checked into the codebase):
130
131{}
132
133IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task."#,
134            base_prompt,
135            self.agents_md_path.display(),
136            info.content
137        )
138    }
139
140    /// 获取 AGENTS.md 内容(简化版)
141    pub fn get_content(&self) -> Option<String> {
142        let info = self.parse();
143        if info.exists {
144            Some(info.content)
145        } else {
146            None
147        }
148    }
149
150    /// 检查 AGENTS.md 是否存在
151    pub fn exists(&self) -> bool {
152        self.agents_md_path.exists()
153    }
154
155    /// 获取文件路径
156    pub fn path(&self) -> &Path {
157        &self.agents_md_path
158    }
159
160    /// 监听 AGENTS.md 变化
161    pub fn watch<F>(&self, callback: F) -> Result<(), notify::Error>
162    where
163        F: Fn(String) + Send + Sync + 'static,
164    {
165        if !self.exists() {
166            tracing::warn!(
167                "AGENTS.md 不存在,无法监听: {}",
168                self.agents_md_path.display()
169            );
170            return Ok(());
171        }
172
173        self.change_callbacks.write().push(Box::new(callback));
174
175        let mut watcher_guard = self.watcher.write();
176        if watcher_guard.is_none() {
177            let callbacks = self.change_callbacks.clone();
178            let path = self.agents_md_path.clone();
179
180            let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
181                if let Ok(event) = res {
182                    if event.kind.is_modify() {
183                        if let Ok(content) = fs::read_to_string(&path) {
184                            let cbs = callbacks.read();
185                            for cb in cbs.iter() {
186                                cb(content.clone());
187                            }
188                        }
189                    }
190                }
191            })?;
192
193            watcher.watch(&self.agents_md_path, RecursiveMode::NonRecursive)?;
194            *watcher_guard = Some(watcher);
195        }
196
197        Ok(())
198    }
199
200    /// 停止监听
201    pub fn unwatch(&self) {
202        let mut watcher_guard = self.watcher.write();
203        *watcher_guard = None;
204        self.change_callbacks.write().clear();
205    }
206
207    /// 创建默认的 AGENTS.md 模板
208    pub fn create_template(project_name: &str, project_type: Option<&str>) -> String {
209        let pt = project_type.unwrap_or("software");
210        format!(
211            r#"# AGENTS.md
212
213This file provides guidance to AI Agent when working with code in this repository.
214
215## Project Overview
216
217{} is a {} project.
218
219## Development Guidelines
220
221### Code Style
222
223- Follow consistent formatting
224- Write clear, descriptive comments
225- Use meaningful variable names
226
227### Testing
228
229- Write tests for new features
230- Ensure all tests pass before committing
231- Maintain test coverage above 80%
232
233### Git Workflow
234
235- Use feature branches
236- Write clear commit messages
237- Keep commits atomic and focused
238
239## Important Notes
240
241- Add project-specific guidelines here
242- Document any special requirements
243- Include build/deployment instructions if needed
244"#,
245            project_name, pt
246        )
247    }
248
249    /// 在项目中创建 AGENTS.md
250    pub fn create(&self, content: Option<&str>) -> Result<(), std::io::Error> {
251        if self.exists() {
252            tracing::warn!("AGENTS.md 已存在");
253            return Ok(());
254        }
255
256        let project_name = self
257            .agents_md_path
258            .parent()
259            .and_then(|p| p.file_name())
260            .and_then(|n| n.to_str())
261            .unwrap_or("project");
262
263        let template = content
264            .map(|s| s.to_string())
265            .unwrap_or_else(|| Self::create_template(project_name, None));
266
267        fs::write(&self.agents_md_path, template)
268    }
269
270    /// 更新 AGENTS.md
271    pub fn update(&self, content: &str) -> Result<(), std::io::Error> {
272        fs::write(&self.agents_md_path, content)
273    }
274
275    /// 验证 AGENTS.md 格式
276    pub fn validate(&self) -> ValidationResult {
277        let info = self.parse();
278        let mut warnings = Vec::new();
279
280        if !info.exists {
281            return ValidationResult {
282                valid: false,
283                warnings: vec!["AGENTS.md 文件不存在".to_string()],
284            };
285        }
286
287        if info.content.trim().is_empty() {
288            warnings.push("AGENTS.md 文件为空".to_string());
289        }
290
291        // 检查是否包含标题
292        if !info.content.contains('#') {
293            warnings.push("建议使用 Markdown 标题组织内容".to_string());
294        }
295
296        // 检查文件大小(过大可能影响性能)
297        if info.content.len() > 50000 {
298            warnings.push("AGENTS.md 文件过大(>50KB),可能影响性能".to_string());
299        }
300
301        ValidationResult {
302            valid: true,
303            warnings,
304        }
305    }
306
307    /// 获取 AGENTS.md 的统计信息
308    pub fn get_stats(&self) -> Option<AgentsMdStats> {
309        let info = self.parse();
310
311        if !info.exists {
312            return None;
313        }
314
315        let size = fs::metadata(&self.agents_md_path)
316            .map(|m| m.len())
317            .unwrap_or(0);
318
319        Some(AgentsMdStats {
320            lines: info.content.lines().count(),
321            chars: info.content.len(),
322            size,
323        })
324    }
325}
326
327impl Default for AgentsMdParser {
328    fn default() -> Self {
329        Self::new(None)
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use tempfile::TempDir;
337
338    #[test]
339    fn test_parser_no_file() {
340        let temp_dir = TempDir::new().unwrap();
341        let parser = AgentsMdParser::new(Some(temp_dir.path()));
342
343        let info = parser.parse();
344        assert!(!info.exists);
345        assert!(info.content.is_empty());
346    }
347
348    #[test]
349    fn test_parser_with_file() {
350        let temp_dir = TempDir::new().unwrap();
351        let agents_md = temp_dir.path().join("AGENTS.md");
352        fs::write(&agents_md, "# Test\n\nHello world").unwrap();
353
354        let parser = AgentsMdParser::new(Some(temp_dir.path()));
355        let info = parser.parse();
356
357        assert!(info.exists);
358        assert!(info.content.contains("Hello world"));
359    }
360
361    #[test]
362    fn test_inject_into_system_prompt_no_file() {
363        let temp_dir = TempDir::new().unwrap();
364        let parser = AgentsMdParser::new(Some(temp_dir.path()));
365
366        let result = parser.inject_into_system_prompt("base prompt");
367        assert_eq!(result, "base prompt");
368    }
369
370    #[test]
371    fn test_inject_into_system_prompt_with_file() {
372        let temp_dir = TempDir::new().unwrap();
373        let agents_md = temp_dir.path().join("AGENTS.md");
374        fs::write(&agents_md, "# Instructions\n\nDo this").unwrap();
375
376        let parser = AgentsMdParser::new(Some(temp_dir.path()));
377        let result = parser.inject_into_system_prompt("base prompt");
378
379        assert!(result.contains("base prompt"));
380        assert!(result.contains("agentsMd"));
381        assert!(result.contains("Do this"));
382    }
383
384    #[test]
385    fn test_get_content() {
386        let temp_dir = TempDir::new().unwrap();
387        let agents_md = temp_dir.path().join("AGENTS.md");
388        fs::write(&agents_md, "content here").unwrap();
389
390        let parser = AgentsMdParser::new(Some(temp_dir.path()));
391        let content = parser.get_content();
392
393        assert!(content.is_some());
394        assert_eq!(content.unwrap(), "content here");
395    }
396
397    #[test]
398    fn test_exists() {
399        let temp_dir = TempDir::new().unwrap();
400        let parser = AgentsMdParser::new(Some(temp_dir.path()));
401        assert!(!parser.exists());
402
403        let agents_md = temp_dir.path().join("AGENTS.md");
404        fs::write(&agents_md, "test").unwrap();
405
406        let parser2 = AgentsMdParser::new(Some(temp_dir.path()));
407        assert!(parser2.exists());
408    }
409
410    #[test]
411    fn test_create_template() {
412        let template = AgentsMdParser::create_template("my-project", Some("Rust"));
413        assert!(template.contains("my-project"));
414        assert!(template.contains("Rust"));
415        assert!(template.contains("# AGENTS.md"));
416    }
417
418    #[test]
419    fn test_create() {
420        let temp_dir = TempDir::new().unwrap();
421        let parser = AgentsMdParser::new(Some(temp_dir.path()));
422
423        parser.create(None).unwrap();
424        assert!(parser.exists());
425
426        let content = parser.get_content().unwrap();
427        assert!(content.contains("# AGENTS.md"));
428    }
429
430    #[test]
431    fn test_update() {
432        let temp_dir = TempDir::new().unwrap();
433        let agents_md = temp_dir.path().join("AGENTS.md");
434        fs::write(&agents_md, "old content").unwrap();
435
436        let parser = AgentsMdParser::new(Some(temp_dir.path()));
437        parser.update("new content").unwrap();
438
439        let content = parser.get_content().unwrap();
440        assert_eq!(content, "new content");
441    }
442
443    #[test]
444    fn test_validate_no_file() {
445        let temp_dir = TempDir::new().unwrap();
446        let parser = AgentsMdParser::new(Some(temp_dir.path()));
447
448        let result = parser.validate();
449        assert!(!result.valid);
450        assert!(result.warnings.iter().any(|w| w.contains("不存在")));
451    }
452
453    #[test]
454    fn test_validate_empty_file() {
455        let temp_dir = TempDir::new().unwrap();
456        let agents_md = temp_dir.path().join("AGENTS.md");
457        fs::write(&agents_md, "   ").unwrap();
458
459        let parser = AgentsMdParser::new(Some(temp_dir.path()));
460        let result = parser.validate();
461
462        assert!(result.valid);
463        assert!(result.warnings.iter().any(|w| w.contains("为空")));
464    }
465
466    #[test]
467    fn test_validate_no_headers() {
468        let temp_dir = TempDir::new().unwrap();
469        let agents_md = temp_dir.path().join("AGENTS.md");
470        fs::write(&agents_md, "just plain text").unwrap();
471
472        let parser = AgentsMdParser::new(Some(temp_dir.path()));
473        let result = parser.validate();
474
475        assert!(result.valid);
476        assert!(result.warnings.iter().any(|w| w.contains("标题")));
477    }
478
479    #[test]
480    fn test_get_stats() {
481        let temp_dir = TempDir::new().unwrap();
482        let agents_md = temp_dir.path().join("AGENTS.md");
483        fs::write(&agents_md, "line1\nline2\nline3").unwrap();
484
485        let parser = AgentsMdParser::new(Some(temp_dir.path()));
486        let stats = parser.get_stats().unwrap();
487
488        assert_eq!(stats.lines, 3);
489        assert_eq!(stats.chars, 17);
490    }
491
492    #[test]
493    fn test_get_stats_no_file() {
494        let temp_dir = TempDir::new().unwrap();
495        let parser = AgentsMdParser::new(Some(temp_dir.path()));
496
497        assert!(parser.get_stats().is_none());
498    }
499}