Skip to main content

llama_cpp_v3_agent_sdk/
agents_md.rs

1//! AGENTS.md discovery and loading.
2//!
3//! The agent automatically discovers and loads `AGENTS.md` files for
4//! project-specific guidance. Files are searched from the current working
5//! directory upward to the git root, plus a global `~/.llama-agent/AGENTS.md`.
6//!
7//! In monorepos, nested `AGENTS.md` files are all loaded — the most local
8//! file takes highest precedence.
9
10use std::path::{Path, PathBuf};
11
12/// A discovered AGENTS.md file with its content and source path.
13#[derive(Debug, Clone)]
14pub struct AgentsMdFile {
15    /// Absolute path to the AGENTS.md file.
16    pub path: PathBuf,
17    /// Full content of the file.
18    pub content: String,
19}
20
21/// Discovers and loads AGENTS.md files.
22pub struct AgentsMdRegistry {
23    files: Vec<AgentsMdFile>,
24}
25
26impl AgentsMdRegistry {
27    pub fn new() -> Self {
28        Self { files: Vec::new() }
29    }
30
31    /// Discover AGENTS.md files from CWD up to the git root, plus global.
32    pub fn discover(&mut self) {
33        self.files.clear();
34
35        // 1. Walk from CWD upward to git root
36        if let Ok(cwd) = std::env::current_dir() {
37            let git_root = find_git_root(&cwd);
38            let stop_at = git_root.as_deref();
39
40            let mut dir = Some(cwd.as_path());
41            while let Some(current) = dir {
42                let agents_path = current.join("AGENTS.md");
43                if agents_path.is_file() {
44                    if let Ok(content) = std::fs::read_to_string(&agents_path) {
45                        self.files.push(AgentsMdFile {
46                            path: agents_path,
47                            content,
48                        });
49                    }
50                }
51
52                // Stop at git root
53                if let Some(root) = stop_at {
54                    if current == root {
55                        break;
56                    }
57                }
58
59                dir = current.parent();
60            }
61        }
62
63        // 2. Global ~/.llama-agent/AGENTS.md
64        if let Some(home) = dirs::home_dir() {
65            let global_path = home.join(".llama-agent").join("AGENTS.md");
66            if global_path.is_file() {
67                // Don't add if already found (dedup by path)
68                if !self.files.iter().any(|f| f.path == global_path) {
69                    if let Ok(content) = std::fs::read_to_string(&global_path) {
70                        self.files.push(AgentsMdFile {
71                            path: global_path,
72                            content,
73                        });
74                    }
75                }
76            }
77        }
78    }
79
80    /// Get all discovered AGENTS.md files (ordered by precedence: most local first).
81    pub fn files(&self) -> &[AgentsMdFile] {
82        &self.files
83    }
84
85    /// Generate a system prompt fragment with all AGENTS.md content.
86    ///
87    /// Files are included in order of precedence (most local first).
88    pub fn agents_md_prompt(&self) -> String {
89        if self.files.is_empty() {
90            return String::new();
91        }
92
93        let mut lines = Vec::new();
94        lines.push("# Project Guidelines (from AGENTS.md)\n".to_string());
95
96        for file in &self.files {
97            let path_display = file.path.display().to_string();
98            lines.push(format!("<!-- Source: {} -->\n", path_display));
99            lines.push(file.content.clone());
100            lines.push(String::new());
101        }
102
103        lines.join("\n")
104    }
105
106    /// Total number of discovered files.
107    pub fn len(&self) -> usize {
108        self.files.len()
109    }
110
111    pub fn is_empty(&self) -> bool {
112        self.files.is_empty()
113    }
114}
115
116impl Default for AgentsMdRegistry {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122/// Walk upward from `start` to find a `.git` directory.
123fn find_git_root(start: &Path) -> Option<PathBuf> {
124    let mut dir = Some(start);
125    while let Some(current) = dir {
126        if current.join(".git").exists() {
127            return Some(current.to_path_buf());
128        }
129        dir = current.parent();
130    }
131    None
132}