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,
9 Beta,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct FeatureGate {
15 pub enabled: bool,
16 pub stage: FeatureStage,
17}
18
19impl FeatureGate {
20 pub const fn new(enabled: bool, stage: FeatureStage) -> Self {
21 Self { enabled, stage }
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct OpenResponsesFeature {
28 pub enabled: bool,
29 pub emit_events: bool,
30 pub map_tool_calls: bool,
31 pub include_reasoning: bool,
32 pub stage: FeatureStage,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FeatureSet {
38 pub request_user_input: FeatureGate,
39 pub auto_compaction: FeatureGate,
40 pub open_responses: OpenResponsesFeature,
41}
42
43impl FeatureSet {
44 pub fn from_config(config: Option<&VTCodeConfig>) -> Self {
45 let default_config;
46 let cfg = if let Some(cfg) = config {
47 cfg
48 } else {
49 default_config = VTCodeConfig::default();
50 &default_config
51 };
52
53 Self {
54 request_user_input: FeatureGate::new(
55 cfg.chat.ask_questions.enabled,
56 FeatureStage::Stable,
57 ),
58 auto_compaction: FeatureGate::new(
59 cfg.agent.harness.auto_compaction_enabled,
60 FeatureStage::Beta,
61 ),
62 open_responses: OpenResponsesFeature {
63 enabled: cfg.agent.open_responses.enabled,
64 emit_events: cfg.agent.open_responses.enabled
65 && cfg.agent.open_responses.emit_events,
66 map_tool_calls: cfg.agent.open_responses.enabled
67 && cfg.agent.open_responses.map_tool_calls,
68 include_reasoning: cfg.agent.open_responses.enabled
69 && cfg.agent.open_responses.include_reasoning,
70 stage: FeatureStage::Beta,
71 },
72 }
73 }
74
75 pub fn request_user_input_enabled(
76 &self,
77 _planning_active: bool,
78 interactive_session: bool,
79 ) -> bool {
80 interactive_session && self.request_user_input.enabled
81 }
82
83 pub fn auto_compaction_enabled(&self, supports_server_compaction: bool) -> bool {
84 self.auto_compaction.enabled && supports_server_compaction
85 }
86
87 pub fn tool_enabled_for_mode(
88 tool_name: &str,
89 planning_active: bool,
90 request_user_input_enabled: bool,
91 ) -> bool {
92 match tool_name {
93 tools::REQUEST_USER_INPUT => request_user_input_enabled,
94 _ if !planning_active => true,
95 _ => builtin_tool_behavior(tool_name)
96 .map(|behavior| !matches!(behavior.mutation_model, ToolMutationModel::Mutating))
97 .unwrap_or(true),
98 }
99 }
100
101 pub fn allows_tool_name(
102 &self,
103 tool_name: &str,
104 planning_active: bool,
105 interactive_session: bool,
106 ) -> 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::APPLY_PATCH, true, true));
153 assert!(!features.allows_tool_name(tools::WRITE_FILE, true, true));
154 assert!(features.allows_tool_name(tools::UNIFIED_FILE, true, true));
155 assert!(features.allows_tool_name(tools::UNIFIED_EXEC, true, true));
156 assert!(features.allows_tool_name(tools::TASK_TRACKER, true, true));
157 }
158
159 #[test]
160 fn auto_compaction_requires_provider_support() {
161 let mut cfg = VTCodeConfig::default();
162 cfg.agent.harness.auto_compaction_enabled = true;
163
164 let features = FeatureSet::from_config(Some(&cfg));
165
166 assert!(!features.auto_compaction_enabled(false));
167 assert!(features.auto_compaction_enabled(true));
168 assert_eq!(features.auto_compaction.stage, FeatureStage::Beta);
169 }
170
171 #[test]
172 fn open_responses_gate_tracks_emit_settings() {
173 let mut cfg = VTCodeConfig::default();
174 cfg.agent.open_responses.enabled = true;
175 cfg.agent.open_responses.emit_events = false;
176 cfg.agent.open_responses.map_tool_calls = true;
177 cfg.agent.open_responses.include_reasoning = true;
178
179 let features = FeatureSet::from_config(Some(&cfg));
180
181 assert!(features.open_responses.enabled);
182 assert!(!features.open_responses.emit_events);
183 assert!(features.open_responses.map_tool_calls);
184 assert!(features.open_responses.include_reasoning);
185 }
186}