1use crate::config::VTCodeConfig;
2use crate::config::constants::tools;
3use crate::tools::tool_intent::{ToolMutationModel, builtin_tool_behavior};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FeatureStage {
8 Stable,
10 Beta,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct FeatureGate {
17 pub enabled: bool,
19 pub stage: FeatureStage,
21}
22
23impl FeatureGate {
24 pub const fn new(enabled: bool, stage: FeatureStage) -> Self {
26 Self { enabled, stage }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct OpenResponsesFeature {
33 pub enabled: bool,
35 pub emit_events: bool,
37 pub map_tool_calls: bool,
39 pub include_reasoning: bool,
41 pub stage: FeatureStage,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct FeatureSet {
48 pub request_user_input: FeatureGate,
50 pub auto_compaction: FeatureGate,
52 pub open_responses: OpenResponsesFeature,
54}
55
56impl FeatureSet {
57 pub fn from_config(config: Option<&VTCodeConfig>) -> Self {
60 let default_config;
61 let cfg = if let Some(cfg) = config {
62 cfg
63 } else {
64 default_config = VTCodeConfig::default();
65 &default_config
66 };
67
68 Self {
69 request_user_input: FeatureGate::new(cfg.chat.ask_questions.enabled, FeatureStage::Stable),
70 auto_compaction: FeatureGate::new(cfg.agent.harness.auto_compaction_enabled, FeatureStage::Beta),
71 open_responses: OpenResponsesFeature {
72 enabled: cfg.agent.open_responses.enabled,
73 emit_events: cfg.agent.open_responses.enabled && cfg.agent.open_responses.emit_events,
74 map_tool_calls: cfg.agent.open_responses.enabled && cfg.agent.open_responses.map_tool_calls,
75 include_reasoning: cfg.agent.open_responses.enabled && cfg.agent.open_responses.include_reasoning,
76 stage: FeatureStage::Beta,
77 },
78 }
79 }
80
81 pub fn request_user_input_enabled(&self, _planning_active: bool, interactive_session: bool) -> bool {
83 interactive_session && self.request_user_input.enabled
84 }
85
86 pub fn auto_compaction_enabled(&self, supports_server_compaction: bool) -> bool {
88 self.auto_compaction.enabled && supports_server_compaction
89 }
90
91 pub fn tool_enabled_for_mode(tool_name: &str, planning_active: bool, request_user_input_enabled: bool) -> bool {
93 match tool_name {
94 tools::REQUEST_USER_INPUT => request_user_input_enabled,
95 tools::WRITE_STDIN | tools::APPLY_PATCH if planning_active => false,
96 _ if !planning_active => true,
97 _ => {
98 builtin_tool_behavior(tool_name)
99 .map(|behavior| !matches!(behavior.mutation_model, ToolMutationModel::Mutating))
100 .unwrap_or(false) }
102 }
103 }
104
105 pub fn allows_tool_name(&self, tool_name: &str, planning_active: bool, interactive_session: bool) -> bool {
107 Self::tool_enabled_for_mode(
108 tool_name,
109 planning_active,
110 self.request_user_input_enabled(planning_active, interactive_session),
111 )
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::{FeatureSet, FeatureStage};
118 use crate::config::VTCodeConfig;
119 use crate::config::constants::tools;
120
121 #[test]
122 fn request_user_input_requires_interactive_session() {
123 let cfg = VTCodeConfig::default();
124 let features = FeatureSet::from_config(Some(&cfg));
125
126 assert!(features.request_user_input_enabled(false, true));
127 assert!(features.request_user_input_enabled(true, true));
128 assert!(!features.request_user_input_enabled(true, false));
129 assert!(features.allows_tool_name(tools::REQUEST_USER_INPUT, false, true));
130 assert!(features.allows_tool_name(tools::REQUEST_USER_INPUT, true, true));
131 assert!(!features.allows_tool_name(tools::REQUEST_USER_INPUT, true, false));
132 assert!(features.allows_tool_name(tools::TASK_TRACKER, true, false));
133 assert!(features.allows_tool_name(tools::TASK_TRACKER, false, true));
134 }
135
136 #[test]
137 fn request_user_input_honors_chat_setting_outside_planning_workflow() {
138 let mut cfg = VTCodeConfig::default();
139 cfg.chat.ask_questions.enabled = false;
140
141 let features = FeatureSet::from_config(Some(&cfg));
142
143 assert!(!features.request_user_input_enabled(false, true));
144 assert!(!features.request_user_input_enabled(true, true));
145 }
146
147 #[test]
148 fn planning_workflow_hides_mutating_only_tools_but_keeps_conditional_tools() {
149 let cfg = VTCodeConfig::default();
150 let features = FeatureSet::from_config(Some(&cfg));
151
152 assert!(!features.allows_tool_name(tools::WRITE_STDIN, true, true));
153 assert!(!features.allows_tool_name(tools::APPLY_PATCH, true, true));
154 assert!(features.allows_tool_name(tools::EXEC_COMMAND, true, true));
155 assert!(features.allows_tool_name(tools::WRITE_STDIN, false, true));
156 assert!(features.allows_tool_name(tools::APPLY_PATCH, false, true));
157 assert!(!features.allows_tool_name(tools::WRITE_FILE, true, true));
158 assert!(features.allows_tool_name(tools::UNIFIED_FILE, true, true));
159 assert!(features.allows_tool_name(tools::UNIFIED_EXEC, true, true));
160 assert!(features.allows_tool_name(tools::TASK_TRACKER, true, true));
161 }
162
163 #[test]
164 fn auto_compaction_requires_provider_support() {
165 let mut cfg = VTCodeConfig::default();
166 cfg.agent.harness.auto_compaction_enabled = true;
167
168 let features = FeatureSet::from_config(Some(&cfg));
169
170 assert!(!features.auto_compaction_enabled(false));
171 assert!(features.auto_compaction_enabled(true));
172 assert_eq!(features.auto_compaction.stage, FeatureStage::Beta);
173 }
174
175 #[test]
176 fn open_responses_gate_tracks_emit_settings() {
177 let mut cfg = VTCodeConfig::default();
178 cfg.agent.open_responses.enabled = true;
179 cfg.agent.open_responses.emit_events = false;
180 cfg.agent.open_responses.map_tool_calls = true;
181 cfg.agent.open_responses.include_reasoning = true;
182
183 let features = FeatureSet::from_config(Some(&cfg));
184
185 assert!(features.open_responses.enabled);
186 assert!(!features.open_responses.emit_events);
187 assert!(features.open_responses.map_tool_calls);
188 assert!(features.open_responses.include_reasoning);
189 }
190}