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(
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}