Skip to main content

kiss_coding/
system_prompt.rs

1//! System prompt assembly: identity + tool list + guidelines, appended
2//! custom text, project context blocks, skills XML, and the cwd line.
3
4use crate::context_files::ContextFile;
5use crate::skills::Skill;
6
7pub struct SystemPromptOptions<'a> {
8    /// Replaces the default prompt body (context/skills/cwd still append).
9    pub custom_prompt: Option<&'a str>,
10    pub append: Option<&'a str>,
11    pub selected_tools: &'a [String],
12    pub cwd: &'a str,
13    pub context_files: &'a [ContextFile],
14    pub skills: &'a [Skill],
15}
16
17fn tool_snippet(name: &str) -> Option<&'static str> {
18    Some(match name {
19        "read" => "Read file contents (text and images)",
20        "bash" => "Execute shell commands",
21        "edit" => "Make targeted text replacements in files",
22        "write" => "Create or overwrite files",
23        "grep" => "Search file contents for patterns (respects .gitignore)",
24        "find" => "Find files by glob pattern (respects .gitignore)",
25        "ls" => "List directory contents",
26        _ => return None,
27    })
28}
29
30pub fn build_system_prompt(options: &SystemPromptOptions) -> String {
31    let cwd = options.cwd.replace('\\', "/");
32    let mut prompt = match options.custom_prompt {
33        Some(custom) => custom.to_string(),
34        None => default_prompt(options),
35    };
36
37    if let Some(append) = options.append {
38        prompt.push_str("\n\n");
39        prompt.push_str(append);
40    }
41
42    if !options.context_files.is_empty() {
43        prompt.push_str("\n\n<project_context>\n\n");
44        prompt.push_str("Project-specific instructions and guidelines:\n\n");
45        for file in options.context_files {
46            prompt.push_str(&format!(
47                "<project_instructions path=\"{}\">\n{}\n</project_instructions>\n\n",
48                file.path.display(),
49                file.content
50            ));
51        }
52        prompt.push_str("</project_context>\n");
53    }
54
55    let file_read_tool = options
56        .selected_tools
57        .iter()
58        .find(|tool| matches!(tool.as_str(), "read" | "bash"));
59    if let Some(tool) = file_read_tool
60        && !options.skills.is_empty()
61    {
62        prompt.push('\n');
63        prompt.push_str(&crate::skills::format_skills_for_prompt_with_tool(
64            options.skills,
65            tool,
66        ));
67        prompt.push('\n');
68    }
69
70    prompt.push_str(&format!("\nCurrent working directory: {cwd}"));
71    prompt
72}
73
74fn default_prompt(options: &SystemPromptOptions) -> String {
75    let visible: Vec<String> = options
76        .selected_tools
77        .iter()
78        .filter_map(|name| tool_snippet(name).map(|snippet| format!("- {name}: {snippet}")))
79        .collect();
80    let tools_list = if visible.is_empty() {
81        "(none)".to_string()
82    } else {
83        visible.join("\n")
84    };
85
86    let mut guidelines: Vec<&str> = Vec::new();
87    let has = |t: &str| options.selected_tools.iter().any(|x| x == t);
88    if has("bash") && !has("grep") && !has("find") && !has("ls") {
89        guidelines.push("Use bash for file operations like ls, rg, find");
90    }
91    guidelines.push("Be concise in your responses");
92    guidelines.push("Show file paths clearly when working with files");
93    let guidelines_list = guidelines
94        .iter()
95        .map(|g| format!("- {g}"))
96        .collect::<Vec<_>>()
97        .join("\n");
98
99    format!(
100        "You are an expert coding assistant operating inside kiss, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n{tools_list}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n{guidelines_list}"
101    )
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    fn base_options<'a>(tools: &'a [String]) -> SystemPromptOptions<'a> {
109        SystemPromptOptions {
110            custom_prompt: None,
111            append: None,
112            selected_tools: tools,
113            cwd: "/work",
114            context_files: &[],
115            skills: &[],
116        }
117    }
118
119    #[test]
120    fn default_prompt_lists_tools_and_cwd() {
121        let tools = vec!["read".to_string(), "bash".to_string()];
122        let p = build_system_prompt(&base_options(&tools));
123        assert!(p.contains("- read:"));
124        assert!(p.contains("- bash:"));
125        assert!(p.contains("Use bash for file operations"));
126        assert!(p.ends_with("Current working directory: /work"));
127    }
128
129    #[test]
130    fn skills_are_available_with_bash_as_the_only_reader() {
131        let tools = vec!["bash".to_string()];
132        let skill = Skill {
133            name: "example".into(),
134            description: "Example skill".into(),
135            file_path: "/skills/example/SKILL.md".into(),
136            disable_model_invocation: false,
137        };
138        let mut options = base_options(&tools);
139        options.skills = std::slice::from_ref(&skill);
140        let prompt = build_system_prompt(&options);
141        assert!(prompt.contains("<name>example</name>"));
142        assert!(prompt.contains("Use bash to load a skill's file"));
143
144        let no_reader = vec!["edit".to_string()];
145        options.selected_tools = &no_reader;
146        assert!(!build_system_prompt(&options).contains("<available_skills>"));
147    }
148
149    #[test]
150    fn custom_prompt_keeps_appends() {
151        let tools = vec!["read".to_string()];
152        let mut o = base_options(&tools);
153        o.custom_prompt = Some("You are a poet.");
154        o.append = Some("Rhyme everything.");
155        let p = build_system_prompt(&o);
156        assert!(p.starts_with("You are a poet."));
157        assert!(p.contains("Rhyme everything."));
158        assert!(!p.contains("expert coding assistant"));
159    }
160
161    #[test]
162    fn context_files_wrapped() {
163        let tools = vec!["read".to_string()];
164        let files = vec![ContextFile {
165            path: "/p/AGENTS.md".into(),
166            content: "be nice".into(),
167        }];
168        let mut o = base_options(&tools);
169        o.context_files = &files;
170        let p = build_system_prompt(&o);
171        assert!(p.contains("<project_context>"));
172        assert!(p.contains("<project_instructions path=\"/p/AGENTS.md\">"));
173        assert!(p.contains("be nice"));
174    }
175}