vtcode-core 0.136.2

Core library for VT Code - a Rust-based terminal coding agent
use crate::core::loop_detector::MAX_TOTAL_READONLY_CALLS;
use crate::prompts::sections::{SectionBoundaryMode, find_prompt_section_bounds};
use std::fmt::Write as _;

/// Per-file-path read cap advertised in the system prompt. Must match
/// `guards::MAX_SAME_FILE_PATH_READ_CALLS`. Reading the same file more than
/// this many times in a turn triggers recovery.
const MAX_SAME_FILE_PATH_READS: usize = 6;

/// Keep prompt guidance aligned with the selected user-visible harness limits.
///
/// This operation is idempotent: existing `[Harness Limits]` sections are removed before
/// inserting the current values.
pub fn upsert_harness_limits_section(
    prompt: &mut String,
    max_tool_calls_per_turn: usize,
    max_tool_wall_clock_secs: u64,
    max_tool_retries: u32,
) {
    let max_tool_calls_label = if max_tool_calls_per_turn == 0 {
        "unlimited".to_string()
    } else {
        max_tool_calls_per_turn.to_string()
    };

    while let Some((section_start, section_end)) =
        find_prompt_section_bounds(prompt, "[Harness Limits]", SectionBoundaryMode::BracketOnly)
    {
        prompt.replace_range(section_start..section_end, "");
    }

    while prompt.ends_with('\n') {
        prompt.pop();
    }

    let section = format!(
        "[Harness Limits]\n\
         - max_tool_calls_per_turn: {max_tool_calls_label}\n\
         - max_tool_wall_clock_secs: {max_tool_wall_clock_secs}\n\
         - max_tool_retries: {max_tool_retries}\n\
         - max_readonly_tool_calls: {MAX_TOTAL_READONLY_CALLS} (global budget across all read-only tools; produce output before exhausting)\n\
         - max_same_file_path_reads: {MAX_SAME_FILE_PATH_READS} (per file path per turn; read a file once in full rather than paginating)"
    );

    if prompt.is_empty() {
        let _ = writeln!(prompt, "{section}");
    } else {
        let _ = writeln!(prompt, "\n{section}");
    }
}

#[cfg(test)]
mod tests {
    use super::upsert_harness_limits_section;

    #[test]
    fn upsert_harness_limits_adds_single_section() {
        let mut prompt = "Base prompt".to_string();

        upsert_harness_limits_section(&mut prompt, 12, 180, 2);

        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
        assert!(prompt.contains("- max_tool_calls_per_turn: 12"));
        assert!(prompt.contains("- max_tool_wall_clock_secs: 180"));
        assert!(prompt.contains("- max_tool_retries: 2"));
        assert!(prompt.contains("- max_readonly_tool_calls: 30"));
        assert!(prompt.contains("- max_same_file_path_reads: 6"));
    }

    #[test]
    fn upsert_harness_limits_replaces_existing_values() {
        let mut prompt = "Base prompt\n[Harness Limits]\n- max_tool_calls_per_turn: 3\n- max_tool_wall_clock_secs: 60\n- max_tool_retries: 1\n".to_string();

        upsert_harness_limits_section(&mut prompt, 9, 240, 4);

        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
        assert!(prompt.contains("- max_tool_calls_per_turn: 9"));
        assert!(prompt.contains("- max_tool_wall_clock_secs: 240"));
        assert!(prompt.contains("- max_tool_retries: 4"));
        assert!(!prompt.contains("- max_tool_calls_per_turn: 3"));
    }

    #[test]
    fn upsert_harness_limits_preserves_trailing_prompt_sections() {
        let mut prompt = "Base prompt\n[Harness Limits]\n- max_tool_calls_per_turn: 3\n- max_tool_wall_clock_secs: 60\n- max_tool_retries: 1\n[Additional Context]\nKeep this section".to_string();

        upsert_harness_limits_section(&mut prompt, 11, 90, 3);

        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
        assert!(prompt.contains("[Additional Context]\nKeep this section"));
        assert!(prompt.contains("- max_tool_retries: 3"));
        assert!(prompt.contains("- max_readonly_tool_calls: 30"));
    }

    #[test]
    fn upsert_harness_limits_replaces_indented_section_header() {
        let mut prompt = "Base prompt\n  [Harness Limits]\n- max_tool_calls_per_turn: 1\n- max_tool_wall_clock_secs: 1\n- max_tool_retries: 1\n".to_string();

        upsert_harness_limits_section(&mut prompt, 5, 30, 2);

        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
        assert!(prompt.contains("- max_tool_calls_per_turn: 5"));
        assert!(!prompt.contains("- max_tool_calls_per_turn: 1"));
    }

    #[test]
    fn upsert_harness_limits_removes_duplicate_sections() {
        let mut prompt = "Base prompt\n[Harness Limits]\n- max_tool_calls_per_turn: 2\n- max_tool_wall_clock_secs: 10\n- max_tool_retries: 1\n[Other]\nkeep\n[Harness Limits]\n- max_tool_calls_per_turn: 3\n- max_tool_wall_clock_secs: 20\n- max_tool_retries: 2\n".to_string();

        upsert_harness_limits_section(&mut prompt, 7, 70, 3);

        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
        assert!(prompt.contains("- max_tool_calls_per_turn: 7"));
        assert!(prompt.contains("[Other]\nkeep"));
    }

    #[test]
    fn upsert_harness_limits_renders_unlimited_when_tool_cap_disabled() {
        let mut prompt = "Base prompt".to_string();

        upsert_harness_limits_section(&mut prompt, 0, 600, 2);

        assert!(prompt.contains("- max_tool_calls_per_turn: unlimited"));
        assert!(prompt.contains("- max_tool_wall_clock_secs: 600"));
        assert!(prompt.contains("- max_tool_retries: 2"));
        assert!(prompt.contains("- max_readonly_tool_calls: 30"));
        assert!(prompt.contains("- max_same_file_path_reads: 6"));
    }
}