Skip to main content

vtcode_config/types/
mod.rs

1//! Common types and interfaces used throughout the application
2
3use crate::core::PromptCachingConfig;
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::BTreeMap;
6use std::env;
7use std::fmt;
8use std::path::PathBuf;
9
10// Re-export from vtcode-commons so downstream code can use `vtcode_config::types::ReasoningEffortLevel`.
11pub use vtcode_commons::reasoning::ReasoningEffortLevel;
12
13/// System prompt mode (inspired by pi-coding-agent philosophy)
14/// Controls verbosity and complexity of system prompts sent to models
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "lowercase")]
18#[derive(Default)]
19pub enum SystemPromptMode {
20    /// Minimal prompt (~500 tokens base) - Pi-inspired, modern models need less guidance
21    /// Best for: Power users, token-constrained contexts, fast responses
22    #[default]
23    Minimal,
24    /// Lightweight prompt (~750 tokens base) - Essential guidance only
25    /// Best for: Resource-constrained operations, simple tasks
26    Lightweight,
27    /// Default prompt (~900 tokens base) - Full guidance with all features
28    /// Best for: General usage, comprehensive error handling
29    Default,
30    /// Specialized prompt (~900 tokens base) - Complex refactoring and analysis
31    /// Best for: Multi-file changes, sophisticated code analysis
32    Specialized,
33}
34
35impl SystemPromptMode {
36    /// Return the textual representation for configuration
37    fn as_str(self) -> &'static str {
38        match self {
39            Self::Minimal => "minimal",
40            Self::Lightweight => "lightweight",
41            Self::Default => "default",
42            Self::Specialized => "specialized",
43        }
44    }
45
46    /// Parse system prompt mode from user configuration
47    pub fn parse(value: &str) -> Option<Self> {
48        let normalized = value.trim();
49        if normalized.eq_ignore_ascii_case("minimal") {
50            Some(Self::Minimal)
51        } else if normalized.eq_ignore_ascii_case("lightweight") {
52            Some(Self::Lightweight)
53        } else if normalized.eq_ignore_ascii_case("default") {
54            Some(Self::Default)
55        } else if normalized.eq_ignore_ascii_case("specialized") {
56            Some(Self::Specialized)
57        } else {
58            None
59        }
60    }
61
62    /// Allowed configuration values for validation
63    pub fn allowed_values() -> &'static [&'static str] {
64        &["minimal", "lightweight", "default", "specialized"]
65    }
66}
67
68impl fmt::Display for SystemPromptMode {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str(self.as_str())
71    }
72}
73
74impl<'de> Deserialize<'de> for SystemPromptMode {
75    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76    where
77        D: Deserializer<'de>,
78    {
79        let raw = String::deserialize(deserializer)?;
80        if let Some(parsed) = Self::parse(&raw) {
81            Ok(parsed)
82        } else {
83            Ok(Self::default())
84        }
85    }
86}
87
88/// Tool documentation mode (inspired by pi-coding-agent progressive disclosure)
89/// Controls how much tool documentation is loaded upfront vs on-demand
90#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
92#[serde(rename_all = "lowercase")]
93#[derive(Default)]
94pub enum ToolDocumentationMode {
95    /// First sentence of each tool description (at most 64 chars) and no
96    /// parameter descriptions - Pi-style, power users
97    /// Best for: Maximum efficiency, experienced users, token-constrained contexts
98    Minimal,
99    /// Complete tool and parameter descriptions; only unusually long tails are
100    /// trimmed at a sentence boundary (measured ~1,793 tokens for the default
101    /// catalog; budgeted ≤ 2,000 in `emitted_model_tool_schema_fits_within_first_request_budget`)
102    /// Best for: General usage (recommended)
103    #[default]
104    Progressive,
105    /// Every tool and parameter description sent unmodified
106    /// Best for: Debugging tool definitions, comprehensive documentation
107    Full,
108}
109
110impl ToolDocumentationMode {
111    /// Return the textual representation for configuration
112    fn as_str(self) -> &'static str {
113        match self {
114            Self::Minimal => "minimal",
115            Self::Progressive => "progressive",
116            Self::Full => "full",
117        }
118    }
119
120    /// Parse tool documentation mode from user configuration
121    fn parse(value: &str) -> Option<Self> {
122        let normalized = value.trim();
123        if normalized.eq_ignore_ascii_case("minimal") {
124            Some(Self::Minimal)
125        } else if normalized.eq_ignore_ascii_case("progressive") {
126            Some(Self::Progressive)
127        } else if normalized.eq_ignore_ascii_case("full") {
128            Some(Self::Full)
129        } else {
130            None
131        }
132    }
133
134    /// Allowed configuration values for validation
135    pub fn allowed_values() -> &'static [&'static str] {
136        &["minimal", "progressive", "full"]
137    }
138}
139
140impl fmt::Display for ToolDocumentationMode {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.write_str(self.as_str())
143    }
144}
145
146impl<'de> Deserialize<'de> for ToolDocumentationMode {
147    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148    where
149        D: Deserializer<'de>,
150    {
151        let raw = String::deserialize(deserializer)?;
152        if let Some(parsed) = Self::parse(&raw) {
153            Ok(parsed)
154        } else {
155            Ok(Self::default())
156        }
157    }
158}
159
160/// Configured shell syntax profile used for model-facing command examples.
161///
162/// This is prompt guidance only. Command approval, sandboxing, and allow-list
163/// policy are enforced elsewhere in the runtime.
164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
166#[serde(rename_all = "snake_case")]
167#[derive(Default)]
168pub enum ShellPromptProfile {
169    /// Detect from the current platform. Unix-like shells are used for Linux,
170    /// macOS, and WSL. Native Windows uses PowerShell.
171    #[default]
172    Auto,
173    /// Use Unix-like shell syntax in prompt examples.
174    UnixLike,
175    /// Use native PowerShell syntax in prompt examples.
176    ///
177    /// The canonical wire value is `powershell` (matching [`Self::as_str`],
178    /// [`Self::allowed_values`], and the docs); `snake_case` would otherwise
179    /// render it as the drifting `power_shell`.
180    #[serde(rename = "powershell")]
181    PowerShell,
182}
183
184impl ShellPromptProfile {
185    /// Return the textual representation for configuration.
186    fn as_str(self) -> &'static str {
187        match self {
188            Self::Auto => "auto",
189            Self::UnixLike => "unix_like",
190            Self::PowerShell => "powershell",
191        }
192    }
193
194    /// Parse a shell prompt profile from user configuration.
195    fn parse(value: &str) -> Option<Self> {
196        let normalized = value.trim();
197        if normalized.eq_ignore_ascii_case("auto") {
198            Some(Self::Auto)
199        } else if normalized.eq_ignore_ascii_case("unix_like")
200            || normalized.eq_ignore_ascii_case("unix-like")
201            || normalized.eq_ignore_ascii_case("unix")
202            || normalized.eq_ignore_ascii_case("posix")
203        {
204            Some(Self::UnixLike)
205        } else if normalized.eq_ignore_ascii_case("powershell")
206            || normalized.eq_ignore_ascii_case("power_shell")
207            || normalized.eq_ignore_ascii_case("windows")
208        {
209            Some(Self::PowerShell)
210        } else {
211            None
212        }
213    }
214
215    /// Allowed configuration values for validation.
216    pub fn allowed_values() -> &'static [&'static str] {
217        &["auto", "unix_like", "powershell"]
218    }
219
220    /// Resolve this configured value against the current runtime platform.
221    pub fn resolve_for_current_platform(self) -> ResolvedShellPromptProfile {
222        self.resolve_for_platform(ShellProfilePlatform::current())
223    }
224
225    /// Resolve this configured value against an explicit platform.
226    fn resolve_for_platform(self, platform: ShellProfilePlatform) -> ResolvedShellPromptProfile {
227        match self {
228            Self::Auto => match platform {
229                ShellProfilePlatform::NativeWindows => ResolvedShellPromptProfile::PowerShell,
230                ShellProfilePlatform::UnixLike | ShellProfilePlatform::Wsl => ResolvedShellPromptProfile::UnixLike,
231            },
232            Self::UnixLike => ResolvedShellPromptProfile::UnixLike,
233            Self::PowerShell => ResolvedShellPromptProfile::PowerShell,
234        }
235    }
236}
237
238impl fmt::Display for ShellPromptProfile {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        f.write_str(self.as_str())
241    }
242}
243
244impl<'de> Deserialize<'de> for ShellPromptProfile {
245    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246    where
247        D: Deserializer<'de>,
248    {
249        let raw = String::deserialize(deserializer)?;
250        if let Some(parsed) = Self::parse(&raw) {
251            Ok(parsed)
252        } else {
253            Ok(Self::default())
254        }
255    }
256}
257
258/// Platform category used to resolve [`ShellPromptProfile::Auto`].
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
261#[serde(rename_all = "snake_case")]
262pub enum ShellProfilePlatform {
263    UnixLike,
264    Wsl,
265    NativeWindows,
266}
267
268impl ShellProfilePlatform {
269    fn current() -> Self {
270        if cfg!(windows) {
271            Self::NativeWindows
272        } else if is_wsl_environment() {
273            Self::Wsl
274        } else {
275            Self::UnixLike
276        }
277    }
278}
279
280/// Prompt profile after automatic platform resolution.
281#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
283#[serde(rename_all = "snake_case")]
284pub enum ResolvedShellPromptProfile {
285    UnixLike,
286    /// Canonical wire value is `powershell` (matching [`Self::as_str`]); the
287    /// derived `snake_case` form would otherwise be the drifting `power_shell`.
288    #[serde(rename = "powershell")]
289    PowerShell,
290}
291
292impl ResolvedShellPromptProfile {
293    fn as_str(self) -> &'static str {
294        match self {
295            Self::UnixLike => "unix_like",
296            Self::PowerShell => "powershell",
297        }
298    }
299}
300
301impl fmt::Display for ResolvedShellPromptProfile {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        f.write_str(self.as_str())
304    }
305}
306
307fn is_wsl_environment() -> bool {
308    if env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some() {
309        return true;
310    }
311
312    for path in ["/proc/sys/kernel/osrelease", "/proc/version"] {
313        if let Ok(contents) = std::fs::read_to_string(path)
314            && contents.to_ascii_lowercase().contains("microsoft")
315        {
316            return true;
317        }
318    }
319
320    false
321}
322
323/// Verbosity level for model output (GPT-5.4-family and compatible models)
324#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
326#[serde(rename_all = "lowercase")]
327#[derive(Default)]
328pub enum VerbosityLevel {
329    Low,
330    #[default]
331    Medium,
332    High,
333}
334
335impl VerbosityLevel {
336    /// Return the textual representation expected by downstream APIs
337    pub fn as_str(self) -> &'static str {
338        match self {
339            Self::Low => "low",
340            Self::Medium => "medium",
341            Self::High => "high",
342        }
343    }
344
345    /// Attempt to parse a verbosity level from user configuration input
346    pub fn parse(value: &str) -> Option<Self> {
347        let normalized = value.trim();
348        if normalized.eq_ignore_ascii_case("low") {
349            Some(Self::Low)
350        } else if normalized.eq_ignore_ascii_case("medium") {
351            Some(Self::Medium)
352        } else if normalized.eq_ignore_ascii_case("high") {
353            Some(Self::High)
354        } else {
355            None
356        }
357    }
358
359    /// Enumerate the allowed configuration values
360    pub fn allowed_values() -> &'static [&'static str] {
361        &["low", "medium", "high"]
362    }
363}
364
365impl fmt::Display for VerbosityLevel {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        f.write_str(self.as_str())
368    }
369}
370
371impl<'de> Deserialize<'de> for VerbosityLevel {
372    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
373    where
374        D: Deserializer<'de>,
375    {
376        let raw = String::deserialize(deserializer)?;
377        if let Some(parsed) = Self::parse(&raw) {
378            Ok(parsed)
379        } else {
380            Ok(Self::default())
381        }
382    }
383}
384
385/// Preferred rendering surface for the interactive chat UI
386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
388#[serde(rename_all = "lowercase")]
389#[derive(Default)]
390pub enum UiSurfacePreference {
391    Auto,
392    Alternate,
393    #[default]
394    Inline,
395}
396
397impl UiSurfacePreference {
398    /// String representation used in configuration and logging
399    fn as_str(self) -> &'static str {
400        match self {
401            Self::Auto => "auto",
402            Self::Alternate => "alternate",
403            Self::Inline => "inline",
404        }
405    }
406
407    /// Parse a surface preference from configuration input
408    fn parse(value: &str) -> Option<Self> {
409        let normalized = value.trim();
410        if normalized.eq_ignore_ascii_case("auto") {
411            Some(Self::Auto)
412        } else if normalized.eq_ignore_ascii_case("alternate") || normalized.eq_ignore_ascii_case("alt") {
413            Some(Self::Alternate)
414        } else if normalized.eq_ignore_ascii_case("inline") {
415            Some(Self::Inline)
416        } else {
417            None
418        }
419    }
420
421    /// Enumerate the accepted configuration values for validation messaging
422    pub fn allowed_values() -> &'static [&'static str] {
423        &["auto", "alternate", "inline"]
424    }
425}
426
427impl fmt::Display for UiSurfacePreference {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        f.write_str(self.as_str())
430    }
431}
432
433impl<'de> Deserialize<'de> for UiSurfacePreference {
434    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435    where
436        D: Deserializer<'de>,
437    {
438        let raw = String::deserialize(deserializer)?;
439        if let Some(parsed) = Self::parse(&raw) {
440            Ok(parsed)
441        } else {
442            Ok(Self::default())
443        }
444    }
445}
446
447/// Source describing how the active model was selected
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
449pub enum ModelSelectionSource {
450    /// Model provided by workspace configuration
451    #[default]
452    WorkspaceConfig,
453    /// Model provided by CLI override
454    CliOverride,
455}
456
457/// Configuration for the agent
458#[derive(Debug, Clone)]
459pub struct AgentConfig {
460    pub model: String,
461    pub api_key: String,
462    pub provider: String,
463    pub openai_chatgpt_auth: Option<crate::auth::OpenAIChatGptAuthHandle>,
464    pub api_key_env: String,
465    pub workspace: PathBuf,
466    pub verbose: bool,
467    pub quiet: bool,
468    pub theme: String,
469    pub reasoning_effort: ReasoningEffortLevel,
470    pub ui_surface: UiSurfacePreference,
471    pub prompt_cache: PromptCachingConfig,
472    pub model_source: ModelSelectionSource,
473    pub custom_api_keys: BTreeMap<String, String>,
474    pub checkpointing_enabled: bool,
475    pub checkpointing_storage_dir: Option<PathBuf>,
476    pub checkpointing_max_snapshots: usize,
477    pub checkpointing_max_age_days: Option<u64>,
478    pub max_conversation_turns: usize,
479    pub model_behavior: Option<crate::core::ModelConfig>,
480}
481
482/// Workshop agent capability levels
483#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
484pub enum CapabilityLevel {
485    /// Basic chat only
486    Basic,
487    /// Can read files
488    FileReading,
489    /// Can read files and list directories
490    FileListing,
491    /// Can read files, list directories, and run bash commands
492    Bash,
493    /// Can read files, list directories, run bash commands, and edit files
494    Editing,
495    /// Full capabilities including code search
496    CodeSearch,
497}
498
499/// Session information
500#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct SessionInfo {
502    pub session_id: String,
503    pub start_time: u64,
504    pub total_turns: usize,
505    pub total_decisions: usize,
506    pub error_count: usize,
507}
508
509/// Error information for tracking
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct ErrorInfo {
512    error_type: String,
513    message: String,
514    turn_number: usize,
515    recoverable: bool,
516    timestamp: u64,
517}
518
519/// Performance metrics
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct PerformanceMetrics {
522    pub session_duration_seconds: u64,
523    pub total_api_calls: usize,
524    pub total_tokens_used: Option<usize>,
525    pub average_response_time_ms: f64,
526    pub tool_execution_count: usize,
527    pub error_count: usize,
528    pub recovery_success_rate: f64,
529}
530
531/// Analysis depth for workspace analysis
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub enum AnalysisDepth {
534    Basic,
535    Standard,
536    Deep,
537}
538
539/// Output format for commands
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub enum OutputFormat {
542    Text,
543    Json,
544    Html,
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn test_reasoning_effort_parse_and_allowed_values_include_max() {
553        assert_eq!(ReasoningEffortLevel::parse("max"), Some(ReasoningEffortLevel::Max));
554        assert_eq!(ReasoningEffortLevel::Max.as_str(), "max");
555        assert!(ReasoningEffortLevel::allowed_values().contains(&"max"));
556    }
557
558    #[test]
559    fn shell_prompt_profile_parses_aliases() {
560        assert_eq!(ShellPromptProfile::parse("auto"), Some(ShellPromptProfile::Auto));
561        assert_eq!(ShellPromptProfile::parse("unix-like"), Some(ShellPromptProfile::UnixLike));
562        assert_eq!(ShellPromptProfile::parse("posix"), Some(ShellPromptProfile::UnixLike));
563        assert_eq!(ShellPromptProfile::parse("PowerShell"), Some(ShellPromptProfile::PowerShell));
564        assert_eq!(ShellPromptProfile::parse("unknown"), None);
565    }
566
567    #[test]
568    fn ui_surface_defaults_to_inline() {
569        assert_eq!(UiSurfacePreference::default(), UiSurfacePreference::Inline);
570    }
571
572    #[test]
573    fn auto_shell_prompt_profile_selects_platform_defaults() {
574        let profile = ShellPromptProfile::Auto;
575
576        assert_eq!(profile.resolve_for_platform(ShellProfilePlatform::UnixLike), ResolvedShellPromptProfile::UnixLike);
577        assert_eq!(profile.resolve_for_platform(ShellProfilePlatform::Wsl), ResolvedShellPromptProfile::UnixLike);
578        assert_eq!(
579            profile.resolve_for_platform(ShellProfilePlatform::NativeWindows),
580            ResolvedShellPromptProfile::PowerShell
581        );
582    }
583
584    #[test]
585    fn explicit_shell_prompt_profiles_override_platform_defaults() {
586        assert_eq!(
587            ShellPromptProfile::UnixLike.resolve_for_platform(ShellProfilePlatform::NativeWindows),
588            ResolvedShellPromptProfile::UnixLike
589        );
590        assert_eq!(
591            ShellPromptProfile::PowerShell.resolve_for_platform(ShellProfilePlatform::Wsl),
592            ResolvedShellPromptProfile::PowerShell
593        );
594    }
595
596    /// Every `as_str()`/`parse()`/serde triangle in this module stays in
597    /// lockstep; see [`crate::test_support::assert_string_enum_lockstep`].
598    #[test]
599    fn string_enums_keep_as_str_and_serde_in_lockstep() {
600        use crate::test_support::assert_string_enum_lockstep;
601
602        assert_string_enum_lockstep!(
603            SystemPromptMode,
604            [
605                SystemPromptMode::Minimal,
606                SystemPromptMode::Lightweight,
607                SystemPromptMode::Default,
608                SystemPromptMode::Specialized,
609            ]
610        );
611        assert_string_enum_lockstep!(
612            ToolDocumentationMode,
613            [
614                ToolDocumentationMode::Minimal,
615                ToolDocumentationMode::Progressive,
616                ToolDocumentationMode::Full,
617            ]
618        );
619        assert_string_enum_lockstep!(
620            ShellPromptProfile,
621            [
622                ShellPromptProfile::Auto,
623                ShellPromptProfile::UnixLike,
624                ShellPromptProfile::PowerShell,
625            ]
626        );
627        assert_string_enum_lockstep!(
628            VerbosityLevel,
629            [VerbosityLevel::Low, VerbosityLevel::Medium, VerbosityLevel::High]
630        );
631        assert_string_enum_lockstep!(
632            UiSurfacePreference,
633            [
634                UiSurfacePreference::Auto,
635                UiSurfacePreference::Alternate,
636                UiSurfacePreference::Inline,
637            ]
638        );
639
640        // The shared `allowed_values()` lists are the model-facing validation
641        // surface; pin them against `parse()` so a rename cannot silently drop
642        // a value from the accepted set.
643        for value in SystemPromptMode::allowed_values() {
644            assert!(SystemPromptMode::parse(value).is_some(), "allowed value {value:?} must parse");
645        }
646        for value in ToolDocumentationMode::allowed_values() {
647            assert!(ToolDocumentationMode::parse(value).is_some(), "allowed value {value:?} must parse");
648        }
649        for value in ShellPromptProfile::allowed_values() {
650            assert!(ShellPromptProfile::parse(value).is_some(), "allowed value {value:?} must parse");
651        }
652        for value in VerbosityLevel::allowed_values() {
653            assert!(VerbosityLevel::parse(value).is_some(), "allowed value {value:?} must parse");
654        }
655        for value in UiSurfacePreference::allowed_values() {
656            assert!(UiSurfacePreference::parse(value).is_some(), "allowed value {value:?} must parse");
657        }
658    }
659
660    /// `ResolvedShellPromptProfile` has no `parse()`, so it is not covered by
661    /// [`crate::test_support::assert_string_enum_lockstep`]. It still exposes the
662    /// same value through `Display`/`as_str()` and the derived `snake_case`
663    /// serde form — and it carries a schema derive — so pin those together.
664    #[test]
665    fn resolved_shell_prompt_profile_matches_display_and_serde() {
666        for profile in [
667            ResolvedShellPromptProfile::UnixLike,
668            ResolvedShellPromptProfile::PowerShell,
669        ] {
670            let expected = profile.to_string();
671            assert_eq!(
672                serde_json::to_value(profile).expect("resolved profile serializes"),
673                serde_json::json!(expected),
674                "serde wire form drifted from Display for {profile:?}"
675            );
676        }
677    }
678}