Skip to main content

vtcode_core/prompts/
harness_limits.rs

1use crate::core::loop_detector::MAX_TOTAL_READONLY_CALLS;
2use crate::prompts::sections::{SectionBoundaryMode, remove_prompt_section, trim_trailing_newlines};
3use std::fmt::Write as _;
4
5/// Per-file-path read cap advertised in the system prompt. Must match
6/// `guards::MAX_SAME_FILE_PATH_READ_CALLS` (pinned by
7/// `read_guard.rs::advertised_per_path_read_cap_matches_enforcement`).
8/// Reading the same file more than this many times in a turn triggers recovery.
9const MAX_SAME_FILE_PATH_READS: usize = 6;
10
11/// Keep prompt guidance aligned with the selected user-visible harness limits.
12///
13/// This operation is idempotent: existing `[Harness Limits]` sections are removed before
14/// inserting the current values.
15pub fn upsert_harness_limits_section(
16    prompt: &mut String,
17    max_tool_calls_per_turn: usize,
18    max_tool_wall_clock_secs: u64,
19    max_tool_retries: u32,
20) {
21    let max_tool_calls_label = if max_tool_calls_per_turn == 0 {
22        "unlimited".to_string()
23    } else {
24        max_tool_calls_per_turn.to_string()
25    };
26    let max_tool_wall_clock_label = if max_tool_wall_clock_secs == 0 {
27        "unlimited".to_string()
28    } else {
29        format!("{max_tool_wall_clock_secs} seconds")
30    };
31
32    remove_prompt_section(prompt, "[Harness Limits]", SectionBoundaryMode::BracketOnly);
33    trim_trailing_newlines(prompt);
34
35    let section = format!(
36        "[Harness Limits]\n\
37         - max_tool_calls_per_turn: {max_tool_calls_label}\n\
38         - max_tool_wall_clock_secs: {max_tool_wall_clock_label} (per turn; run long builds with exec_command, then call write_stdin next_wait_args with action:\"wait\" verbatim)\n\
39         - max_tool_retries: {max_tool_retries}\n\
40         - max_readonly_tool_calls: {MAX_TOTAL_READONLY_CALLS} (global budget across all read-only tools; produce output before exhausting)\n\
41         - max_same_file_path_reads: {MAX_SAME_FILE_PATH_READS} (per file path per turn; read a file once in full rather than paginating)\n\
42         - exec wait/inspect are exempt from max_tool_calls_per_turn\n\
43         - long commands: set yield_time_ms above 10000 on the initial run for a single-call long run (no outer timeout), or reuse next_wait_args in write_stdin to block until exit or deadline"
44    );
45
46    if prompt.is_empty() {
47        let _ = writeln!(prompt, "{section}");
48    } else {
49        let _ = writeln!(prompt, "\n{section}");
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::upsert_harness_limits_section;
56
57    #[test]
58    fn upsert_harness_limits_adds_single_section() {
59        let mut prompt = "Base prompt".to_string();
60
61        upsert_harness_limits_section(&mut prompt, 12, 180, 2);
62
63        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
64        assert!(prompt.contains("- max_tool_calls_per_turn: 12"));
65        assert!(prompt.contains("- max_tool_wall_clock_secs: 180"));
66        assert!(prompt.contains("- max_tool_retries: 2"));
67        assert!(prompt.contains("- max_readonly_tool_calls: 30"));
68        assert!(prompt.contains("- max_same_file_path_reads: 6"));
69        assert!(prompt.contains("exec wait/inspect are exempt from max_tool_calls_per_turn"));
70        assert!(prompt.contains("single-call long run"));
71    }
72
73    #[test]
74    fn upsert_harness_limits_states_wait_inspect_exemption_once() {
75        // The wait/inspect budget exemption is one directive: the long-commands
76        // line must not restate it. Asymmetric pin: deleting the canonical
77        // line drops the count to 0, re-adding the restatement raises it to 2.
78        let mut prompt = "Base prompt".to_string();
79
80        upsert_harness_limits_section(&mut prompt, 12, 180, 2);
81
82        assert_eq!(prompt.matches("exempt").count(), 1);
83        assert!(prompt.contains("exec wait/inspect are exempt from max_tool_calls_per_turn"));
84        assert!(prompt.contains("single-call long run"));
85        assert!(prompt.contains("write_stdin next_wait_args"));
86    }
87
88    #[test]
89    fn upsert_harness_limits_replaces_existing_values() {
90        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();
91
92        upsert_harness_limits_section(&mut prompt, 9, 240, 4);
93
94        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
95        assert!(prompt.contains("- max_tool_calls_per_turn: 9"));
96        assert!(prompt.contains("- max_tool_wall_clock_secs: 240"));
97        assert!(prompt.contains("- max_tool_retries: 4"));
98        assert!(!prompt.contains("- max_tool_calls_per_turn: 3"));
99    }
100
101    #[test]
102    fn upsert_harness_limits_preserves_trailing_prompt_sections() {
103        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();
104
105        upsert_harness_limits_section(&mut prompt, 11, 90, 3);
106
107        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
108        assert!(prompt.contains("[Additional Context]\nKeep this section"));
109        assert!(prompt.contains("- max_tool_retries: 3"));
110        assert!(prompt.contains("- max_readonly_tool_calls: 30"));
111    }
112
113    #[test]
114    fn upsert_harness_limits_replaces_indented_section_header() {
115        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();
116
117        upsert_harness_limits_section(&mut prompt, 5, 30, 2);
118
119        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
120        assert!(prompt.contains("- max_tool_calls_per_turn: 5"));
121        assert!(!prompt.contains("- max_tool_calls_per_turn: 1"));
122    }
123
124    #[test]
125    fn upsert_harness_limits_removes_duplicate_sections() {
126        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();
127
128        upsert_harness_limits_section(&mut prompt, 7, 70, 3);
129
130        assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
131        assert!(prompt.contains("- max_tool_calls_per_turn: 7"));
132        assert!(prompt.contains("[Other]\nkeep"));
133    }
134
135    #[test]
136    fn upsert_harness_limits_renders_unlimited_when_tool_cap_disabled() {
137        let mut prompt = "Base prompt".to_string();
138
139        upsert_harness_limits_section(&mut prompt, 0, 600, 2);
140
141        assert!(prompt.contains("- max_tool_calls_per_turn: unlimited"));
142        assert!(prompt.contains("- max_tool_wall_clock_secs: 600 seconds"));
143        assert!(prompt.contains("- max_tool_retries: 2"));
144        assert!(prompt.contains("- max_readonly_tool_calls: 30"));
145        assert!(prompt.contains("- max_same_file_path_reads: 6"));
146    }
147
148    #[test]
149    fn upsert_harness_limits_renders_unlimited_wall_clock() {
150        let mut prompt = "Base prompt".to_string();
151
152        upsert_harness_limits_section(&mut prompt, 4, 0, 2);
153
154        assert!(prompt.contains("- max_tool_wall_clock_secs: unlimited"));
155        assert!(prompt.contains("write_stdin next_wait_args"));
156    }
157}