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    /// Feature is stable and fully supported.
9    Stable,
10    /// Feature is in beta and may change without notice.
11    Beta,
12}
13
14/// Generic feature gate with stage metadata.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct FeatureGate {
17    /// Whether the feature is currently enabled.
18    pub enabled: bool,
19    /// Lifecycle stage of the feature.
20    pub stage: FeatureStage,
21}
22
23impl FeatureGate {
24    /// Create a new feature gate with the given enabled state and stage.
25    pub const fn new(enabled: bool, stage: FeatureStage) -> Self {
26        Self { enabled, stage }
27    }
28}
29
30/// Open Responses-specific feature gate data.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct OpenResponsesFeature {
33    /// Whether Open Responses support is enabled.
34    pub enabled: bool,
35    /// Whether to emit Open Responses-specific lifecycle events.
36    pub emit_events: bool,
37    /// Whether to map tool calls to Open Responses format.
38    pub map_tool_calls: bool,
39    /// Whether to include reasoning content in responses.
40    pub include_reasoning: bool,
41    /// Lifecycle stage of the feature.
42    pub stage: FeatureStage,
43}
44
45/// Immutable session-scoped feature flags derived from config.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct FeatureSet {
48    /// Gate for the `request_user_input` tool.
49    pub request_user_input: FeatureGate,
50    /// Gate for automatic context compaction.
51    pub auto_compaction: FeatureGate,
52    /// Gate for Open Responses-specific features.
53    pub open_responses: OpenResponsesFeature,
54}
55
56impl FeatureSet {
57    /// Build a [`FeatureSet`] from the workspace configuration, falling back
58    /// to defaults when no config is provided.
59    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    /// Whether the `request_user_input` tool is available in the current context.
82    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    /// Whether auto-compaction is enabled, requiring provider server-side support.
87    pub fn auto_compaction_enabled(&self, supports_server_compaction: bool) -> bool {
88        self.auto_compaction.enabled && supports_server_compaction
89    }
90
91    /// Whether a specific tool is allowed in the current planning mode.
92    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) // fail-closed: deny unknown tools in planning mode
101            }
102        }
103    }
104
105    /// Whether a tool name is allowed given the current feature gates and session state.
106    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}