vtcode_core/prompts/
harness_limits.rs1use crate::core::loop_detector::MAX_TOTAL_READONLY_CALLS;
2use crate::prompts::sections::{SectionBoundaryMode, find_prompt_section_bounds};
3use std::fmt::Write as _;
4
5const MAX_SAME_FILE_PATH_READS: usize = 6;
9
10pub fn upsert_harness_limits_section(
15 prompt: &mut String,
16 max_tool_calls_per_turn: usize,
17 max_tool_wall_clock_secs: u64,
18 max_tool_retries: u32,
19) {
20 let max_tool_calls_label = if max_tool_calls_per_turn == 0 {
21 "unlimited".to_string()
22 } else {
23 max_tool_calls_per_turn.to_string()
24 };
25 let max_tool_wall_clock_label = if max_tool_wall_clock_secs == 0 {
26 "unlimited".to_string()
27 } else {
28 format!("{max_tool_wall_clock_secs} seconds")
29 };
30
31 while let Some((section_start, section_end)) =
32 find_prompt_section_bounds(prompt, "[Harness Limits]", SectionBoundaryMode::BracketOnly)
33 {
34 prompt.replace_range(section_start..section_end, "");
35 }
36
37 while prompt.ends_with('\n') {
38 prompt.pop();
39 }
40
41 let section = format!(
42 "[Harness Limits]\n\
43 - max_tool_calls_per_turn: {max_tool_calls_label}\n\
44 - 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\
45 - max_tool_retries: {max_tool_retries}\n\
46 - max_readonly_tool_calls: {MAX_TOTAL_READONLY_CALLS} (global budget across all read-only tools; produce output before exhausting)\n\
47 - 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\
48 - exec wait/inspect are exempt from max_tool_calls_per_turn\n\
49 - 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; wait/inspect stay budget-exempt"
50 );
51
52 if prompt.is_empty() {
53 let _ = writeln!(prompt, "{section}");
54 } else {
55 let _ = writeln!(prompt, "\n{section}");
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::upsert_harness_limits_section;
62
63 #[test]
64 fn upsert_harness_limits_adds_single_section() {
65 let mut prompt = "Base prompt".to_string();
66
67 upsert_harness_limits_section(&mut prompt, 12, 180, 2);
68
69 assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
70 assert!(prompt.contains("- max_tool_calls_per_turn: 12"));
71 assert!(prompt.contains("- max_tool_wall_clock_secs: 180"));
72 assert!(prompt.contains("- max_tool_retries: 2"));
73 assert!(prompt.contains("- max_readonly_tool_calls: 30"));
74 assert!(prompt.contains("- max_same_file_path_reads: 6"));
75 assert!(prompt.contains("exec wait/inspect are exempt from max_tool_calls_per_turn"));
76 assert!(prompt.contains("single-call long run"));
77 }
78
79 #[test]
80 fn upsert_harness_limits_replaces_existing_values() {
81 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();
82
83 upsert_harness_limits_section(&mut prompt, 9, 240, 4);
84
85 assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
86 assert!(prompt.contains("- max_tool_calls_per_turn: 9"));
87 assert!(prompt.contains("- max_tool_wall_clock_secs: 240"));
88 assert!(prompt.contains("- max_tool_retries: 4"));
89 assert!(!prompt.contains("- max_tool_calls_per_turn: 3"));
90 }
91
92 #[test]
93 fn upsert_harness_limits_preserves_trailing_prompt_sections() {
94 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();
95
96 upsert_harness_limits_section(&mut prompt, 11, 90, 3);
97
98 assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
99 assert!(prompt.contains("[Additional Context]\nKeep this section"));
100 assert!(prompt.contains("- max_tool_retries: 3"));
101 assert!(prompt.contains("- max_readonly_tool_calls: 30"));
102 }
103
104 #[test]
105 fn upsert_harness_limits_replaces_indented_section_header() {
106 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();
107
108 upsert_harness_limits_section(&mut prompt, 5, 30, 2);
109
110 assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
111 assert!(prompt.contains("- max_tool_calls_per_turn: 5"));
112 assert!(!prompt.contains("- max_tool_calls_per_turn: 1"));
113 }
114
115 #[test]
116 fn upsert_harness_limits_removes_duplicate_sections() {
117 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();
118
119 upsert_harness_limits_section(&mut prompt, 7, 70, 3);
120
121 assert_eq!(prompt.matches("[Harness Limits]").count(), 1);
122 assert!(prompt.contains("- max_tool_calls_per_turn: 7"));
123 assert!(prompt.contains("[Other]\nkeep"));
124 }
125
126 #[test]
127 fn upsert_harness_limits_renders_unlimited_when_tool_cap_disabled() {
128 let mut prompt = "Base prompt".to_string();
129
130 upsert_harness_limits_section(&mut prompt, 0, 600, 2);
131
132 assert!(prompt.contains("- max_tool_calls_per_turn: unlimited"));
133 assert!(prompt.contains("- max_tool_wall_clock_secs: 600 seconds"));
134 assert!(prompt.contains("- max_tool_retries: 2"));
135 assert!(prompt.contains("- max_readonly_tool_calls: 30"));
136 assert!(prompt.contains("- max_same_file_path_reads: 6"));
137 }
138
139 #[test]
140 fn upsert_harness_limits_renders_unlimited_wall_clock() {
141 let mut prompt = "Base prompt".to_string();
142
143 upsert_harness_limits_section(&mut prompt, 4, 0, 2);
144
145 assert!(prompt.contains("- max_tool_wall_clock_secs: unlimited"));
146 assert!(prompt.contains("write_stdin next_wait_args"));
147 }
148}