use std::path::Path;
use crate::config::constants::prompt_budget as prompt_budget_constants;
use crate::config::types::ShellPromptProfile;
use crate::llm::providers::gemini::wire::Content;
use crate::prompts::context::PromptContext;
use crate::prompts::guidelines::{generate_tool_guidelines_for_profile, render_shell_profile_guidance};
use crate::prompts::output_styles::OutputStyleApplier;
use crate::prompts::render::render_environment_addenda;
use crate::prompts::resources::{apply_system_prompt_layers, resolve_system_prompt_layers};
use crate::prompts::static_prompts::static_profile_prompt;
use crate::prompts::system_prompt_cache::PROMPT_CACHE;
use crate::skills::render::render_prompt_skills_section;
use tracing::warn;
use super::cache::{cache_key_for_identity, prompt_context_digest};
use super::constants::{PROMPT_IDENTITY_NAME, PROMPT_INTRO, PROMPT_TITLE, STRUCTURED_REASONING_INSTRUCTIONS};
use super::tokens::estimate_token_count;
use super::types::{PromptSection, SectionKind, SystemPromptConfig, SystemPromptReport};
use crate::prompts::static_prompts::agent_identity_label;
pub async fn compose_system_instruction_text(
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> String {
compose_system_instruction_with_report(project_root, vtcode_config, prompt_context)
.await
.0
}
pub async fn compose_system_instruction_with_report(
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> (String, SystemPromptReport) {
let (prompt, report, _) =
compose_system_instruction_with_identity(project_root, vtcode_config, prompt_context).await;
(prompt, report)
}
async fn compose_system_instruction_with_identity(
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> (String, SystemPromptReport, u64) {
let sections = build_prompt_sections(project_root, vtcode_config, prompt_context).await;
let instruction_digest = stable_prompt_sections_digest(§ions);
let (max_tokens, warn_enabled, trim_enabled) = system_prompt_budget_settings(vtcode_config);
let (prompt, report) = apply_token_budget(sections, max_tokens, warn_enabled, trim_enabled);
(prompt, report, instruction_digest)
}
pub async fn measure_system_prompt_size(
project_root: &Path,
vtcode_config: &crate::config::VTCodeConfig,
) -> SystemPromptReport {
let sections = build_prompt_sections(project_root, Some(vtcode_config), None).await;
let text = join_prompt_sections(§ions);
let token_estimate = estimate_token_count(&text);
SystemPromptReport {
token_estimate,
over_budget: token_estimate > vtcode_config.agent.max_system_prompt_tokens,
trimmed_sections: Vec::new(),
}
}
fn system_prompt_budget_settings(vtcode_config: Option<&crate::config::VTCodeConfig>) -> (u64, bool, bool) {
vtcode_config.map_or((prompt_budget_constants::DEFAULT_MAX_SYSTEM_PROMPT_TOKENS, true, true), |cfg| {
(
cfg.agent.max_system_prompt_tokens,
cfg.agent.system_prompt_budget_warning,
cfg.agent.trim_system_prompt,
)
})
}
pub(super) async fn build_prompt_sections(
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> Vec<PromptSection> {
let prompt_mode = vtcode_config.map(|c| c.agent.system_prompt_mode).unwrap_or_default();
let static_base_prompt = static_profile_prompt(prompt_mode);
let resolved_layers = resolve_system_prompt_layers(project_root).await;
let mut base_prompt = apply_system_prompt_layers(static_base_prompt, &resolved_layers);
crate::prompts::runtime_guidance::ensure_runtime_guidance(&mut base_prompt);
tracing::trace!(
mode = ?prompt_mode,
base_tokens = estimate_token_count(&base_prompt),
"Selected system prompt mode"
);
if let Some(cfg) = vtcode_config {
let agent_label = agent_identity_label(&cfg.default_primary_agent);
base_prompt = apply_agent_identity(&base_prompt, &agent_label);
}
let mut sections = vec![PromptSection { kind: SectionKind::BaseContract, text: base_prompt }];
if should_include_structured_reasoning(vtcode_config) {
sections.push(PromptSection {
kind: SectionKind::StructuredReasoning,
text: STRUCTURED_REASONING_INSTRUCTIONS.to_string(),
});
}
let shell_profile = vtcode_config
.map(|cfg| cfg.agent.shell_prompt_profile)
.unwrap_or(ShellPromptProfile::Auto)
.resolve_for_current_platform();
sections.push(PromptSection {
kind: SectionKind::ShellProfile,
text: render_shell_profile_guidance(shell_profile),
});
if let Some(ctx) = prompt_context {
if let Some(skills_section) = render_prompt_skills_section(&ctx.available_skill_metadata) {
sections.push(PromptSection { kind: SectionKind::Skills, text: skills_section });
}
let guidelines =
generate_tool_guidelines_for_profile(&ctx.available_tools, ctx.capability_level, shell_profile, true);
if !guidelines.is_empty() {
sections.push(PromptSection {
kind: SectionKind::ToolGuidelines,
text: guidelines.trim_start_matches('\n').to_string(),
});
}
}
if let Some(environment_section) = render_environment_addenda(vtcode_config, prompt_context) {
sections.push(PromptSection {
kind: SectionKind::EnvironmentAddenda,
text: environment_section,
});
}
sections
}
pub(super) fn join_prompt_sections(sections: &[PromptSection]) -> String {
let capacity = sections.iter().map(|section| section.text.len() + 2).sum();
let mut joined = String::with_capacity(capacity);
for (index, section) in sections.iter().enumerate() {
if index > 0 {
joined.push_str("\n\n");
}
joined.push_str(§ion.text);
}
joined
}
pub(super) fn apply_token_budget(
mut sections: Vec<PromptSection>,
max_tokens: u64,
warn_enabled: bool,
trim_enabled: bool,
) -> (String, SystemPromptReport) {
let mut text = join_prompt_sections(§ions);
let mut token_estimate = estimate_token_count(&text);
let mut trimmed_sections: Vec<&'static str> = Vec::new();
if token_estimate > max_tokens {
if trim_enabled {
while token_estimate > max_tokens {
let drop_index = sections
.iter()
.enumerate()
.filter_map(|(index, section)| section.kind.trim_priority().map(|priority| (priority, index)))
.min_by_key(|(priority, _)| *priority)
.map(|(_, index)| index);
let Some(drop_index) = drop_index else {
break;
};
let dropped = sections.remove(drop_index);
trimmed_sections.push(dropped.kind.name());
text = join_prompt_sections(§ions);
token_estimate = estimate_token_count(&text);
}
if !trimmed_sections.is_empty() {
tracing::warn!(
token_estimate,
max_system_prompt_tokens = max_tokens,
dropped_sections = ?trimmed_sections,
"Trimmed system prompt sections to satisfy token budget"
);
}
} else if warn_enabled {
tracing::warn!(
token_estimate,
max_system_prompt_tokens = max_tokens,
"System prompt exceeds configured token budget"
);
}
}
let report = SystemPromptReport {
token_estimate,
over_budget: token_estimate > max_tokens,
trimmed_sections,
};
(text, report)
}
pub(super) fn stable_prompt_sections_digest(sections: &[PromptSection]) -> u64 {
let stable_sections = sections
.iter()
.filter(|section| section.kind.is_cache_stable())
.map(|section| (section.kind.name(), section.text.as_str()))
.collect::<Vec<_>>();
crate::core::agent::hash_utils::hash_value(&stable_sections)
}
pub(super) fn apply_agent_identity(prompt: &str, agent_label: &str) -> String {
let mut result = prompt.to_string();
let old_title = PROMPT_TITLE;
let labeled_title = old_title.replacen(PROMPT_IDENTITY_NAME, agent_label, 1);
let old_intro = PROMPT_INTRO;
let title_found = if let Some(pos) = result.find(old_title) {
result.replace_range(pos..pos + old_title.len(), &labeled_title);
true
} else {
warn!("Could not find prompt title '{}' to apply agent identity", old_title);
false
};
let intro_found = if let Some(pos) = result.find(old_intro) {
let labeled_intro = old_intro.replacen(PROMPT_IDENTITY_NAME, agent_label, 1);
result.replace_range(pos..pos + old_intro.len(), &labeled_intro);
true
} else {
warn!("Could not find prompt intro '{}' to apply agent identity", old_intro);
false
};
if !title_found || !intro_found {
warn!(
agent_label = %agent_label,
title_replaced = title_found,
intro_replaced = intro_found,
"agent identity partially applied"
);
}
result
}
pub(super) fn should_include_structured_reasoning(vtcode_config: Option<&crate::config::VTCodeConfig>) -> bool {
vtcode_config.is_some_and(|cfg| cfg.agent.should_include_structured_reasoning_tags())
}
pub async fn generate_system_instruction_with_config(
config: &SystemPromptConfig,
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
) -> Content {
let (content, _report) =
generate_system_instruction_with_config_and_report(config, project_root, vtcode_config).await;
content
}
pub async fn generate_system_instruction_with_config_and_report(
_config: &SystemPromptConfig,
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
) -> (Content, SystemPromptReport) {
generate_system_instruction_with_context_and_report(_config, project_root, vtcode_config, None).await
}
pub async fn generate_system_instruction_with_context_and_report(
_config: &SystemPromptConfig,
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> (Content, SystemPromptReport) {
let (built_instruction, built_report, instruction_digest) =
compose_system_instruction_with_identity(project_root, vtcode_config, prompt_context).await;
let cache_key = cache_key_for_identity(
project_root,
vtcode_config,
instruction_digest,
prompt_context_digest(prompt_context),
0,
);
let (instruction, report) = match PROMPT_CACHE.get(&cache_key) {
Some(cached) => cached,
None => {
let built = (built_instruction, built_report);
PROMPT_CACHE.insert(cache_key, built.clone());
built
}
};
let styled_instruction = apply_output_style(instruction, vtcode_config, project_root).await;
(Content::system_text(styled_instruction), report)
}
pub async fn apply_output_style(
instruction: String,
vtcode_config: Option<&crate::config::VTCodeConfig>,
project_root: &Path,
) -> String {
if let Some(config) = vtcode_config {
let output_style_applier = OutputStyleApplier::new();
if let Err(e) = output_style_applier.load_styles_from_config(config, project_root).await {
tracing::warn!("Failed to load output styles: {}", e);
instruction } else {
output_style_applier
.apply_style(&config.output_style.active_style, &instruction, config)
.await
}
} else {
instruction }
}