1use std::path::Path;
4
5use crate::config::constants::prompt_budget as prompt_budget_constants;
6use crate::config::types::ShellPromptProfile;
7use crate::llm::providers::gemini::wire::Content;
8use crate::prompts::context::PromptContext;
9use crate::prompts::guidelines::{generate_tool_guidelines_for_profile, render_shell_profile_guidance};
10use crate::prompts::output_styles::OutputStyleApplier;
11use crate::prompts::render::render_environment_addenda;
12use crate::prompts::resources::{apply_system_prompt_layers, resolve_system_prompt_layers};
13use crate::prompts::static_prompts::static_profile_prompt;
14use crate::prompts::system_prompt_cache::PROMPT_CACHE;
15use crate::skills::render::render_prompt_skills_section;
16use tracing::warn;
17
18use super::cache::{cache_key_for_identity, prompt_context_digest};
19use super::constants::{PROMPT_IDENTITY_NAME, PROMPT_INTRO, PROMPT_TITLE, STRUCTURED_REASONING_INSTRUCTIONS};
20use super::tokens::estimate_token_count;
21use super::types::{PromptSection, SectionKind, SystemPromptConfig, SystemPromptReport};
22
23use crate::prompts::static_prompts::agent_identity_label;
24
25pub async fn compose_system_instruction_text(
27 project_root: &Path,
28 vtcode_config: Option<&crate::config::VTCodeConfig>,
29 prompt_context: Option<&PromptContext>,
30) -> String {
31 compose_system_instruction_with_report(project_root, vtcode_config, prompt_context)
32 .await
33 .0
34}
35
36pub async fn compose_system_instruction_with_report(
41 project_root: &Path,
42 vtcode_config: Option<&crate::config::VTCodeConfig>,
43 prompt_context: Option<&PromptContext>,
44) -> (String, SystemPromptReport) {
45 let (prompt, report, _) =
46 compose_system_instruction_with_identity(project_root, vtcode_config, prompt_context).await;
47 (prompt, report)
48}
49
50async fn compose_system_instruction_with_identity(
53 project_root: &Path,
54 vtcode_config: Option<&crate::config::VTCodeConfig>,
55 prompt_context: Option<&PromptContext>,
56) -> (String, SystemPromptReport, u64) {
57 let sections = build_prompt_sections(project_root, vtcode_config, prompt_context).await;
58 let instruction_digest = stable_prompt_sections_digest(§ions);
59 let (max_tokens, warn_enabled, trim_enabled) = system_prompt_budget_settings(vtcode_config);
60 let (prompt, report) = apply_token_budget(sections, max_tokens, warn_enabled, trim_enabled);
61 (prompt, report, instruction_digest)
62}
63
64pub async fn measure_system_prompt_size(
71 project_root: &Path,
72 vtcode_config: &crate::config::VTCodeConfig,
73) -> SystemPromptReport {
74 let sections = build_prompt_sections(project_root, Some(vtcode_config), None).await;
75 let text = join_prompt_sections(§ions);
76 let token_estimate = estimate_token_count(&text);
77 SystemPromptReport {
78 token_estimate,
79 over_budget: token_estimate > vtcode_config.agent.max_system_prompt_tokens,
80 trimmed_sections: Vec::new(),
81 }
82}
83
84fn system_prompt_budget_settings(vtcode_config: Option<&crate::config::VTCodeConfig>) -> (u64, bool, bool) {
88 vtcode_config.map_or((prompt_budget_constants::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS, true, true), |cfg| {
89 (
90 cfg.agent.max_system_prompt_tokens,
91 cfg.agent.system_prompt_budget_warning,
92 cfg.agent.trim_system_prompt,
93 )
94 })
95}
96
97pub(super) async fn build_prompt_sections(
102 project_root: &Path,
103 vtcode_config: Option<&crate::config::VTCodeConfig>,
104 prompt_context: Option<&PromptContext>,
105) -> Vec<PromptSection> {
106 let prompt_mode = vtcode_config.map(|c| c.agent.system_prompt_mode).unwrap_or_default();
107 let static_base_prompt = static_profile_prompt(prompt_mode);
108 let resolved_layers = resolve_system_prompt_layers(project_root).await;
109 let mut base_prompt = apply_system_prompt_layers(static_base_prompt, &resolved_layers);
110 crate::prompts::runtime_guidance::ensure_runtime_guidance(&mut base_prompt);
111
112 tracing::trace!(
113 mode = ?prompt_mode,
114 base_tokens = estimate_token_count(&base_prompt),
115 "Selected system prompt mode"
116 );
117
118 if let Some(cfg) = vtcode_config {
121 let agent_label = agent_identity_label(&cfg.default_primary_agent);
122 base_prompt = apply_agent_identity(&base_prompt, &agent_label);
123 apply_coordinator_role_guidance(&mut base_prompt, cfg.default_primary_agent == "coordinator");
124 }
125
126 let mut sections = vec![PromptSection { kind: SectionKind::BaseContract, text: base_prompt }];
127
128 if should_include_structured_reasoning(vtcode_config) {
129 sections.push(PromptSection {
130 kind: SectionKind::StructuredReasoning,
131 text: STRUCTURED_REASONING_INSTRUCTIONS.to_string(),
132 });
133 }
134
135 let shell_profile = vtcode_config
136 .map(|cfg| cfg.agent.shell_prompt_profile)
137 .unwrap_or(ShellPromptProfile::Auto)
138 .resolve_for_current_platform();
139 sections.push(PromptSection {
140 kind: SectionKind::ShellProfile,
141 text: render_shell_profile_guidance(shell_profile),
142 });
143
144 if let Some(ctx) = prompt_context {
145 if let Some(skills_section) = render_prompt_skills_section(&ctx.available_skill_metadata) {
151 sections.push(PromptSection { kind: SectionKind::Skills, text: skills_section });
152 }
153 let guidelines =
156 generate_tool_guidelines_for_profile(&ctx.available_tools, ctx.capability_level, shell_profile, true);
157 if !guidelines.is_empty() {
158 sections.push(PromptSection {
159 kind: SectionKind::ToolGuidelines,
160 text: guidelines.trim_start_matches('\n').to_string(),
161 });
162 }
163 }
164
165 if let Some(environment_section) = render_environment_addenda(vtcode_config, prompt_context) {
166 sections.push(PromptSection {
167 kind: SectionKind::EnvironmentAddenda,
168 text: environment_section,
169 });
170 }
171
172 sections
173}
174
175pub(super) fn join_prompt_sections(sections: &[PromptSection]) -> String {
179 let capacity = sections.iter().map(|section| section.text.len() + 2).sum();
180 let mut joined = String::with_capacity(capacity);
181 for (index, section) in sections.iter().enumerate() {
182 if index > 0 {
183 joined.push_str("\n\n");
184 }
185 joined.push_str(§ion.text);
186 }
187 joined
188}
189
190pub(super) fn apply_token_budget(
200 mut sections: Vec<PromptSection>,
201 max_tokens: u64,
202 warn_enabled: bool,
203 trim_enabled: bool,
204) -> (String, SystemPromptReport) {
205 let mut text = join_prompt_sections(§ions);
206 let mut token_estimate = estimate_token_count(&text);
207 let mut trimmed_sections: Vec<&'static str> = Vec::new();
208
209 if token_estimate > max_tokens {
210 if trim_enabled {
211 while token_estimate > max_tokens {
212 let drop_index = sections
213 .iter()
214 .enumerate()
215 .filter_map(|(index, section)| section.kind.trim_priority().map(|priority| (priority, index)))
216 .min_by_key(|(priority, _)| *priority)
217 .map(|(_, index)| index);
218 let Some(drop_index) = drop_index else {
219 break;
220 };
221 let dropped = sections.remove(drop_index);
222 trimmed_sections.push(dropped.kind.name());
223 text = join_prompt_sections(§ions);
224 token_estimate = estimate_token_count(&text);
225 }
226
227 if !trimmed_sections.is_empty() {
228 tracing::warn!(
229 token_estimate,
230 max_system_prompt_tokens = max_tokens,
231 dropped_sections = ?trimmed_sections,
232 "Trimmed system prompt sections to satisfy token budget"
233 );
234 }
235 } else if warn_enabled {
236 tracing::warn!(
237 token_estimate,
238 max_system_prompt_tokens = max_tokens,
239 "System prompt exceeds configured token budget"
240 );
241 }
242 }
243
244 let report = SystemPromptReport {
245 token_estimate,
246 over_budget: token_estimate > max_tokens,
247 trimmed_sections,
248 };
249 (text, report)
250}
251
252pub(super) fn stable_prompt_sections_digest(sections: &[PromptSection]) -> u64 {
254 let stable_sections = sections
255 .iter()
256 .filter(|section| section.kind.is_cache_stable())
257 .map(|section| (section.kind.name(), section.text.as_str()))
258 .collect::<Vec<_>>();
259 crate::core::agent::hash_utils::hash_value(&stable_sections)
260}
261
262pub fn apply_coordinator_role_guidance(prompt: &mut String, coordinator_active: bool) {
264 use crate::prompts::sections::{SectionBoundaryMode, find_prompt_section_bounds};
265
266 const ORDINARY_DELEGATION: &str = "- Delegate only sizeable, independent work to subagents; keep small tasks and verification in the main thread.";
267 const COORDINATOR_DELEGATION: &str = "- As coordinator, delegate execution and verification to scheduler-owned matrix workers; keep decisions in the main thread.";
268
269 if !coordinator_active && !prompt.contains("## Coordinator Role") && !prompt.contains(COORDINATOR_DELEGATION) {
270 return;
271 }
272
273 let coordinator_role =
274 format!("## Coordinator Role\n{}\n", vtcode_config::subagents::builtin_primary_coordinator_agent().prompt);
275 if coordinator_active
276 && prompt.matches("## Coordinator Role").count() == 1
277 && prompt.contains(COORDINATOR_DELEGATION)
278 && let Some((start, end)) =
279 find_prompt_section_bounds(prompt, "## Coordinator Role", SectionBoundaryMode::BracketOrMarkdown)
280 && prompt
281 .get(start..end)
282 .is_some_and(|section| section.trim() == coordinator_role.trim())
283 {
284 return;
285 }
286
287 while let Some((start, end)) =
288 find_prompt_section_bounds(prompt, "## Coordinator Role", SectionBoundaryMode::BracketOrMarkdown)
289 {
290 prompt.replace_range(start..end, "");
291 }
292 let (previous, current) = if coordinator_active {
293 (ORDINARY_DELEGATION, COORDINATOR_DELEGATION)
294 } else {
295 (COORDINATOR_DELEGATION, ORDINARY_DELEGATION)
296 };
297 *prompt = prompt.replace(previous, current);
298 if coordinator_active {
299 prompt.push_str("\n\n");
300 prompt.push_str(&coordinator_role);
301 }
302}
303
304pub(super) fn apply_agent_identity(prompt: &str, agent_label: &str) -> String {
308 let mut result = prompt.to_string();
309 let old_title = PROMPT_TITLE;
310 let labeled_title = old_title.replacen(PROMPT_IDENTITY_NAME, agent_label, 1);
311 let old_intro = PROMPT_INTRO;
312
313 let title_found = if let Some(pos) = result.find(old_title) {
314 result.replace_range(pos..pos + old_title.len(), &labeled_title);
315 true
316 } else {
317 warn!("Could not find prompt title '{}' to apply agent identity", old_title);
318 false
319 };
320
321 let intro_found = if let Some(pos) = result.find(old_intro) {
322 let labeled_intro = old_intro.replacen(PROMPT_IDENTITY_NAME, agent_label, 1);
323 result.replace_range(pos..pos + old_intro.len(), &labeled_intro);
324 true
325 } else {
326 warn!("Could not find prompt intro '{}' to apply agent identity", old_intro);
327 false
328 };
329
330 if !title_found || !intro_found {
331 warn!(
332 agent_label = %agent_label,
333 title_replaced = title_found,
334 intro_replaced = intro_found,
335 "agent identity partially applied"
336 );
337 }
338
339 result
340}
341
342pub(super) fn should_include_structured_reasoning(vtcode_config: Option<&crate::config::VTCodeConfig>) -> bool {
345 vtcode_config.is_some_and(|cfg| cfg.agent.should_include_structured_reasoning_tags())
346}
347
348pub async fn generate_system_instruction_with_config(
353 config: &SystemPromptConfig,
354 project_root: &Path,
355 vtcode_config: Option<&crate::config::VTCodeConfig>,
356) -> Content {
357 let (content, _report) =
358 generate_system_instruction_with_config_and_report(config, project_root, vtcode_config).await;
359 content
360}
361
362pub async fn generate_system_instruction_with_config_and_report(
366 _config: &SystemPromptConfig,
367 project_root: &Path,
368 vtcode_config: Option<&crate::config::VTCodeConfig>,
369) -> (Content, SystemPromptReport) {
370 generate_system_instruction_with_context_and_report(_config, project_root, vtcode_config, None).await
371}
372
373pub async fn generate_system_instruction_with_context_and_report(
379 _config: &SystemPromptConfig,
380 project_root: &Path,
381 vtcode_config: Option<&crate::config::VTCodeConfig>,
382 prompt_context: Option<&PromptContext>,
383) -> (Content, SystemPromptReport) {
384 let (built_instruction, built_report, instruction_digest) =
385 compose_system_instruction_with_identity(project_root, vtcode_config, prompt_context).await;
386 let cache_key = cache_key_for_identity(
387 project_root,
388 vtcode_config,
389 instruction_digest,
390 prompt_context_digest(prompt_context),
391 0,
392 );
393 let (instruction, report) = match PROMPT_CACHE.get(&cache_key) {
394 Some(cached) => cached,
395 None => {
396 let built = (built_instruction, built_report);
397 PROMPT_CACHE.insert(cache_key, built.clone());
398 built
399 }
400 };
401
402 let styled_instruction = apply_output_style(instruction, vtcode_config, project_root).await;
404 (Content::system_text(styled_instruction), report)
405}
406
407pub async fn apply_output_style(
409 instruction: String,
410 vtcode_config: Option<&crate::config::VTCodeConfig>,
411 project_root: &Path,
412) -> String {
413 if let Some(config) = vtcode_config {
414 let output_style_applier = OutputStyleApplier::new();
415 if let Err(e) = output_style_applier.load_styles_from_config(config, project_root).await {
416 tracing::warn!("Failed to load output styles: {}", e);
417 instruction } else {
419 output_style_applier
420 .apply_style(&config.output_style.active_style, &instruction, config)
421 .await
422 }
423 } else {
424 instruction }
426}