use crate::db::repository::feedback_ledger::FeedbackLedgerRepository;
use std::path::PathBuf;
const CORE_BRAIN_FILES: &[(&str, &str)] = &[
("SOUL.md", "personality"),
("USER.md", "user profile"),
("TOOLS.md", "tool notes"),
];
pub(crate) const CONTEXTUAL_BRAIN_FILES: &[(&str, &str)] = &[
("IDENTITY.md", "identity — social/cron replies only"),
("AGENTS.md", "workspace rules"),
("CODE.md", "coding standards"),
("SECURITY.md", "security policies"),
("MEMORY.md", "long-term memory"),
("BOOT.md", "startup config"),
("BOOTSTRAP.md", "bootstrap config"),
("HEARTBEAT.md", "heartbeat config"),
];
const BRAIN_FILES: &[(&str, &str)] = &[
("SOUL.md", "personality"),
("USER.md", "user"),
("AGENTS.md", "agents"),
("TOOLS.md", "tools"),
("CODE.md", "code"),
("SECURITY.md", "security"),
("MEMORY.md", "memory"),
("BOOT.md", "boot"),
("BOOTSTRAP.md", "bootstrap"),
("HEARTBEAT.md", "heartbeat"),
];
const BRAIN_PREAMBLE: &str = r#"You are OpenCrabs, an AI orchestration agent with powerful tools to help with software development tasks.
IMPORTANT: You have access to tools for file operations and code exploration. USE THEM PROACTIVELY!
TOOL CALL PROTOCOL — CRITICAL:
- Always call tools directly — never write code yourself, never describe what you plan to do. Just call the tool immediately.
- Do NOT output markdown code blocks (```bash, ```sh, ```python, etc.) — invoke the `bash` / `python` tool instead. Code blocks are TEXT, the system will NOT execute them.
- WRONG: writing ```bash\ngit status\n``` or "Let me run `git log`" — nothing runs.
- RIGHT: emit a tool_call for `bash` with {"command": "git status"} via the structured tool-call API.
- NEVER claim to have run a command, read a file, or fetched a URL when you haven't actually invoked the corresponding tool. If you need work done, call the tool. If you can't, say so.
- Thinking/reasoning is fine, but the final action MUST be either a tool_call or a direct answer — not a code block pretending to be one, not a narration of what you'd do.
CRITICAL RULE: After calling tools and getting results, you MUST provide a final text response to the user.
DO NOT keep calling tools in a loop. Call the necessary tools, get results, then respond with text.
When asked to analyze or explore a codebase:
1. Use 'ls' tool with recursive=true to list all directories and files
2. Use 'glob' tool with patterns like "**/*.rs", "**/*.toml", "**/*.md" to find files
3. Use 'grep' tool to search for patterns, functions, or keywords in code
4. Use 'read_file' tool to read specific files you've identified
5. Use 'bash' tool for git operations like: git log, git diff, git branch
When asked to make changes:
1. Use 'read_file' first to understand the current code
2. Use 'edit_file' to modify existing files
3. Use 'write_file' to create new files
4. Use 'bash' to run tests or build commands
Available tools and their REQUIRED parameters (use exact parameter names):
- ls: List directory contents. Params: path (string), recursive (bool)
- glob: Find files matching patterns. Params: pattern (string, REQUIRED — e.g. "**/*.rs")
- grep: Search for text in files. Params: pattern (string, REQUIRED — the search text), path (string), regex (bool), case_insensitive (bool), file_pattern (string), limit (int), context (int)
- read_file: Read file contents. Params: path (string, REQUIRED)
- edit_file: Modify existing files. Params: path (string, REQUIRED), operation (string, REQUIRED)
- write_file: Create new files. Params: path (string, REQUIRED), content (string, REQUIRED)
- bash: Run shell commands. Params: command (string, REQUIRED)
- execute_code: Test code snippets. Params: language (string, REQUIRED), code (string, REQUIRED)
- web_search: Search the internet. Params: query (string, REQUIRED)
- http_request: Call external APIs. Params: method (string, REQUIRED), url (string, REQUIRED)
- task_manager: Track multi-step work. Params: operation (string, REQUIRED)
- session_context: Remember important facts. Params: operation (string, REQUIRED)
- session_search: Search across sessions. Params: operation (string, REQUIRED — "search" or "list"), query (string), n (int)
- plan: Create structured plans. Params: operation (string, REQUIRED)
CRITICAL: PLAN TOOL USAGE
When a user says "create a plan", "make a plan", or describes a complex multi-step task, you MUST use the plan tool immediately.
DO NOT write a text description of a plan. DO NOT explain what should be done. CALL THE TOOL.
Mandatory steps for plan creation:
1. IMMEDIATELY call plan tool with operation='create' to create a new plan
2. Call plan tool with operation='add_task' for each task (call multiple times)
- IMPORTANT: The 'description' field MUST contain detailed implementation steps
- Include: specific files to create/modify, functions to implement, commands to run
- Format: Use numbered steps or bullet points for clarity
- Be concrete: "Create Login.jsx component with email/password form fields and validation"
NOT vague: "Create login component"
3. Call plan tool with operation='finalize' — this auto-approves the plan immediately
4. Begin executing tasks in order right away using start_task/complete_task — no waiting
NEVER generate text plans. ALWAYS use the plan tool for planning requests.
ALWAYS explore first before answering questions about a codebase. Don't guess - use the tools!
RECURSIVE SELF-IMPROVEMENT:
You have three tools for improving yourself over time:
- feedback_analyze: Query your performance history (tool success rates, failure patterns, recent events). Call with query='summary' or query='tool_stats' or query='failures'.
- feedback_record: Manually log observations — user corrections, patterns you notice, strategies that work well.
- self_improve: Propose or apply changes to your brain files (SOUL.md, TOOLS.md, etc.). Runs autonomously — no human approval needed. Changes are logged to ~/.opencrabs/rsi/improvements.md and archived in ~/.opencrabs/rsi/history/.
Your tool executions are automatically tracked. When you notice recurring failures, user frustration, or repeated corrections:
1. Call feedback_analyze with query='failures' to understand what's going wrong
2. Call feedback_record to log the pattern you observed
3. Call self_improve with action='apply' to apply a concrete improvement — brain file is edited, improvement is logged to rsi/improvements.md, and a daily archive entry is created
Do NOT call these tools every turn. Use them when you notice a pattern across multiple interactions, or when a user explicitly corrects you in a way that could apply to future conversations. Report significant improvements to the TUI or connected channels so the user knows what changed."#;
pub struct BrainLoader {
workspace_path: PathBuf,
}
impl BrainLoader {
pub fn new(workspace_path: PathBuf) -> Self {
Self { workspace_path }
}
pub fn resolve_path() -> PathBuf {
crate::config::opencrabs_home()
}
pub fn load_file(&self, name: &str) -> Option<String> {
let path = self.workspace_path.join(name);
std::fs::read_to_string(&path).ok()
}
pub fn build_system_brain(
&self,
runtime_info: Option<&RuntimeInfo>,
slash_commands_section: Option<&str>,
) -> String {
let mut prompt = String::with_capacity(8192);
prompt.push_str(BRAIN_PREAMBLE);
prompt.push_str("\n\n");
for (filename, label) in BRAIN_FILES {
if let Some(content) = self.load_file(filename) {
let trimmed = content.trim();
if !trimmed.is_empty() {
prompt.push_str(&format!(
"--- {} ({}) ---\n{}\n\n",
filename, label, trimmed
));
}
}
}
if let Some(info) = runtime_info {
prompt.push_str("--- Runtime Info ---\n");
if let Some(ref model) = info.model {
prompt.push_str(&format!("Model: {}\n", model));
}
if let Some(ref provider) = info.provider {
prompt.push_str(&format!("Provider: {}\n", provider));
}
if let Some(ref wd) = info.working_directory {
prompt.push_str(&format!("Working directory: {}\n", wd));
}
prompt.push_str(&format!("OS: {}\n", std::env::consts::OS));
prompt.push_str(&format!(
"Timestamp: {}\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
prompt.push('\n');
}
if let Some(commands_section) = slash_commands_section
&& !commands_section.is_empty()
{
prompt.push_str("--- Available Slash Commands ---\n");
prompt.push_str(commands_section);
prompt.push_str("\n\n");
}
prompt
}
pub fn build_core_brain(
&self,
runtime_info: Option<&RuntimeInfo>,
slash_commands_section: Option<&str>,
) -> String {
let mut prompt = String::with_capacity(4096);
prompt.push_str(BRAIN_PREAMBLE);
prompt.push_str("\n\n");
for (filename, label) in CORE_BRAIN_FILES {
if let Some(content) = self.load_file(filename) {
let trimmed = content.trim();
if !trimmed.is_empty() {
prompt.push_str(&format!(
"--- {} ({}) ---\n{}\n\n",
filename, label, trimmed
));
}
}
}
let available: Vec<(&str, &str)> = CONTEXTUAL_BRAIN_FILES
.iter()
.filter(|(name, _)| self.workspace_path.join(name).exists())
.copied()
.collect();
if !available.is_empty() {
prompt.push_str("--- Available Context Files ---\n");
prompt.push_str(
"The following brain files contain detailed context. \
Load them on demand using the `load_brain_file` tool when relevant — \
do NOT load them unless the request actually needs that context. \
To update or edit a brain file, use the `write_opencrabs_file` tool.\n\n",
);
for (name, desc) in &available {
prompt.push_str(&format!("- **{}**: {}\n", name, desc));
}
let has = |name: &str| available.iter().any(|(n, _)| *n == name);
prompt.push_str("\nLoad proactively when:\n");
if has("USER.md") {
prompt.push_str("- User asks personal questions or preferences → load USER.md\n");
}
if has("MEMORY.md") {
prompt.push_str(
"- Starting a project session or recalling past work → load MEMORY.md\n",
);
}
if has("AGENTS.md") || has("SECURITY.md") {
let files: Vec<&str> = ["AGENTS.md", "SECURITY.md"]
.iter()
.copied()
.filter(|n| has(n))
.collect();
prompt.push_str(&format!(
"- Policy / rule / safety check needed → load {}\n",
files.join(" or ")
));
}
if has("TOOLS.md") {
prompt
.push_str("- Working with environment-specific tool configs → load TOOLS.md\n");
}
prompt.push('\n');
if has("MEMORY.md") {
prompt.push_str(
"Write proactively to MEMORY.md (via `write_opencrabs_file`) when:\n\
- You discover a fact, pattern, or context that would be valuable across sessions\n\
- The user corrects you on something non-obvious that isn't already in MEMORY.md\n\
- You learn project-specific knowledge (integrations, team structure, workflows)\n\
- A self-heal event fires (phantom tool call, gaslighting strip) — record what \
triggered it and the correct behavior so you avoid it next time\n\
Do NOT write ephemeral task details or anything derivable from code/git. \
Load MEMORY.md first to avoid duplicates before writing.\n\n",
);
}
}
if let Some(info) = runtime_info {
prompt.push_str("--- Runtime Info ---\n");
if let Some(ref model) = info.model {
prompt.push_str(&format!("Model: {}\n", model));
}
if let Some(ref provider) = info.provider {
prompt.push_str(&format!("Provider: {}\n", provider));
}
if let Some(ref wd) = info.working_directory {
prompt.push_str(&format!("Working directory: {}\n", wd));
}
prompt.push_str(&format!("OS: {}\n", std::env::consts::OS));
prompt.push_str(&format!(
"Timestamp: {}\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
prompt.push('\n');
}
if let Some(commands_section) = slash_commands_section
&& !commands_section.is_empty()
{
prompt.push_str("--- Available Slash Commands ---\n");
prompt.push_str(commands_section);
prompt.push_str("\n\n");
}
prompt
}
}
pub async fn build_feedback_digest(pool: crate::db::Pool) -> Option<String> {
let repo = FeedbackLedgerRepository::new(pool);
let total = repo.total_count().await.ok()?;
if total < 10 {
return None; }
let mut out = String::from("--- Performance History ---\n");
out.push_str(&format!("Total tool executions recorded: {total}\n"));
if let Ok(stats) = repo.stats_by_dimension("tool_").await {
let mut header_written = false;
for s in stats
.iter()
.filter(|s| s.failures > 0 && s.success_rate < 0.9)
.take(5)
{
if !header_written {
out.push_str("Tools with notable failure rates:\n");
header_written = true;
}
out.push_str(&format!(
" {} — {:.0}% success ({} ok, {} fail)\n",
s.dimension,
s.success_rate * 100.0,
s.successes,
s.failures
));
}
}
if let Ok(entries) = repo.by_event_type("tool_failure", 5).await
&& !entries.is_empty()
{
out.push_str("Recent failures:\n");
for e in &entries {
let meta = e.metadata.as_deref().unwrap_or("(no details)");
let short: String = meta.chars().take(80).collect();
out.push_str(&format!(" {} — {}\n", e.dimension, short));
}
}
if let Ok(corrections) = repo.by_event_type("user_correction", 50).await
&& !corrections.is_empty()
{
out.push_str(&format!(
"User corrections recorded: {}\n",
corrections.len()
));
}
out.push_str(
"Use feedback_analyze for deeper analysis. \
If you see patterns, use self_improve to apply fixes autonomously.\n\n",
);
Some(out)
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeInfo {
pub model: Option<String>,
pub provider: Option<String>,
pub working_directory: Option<String>,
}
#[cfg(test)]
#[path = "prompt_builder_tests.rs"]
mod prompt_builder_tests;
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_build_prompt_no_files() {
let dir = TempDir::new().unwrap();
let loader = BrainLoader::new(dir.path().to_path_buf());
let prompt = loader.build_system_brain(None, None);
assert!(prompt.contains("You are OpenCrabs"));
assert!(prompt.contains("CRITICAL RULE"));
}
#[test]
fn test_build_prompt_with_soul() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("SOUL.md"), "I am a helpful crab.").unwrap();
let loader = BrainLoader::new(dir.path().to_path_buf());
let prompt = loader.build_system_brain(None, None);
assert!(prompt.contains("You are OpenCrabs"));
assert!(prompt.contains("I am a helpful crab."));
assert!(prompt.contains("SOUL.md"));
}
#[test]
fn test_build_prompt_with_runtime_info() {
let dir = TempDir::new().unwrap();
let loader = BrainLoader::new(dir.path().to_path_buf());
let info = RuntimeInfo {
model: Some("claude-sonnet-4-20250514".to_string()),
provider: Some("anthropic".to_string()),
working_directory: Some("/home/user/project".to_string()),
};
let prompt = loader.build_system_brain(Some(&info), None);
assert!(prompt.contains("claude-sonnet-4-20250514"));
assert!(prompt.contains("anthropic"));
assert!(prompt.contains("/home/user/project"));
}
#[test]
fn test_skips_empty_files() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("SOUL.md"), " \n ").unwrap();
let loader = BrainLoader::new(dir.path().to_path_buf());
let prompt = loader.build_system_brain(None, None);
assert!(!prompt.contains("--- SOUL.md ("));
}
}