use crate::config::constants::prompt_budget as prompt_budget_constants;
use crate::config::types::SystemPromptMode;
use crate::llm::providers::gemini::wire::Content;
use crate::project_doc::read_project_doc;
use crate::prompts::context::PromptContext;
use crate::prompts::guidelines::generate_tool_guidelines;
use crate::prompts::output_styles::OutputStyleApplier;
use crate::prompts::resources::{apply_system_prompt_layers, resolve_system_prompt_layers};
use crate::prompts::system_prompt_cache::PROMPT_CACHE;
use crate::prompts::temporal::generate_temporal_context;
use crate::skills::render::render_prompt_skills_section;
use std::env;
use std::path::Path;
use std::sync::OnceLock;
use tracing::warn;
pub const PLAN_MODE_READ_ONLY_HEADER: &str = "# PLAN MODE (READ-ONLY)";
pub const PLAN_MODE_READ_ONLY_NOTICE_LINE: &str = "Plan Mode is active. Mutating tools are blocked except for optional plan artifact writes under `.vtcode/plans/` (or an explicit custom plan path).";
pub const PLAN_MODE_EXIT_INSTRUCTION_LINE: &str =
"Call `exit_plan_mode` when ready to transition to implementation.";
pub const PLAN_MODE_PLAN_QUALITY_LINE: &str = "Explore repository facts first, ask only material blocking questions, keep planning read-only, and emit exactly one decision-complete `<proposed_plan>` block with a summary, implementation steps, test cases, and assumptions/defaults. If something is still unresolved, end with `Next open decision: ...`.";
pub const PLAN_MODE_INTERVIEW_POLICY_LINE: &str = "In Plan Mode, prefer model-generated `request_user_input` interview questions informed by discovered repository context, keep custom notes/free-form responses available as first-class input, and continue interviewing until material scope/decomposition/verification decisions are closed before finalizing `<proposed_plan>`.";
pub const PLAN_MODE_NO_REQUEST_USER_INPUT_POLICY_LINE: &str = "In this runtime, `request_user_input` is unavailable. In Plan Mode, continue exploring repository facts in read-only mode, finish any unblocked planning work, and surface material blockers explicitly in plain text instead of emitting interview tool calls.";
pub const PLAN_MODE_NO_AUTO_EXIT_LINE: &str = "Do not auto-exit Plan Mode just because a plan exists; wait for explicit implementation intent.";
pub const PLAN_MODE_TASK_TRACKER_LINE: &str =
"`task_tracker` remains available in Plan Mode (`plan_task_tracker` is a compatibility alias).";
pub const PLAN_MODE_IMPLEMENT_REMINDER: &str = "• Still in Plan Mode (read-only). Say “implement” to execute, or “stay in plan mode” to revise. If automatic Plan->Edit switching fails, manually switch with `/plan off` or `/mode` (or press `Shift+Tab`/`Alt+M` in interactive mode).";
const PROMPT_TITLE: &str = "# VT Code";
const PROMPT_INTRO: &str = "You are VT Code. Be concise, direct, and safe.";
const CONTRACT_HEADER: &str = "## Contract";
const HANDLE_CONTEXT_PROMPT_LINE: &str =
"Prefer explicit handles plus an owning context when state relationships get tangled.";
const DEFAULT_CONTRACT_LINES: &[&str] = &[
"Start with `AGENTS.md`; inspect code and match local patterns. Use `@file` when helpful.",
"If context is missing, say so plainly, do not guess, and finish any unblocked portion first.",
"Take safe, reversible steps without asking; ask only for material behavior, API, UX, credential, or external changes.",
HANDLE_CONTEXT_PROMPT_LINE,
"Keep control on the main thread. Delegate only bounded, independent work that will not block the next local step.",
"Prefer simple changes. Measure before optimizing.",
"Verify changes yourself; never claim a check passed unless you ran it.",
"Keep outputs concise and in the requested format. Keep user updates brief and high-signal.",
"Use retrieved evidence for citation-sensitive work and preserve task goal, touched files, outcomes, and decisions across compaction.",
"NEVER use emoji in any output. Use plain text only.",
];
const MINIMAL_CONTRACT_LINES: &[&str] = &[
"Start with `AGENTS.md`; inspect code first.",
"If context is missing, say so plainly, do not guess, and finish any unblocked portion first.",
HANDLE_CONTEXT_PROMPT_LINE,
"Take safe, reversible steps without asking and verify changes yourself.",
"Keep delegation bounded and explicit.",
"Preserve task goal, touched files, and outcomes across compaction.",
"Use retrieved evidence for citation-sensitive work.",
"Keep outputs concise and in the requested format.",
"NEVER use emoji in any output. Use plain text only.",
];
const DEFAULT_MODE_DELTA: &str = r#"## Mode
- Use `task_tracker` for non-trivial work.
- Use Plan Mode for research/spec work; stay read-only there until implementation intent is explicit."#;
const MINIMAL_MODE_DELTA: &str = r#"## Mode
- Stay lightweight and precise; use `task_tracker` once the task stops being trivial.
- Use `AGENTS.md` as the map and open repo docs only when structural rules matter."#;
const LIGHTWEIGHT_MODE_DELTA: &str = r#"## Mode
- Act and verify in one thread.
- Use `task_tracker` for non-trivial work."#;
const SPECIALIZED_MODE_DELTA: &str = r#"## Mode
- Explore, plan, then execute.
- Use `task_tracker` for multi-step work and Plan Mode when scope or verification is still open.
- End plan work with one `<proposed_plan>` block; if a path stalls, re-plan into smaller verified slices.
- Use `AGENTS.md` and `docs/harness/ARCHITECTURAL_INVARIANTS.md` when repo-wide invariants matter."#;
static DEFAULT_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
static MINIMAL_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
static DEFAULT_LIGHTWEIGHT_PROMPT: OnceLock<String> = OnceLock::new();
static DEFAULT_SPECIALIZED_PROMPT: OnceLock<String> = OnceLock::new();
pub fn default_system_prompt() -> &'static str {
static_mode_prompt(SystemPromptMode::Default)
}
pub fn minimal_system_prompt() -> &'static str {
static_mode_prompt(SystemPromptMode::Minimal)
}
pub fn default_lightweight_prompt() -> &'static str {
static_mode_prompt(SystemPromptMode::Lightweight)
}
pub fn specialized_system_prompt() -> &'static str {
static_mode_prompt(SystemPromptMode::Specialized)
}
pub fn minimal_instruction_text() -> String {
minimal_system_prompt().to_string()
}
pub fn lightweight_instruction_text() -> String {
default_lightweight_prompt().to_string()
}
pub fn specialized_instruction_text() -> String {
specialized_system_prompt().to_string()
}
const STRUCTURED_REASONING_INSTRUCTIONS: &str = r#"
## Structured Reasoning
Use tags when helpful: `<analysis>` facts/options, `<plan>` steps, `<uncertainty>` blockers, `<verification>` checks.
"#;
#[derive(Debug, Clone, Default)]
pub struct SystemPromptConfig;
pub async fn generate_system_instruction(_config: &SystemPromptConfig) -> Content {
let instruction = default_system_prompt().to_string();
if let Ok(current_dir) = env::current_dir() {
let styled_instruction = apply_output_style(instruction, None, ¤t_dir).await;
Content::system_text(styled_instruction)
} else {
Content::system_text(instruction)
}
}
pub async fn read_agent_guidelines(project_root: &Path) -> Option<String> {
let max_bytes = prompt_budget_constants::DEFAULT_MAX_BYTES;
match read_project_doc(project_root, max_bytes).await {
Ok(Some(bundle)) => Some(bundle.contents),
Ok(None) => None,
Err(err) => {
warn!("failed to load project documentation: {err:#}");
None
}
}
}
pub async fn compose_system_instruction_text(
_project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> String {
let prompt_mode = vtcode_config
.map(|c| c.agent.system_prompt_mode)
.unwrap_or(SystemPromptMode::Default);
let static_base_prompt = static_mode_prompt(prompt_mode);
let resolved_layers = resolve_system_prompt_layers(_project_root).await;
let base_prompt = apply_system_prompt_layers(static_base_prompt, &resolved_layers);
tracing::trace!(
mode = ?prompt_mode,
base_tokens_approx = base_prompt.len() / 4, "Selected system prompt mode"
);
let base_len = base_prompt.len();
let config_overhead = vtcode_config.map_or(0, |_| 1024);
let estimated_capacity = base_len + config_overhead + 1024;
let mut instruction = String::with_capacity(estimated_capacity);
instruction.push_str(&base_prompt);
if should_include_structured_reasoning(vtcode_config, prompt_mode) {
append_prompt_section(&mut instruction, STRUCTURED_REASONING_INSTRUCTIONS);
}
if let Some(ctx) = prompt_context {
let guidelines = generate_tool_guidelines(&ctx.available_tools, ctx.capability_level);
if !guidelines.is_empty() {
append_prompt_section(&mut instruction, guidelines.trim_start_matches('\n'));
}
if let Some(skills_section) = render_prompt_skills_section(&ctx.available_skill_metadata) {
append_prompt_section(&mut instruction, &skills_section);
}
}
if let Some(environment_section) = render_environment_addenda(vtcode_config, prompt_context) {
append_prompt_section(&mut instruction, &environment_section);
}
instruction
}
fn append_prompt_section(prompt: &mut String, section: &str) {
prompt.push_str("\n\n");
prompt.push_str(section);
}
fn static_mode_prompt(prompt_mode: SystemPromptMode) -> &'static str {
match prompt_mode {
SystemPromptMode::Default => DEFAULT_SYSTEM_PROMPT.get_or_init(|| {
build_mode_prompt(
&build_contract_prompt(DEFAULT_CONTRACT_LINES),
DEFAULT_MODE_DELTA,
)
}),
SystemPromptMode::Minimal => MINIMAL_SYSTEM_PROMPT.get_or_init(|| {
build_mode_prompt(
&build_contract_prompt(MINIMAL_CONTRACT_LINES),
MINIMAL_MODE_DELTA,
)
}),
SystemPromptMode::Lightweight => DEFAULT_LIGHTWEIGHT_PROMPT.get_or_init(|| {
build_mode_prompt(
&build_contract_prompt(DEFAULT_CONTRACT_LINES),
LIGHTWEIGHT_MODE_DELTA,
)
}),
SystemPromptMode::Specialized => DEFAULT_SPECIALIZED_PROMPT.get_or_init(|| {
build_mode_prompt(
&build_contract_prompt(DEFAULT_CONTRACT_LINES),
SPECIALIZED_MODE_DELTA,
)
}),
}
}
fn build_contract_prompt(contract_lines: &[&str]) -> String {
let lines_len = contract_lines.iter().map(|line| line.len()).sum::<usize>();
let mut prompt = String::with_capacity(
PROMPT_TITLE.len()
+ PROMPT_INTRO.len()
+ CONTRACT_HEADER.len()
+ lines_len
+ contract_lines.len() * 3
+ 8,
);
prompt.push_str(PROMPT_TITLE);
prompt.push_str("\n\n");
prompt.push_str(PROMPT_INTRO);
prompt.push_str("\n\n");
prompt.push_str(CONTRACT_HEADER);
prompt.push_str("\n\n");
for line in contract_lines {
prompt.push_str("- ");
prompt.push_str(line);
prompt.push('\n');
}
if !contract_lines.is_empty() {
prompt.pop();
}
prompt
}
fn build_mode_prompt(base_prompt: &str, mode_delta: &str) -> String {
let mut prompt = String::with_capacity(base_prompt.len() + mode_delta.len() + 2);
prompt.push_str(base_prompt);
prompt.push_str("\n\n");
prompt.push_str(mode_delta);
prompt
}
fn render_environment_addenda(
vtcode_config: Option<&crate::config::VTCodeConfig>,
prompt_context: Option<&PromptContext>,
) -> Option<String> {
let mut lines = Vec::new();
if let Some(ctx) = prompt_context
&& !ctx.languages.is_empty()
{
lines.push(format!(
"- Languages: {}. Match structural-search `lang` when needed.",
ctx.languages.join(", ")
));
}
if let Some(cfg) = vtcode_config {
if let Some(interaction_line) = render_interaction_addendum(cfg) {
lines.push(interaction_line);
}
if cfg.mcp.enabled {
lines.push("- Sources: prefer MCP before external fetches when available.".to_string());
}
if cfg.agent.include_temporal_context && !cfg.prompt_cache.cache_friendly_prompt_shaping {
lines.push(
generate_temporal_context(cfg.agent.temporal_context_use_utc)
.trim()
.replacen("Current date and time", "- Time", 1)
.to_string(),
);
}
if cfg.agent.include_working_directory
&& let Some(ctx) = prompt_context
&& let Some(cwd) = &ctx.current_directory
{
lines.push(format!("- Working directory: {}", cwd.display()));
}
}
if lines.is_empty() {
None
} else {
Some(format!("## Environment\n{}", lines.join("\n")))
}
}
fn render_interaction_addendum(cfg: &crate::config::VTCodeConfig) -> Option<String> {
match (cfg.security.human_in_the_loop, cfg.chat.ask_questions.enabled) {
(true, true) => None,
(true, false) => Some(
"- Interaction: approval may gate sensitive actions; no `request_user_input`, so make reasonable assumptions unless Plan Mode needs follow-up.".to_string(),
),
(false, true) => Some(
"- Interaction: approval reduced by config; use `request_user_input` for material blockers.".to_string(),
),
(false, false) => Some(
"- Interaction: approval reduced by config; no `request_user_input`, so make reasonable assumptions unless Plan Mode needs follow-up.".to_string(),
),
}
}
fn should_include_structured_reasoning(
vtcode_config: Option<&crate::config::VTCodeConfig>,
mode: SystemPromptMode,
) -> bool {
if let Some(cfg) = vtcode_config {
return cfg.agent.should_include_structured_reasoning_tags();
}
matches!(mode, SystemPromptMode::Specialized)
}
pub async fn generate_system_instruction_with_config(
_config: &SystemPromptConfig,
project_root: &Path,
vtcode_config: Option<&crate::config::VTCodeConfig>,
) -> Content {
let cache_key = cache_key(project_root, vtcode_config);
let instruction = match PROMPT_CACHE.get(&cache_key) {
Some(cached) => cached,
None => {
let built = compose_system_instruction_text(project_root, vtcode_config, None).await;
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)
}
pub async fn generate_system_instruction_with_guidelines(
_config: &SystemPromptConfig,
project_root: &Path,
) -> Content {
let cache_key = cache_key(project_root, None);
let instruction = match PROMPT_CACHE.get(&cache_key) {
Some(cached) => cached,
None => {
let built = compose_system_instruction_text(project_root, None, None).await;
PROMPT_CACHE.insert(cache_key, built.clone());
built
}
};
let styled_instruction = apply_output_style(instruction, None, project_root).await;
Content::system_text(styled_instruction)
}
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 }
}
fn cache_key(project_root: &Path, vtcode_config: Option<&crate::config::VTCodeConfig>) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
project_root.hash(&mut hasher);
if let Some(cfg) = vtcode_config {
cfg.agent.include_working_directory.hash(&mut hasher);
cfg.agent.include_temporal_context.hash(&mut hasher);
cfg.prompt_cache
.cache_friendly_prompt_shaping
.hash(&mut hasher);
cfg.agent
.include_structured_reasoning_tags
.hash(&mut hasher);
std::mem::discriminant(&cfg.agent.system_prompt_mode).hash(&mut hasher);
} else {
"default".hash(&mut hasher);
}
format!("sys_prompt:{:016x}", hasher.finish())
}
pub fn generate_minimal_instruction() -> Content {
Content::system_text(minimal_instruction_text())
}
pub fn generate_lightweight_instruction() -> Content {
Content::system_text(lightweight_instruction_text())
}
pub fn generate_specialized_instruction() -> Content {
Content::system_text(specialized_instruction_text())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::VTCodeConfig;
use crate::config::types::SystemPromptMode;
use std::path::PathBuf;
#[tokio::test]
async fn test_minimal_mode_selection() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Minimal;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
result.len() < 2200,
"Minimal mode should produce <2.2K chars (was {} chars)",
result.len()
);
assert!(
result.contains("VT Code") || result.contains("VT Code"),
"Should contain VT Code identifier"
);
}
#[tokio::test]
async fn test_default_mode_selection() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Default;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
result.len() <= 1400,
"Default mode should stay sparse (<=1.4K chars, was {} chars)",
result.len()
);
assert!(result.contains("task_tracker"));
assert!(result.contains("@file"));
assert!(result.contains("Plan Mode"));
}
#[tokio::test]
async fn test_lightweight_mode_selection() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(result.len() > 100, "Lightweight should be >100 chars");
assert!(
result.len() < 1200,
"Lightweight should be compact (<1.2K chars, was {} chars)",
result.len()
);
assert!(result.contains("task_tracker"));
assert!(result.contains("@file"));
assert!(result.contains("Act and verify in one thread"));
}
#[tokio::test]
async fn test_lightweight_mode_skips_structured_reasoning_by_default() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
config.agent.include_structured_reasoning_tags = None;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
!result.contains("## Structured Reasoning"),
"Lightweight mode should omit structured reasoning by default"
);
}
#[tokio::test]
async fn test_lightweight_mode_allows_explicit_structured_reasoning() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
config.agent.include_structured_reasoning_tags = Some(true);
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
result.contains("## Structured Reasoning"),
"Lightweight mode should include structured reasoning when explicitly enabled"
);
}
#[tokio::test]
async fn test_default_mode_omits_structured_reasoning_by_default() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Default;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
config.agent.include_structured_reasoning_tags = None;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
!result.contains("## Structured Reasoning"),
"Default mode should omit structured reasoning by default"
);
}
#[tokio::test]
async fn test_specialized_mode_selection() {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Specialized;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
result.len() <= 1700,
"Specialized should stay sparse (<=1.7K chars, was {} chars)",
result.len()
);
assert!(result.contains("task_tracker"));
assert!(result.contains("<proposed_plan>"));
assert!(result.contains("ARCHITECTURAL_INVARIANTS"));
}
#[test]
fn test_prompt_mode_enum_parsing() {
assert_eq!(
SystemPromptMode::parse("minimal"),
Some(SystemPromptMode::Minimal)
);
assert_eq!(
SystemPromptMode::parse("LIGHTWEIGHT"),
Some(SystemPromptMode::Lightweight)
);
assert_eq!(
SystemPromptMode::parse("Default"),
Some(SystemPromptMode::Default)
);
assert_eq!(
SystemPromptMode::parse("specialized"),
Some(SystemPromptMode::Specialized)
);
assert_eq!(SystemPromptMode::parse("invalid"), None);
}
#[test]
fn test_minimal_prompt_token_count() {
let approx_tokens = minimal_system_prompt().len() / 4;
assert!(
approx_tokens < 220,
"Minimal prompt should stay compact, got ~{}",
approx_tokens
);
}
#[test]
fn test_default_prompt_token_count() {
let approx_tokens = default_system_prompt().len() / 4;
assert!(
approx_tokens < 350,
"Default prompt should stay compact, got ~{}",
approx_tokens
);
}
#[tokio::test]
async fn test_default_live_prompt_budget_with_instruction_summary() {
use crate::project_doc::build_instruction_appendix_with_context;
let workspace = tempfile::TempDir::new().expect("workspace");
std::fs::write(workspace.path().join(".git"), "gitdir: /tmp/git").expect("git marker");
std::fs::write(
workspace.path().join("AGENTS.md"),
"- run ./scripts/check.sh\n- avoid adding to vtcode-core\n- use Conventional Commits\n- start with docs/ARCHITECTURE.md\n",
)
.expect("write agents");
std::fs::create_dir_all(workspace.path().join(".vtcode/rules")).expect("rules dir");
std::fs::write(
workspace.path().join(".vtcode/rules/rust.md"),
"---\npaths:\n - \"**/*.rs\"\n---\n# Rust\n- keep changes surgical\n",
)
.expect("write rust rule");
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
let base = compose_system_instruction_text(workspace.path(), Some(&config), None).await;
let appendix = build_instruction_appendix_with_context(
&config.agent,
workspace.path(),
&[workspace.path().join("src/lib.rs")],
)
.await
.expect("instruction appendix");
let prompt = format!("{base}\n\n# INSTRUCTIONS\n{appendix}");
let approx_tokens = prompt.len() / 4;
assert!(prompt.contains("### Instruction map"));
assert!(prompt.contains("### On-demand loading"));
assert!(approx_tokens <= 1100, "got ~{} tokens", approx_tokens);
}
#[tokio::test]
async fn test_generated_prompts_use_task_tracker_not_update_plan() {
let project_root = PathBuf::from(".");
for (mode_name, mode) in [
("default", SystemPromptMode::Default),
("minimal", SystemPromptMode::Minimal),
("specialized", SystemPromptMode::Specialized),
] {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = mode;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
assert!(
result.contains("task_tracker"),
"{mode_name} prompt should reference task_tracker"
);
assert!(
!result.contains("update_plan"),
"{mode_name} prompt should not reference deprecated update_plan"
);
}
}
#[tokio::test]
async fn test_default_and_specialized_prompts_drop_rigid_summary_template() {
let project_root = PathBuf::from(".");
for (mode_name, mode) in [
("default", SystemPromptMode::Default),
("specialized", SystemPromptMode::Specialized),
] {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = mode;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
assert!(
!result.contains("References\n"),
"{mode_name} prompt should not force a References section"
);
assert!(
!result.contains("Next action"),
"{mode_name} prompt should not force a Next action section"
);
assert!(
!result.contains("Scope checkpoint"),
"{mode_name} prompt should not require the old plan blueprint bullets"
);
}
}
#[tokio::test]
async fn test_generated_prompts_keep_sparse_execution_contract() {
let project_root = PathBuf::from(".");
for (mode_name, mode) in [
("default", SystemPromptMode::Default),
("minimal", SystemPromptMode::Minimal),
("lightweight", SystemPromptMode::Lightweight),
("specialized", SystemPromptMode::Specialized),
] {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = mode;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
let normalized = result.to_ascii_lowercase();
assert!(
normalized.contains("compact") || normalized.contains("concise"),
"{mode_name} prompt should keep output guidance compact"
);
assert!(
normalized.contains("low-risk") || normalized.contains("reversible"),
"{mode_name} prompt should include follow-through guidance"
);
assert!(
normalized.contains("verify") || normalized.contains("validation"),
"{mode_name} prompt should include verification guidance"
);
assert!(
normalized.contains("do not guess"),
"{mode_name} prompt should gate missing context"
);
assert!(
normalized.contains("unblocked portion")
|| normalized.contains("unblocked slices")
|| normalized.contains("answerable without a missing detail"),
"{mode_name} prompt should require partial progress before clarification"
);
assert!(
normalized.contains("retrieved sources")
|| normalized.contains("retrieved evidence"),
"{mode_name} prompt should include grounding/citation guidance"
);
assert!(
!result.contains('ƒ'),
"{mode_name} prompt should not contain stray prompt characters"
);
}
}
#[test]
fn test_prompt_text_avoids_hardcoded_loop_thresholds() {
let specialized_prompt = specialized_instruction_text();
assert!(!default_system_prompt().contains("stuck twice"));
assert!(!minimal_system_prompt().contains("stuck twice"));
assert!(!specialized_prompt.contains("stuck twice"));
assert!(!specialized_prompt.contains("10+ calls without progress"));
assert!(!specialized_prompt.contains("Same tool+params twice"));
}
#[test]
fn test_harness_awareness_in_prompts() {
assert!(
default_system_prompt().contains("AGENTS.md"),
"Default prompt should reference AGENTS.md as map"
);
assert!(
specialized_instruction_text().contains("ARCHITECTURAL_INVARIANTS"),
"Specialized prompt should reference architectural invariants"
);
assert!(
minimal_system_prompt().contains("AGENTS.md"),
"Minimal prompt should still reference AGENTS.md"
);
}
#[test]
fn test_prompts_reject_guessing_when_context_is_missing() {
assert!(
default_system_prompt().contains("do not guess"),
"Default prompt should reject guessing"
);
assert!(
specialized_instruction_text().contains("do not guess"),
"Specialized prompt should reject guessing"
);
assert!(
minimal_system_prompt().contains("do not guess"),
"Minimal prompt should still reject guessing"
);
}
#[test]
fn test_prompts_include_compaction_preservation_contract() {
assert!(
default_system_prompt().contains("touched files"),
"Default prompt should preserve touched files across compaction"
);
assert!(
default_system_prompt().contains("decisions across compaction"),
"Default prompt should preserve decision rationale across compaction"
);
assert!(
minimal_system_prompt().contains("touched files"),
"Minimal prompt should preserve touched files across compaction"
);
}
#[test]
fn test_default_prompt_stays_lean_but_complete() {
let prompt = default_system_prompt();
assert!(
prompt.contains("## Contract"),
"Default prompt should include the lean contract section"
);
assert!(
prompt.contains("Keep outputs concise and in the requested format"),
"Default prompt should clamp output shape"
);
assert!(
prompt.contains("Verify changes yourself"),
"Default prompt should require verification before finalizing"
);
assert!(
prompt.contains("Keep user updates brief and high-signal"),
"Default prompt should constrain progress updates"
);
}
#[test]
fn test_prompts_encode_explicit_delegation_contract() {
let prompt = default_system_prompt();
assert!(
prompt.contains("Keep control on the main thread"),
"Default prompt should keep control on the main thread"
);
assert!(
prompt.contains("Delegate only bounded, independent work"),
"Default prompt should restrict delegation to bounded independent work"
);
assert!(
minimal_system_prompt().contains("Keep delegation bounded and explicit"),
"Minimal prompt should preserve the delegation contract"
);
}
#[test]
fn test_prompts_prefer_handle_context_design_for_tangled_state() {
assert!(
default_system_prompt().contains(HANDLE_CONTEXT_PROMPT_LINE),
"Default prompt should prefer explicit handle/context designs for tangled state"
);
assert!(
minimal_system_prompt().contains(HANDLE_CONTEXT_PROMPT_LINE),
"Minimal prompt should keep the handle/context guidance"
);
}
#[test]
fn test_default_prompt_omits_accuracy_addendum() {
let runtime = tokio::runtime::Runtime::new().expect("runtime");
let config = VTCodeConfig::default();
let prompt = runtime.block_on(compose_system_instruction_text(
&PathBuf::from("."),
Some(&config),
None,
));
assert!(
!prompt.contains("## Accuracy Optimization"),
"Runtime prompt should omit the accuracy optimization section"
);
assert!(
prompt.contains("do not guess"),
"Prompt should still preserve the uncertainty guardrail"
);
}
#[tokio::test]
async fn test_generated_prompts_keep_mode_deltas_bounded() {
let project_root = PathBuf::from(".");
for (mode_name, mode) in [
("default", SystemPromptMode::Default),
("minimal", SystemPromptMode::Minimal),
("lightweight", SystemPromptMode::Lightweight),
("specialized", SystemPromptMode::Specialized),
] {
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = mode;
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 0;
let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
assert!(
result.contains("## Contract"),
"{mode_name} prompt should reuse the canonical base prompt"
);
assert!(
result.matches("## Mode").count() == 1,
"{mode_name} prompt should add only one mode delta"
);
}
}
#[test]
fn test_search_guidance_prefers_structural_and_rg() {
let guidelines = generate_tool_guidelines(
&["unified_search".to_string(), "unified_exec".to_string()],
None,
);
assert!(
guidelines.contains("Prefer search over shell"),
"Tool guidance should prefer search over shell exploration"
);
assert!(
guidelines.contains("git diff -- <path>"),
"Tool guidance should keep diff guidance explicit"
);
}
#[tokio::test]
async fn test_dynamic_guidelines_read_only() {
use crate::config::types::CapabilityLevel;
let mut config = VTCodeConfig::default();
config.agent.system_prompt_mode = SystemPromptMode::Default;
let mut ctx = PromptContext::default();
ctx.add_tool("unified_search".to_string());
ctx.capability_level = Some(CapabilityLevel::FileReading);
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
assert!(
result.contains("Mode: read-only"),
"Should detect read-only mode when no edit/write/exec tools available"
);
assert!(
result.contains("do not modify files"),
"Should explain read-only constraints"
);
}
#[tokio::test]
async fn test_dynamic_guidelines_tool_preferences() {
let config = VTCodeConfig::default();
let mut ctx = PromptContext::default();
ctx.add_tool("unified_exec".to_string());
ctx.add_tool("unified_search".to_string());
ctx.add_tool("unified_file".to_string());
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
assert!(
result.contains("unified_search") || result.contains("unified_file"),
"Should suggest canonical search/file tools"
);
}
#[tokio::test]
async fn test_live_prompt_renders_workspace_language_hints() {
let workspace = tempfile::TempDir::new().expect("workspace tempdir");
std::fs::create_dir_all(workspace.path().join("src")).expect("create src");
std::fs::create_dir_all(workspace.path().join("web")).expect("create web");
std::fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
std::fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
let config = VTCodeConfig::default();
let ctx = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
let result =
compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
assert!(result.contains("## Environment"));
assert!(result.contains("Rust, TypeScript"));
assert!(result.contains("structural-search `lang`"));
}
#[tokio::test]
async fn test_live_prompt_omits_workspace_language_hints_without_languages() {
let workspace = tempfile::TempDir::new().expect("workspace tempdir");
let config = VTCodeConfig::default();
let ctx = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
let result =
compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
assert!(!result.contains("Languages:"));
}
#[tokio::test]
async fn test_live_prompt_omits_project_docs_and_user_instructions_from_base_prompt() {
let workspace = tempfile::TempDir::new().expect("workspace tempdir");
std::fs::write(
workspace.path().join("AGENTS.md"),
"- Root summary\n\nFollow the root guidance.\n",
)
.expect("write agents");
let mut config = VTCodeConfig::default();
config.agent.user_instructions = Some("keep responses terse".to_string());
config.agent.include_temporal_context = false;
config.agent.include_working_directory = false;
config.agent.instruction_max_bytes = 4096;
let result = compose_system_instruction_text(workspace.path(), Some(&config), None).await;
assert!(!result.contains("## AGENTS.MD INSTRUCTION HIERARCHY"));
assert!(!result.contains("### Instruction map"));
assert!(!result.contains("### Key points"));
assert!(!result.contains("keep responses terse"));
assert!(!result.contains("Root summary"));
assert!(!result.contains("Follow the root guidance."));
}
#[tokio::test]
async fn test_workspace_prompt_resources_override_base_and_keep_dynamic_sections() {
use crate::skills::model::{SkillMetadata, SkillScope};
let workspace = tempfile::TempDir::new().expect("workspace tempdir");
let prompts_dir = workspace.path().join(".vtcode/prompts");
std::fs::create_dir_all(&prompts_dir).expect("create prompts dir");
std::fs::write(prompts_dir.join("system.md"), "# Workspace system base").expect("system");
std::fs::write(
prompts_dir.join("append-system.md"),
"Workspace prompt appendix",
)
.expect("append");
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = false;
config.agent.include_working_directory = true;
let mut ctx = PromptContext::default();
ctx.add_tool("unified_search".to_string());
ctx.add_skill_metadata(SkillMetadata {
name: "skill-creator".to_string(),
description: "Create skills".to_string(),
short_description: None,
path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
scope: SkillScope::System,
manifest: None,
});
ctx.set_current_directory(workspace.path().to_path_buf());
let result =
compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
assert!(result.starts_with("# Workspace system base"));
assert!(result.contains("Workspace prompt appendix"));
assert!(result.contains("## Active Tools"));
assert!(result.contains("## Skills"));
assert!(result.contains("## Environment"));
let appendix_pos = result
.find("Workspace prompt appendix")
.expect("append text");
let tools_pos = result.find("## Active Tools").expect("tools section");
let skills_pos = result.find("## Skills").expect("skills section");
let env_pos = result.find("## Environment").expect("environment section");
assert!(appendix_pos < tools_pos);
assert!(tools_pos < skills_pos);
assert!(skills_pos < env_pos);
}
#[tokio::test]
async fn test_temporal_context_inclusion() {
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = true;
config.prompt_cache.cache_friendly_prompt_shaping = false;
config.agent.temporal_context_use_utc = false;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
result.contains("Time:"),
"Should include temporal context when enabled"
);
let env_pos = result.find("## Environment");
let temporal_pos = result.find("Time:");
if let (Some(t), Some(e)) = (temporal_pos, env_pos) {
assert!(
t > e,
"Temporal context should appear inside the environment section"
);
}
}
#[tokio::test]
async fn test_temporal_context_utc_format() {
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = true;
config.prompt_cache.cache_friendly_prompt_shaping = false;
config.agent.temporal_context_use_utc = true;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
result.contains("UTC"),
"Should indicate UTC when temporal_context_use_utc is true"
);
assert!(
result.contains("T") && result.contains("Z"),
"Should use RFC3339 format for UTC (contains T and Z)"
);
}
#[tokio::test]
async fn test_temporal_context_disabled() {
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = false;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
!result.contains("Time:"),
"Should not include temporal context when disabled"
);
}
#[tokio::test]
async fn test_cache_friendly_temporal_context_stays_out_of_base_prompt() {
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = true;
config.prompt_cache.cache_friendly_prompt_shaping = true;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
!result.contains("Time:"),
"Stable system prompt should omit temporal context when cache-friendly shaping is enabled"
);
}
#[tokio::test]
async fn test_configuration_awareness_stays_behavior_focused() {
let mut config = VTCodeConfig::default();
config.security.human_in_the_loop = true;
config.chat.ask_questions.enabled = false;
config.mcp.enabled = true;
config.ide_context.enabled = true;
config.ide_context.inject_into_prompt = true;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(result.contains("## Environment"));
assert!(result.contains("Interaction: approval may gate sensitive actions"));
assert!(result.contains("request_user_input"));
assert!(result.contains("Sources: prefer MCP"));
assert!(!result.contains("PTY functionality"));
assert!(!result.contains("Loop guards"));
assert!(!result.contains(".vtcode/context/tool_outputs/"));
assert!(!result.contains("IDE context:"));
}
#[tokio::test]
async fn test_configuration_awareness_mentions_reduced_approval_when_disabled() {
let mut config = VTCodeConfig::default();
config.security.human_in_the_loop = false;
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(result.contains("Interaction: approval reduced by config"));
}
#[tokio::test]
async fn test_default_environment_omits_default_interaction_guidance() {
let config = VTCodeConfig::default();
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
assert!(
!result.contains("Interaction:"),
"Default-on interaction guidance should stay out of the prompt"
);
}
#[tokio::test]
async fn test_working_directory_inclusion() {
let mut config = VTCodeConfig::default();
config.agent.include_working_directory = true;
let mut ctx = PromptContext::default();
ctx.set_current_directory(PathBuf::from("/tmp/test"));
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
assert!(
result.contains("Working directory"),
"Should include working directory label"
);
assert!(
result.contains("/tmp/test"),
"Should show actual directory path"
);
let wd_pos = result.find("Working directory");
let env_pos = result.find("## Environment");
if let (Some(w), Some(e)) = (wd_pos, env_pos) {
assert!(
w > e,
"Working directory should appear inside the environment section"
);
}
}
#[tokio::test]
async fn test_working_directory_disabled() {
let mut config = VTCodeConfig::default();
config.agent.include_working_directory = false;
let mut ctx = PromptContext::default();
ctx.set_current_directory(PathBuf::from("/tmp/test"));
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
assert!(
!result.contains("Working directory"),
"Should not include working directory when disabled"
);
}
#[tokio::test]
async fn test_backward_compatibility() {
let config = VTCodeConfig::default();
let result = compose_system_instruction_text(
&PathBuf::from("."),
Some(&config),
None, )
.await;
assert!(result.len() > 600, "Should generate substantial prompt");
assert!(
result.contains("VT Code"),
"Should contain base prompt content"
);
assert!(
!result.contains("## Active Tools"),
"Should not have tool guidelines without prompt context"
);
}
#[tokio::test]
async fn test_all_enhancements_combined() {
use crate::skills::model::{SkillMetadata, SkillScope};
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = true;
config.agent.include_working_directory = true;
config.prompt_cache.cache_friendly_prompt_shaping = false;
let mut ctx = PromptContext::default();
ctx.add_tool("unified_file".to_string());
ctx.add_tool("unified_search".to_string());
ctx.infer_capability_level();
ctx.set_current_directory(PathBuf::from("/workspace"));
ctx.add_skill_metadata(SkillMetadata {
name: "rust-skills".to_string(),
description: "Rust coding guidance".to_string(),
short_description: None,
path: PathBuf::from("/tmp/rust-skills/SKILL.md"),
scope: SkillScope::System,
manifest: None,
});
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
assert!(
result.contains("## Active Tools"),
"Should have dynamic guidelines"
);
assert!(
result.contains("## Skills"),
"Should have lean skills routing"
);
assert!(
result.contains("## Environment"),
"Should have environment addenda"
);
assert!(result.contains("Time:"), "Should have temporal context");
assert!(
result.contains("Working directory"),
"Should have working directory"
);
assert!(result.contains("/workspace"), "Should show workspace path");
assert!(
result.contains("Read before edit"),
"Should have read-before-edit guideline"
);
}
#[tokio::test]
async fn test_prompt_layers_render_in_stable_order() {
use crate::skills::model::{SkillMetadata, SkillScope};
let mut config = VTCodeConfig::default();
config.agent.include_temporal_context = true;
config.agent.include_working_directory = true;
let mut ctx = PromptContext::default();
ctx.add_tool("unified_search".to_string());
ctx.add_tool("unified_exec".to_string());
ctx.add_skill_metadata(SkillMetadata {
name: "skill-creator".to_string(),
description: "Create skills".to_string(),
short_description: None,
path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
scope: SkillScope::System,
manifest: None,
});
ctx.add_language("Rust".to_string());
ctx.set_current_directory(PathBuf::from("/workspace"));
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
let mode_pos = result.find("## Mode").expect("mode section");
let tools_pos = result.find("## Active Tools").expect("tools section");
let skills_pos = result.find("## Skills").expect("skills section");
let env_pos = result.find("## Environment").expect("environment section");
assert!(mode_pos < tools_pos, "mode should precede tools");
assert!(tools_pos < skills_pos, "tools should precede skills");
assert!(skills_pos < env_pos, "skills should precede environment");
}
#[tokio::test]
async fn test_skills_section_stays_lean_and_routing_focused() {
use crate::skills::model::SkillScope;
use crate::skills::types::SkillManifest;
let config = VTCodeConfig::default();
let mut ctx = PromptContext::default();
ctx.available_skill_metadata
.push(crate::skills::model::SkillMetadata {
name: "skill-creator".to_string(),
description: "Create or update skills".to_string(),
short_description: None,
path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
scope: SkillScope::System,
manifest: Some(SkillManifest {
when_to_use: Some("Use when creating or updating a skill.".to_string()),
when_not_to_use: Some("Avoid for unrelated implementation work.".to_string()),
..SkillManifest::default()
}),
});
let result =
compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
assert!(result.contains("## Skills"));
assert!(result.contains("skill-creator: Create or update skills"));
assert!(result.contains("Use a skill only when the user names it"));
assert!(!result.contains("Discovery: Available skills are listed"));
assert!(!result.contains("/tmp/skill-creator/SKILL.md"));
assert!(!result.contains("use: Use when creating or updating a skill."));
assert!(!result.contains("avoid: Avoid for unrelated implementation work."));
}
#[test]
fn test_static_prompts_have_no_placeholders() {
let _minimal = generate_minimal_instruction();
let _lightweight = generate_lightweight_instruction();
let _specialized = generate_specialized_instruction();
let minimal_text = minimal_instruction_text();
let lightweight_text = lightweight_instruction_text();
let specialized_text = specialized_instruction_text();
assert!(
!minimal_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
"Minimal prompt has uninterpolated placeholder"
);
assert!(
!lightweight_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
"Lightweight prompt has uninterpolated placeholder"
);
assert!(
!specialized_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
"Specialized prompt has uninterpolated placeholder"
);
assert!(
!default_system_prompt().contains("__UNIFIED_TOOL_GUIDANCE__"),
"Default prompt has uninterpolated placeholder"
);
}
}