diff --git a/src/prompts/react.rs b/src/prompts/react.rs index 79d0b56..22d086d 100644 --- a/src/prompts/react.rs +++ b/src/prompts/react.rs @@ -1,4 +1,6 @@ use crate::thinker::ToolDescription; +use std::fs; +use std::sync::OnceLock; const INTRO: &str = "You are Golem, an AI agent that solves tasks using a ReAct loop.\n\nCRITICAL: Your entire response must be a single JSON object. No prose, no explanation, no markdown — just JSON."; @@ -34,13 +36,38 @@ const RULES: &[&str] = &[ "When you have enough information, respond with the answer format.", ]; +static AGENT_INSTRUCTIONS_CACHE: OnceLock> = OnceLock::new(); +// Explicit filename variants keep behavior predictable across platforms and +// avoid case-insensitive filesystem assumptions. +static INSTRUCTION_FILE_CANDIDATES: &[&str] = &[ + "AGENTS.md", + "agents.md", + "CLAUDE.md", + "claude.md", + "GEMINI.md", + "gemini.md", + ".github/copilot-instructions.md", +]; + pub fn build_react_system_prompt(tools: &[ToolDescription]) -> String { - build_react_system_prompt_with_session(tools, false) + build_react_system_prompt_with_session_and_agents(tools, false, load_agent_instructions()) } pub fn build_react_system_prompt_with_session( tools: &[ToolDescription], has_session_history: bool, +) -> String { + build_react_system_prompt_with_session_and_agents( + tools, + has_session_history, + load_agent_instructions(), + ) +} + +fn build_react_system_prompt_with_session_and_agents( + tools: &[ToolDescription], + has_session_history: bool, + agents_md: Option<&str>, ) -> String { let mut prompt = String::with_capacity(1024); @@ -53,6 +80,13 @@ pub fn build_react_system_prompt_with_session( prompt.push('\n'); } + if let Some(agents_md) = agents_md { + prompt.push('\n'); + prompt.push_str("Repository agent instructions:\n"); + prompt.push_str(&sanitize_agents_md(agents_md)); + prompt.push('\n'); + } + // Tool list if !tools.is_empty() { prompt.push_str("\nAvailable tools:\n"); @@ -81,6 +115,32 @@ pub fn build_react_system_prompt_with_session( prompt } +fn load_agent_instructions() -> Option<&'static str> { + AGENT_INSTRUCTIONS_CACHE + .get_or_init(|| { + // Ordered by precedence. Keep AGENTS/agents first, then common + // single-file alternatives used by other agents. + for path in INSTRUCTION_FILE_CANDIDATES { + // Missing/unreadable files are expected in many repos; ignore and + // continue scanning fallback candidates. + if let Ok(content) = fs::read_to_string(path) { + let trimmed = content.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + None + }) + .as_deref() +} + +fn sanitize_agents_md(content: &str) -> String { + // Keep sanitization minimal: strip markdown code fences so we preserve the + // prompt's no-fences constraint while leaving other text unchanged. + content.replace("```", "'''") +} + #[cfg(test)] mod tests { use super::*; @@ -187,4 +247,29 @@ mod tests { let prompt = build_react_system_prompt(&[]); assert!(!prompt.contains("prior tasks")); } + + #[test] + fn includes_agents_md_when_present() { + let prompt = build_react_system_prompt_with_session_and_agents( + &[], + false, + Some("# AGENTS\n```bash\nfollow repo instructions\n```"), + ); + assert!(prompt.contains("Repository agent instructions:")); + assert!(prompt.contains("# AGENTS\n'''bash\nfollow repo instructions\n'''")); + } + + #[test] + fn omits_agents_md_section_when_missing() { + let prompt = build_react_system_prompt_with_session_and_agents(&[], false, None); + assert!(!prompt.contains("Repository agent instructions:")); + } + + #[test] + fn instruction_candidates_include_other_agent_files() { + let files = INSTRUCTION_FILE_CANDIDATES; + assert!(files.contains(&"CLAUDE.md")); + assert!(files.contains(&"GEMINI.md")); + assert!(files.contains(&".github/copilot-instructions.md")); + } }