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(&self, _plan_mode: bool, interactive_session: bool) -> bool {
76 interactive_session && self.request_user_input.enabled
77 }
78
79 pub fn auto_compaction_enabled(&self, supports_server_compaction: bool) -> bool {
80 self.auto_compaction.enabled && supports_server_compaction
81 }
82
83 pub fn tool_enabled_for_mode(
84 tool_name: &str,
85 plan_mode: bool,
86 request_user_input_enabled: bool,
87 ) -> bool {
88 match tool_name {
89 tools::REQUEST_USER_INPUT => request_user_input_enabled,
90 _ if !plan_mode => true,
91 _ => builtin_tool_behavior(tool_name)
92 .map(|behavior| !matches!(behavior.mutation_model, ToolMutationModel::Mutating))
93 .unwrap_or(true),
94 }
95 }
96
97 pub fn allows_tool_name(
98 &self,
99 tool_name: &str,
100 plan_mode: bool,
101 interactive_session: bool,
102 ) -> bool {
103 Self::tool_enabled_for_mode(
104 tool_name,
105 plan_mode,
106 self.request_user_input_enabled(plan_mode, interactive_session),
107 )
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::{FeatureSet, FeatureStage};
114 use crate::config::VTCodeConfig;
115 use crate::config::constants::tools;
116
117 #[test]
118 fn request_user_input_requires_interactive_session() {
119 let cfg = VTCodeConfig::default();
120 let features = FeatureSet::from_config(Some(&cfg));
121
122 assert!(features.request_user_input_enabled(false, true));
123 assert!(features.request_user_input_enabled(true, true));
124 assert!(!features.request_user_input_enabled(true, false));
125 assert!(features.allows_tool_name(tools::REQUEST_USER_INPUT, false, true));
126 assert!(features.allows_tool_name(tools::REQUEST_USER_INPUT, true, true));
127 assert!(!features.allows_tool_name(tools::REQUEST_USER_INPUT, true, false));
128 assert!(features.allows_tool_name(tools::PLAN_TASK_TRACKER, true, false));
129 assert!(features.allows_tool_name(tools::PLAN_TASK_TRACKER, false, true));
130 }
131
132 #[test]
133 fn request_user_input_honors_chat_setting_outside_plan_mode() {
134 let mut cfg = VTCodeConfig::default();
135 cfg.chat.ask_questions.enabled = false;
136
137 let features = FeatureSet::from_config(Some(&cfg));
138
139 assert!(!features.request_user_input_enabled(false, true));
140 assert!(!features.request_user_input_enabled(true, true));
141 }
142
143 #[test]
144 fn plan_mode_hides_mutating_only_tools_but_keeps_conditional_tools() {
145 let cfg = VTCodeConfig::default();
146 let features = FeatureSet::from_config(Some(&cfg));
147
148 assert!(!features.allows_tool_name(tools::APPLY_PATCH, true, true));
149 assert!(!features.allows_tool_name(tools::WRITE_FILE, true, true));
150 assert!(features.allows_tool_name(tools::UNIFIED_FILE, true, true));
151 assert!(features.allows_tool_name(tools::UNIFIED_EXEC, true, true));
152 assert!(features.allows_tool_name(tools::TASK_TRACKER, true, true));
153 }
154
155 #[test]
156 fn auto_compaction_requires_provider_support() {
157 let mut cfg = VTCodeConfig::default();
158 cfg.agent.harness.auto_compaction_enabled = true;
159
160 let features = FeatureSet::from_config(Some(&cfg));
161
162 assert!(!features.auto_compaction_enabled(false));
163 assert!(features.auto_compaction_enabled(true));
164 assert_eq!(features.auto_compaction.stage, FeatureStage::Beta);
165 }
166
167 #[test]
168 fn open_responses_gate_tracks_emit_settings() {
169 let mut cfg = VTCodeConfig::default();
170 cfg.agent.open_responses.enabled = true;
171 cfg.agent.open_responses.emit_events = false;
172 cfg.agent.open_responses.map_tool_calls = true;
173 cfg.agent.open_responses.include_reasoning = true;
174
175 let features = FeatureSet::from_config(Some(&cfg));
176
177 assert!(features.open_responses.enabled);
178 assert!(!features.open_responses.emit_events);
179 assert!(features.open_responses.map_tool_calls);
180 assert!(features.open_responses.include_reasoning);
181 }
182}