vtcode_core/prompts/
static_prompts.rs1use std::sync::OnceLock;
4
5use crate::config::types::SystemPromptMode;
6use crate::prompts::runtime_guidance::{RUNTIME_GUIDANCE_MAX_ESTIMATED_TOKENS, RUNTIME_GUIDANCE_SECTION};
7use crate::prompts::system::{
8 CONTRACT_HEADER, DEFAULT_OPERATING_PROFILE_DELTA, DEFAULT_SPECIFIC_LINES, LIGHTWEIGHT_OPERATING_PROFILE_DELTA,
9 MINIMAL_OPERATING_PROFILE_DELTA, MINIMAL_SPECIFIC_LINES, PROMPT_INTRO, PROMPT_ROLE_PARAGRAPH, PROMPT_TITLE,
10 SHARED_CONTRACT_LINES, SPECIALIZED_OPERATING_PROFILE_DELTA,
11};
12use vtcode_commons::estimate_tokens;
13
14pub fn agent_identity_label(agent_name: &str) -> String {
18 match agent_name {
19 "build" => "VT Code (Build mode)".to_string(),
20 "auto" => "VT Code (Auto mode)".to_string(),
21 "coordinator" => "VT Code (Coordinator mode)".to_string(),
22 "duck" => "VT Code (Duck mode)".to_string(),
23 "plan" => "VT Code (Plan mode)".to_string(),
24 "explorer" => "VT Code (Explorer mode)".to_string(),
25 "worker" => "VT Code (Worker mode)".to_string(),
26 other => format!("VT Code ({other})"),
27 }
28}
29
30static DEFAULT_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
31static MINIMAL_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
32static DEFAULT_LIGHTWEIGHT_PROMPT: OnceLock<String> = OnceLock::new();
33static DEFAULT_SPECIALIZED_PROMPT: OnceLock<String> = OnceLock::new();
34
35fn append_runtime_guidance(prompt: &mut String) {
36 assert!(estimate_tokens(RUNTIME_GUIDANCE_SECTION) <= RUNTIME_GUIDANCE_MAX_ESTIMATED_TOKENS);
37 prompt.push_str(RUNTIME_GUIDANCE_SECTION);
38 prompt.push('\n');
39}
40
41fn append_contract_block(prompt: &mut String, specific_lines: &[&str]) {
42 for line in SHARED_CONTRACT_LINES {
43 prompt.push_str("- ");
44 prompt.push_str(line);
45 prompt.push('\n');
46 }
47 for line in specific_lines {
48 prompt.push_str("- ");
49 prompt.push_str(line);
50 prompt.push('\n');
51 }
52 prompt.pop();
53 prompt.push('\n');
54 prompt.push('\n');
55}
56
57fn build_static_prompt(include_role: bool, specific_lines: &[&str], operating_delta: &str) -> String {
58 let mut prompt = String::new();
59 prompt.push_str(PROMPT_TITLE);
60 prompt.push_str("\n\n");
61 prompt.push_str(PROMPT_INTRO);
62 prompt.push_str("\n\n");
63 if include_role {
64 prompt.push_str(PROMPT_ROLE_PARAGRAPH);
65 prompt.push_str("\n\n");
66 }
67 append_runtime_guidance(&mut prompt);
68 prompt.push_str(CONTRACT_HEADER);
69 prompt.push_str("\n\n");
70 append_contract_block(&mut prompt, specific_lines);
71 prompt.push_str(operating_delta);
72 prompt
73}
74
75pub fn default_system_prompt() -> &'static str {
76 static_profile_prompt(SystemPromptMode::Default)
77}
78
79pub fn minimal_system_prompt() -> &'static str {
80 static_profile_prompt(SystemPromptMode::Minimal)
81}
82
83pub fn default_lightweight_prompt() -> &'static str {
84 static_profile_prompt(SystemPromptMode::Lightweight)
85}
86
87pub fn specialized_system_prompt() -> &'static str {
88 static_profile_prompt(SystemPromptMode::Specialized)
89}
90
91pub fn minimal_instruction_text() -> String {
92 minimal_system_prompt().to_string()
93}
94
95pub fn lightweight_instruction_text() -> String {
96 default_lightweight_prompt().to_string()
97}
98
99pub fn specialized_instruction_text() -> String {
100 specialized_system_prompt().to_string()
101}
102
103pub fn static_profile_prompt(prompt_mode: SystemPromptMode) -> &'static str {
104 match prompt_mode {
105 SystemPromptMode::Default => DEFAULT_SYSTEM_PROMPT
106 .get_or_init(|| build_static_prompt(true, DEFAULT_SPECIFIC_LINES, DEFAULT_OPERATING_PROFILE_DELTA)),
107 SystemPromptMode::Minimal => MINIMAL_SYSTEM_PROMPT
108 .get_or_init(|| build_static_prompt(false, MINIMAL_SPECIFIC_LINES, MINIMAL_OPERATING_PROFILE_DELTA)),
109 SystemPromptMode::Lightweight => DEFAULT_LIGHTWEIGHT_PROMPT
110 .get_or_init(|| build_static_prompt(false, DEFAULT_SPECIFIC_LINES, LIGHTWEIGHT_OPERATING_PROFILE_DELTA)),
111 SystemPromptMode::Specialized => DEFAULT_SPECIALIZED_PROMPT
112 .get_or_init(|| build_static_prompt(true, DEFAULT_SPECIFIC_LINES, SPECIALIZED_OPERATING_PROFILE_DELTA)),
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::config::constants::prompt_budget;
120 use crate::prompts::runtime_guidance::RUNTIME_GUIDANCE_SECTION;
121
122 #[test]
123 fn static_prompts_include_required_sections() {
124 let modes = [
125 (SystemPromptMode::Default, default_system_prompt()),
126 (SystemPromptMode::Minimal, minimal_system_prompt()),
127 (SystemPromptMode::Lightweight, default_lightweight_prompt()),
128 (SystemPromptMode::Specialized, specialized_system_prompt()),
129 ];
130 for (mode, prompt) in modes {
131 assert!(prompt.contains(PROMPT_TITLE), "{mode:?} missing title");
132 assert!(prompt.contains(CONTRACT_HEADER), "{mode:?} missing contract");
133 assert!(prompt.contains("AGENTS.md"), "{mode:?} missing AGENTS.md ref");
134 assert_eq!(
135 prompt.matches(RUNTIME_GUIDANCE_SECTION).count(),
136 1,
137 "{mode:?} should include runtime guidance exactly once"
138 );
139 assert!(
140 prompt.find(RUNTIME_GUIDANCE_SECTION) < prompt.find(CONTRACT_HEADER),
141 "{mode:?} should place runtime guidance before the contract"
142 );
143 assert!(
144 (estimate_tokens(prompt) as u64) <= prompt_budget::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS,
145 "{mode:?} static prompt should fit the default budget"
146 );
147 }
148 }
149
150 #[test]
151 fn shared_contract_lines_and_runtime_bullets_appear_exactly_once_per_profile() {
152 let runtime_bullets = RUNTIME_GUIDANCE_SECTION
153 .lines()
154 .filter(|line| line.starts_with("- "))
155 .collect::<Vec<_>>();
156 assert!(runtime_bullets.len() >= 10, "runtime guidance bullets were not found");
157 let profiles = [
158 (SystemPromptMode::Default, DEFAULT_SPECIFIC_LINES),
159 (SystemPromptMode::Minimal, MINIMAL_SPECIFIC_LINES),
160 (SystemPromptMode::Lightweight, DEFAULT_SPECIFIC_LINES),
161 (SystemPromptMode::Specialized, DEFAULT_SPECIFIC_LINES),
162 ];
163
164 for (mode, specific_lines) in profiles {
165 let prompt = static_profile_prompt(mode);
166 for line in SHARED_CONTRACT_LINES.iter().chain(specific_lines) {
167 let bullet = format!("- {line}\n");
168 assert_eq!(
169 prompt.matches(line).count(),
170 1,
171 "{mode:?} should state contract line {line:?} exactly once"
172 );
173 assert_eq!(
174 prompt.matches(&bullet).count(),
175 1,
176 "{mode:?} should render {line:?} as one contract bullet"
177 );
178 }
179 for bullet in &runtime_bullets {
180 assert_eq!(
181 prompt.matches(bullet).count(),
182 1,
183 "{mode:?} should state runtime guidance bullet {bullet:?} exactly once"
184 );
185 }
186 }
187 }
188
189 #[test]
190 fn static_prompts_do_not_embed_maintainer_files() {
191 let prompts = [
192 default_system_prompt(),
193 minimal_system_prompt(),
194 default_lightweight_prompt(),
195 specialized_system_prompt(),
196 ];
197 let maintainer_only_markers = [
198 "Keep this file concise and under 150 lines",
199 "vtcode-exec-events::ThreadEvent",
200 "RUSTFLAGS: \"-D warnings\"",
201 "Cargo workspace, ~30 crates",
202 "Root summary",
203 ];
204
205 for prompt in prompts {
206 for marker in maintainer_only_markers {
207 assert!(!prompt.contains(marker), "static prompt unexpectedly contains {marker:?}");
208 }
209 }
210 }
211}