Skip to main content

vtcode_core/prompts/
runtime_contract.rs

1use 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    // Checkpoint guidance is already in the operating profiles; omit here to
43    // avoid token waste from duplication.
44    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    /// Regression guard for checkpoint turn_647: a simple planning request
143    /// burned 70+ tool calls until the wall-clock budget was exhausted with
144    /// no plan delivered. The research-scope line must always be present
145    /// while planning is active, regardless of `request_user_input`
146    /// availability, so the model has a concrete signal to stop researching
147    /// and draft.
148    #[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}