Skip to main content

vtcode_core/core/agent/
features.rs

1use crate::config::VTCodeConfig;
2use crate::config::constants::tools;
3use crate::tools::tool_intent::{ToolMutationModel, builtin_tool_behavior};
4
5/// Lifecycle stage for a feature gate.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FeatureStage {
8    Stable,
9    Beta,
10}
11
12/// Generic feature gate with stage metadata.
13#[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/// Open Responses-specific feature gate data.
26#[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/// Immutable session-scoped feature flags derived from config.
36#[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}