vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use std::path::Path;
use vtcode_core::config::loader::ConfigManager;
use vtcode_core::config::types::SystemPromptMode;
use vtcode_core::prompts::PromptContext;
use vtcode_core::prompts::system::{
    compose_system_instruction_text, default_lightweight_prompt, default_system_prompt,
    minimal_system_prompt, specialized_system_prompt,
};

fn fallback_base_system_prompt(vt_cfg: Option<&vtcode_core::config::VTCodeConfig>) -> &'static str {
    match vt_cfg.map(|cfg| cfg.agent.system_prompt_mode) {
        Some(SystemPromptMode::Minimal) => minimal_system_prompt(),
        Some(SystemPromptMode::Lightweight) => default_lightweight_prompt(),
        Some(SystemPromptMode::Specialized) => specialized_system_prompt(),
        _ => default_system_prompt(),
    }
}

pub(crate) async fn read_system_prompt(
    workspace: &Path,
    session_addendum: Option<&str>,
    available_tools: &[String],
    available_subagents: &[(String, String, bool)],
) -> String {
    let mut prompt_context =
        PromptContext::from_workspace_tools(workspace, available_tools.iter().cloned());
    prompt_context.load_available_skills();

    // Load configuration
    let vt_cfg = ConfigManager::load_from_workspace(workspace)
        .ok()
        .map(|manager| manager.config().clone());

    // Use the new compose_system_instruction_text with enhancements
    let mut prompt =
        compose_system_instruction_text(workspace, vt_cfg.as_ref(), Some(&prompt_context)).await;

    // Fallback prompt if composition fails (should rarely happen)
    // Use centralized vtcode-core prompt variants to preserve safety/loop/plan guidance.
    if prompt.is_empty() {
        prompt = fallback_base_system_prompt(vt_cfg.as_ref()).to_string();
    }

    if let Some(addendum) = session_addendum {
        let trimmed = addendum.trim();
        if !trimmed.is_empty() {
            prompt.push_str("\n\n");
            prompt.push_str(trimmed);
        }
    }

    if !available_subagents.is_empty() {
        let mut section = String::from("## Subagents\n");
        section.push_str(
            "Delegated child agents available in this session. Treat the main thread as the controller: keep the next blocking step local, and delegate only bounded independent work. Read-only agents may be used proactively when their description matches; write-capable agents require explicit delegation.\n",
        );
        section.push_str(
            "Users can explicitly target one with natural language or an `@agent-<name>` mention.\n",
        );
        section.push_str(
            "If the user explicitly selects a subagent for the task, delegate with `spawn_agent` to that subagent instead of handling the task on the main thread. Join child results back into the parent flow before you depend on them.\n",
        );
        for (name, description, read_only) in available_subagents {
            let suffix = if *read_only {
                " Read-only."
            } else {
                " Explicit delegation only."
            };
            section.push_str(&format!("- {name}: {description}{suffix}\n"));
        }
        prompt.push_str("\n\n");
        prompt.push_str(section.trim_end());
    }

    prompt
}

#[cfg(test)]
mod tests {
    use super::*;
    use vtcode_core::config::VTCodeConfig;
    use vtcode_core::config::types::SystemPromptMode;

    #[test]
    fn test_fallback_base_system_prompt_defaults_to_default() {
        assert_eq!(fallback_base_system_prompt(None), default_system_prompt());
    }

    #[test]
    fn test_fallback_base_system_prompt_uses_minimal_mode() {
        let mut config = VTCodeConfig::default();
        config.agent.system_prompt_mode = SystemPromptMode::Minimal;

        assert_eq!(
            fallback_base_system_prompt(Some(&config)),
            minimal_system_prompt()
        );
    }

    #[test]
    fn test_fallback_base_system_prompt_uses_lightweight_mode() {
        let mut config = VTCodeConfig::default();
        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;

        assert_eq!(
            fallback_base_system_prompt(Some(&config)),
            default_lightweight_prompt()
        );
    }

    #[test]
    fn test_fallback_base_system_prompt_uses_specialized_mode() {
        let mut config = VTCodeConfig::default();
        config.agent.system_prompt_mode = SystemPromptMode::Specialized;

        assert_eq!(
            fallback_base_system_prompt(Some(&config)),
            specialized_system_prompt()
        );
    }

    #[tokio::test]
    async fn test_read_system_prompt_includes_explicit_subagent_execution_model() {
        let workspace = tempfile::TempDir::new().expect("workspace");

        let prompt = read_system_prompt(
            workspace.path(),
            None,
            &[],
            &[(
                "explorer".to_string(),
                "Read-only repo explorer".to_string(),
                true,
            )],
        )
        .await;

        assert!(prompt.contains("main thread as the controller"));
        assert!(prompt.contains("bounded independent work"));
        assert!(prompt.contains("Join child results back into the parent flow"));
    }
}