vtcode_core/prompts/
runtime_contract.rs1use std::fmt::Write as _;
2
3use super::system::{
4 PLANNING_WORKFLOW_EXIT_INSTRUCTION_LINE, PLANNING_WORKFLOW_INTERVIEW_POLICY_LINE,
5 PLANNING_WORKFLOW_NO_AUTO_EXIT_LINE, PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE,
6 PLANNING_WORKFLOW_PLAN_PERSISTENCE_POLICY_LINE, PLANNING_WORKFLOW_PLAN_POLICY_LINE,
7 PLANNING_WORKFLOW_PLAN_QUALITY_LINE, PLANNING_WORKFLOW_READ_ONLY_HEADER, PLANNING_WORKFLOW_READ_ONLY_NOTICE_LINE,
8 PLANNING_WORKFLOW_RESEARCH_SCOPE_LINE, PLANNING_WORKFLOW_TASK_TRACKER_LINE,
9};
10use crate::config::constants::tool_limits;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct RuntimePromptContract {
14 pub full_auto: bool,
15 pub planning_active: bool,
16 pub request_user_input_enabled: bool,
17}
18
19pub fn append_runtime_mode_sections(prompt: &mut String, contract: RuntimePromptContract) {
20 if contract.full_auto {
21 append_full_auto_notice(prompt, contract);
22 }
23
24 if contract.planning_active {
25 append_planning_workflow_notice(prompt, contract.request_user_input_enabled);
26 }
27}
28
29fn append_full_auto_notice(prompt: &mut String, contract: RuntimePromptContract) {
30 let header = if contract.planning_active {
31 "# FULL-AUTO (PLANNING WORKFLOW): Work autonomously within planning workflow constraints."
32 } else {
33 "# FULL-AUTO: Complete task autonomously until done or blocked."
34 };
35
36 if prompt.contains(header) {
37 return;
38 }
39
40 let _ = writeln!(prompt, "\n{header}");
41 let _ = writeln!(prompt, "- Stay within the exposed tool list and adapt when a tool is unavailable or denied.");
42 if !contract.request_user_input_enabled {
45 let _ = writeln!(
46 prompt,
47 "- `request_user_input` is unavailable in this runtime; make reasonable assumptions and continue with the available context."
48 );
49 }
50}
51
52fn append_planning_workflow_notice(prompt: &mut String, request_user_input_enabled: bool) {
53 if prompt.contains(PLANNING_WORKFLOW_READ_ONLY_HEADER) {
54 if !request_user_input_enabled && !prompt.contains(PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE) {
55 let _ = writeln!(prompt, "{PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE}");
56 }
57 return;
58 }
59
60 prompt.push('\n');
61 prompt.push_str(PLANNING_WORKFLOW_READ_ONLY_HEADER);
62 prompt.push('\n');
63 prompt.push_str(PLANNING_WORKFLOW_READ_ONLY_NOTICE_LINE);
64 prompt.push('\n');
65 prompt.push_str(PLANNING_WORKFLOW_EXIT_INSTRUCTION_LINE);
66 prompt.push('\n');
67 prompt.push_str(PLANNING_WORKFLOW_PLAN_PERSISTENCE_POLICY_LINE);
68 prompt.push('\n');
69 prompt.push_str(PLANNING_WORKFLOW_PLAN_POLICY_LINE);
70 prompt.push('\n');
71 prompt.push_str(PLANNING_WORKFLOW_PLAN_QUALITY_LINE);
72 prompt.push('\n');
73 prompt.push_str(PLANNING_WORKFLOW_RESEARCH_SCOPE_LINE);
74 prompt.push('\n');
75 let _ = writeln!(
76 prompt,
77 "- Planning uses a nonzero per-turn tool-call research floor of {} that is separate from max_tool_loops and max_conversation_turns.",
78 tool_limits::PLANNING_WORKFLOW_MIN_TOOL_CALLS_PER_TURN,
79 );
80 prompt.push_str(PLANNING_WORKFLOW_INTERVIEW_POLICY_LINE);
81 prompt.push('\n');
82 if !request_user_input_enabled {
83 prompt.push_str(PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE);
84 prompt.push('\n');
85 }
86 prompt.push_str(PLANNING_WORKFLOW_NO_AUTO_EXIT_LINE);
87 prompt.push('\n');
88 prompt.push_str(PLANNING_WORKFLOW_TASK_TRACKER_LINE);
89 prompt.push('\n');
90}
91
92#[cfg(test)]
93mod tests {
94 use super::{RuntimePromptContract, append_runtime_mode_sections};
95 use crate::prompts::system::{
96 PLANNING_WORKFLOW_INTERVIEW_POLICY_LINE, PLANNING_WORKFLOW_READ_ONLY_HEADER,
97 PLANNING_WORKFLOW_RESEARCH_SCOPE_LINE,
98 };
99
100 #[test]
101 fn planning_workflow_uses_plan_policy_unconditionally() {
102 for request_user_input_enabled in [true, false] {
103 let mut prompt = "Base prompt".to_string();
104
105 append_runtime_mode_sections(
106 &mut prompt,
107 RuntimePromptContract {
108 planning_active: true,
109 request_user_input_enabled,
110 ..RuntimePromptContract::default()
111 },
112 );
113
114 assert!(prompt.contains(PLANNING_WORKFLOW_READ_ONLY_HEADER));
115 assert!(prompt.contains(PLANNING_WORKFLOW_INTERVIEW_POLICY_LINE));
116 assert!(prompt.contains("Emit exactly one final `<proposed_plan>` block"));
117 assert!(
118 prompt.contains("Do not use shell commands or file-writing tools to create or modify `.vtcode/plans/`")
119 );
120 assert!(prompt.contains("runtime owns plan/tracker persistence and validation"));
121 assert!(prompt.contains("approval controls only after successful persistence"));
122 }
123 }
124
125 #[test]
126 fn full_auto_notice_mentions_missing_request_user_input_when_disabled() {
127 let mut prompt = "Base prompt".to_string();
128
129 append_runtime_mode_sections(
130 &mut prompt,
131 RuntimePromptContract {
132 full_auto: true,
133 request_user_input_enabled: false,
134 ..RuntimePromptContract::default()
135 },
136 );
137
138 assert!(prompt.contains("# FULL-AUTO: Complete task autonomously until done or blocked."));
139 assert!(prompt.contains("`request_user_input` is unavailable in this runtime"));
140 }
141
142 #[test]
149 fn planning_workflow_always_includes_research_scope_guidance() {
150 for request_user_input_enabled in [true, false] {
151 let mut prompt = "Base prompt".to_string();
152 append_runtime_mode_sections(
153 &mut prompt,
154 RuntimePromptContract {
155 planning_active: true,
156 request_user_input_enabled,
157 ..RuntimePromptContract::default()
158 },
159 );
160 assert!(prompt.contains(PLANNING_WORKFLOW_RESEARCH_SCOPE_LINE));
161 assert!(prompt.contains("Planning uses a nonzero per-turn tool-call research floor of 120"));
162 }
163 }
164
165 #[test]
166 fn planning_workflow_warns_about_dynamic_find_expansion() {
167 let mut prompt = "Base prompt".to_string();
168
169 append_runtime_mode_sections(
170 &mut prompt,
171 RuntimePromptContract {
172 planning_active: true,
173 ..RuntimePromptContract::default()
174 },
175 );
176
177 assert!(prompt.contains("for `find`, use literal paths and quoted patterns only"));
178 assert!(prompt.contains("prefer `rg --files`"));
179 assert!(prompt.contains(
180 "keep them free of `$()`, backticks, variable or brace expansion, and dynamically spliced options"
181 ));
182 }
183}