aster/config/
agents_md_parser.rs1use 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
12pub(crate) type ChangeCallback = Box<dyn Fn(String) + Send + Sync>;
14
15pub(crate) type ChangeCallbackList = Arc<RwLock<Vec<ChangeCallback>>>;
17
18#[derive(Debug, Clone)]
20pub struct AgentsMdInfo {
21 pub content: String,
23 pub path: PathBuf,
25 pub exists: bool,
27 pub last_modified: Option<SystemTime>,
29}
30
31#[derive(Debug, Clone)]
33pub struct AgentsMdStats {
34 pub lines: usize,
36 pub chars: usize,
38 pub size: u64,
40}
41
42#[derive(Debug, Clone)]
44pub struct ValidationResult {
45 pub valid: bool,
47 pub warnings: Vec<String>,
49}
50
51pub struct AgentsMdParser {
53 agents_md_path: PathBuf,
55 watcher: RwLock<Option<RecommendedWatcher>>,
57 change_callbacks: ChangeCallbackList,
59}
60
61impl AgentsMdParser {
62 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 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 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 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 pub fn exists(&self) -> bool {
152 self.agents_md_path.exists()
153 }
154
155 pub fn path(&self) -> &Path {
157 &self.agents_md_path
158 }
159
160 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 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 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 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 pub fn update(&self, content: &str) -> Result<(), std::io::Error> {
272 fs::write(&self.agents_md_path, content)
273 }
274
275 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 if !info.content.contains('#') {
293 warnings.push("建议使用 Markdown 标题组织内容".to_string());
294 }
295
296 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 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}