1#[cfg(target_arch = "wasm32")]
17use crate::tokio;
18use std::path::{Path, PathBuf};
19
20pub const DEFAULT_SYSTEM_PROMPT: &str = r"You are an autonomous agent. Your task is to accomplish the user's goal by systematically using the tools available to you.
22
23# Core Behavior
24- Break complex tasks into steps and execute them one by one.
25- Use tools to gather information, take actions, and verify results.
26- When multiple tool calls are independent, execute them in parallel.
27- If a tool call fails, analyze the error and try alternative approaches.
28- Continue working until the task is complete or you determine it cannot be completed.
29
30# Decision Making
31- Act on the information you have. Make reasonable assumptions when necessary.
32- If critical information is missing and no tool can provide it, state what you need and why.
33- Prioritize correctness over speed. Verify your work when possible.
34
35# Output
36- When the task is complete, provide a clear summary of what was accomplished.
37- If the task cannot be completed, explain what blocked progress and what was attempted.";
38
39pub const AGENTS_MD_MAX_BYTES: usize = 32 * 1024;
41
42#[derive(Debug, Clone, Default)]
44pub struct SystemPromptConfig {
45 pub system_prompt: Option<String>,
47 pub load_agents_md: bool,
49 pub global_agents_md_path: Option<PathBuf>,
51 pub project_agents_md_path: Option<PathBuf>,
53}
54
55impl SystemPromptConfig {
56 pub fn new() -> Self {
58 Self {
59 system_prompt: None,
60 load_agents_md: true,
61 global_agents_md_path: None,
62 project_agents_md_path: None,
63 }
64 }
65
66 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
68 self.system_prompt = Some(prompt.into());
69 self
70 }
71
72 pub fn without_agents_md(mut self) -> Self {
74 self.load_agents_md = false;
75 self
76 }
77
78 pub fn with_global_agents_md(mut self, path: impl Into<PathBuf>) -> Self {
80 self.global_agents_md_path = Some(path.into());
81 self
82 }
83
84 pub fn with_project_agents_md(mut self, path: impl Into<PathBuf>) -> Self {
86 self.project_agents_md_path = Some(path.into());
87 self
88 }
89
90 pub async fn compose(&self) -> String {
92 let base = self
93 .system_prompt
94 .as_deref()
95 .unwrap_or(DEFAULT_SYSTEM_PROMPT);
96
97 if !self.load_agents_md {
98 return base.to_string();
99 }
100
101 let mut parts = vec![base.to_string()];
102
103 if let Some(content) = self.load_global_agents_md().await {
105 parts.push(format!(
106 "\n# Project Instructions (from global AGENTS.md)\n\n{content}"
107 ));
108 }
109
110 if let Some(content) = self.load_project_agents_md().await {
112 parts.push(format!(
113 "\n# Project Instructions (from AGENTS.md)\n\n{content}"
114 ));
115 }
116
117 parts.join("\n")
118 }
119
120 async fn load_global_agents_md(&self) -> Option<String> {
121 match self.global_agents_md_path.as_deref() {
122 Some(path) => load_agents_md_file(path).await,
123 None => {
124 let path = default_global_agents_md_path()?;
125 load_agents_md_file(&path).await
126 }
127 }
128 }
129
130 async fn load_project_agents_md(&self) -> Option<String> {
131 if let Some(path) = self.project_agents_md_path.as_deref() {
132 return load_agents_md_file(path).await;
133 }
134
135 let cwd = std::env::current_dir().ok()?;
136
137 for candidate in [cwd.join("AGENTS.md"), cwd.join(".rkat/AGENTS.md")] {
138 if let Some(content) = load_agents_md_file(&candidate).await {
139 return Some(content);
140 }
141 }
142
143 None
144 }
145}
146
147pub fn default_global_agents_md_path() -> Option<PathBuf> {
149 std::env::var_os("HOME")
150 .map(PathBuf::from)
151 .map(|h| h.join(".rkat/AGENTS.md"))
152}
153
154pub fn find_project_agents_md() -> Option<PathBuf> {
157 let cwd = std::env::current_dir().ok()?;
158 find_project_agents_md_in(&cwd)
159}
160
161pub fn find_project_agents_md_in(dir: &Path) -> Option<PathBuf> {
164 let root_path = dir.join("AGENTS.md");
166 if root_path.exists() {
167 return Some(root_path);
168 }
169
170 let meerkat_path = dir.join(".rkat/AGENTS.md");
172 if meerkat_path.exists() {
173 return Some(meerkat_path);
174 }
175
176 None
177}
178
179async fn load_agents_md_file(path: &Path) -> Option<String> {
181 if !tokio::fs::try_exists(path).await.ok()? {
182 return None;
183 }
184
185 let content = tokio::fs::read_to_string(path).await.ok()?;
186
187 let trimmed = content.trim();
189 if trimmed.is_empty() {
190 return None;
191 }
192
193 if content.len() > AGENTS_MD_MAX_BYTES {
195 let truncated: String = content.chars().take(AGENTS_MD_MAX_BYTES).collect();
197 return Some(truncated);
198 }
199
200 Some(content)
201}
202
203#[cfg(test)]
204#[allow(clippy::unwrap_used, clippy::expect_used)]
205mod tests {
206 use super::*;
207 use tempfile::TempDir;
208
209 #[tokio::test]
210 async fn test_default_prompt_used_when_no_override() {
211 let config = SystemPromptConfig::new().without_agents_md();
212 let prompt = config.compose().await;
213 assert_eq!(prompt, DEFAULT_SYSTEM_PROMPT);
214 }
215
216 #[tokio::test]
217 async fn test_custom_prompt_overrides_default() {
218 let config = SystemPromptConfig::new()
219 .with_system_prompt("Custom prompt")
220 .without_agents_md();
221 let prompt = config.compose().await;
222 assert_eq!(prompt, "Custom prompt");
223 }
224
225 #[tokio::test]
226 async fn test_agents_md_disabled() {
227 let temp = TempDir::new().unwrap();
228 let agents_path = temp.path().join("AGENTS.md");
229 tokio::fs::write(&agents_path, "# Should not appear")
230 .await
231 .unwrap();
232
233 let config = SystemPromptConfig::new()
234 .with_project_agents_md(&agents_path)
235 .without_agents_md();
236
237 let prompt = config.compose().await;
238 assert!(!prompt.contains("Should not appear"));
239 }
240
241 #[tokio::test]
242 async fn test_project_agents_md_injected() {
243 let temp = TempDir::new().unwrap();
244 let agents_path = temp.path().join("AGENTS.md");
245 tokio::fs::write(&agents_path, "# Build Instructions\nRun `make build`")
246 .await
247 .unwrap();
248
249 let config = SystemPromptConfig::new().with_project_agents_md(&agents_path);
250
251 let prompt = config.compose().await;
252 assert!(prompt.contains(DEFAULT_SYSTEM_PROMPT));
253 assert!(prompt.contains("# Project Instructions (from AGENTS.md)"));
254 assert!(prompt.contains("# Build Instructions"));
255 assert!(prompt.contains("Run `make build`"));
256 }
257
258 #[tokio::test]
259 async fn test_global_agents_md_injected() {
260 let temp = TempDir::new().unwrap();
261 let global_path = temp.path().join("global-agents.md");
262 tokio::fs::write(&global_path, "# Global rules\nAlways be nice")
263 .await
264 .unwrap();
265
266 let config = SystemPromptConfig::new().with_global_agents_md(&global_path);
267
268 let prompt = config.compose().await;
269 assert!(prompt.contains("# Project Instructions (from global AGENTS.md)"));
270 assert!(prompt.contains("# Global rules"));
271 }
272
273 #[tokio::test]
274 async fn test_both_global_and_project_agents_md() {
275 let temp = TempDir::new().unwrap();
276
277 let global_path = temp.path().join("global.md");
278 tokio::fs::write(&global_path, "Global instructions")
279 .await
280 .unwrap();
281
282 let project_path = temp.path().join("project.md");
283 tokio::fs::write(&project_path, "Project instructions")
284 .await
285 .unwrap();
286
287 let config = SystemPromptConfig::new()
288 .with_global_agents_md(&global_path)
289 .with_project_agents_md(&project_path);
290
291 let prompt = config.compose().await;
292
293 let global_pos = prompt.find("Global instructions").unwrap();
295 let project_pos = prompt.find("Project instructions").unwrap();
296 assert!(global_pos < project_pos);
297 }
298
299 #[tokio::test]
300 async fn test_empty_agents_md_ignored() {
301 let temp = TempDir::new().unwrap();
302 let agents_path = temp.path().join("AGENTS.md");
303 tokio::fs::write(&agents_path, " \n\n ").await.unwrap(); let config = SystemPromptConfig::new().with_project_agents_md(&agents_path);
306
307 let prompt = config.compose().await;
308 assert!(!prompt.contains("Project Instructions"));
309 }
310
311 #[tokio::test]
312 async fn test_agents_md_size_limit() {
313 let temp = TempDir::new().unwrap();
314 let agents_path = temp.path().join("AGENTS.md");
315
316 let large_content = "x".repeat(AGENTS_MD_MAX_BYTES + 1000);
318 tokio::fs::write(&agents_path, &large_content)
319 .await
320 .unwrap();
321
322 let config = SystemPromptConfig::new().with_project_agents_md(&agents_path);
323
324 let prompt = config.compose().await;
325
326 let agents_section_start = prompt.find("# Project Instructions").unwrap();
328 let agents_content = &prompt[agents_section_start..];
329 assert!(agents_content.len() <= AGENTS_MD_MAX_BYTES + 100); }
331
332 #[tokio::test]
333 async fn test_find_project_agents_md_root() {
334 let temp = TempDir::new().unwrap();
335 let agents_path = temp.path().join("AGENTS.md");
336 tokio::fs::write(&agents_path, "content").await.unwrap();
337
338 let found = find_project_agents_md_in(temp.path());
339 assert_eq!(found, Some(agents_path));
340 }
341
342 #[tokio::test]
343 async fn test_find_project_agents_md_in_meerkat_dir() {
344 let temp = TempDir::new().unwrap();
345 let meerkat_dir = temp.path().join(".rkat");
346 tokio::fs::create_dir_all(&meerkat_dir).await.unwrap();
347 let agents_path = meerkat_dir.join("AGENTS.md");
348 tokio::fs::write(&agents_path, "content").await.unwrap();
349
350 let found = find_project_agents_md_in(temp.path());
351 assert_eq!(found, Some(agents_path));
352 }
353
354 #[tokio::test]
355 async fn test_find_project_agents_md_root_takes_precedence() {
356 let temp = TempDir::new().unwrap();
357
358 let root_path = temp.path().join("AGENTS.md");
360 tokio::fs::write(&root_path, "root content").await.unwrap();
361
362 let meerkat_dir = temp.path().join(".rkat");
363 tokio::fs::create_dir_all(&meerkat_dir).await.unwrap();
364 tokio::fs::write(meerkat_dir.join("AGENTS.md"), "test content")
365 .await
366 .unwrap();
367
368 let found = find_project_agents_md_in(temp.path());
370 assert_eq!(found, Some(root_path));
371 }
372
373 #[test]
374 fn test_missing_agents_md_returns_none() {
375 let temp = TempDir::new().unwrap();
376 let found = find_project_agents_md_in(temp.path());
377 assert_eq!(found, None);
378 }
379}