Skip to main content

robit_agent/
prompt.rs

1//! System prompt builder — assembles the system prompt from multiple modules.
2
3use std::path::Path;
4
5use crate::datetime::current_date;
6use crate::tool::Tool;
7
8/// Default agent prompt template (user-editable part).
9/// Does NOT include Tools/Skills/Environment sections - those are appended automatically.
10const DEFAULT_AGENT_PROMPT: &str = include_str!("../prompts/default.md");
11
12/// Built-in system prompt - automatically appended to all prompts.
13/// Contains Tools, Skills, and Environment sections (never overridden by users).
14const SYSTEM_PROMPT: &str = include_str!("../prompts/system.md");
15
16pub struct PromptBuilder {
17    custom_prompt: Option<String>,
18}
19
20impl PromptBuilder {
21    pub fn new() -> Self {
22        Self::with_working_dir(None)
23    }
24
25    /// Create PromptBuilder with a specific working directory to check for
26    /// project-local custom prompt.
27    /// Priority: {cwd}/.robit/prompts/agent.md > ~/.robit/prompts/agent.md
28    pub fn with_working_dir(working_dir: Option<&Path>) -> Self {
29        // Check project-local prompt first (higher priority)
30        let local_prompt = working_dir.and_then(|cwd| {
31            let path = cwd.join(".robit/prompts/agent.md");
32            std::fs::read_to_string(&path).ok()
33        });
34
35        if local_prompt.is_some() {
36            return Self {
37                custom_prompt: local_prompt,
38            };
39        }
40
41        // Fallback to global prompt
42        let global_prompt = dirs::home_dir().and_then(|home| {
43            let path = home.join(".robit/prompts/agent.md");
44            std::fs::read_to_string(&path).ok()
45        });
46
47        Self {
48            custom_prompt: global_prompt,
49        }
50    }
51
52    /// Build the complete system prompt.
53    ///
54    /// The prompt is composed of:
55    /// 1. Agent prompt (user-provided from agent.md, or default from default.md)
56    /// 2. System prompt (Tools, Skills, Environment) - automatically appended,
57    ///    defined in system.md (built-in, never overridden by users)
58    ///
59    /// `skills` is a list of (name, description) pairs for enabled skills.
60    pub fn build_system_prompt(
61        &self,
62        tools: &[&dyn Tool],
63        skills: &[(&str, &str)],
64        working_dir: &std::path::Path,
65    ) -> String {
66        let tools_section = Self::build_tools_section(tools);
67        let skills_section = Self::build_skills_section(skills);
68        let os = std::env::consts::OS;
69        let cwd = working_dir.display().to_string();
70        let date = current_date();
71
72        // Select base prompt: custom agent prompt or default agent prompt
73        let agent_prompt = self.custom_prompt.as_deref().unwrap_or(DEFAULT_AGENT_PROMPT);
74
75        // Replace variables in the system prompt (Tools, Skills, Environment sections)
76        let system_part = SYSTEM_PROMPT
77            .replace("{os}", os)
78            .replace("{cwd}", &cwd)
79            .replace("{date}", &date)
80            .replace("{tools_section}", &tools_section)
81            .replace("{skills_section}", &skills_section);
82
83        // Combine: agent prompt + system prompt
84        format!("{}\n\n{}", agent_prompt.trim(), system_part)
85    }
86
87    /// Build the tools description section.
88    fn build_tools_section(tools: &[&dyn Tool]) -> String {
89        if tools.is_empty() {
90            return "(no available tools)".to_string();
91        }
92
93        let mut section = String::new();
94        for tool in tools {
95            section.push_str(&format!(
96                "- **{}**: {}{}\n",
97                tool.name(),
98                tool.description(),
99                if tool.requires_confirmation() {
100                    " (requires user confirmation)"
101                } else {
102                    ""
103                }
104            ));
105        }
106        section
107    }
108
109    /// Build the skills description section.
110    fn build_skills_section(skills: &[(&str, &str)]) -> String {
111        if skills.is_empty() {
112            return "(no available skills)".to_string();
113        }
114
115        let mut section = String::new();
116        for (name, description) in skills {
117            section.push_str(&format!("- **{}**: {}\n", name, description));
118        }
119        section
120    }
121
122}
123
124impl Default for PromptBuilder {
125    fn default() -> Self {
126        Self::new()
127    }
128}