Skip to main content

vtcode_core/prompts/
static_prompts.rs

1//! Static prompt text, constants, and cached static prompt builders.
2
3use 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
14/// Agent identity labels for the system prompt.
15/// Maps agent names to human-readable identity strings that combine VT Code
16/// with the active agent mode, so the LLM knows its role.
17pub 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}