Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion src/prompts/react.rs
Original file line number Diff line number Diff line change
@@ -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.";

Expand Down Expand Up @@ -34,13 +36,38 @@ const RULES: &[&str] = &[
"When you have enough information, respond with the answer format.",
];

static AGENT_INSTRUCTIONS_CACHE: OnceLock<Option<String>> = 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);

Expand All @@ -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");
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -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"));
}
}
Loading