kiss_coding/
system_prompt.rs1use crate::context_files::ContextFile;
5use crate::skills::Skill;
6
7pub struct SystemPromptOptions<'a> {
8 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}