Skip to main content

vtcode_config/core/
automation.rs

1use crate::constants::{defaults, tool_limits, tools};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5/// Automation-specific configuration toggles.
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[derive(Debug, Clone, Deserialize, Serialize, Default)]
8pub struct AutomationConfig {
9    /// Full-auto execution safeguards.
10    #[serde(default)]
11    pub full_auto: FullAutoConfig,
12    /// Session and durable scheduled task controls.
13    #[serde(default)]
14    pub scheduled_tasks: ScheduledTasksConfig,
15    /// Loop engineering controls for external scheduler integration.
16    #[serde(default)]
17    pub loop_engine: LoopEngineConfig,
18}
19
20/// Controls for the built-in scheduled task subsystem.
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct ScheduledTasksConfig {
24    /// Enable scheduler tools and durable `vtcode schedule` jobs.
25    #[serde(default = "default_scheduled_tasks_enabled")]
26    pub enabled: bool,
27}
28
29impl Default for ScheduledTasksConfig {
30    fn default() -> Self {
31        Self { enabled: default_scheduled_tasks_enabled() }
32    }
33}
34
35/// Controls for running the agent without interactive approvals.
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct FullAutoConfig {
39    /// Enable the runtime flag once the workspace is configured for autonomous runs.
40    #[serde(default = "default_full_auto_enabled")]
41    pub enabled: bool,
42
43    /// Maximum number of autonomous agent turns before the exec runner pauses.
44    #[serde(default = "default_full_auto_max_turns")]
45    pub max_turns: usize,
46
47    /// Allow-list of tools that may execute automatically.
48    #[serde(default = "default_full_auto_allowed_tools")]
49    pub allowed_tools: Vec<String>,
50
51    /// Require presence of a profile/acknowledgement file before activation.
52    #[serde(default = "default_require_profile_ack")]
53    pub require_profile_ack: bool,
54
55    /// Optional path to a profile describing acceptable behaviors.
56    #[serde(default)]
57    pub profile_path: Option<PathBuf>,
58
59    /// Run a read-only verifier sub-agent after each mutating tool call.
60    /// When enabled, the harness spawns a verifier that re-reads the diff
61    /// and either approves or rejects the change before it is committed.
62    /// This doubles cost on mutating calls but catches propose-side errors.
63    #[serde(default)]
64    verify_mutations: bool,
65
66    /// Automatically grant tool-loop and session tool-call limit increases
67    /// while a full-auto run is active, instead of prompting. Grants reuse
68    /// the same per-prompt increments and absolute hard caps as manual
69    /// approvals. Set to `false` to restore the interactive prompts.
70    #[serde(default = "default_auto_grant_tool_limits")]
71    pub auto_grant_tool_limits: bool,
72}
73
74impl Default for FullAutoConfig {
75    fn default() -> Self {
76        Self {
77            enabled: default_full_auto_enabled(),
78            max_turns: default_full_auto_max_turns(),
79            allowed_tools: default_full_auto_allowed_tools(),
80            require_profile_ack: default_require_profile_ack(),
81            profile_path: None,
82            verify_mutations: false,
83            auto_grant_tool_limits: default_auto_grant_tool_limits(),
84        }
85    }
86}
87
88fn default_full_auto_enabled() -> bool {
89    false
90}
91
92fn default_scheduled_tasks_enabled() -> bool {
93    false
94}
95
96fn default_full_auto_allowed_tools() -> Vec<String> {
97    vec![
98        tools::EXEC_COMMAND.to_string(),
99        tools::WRITE_STDIN.to_string(),
100        tools::APPLY_PATCH.to_string(),
101        tools::CODE_SEARCH.to_string(),
102        // Workflow-coordination tools stay available in every mode (see
103        // `ALWAYS_AVAILABLE_WORKFLOW_TOOLS`); listing them here keeps the
104        // documented default coherent with runtime behavior.
105        tools::TASK_TRACKER.to_string(),
106        tools::START_PLANNING.to_string(),
107        tools::REQUEST_USER_INPUT.to_string(),
108    ]
109}
110
111fn default_require_profile_ack() -> bool {
112    true
113}
114
115fn default_auto_grant_tool_limits() -> bool {
116    true
117}
118
119fn default_full_auto_max_turns() -> usize {
120    tool_limits::DEFAULT_FULL_AUTO_MAX_TURNS
121}
122
123/// Controls for loop engineering — running vtcode from an external scheduler.
124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
125#[derive(Debug, Clone, Deserialize, Serialize)]
126pub struct LoopEngineConfig {
127    /// Enable loop engineering mode. When false, loop-specific features
128    /// (reconciler, per-iteration skills) are inactive.
129    #[serde(default)]
130    enabled: bool,
131
132    /// Optional circuit breaker: maximum loop iterations before the harness
133    /// refuses further runs. `None` means unlimited.
134    #[serde(default)]
135    max_iterations: Option<usize>,
136
137    /// After a worktree-isolated subagent completes, run the reconciler
138    /// (diff → verify → merge) automatically.
139    #[serde(default = "default_reconcile_on_complete")]
140    pub reconcile_on_complete: bool,
141
142    /// Skill names to preload into the agent context at the start of each
143    /// loop iteration. Empty means no per-iteration skill injection.
144    #[serde(default)]
145    preload_skills: Vec<String>,
146}
147
148impl Default for LoopEngineConfig {
149    fn default() -> Self {
150        Self {
151            enabled: false,
152            max_iterations: None,
153            reconcile_on_complete: default_reconcile_on_complete(),
154            preload_skills: Vec::new(),
155        }
156    }
157}
158
159fn default_reconcile_on_complete() -> bool {
160    true
161}
162
163/// Returns `true` when the loop engine is enabled, respecting the
164/// `VTCODE_DISABLE_LOOP_ENGINE` env-var override.
165pub fn loop_engine_enabled(config: &AutomationConfig) -> bool {
166    if std::env::var("VTCODE_DISABLE_LOOP_ENGINE").is_ok() {
167        return false;
168    }
169    config.loop_engine.enabled
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn full_auto_defaults_to_shared_turn_budget() {
178        assert_eq!(FullAutoConfig::default().max_turns, tool_limits::DEFAULT_FULL_AUTO_MAX_TURNS);
179    }
180
181    #[test]
182    fn full_auto_grants_loop_limits_without_prompting_by_default() {
183        assert!(FullAutoConfig::default().auto_grant_tool_limits);
184    }
185
186    #[test]
187    fn full_auto_loop_grants_survive_missing_field_for_backward_compatibility() {
188        let without_field = toml::from_str::<FullAutoConfig>("enabled = true").expect("minimal full-auto parses");
189        assert!(without_field.auto_grant_tool_limits);
190
191        let opted_out = toml::from_str::<FullAutoConfig>("enabled = true\nauto_grant_tool_limits = false")
192            .expect("explicit opt-out parses");
193        assert!(!opted_out.auto_grant_tool_limits);
194    }
195}