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    PowerShell,
177}
178
179impl ShellPromptProfile {
180    /// Return the textual representation for configuration.
181    fn as_str(self) -> &'static str {
182        match self {
183            Self::Auto => "auto",
184            Self::UnixLike => "unix_like",
185            Self::PowerShell => "powershell",
186        }
187    }
188
189    /// Parse a shell prompt profile from user configuration.
190    fn parse(value: &str) -> Option<Self> {
191        let normalized = value.trim();
192        if normalized.eq_ignore_ascii_case("auto") {
193            Some(Self::Auto)
194        } else if normalized.eq_ignore_ascii_case("unix_like")
195            || normalized.eq_ignore_ascii_case("unix-like")
196            || normalized.eq_ignore_ascii_case("unix")
197            || normalized.eq_ignore_ascii_case("posix")
198        {
199            Some(Self::UnixLike)
200        } else if normalized.eq_ignore_ascii_case("powershell")
201            || normalized.eq_ignore_ascii_case("power_shell")
202            || normalized.eq_ignore_ascii_case("windows")
203        {
204            Some(Self::PowerShell)
205        } else {
206            None
207        }
208    }
209
210    /// Allowed configuration values for validation.
211    pub fn allowed_values() -> &'static [&'static str] {
212        &["auto", "unix_like", "powershell"]
213    }
214
215    /// Resolve this configured value against the current runtime platform.
216    pub fn resolve_for_current_platform(self) -> ResolvedShellPromptProfile {
217        self.resolve_for_platform(ShellProfilePlatform::current())
218    }
219
220    /// Resolve this configured value against an explicit platform.
221    fn resolve_for_platform(self, platform: ShellProfilePlatform) -> ResolvedShellPromptProfile {
222        match self {
223            Self::Auto => match platform {
224                ShellProfilePlatform::NativeWindows => ResolvedShellPromptProfile::PowerShell,
225                ShellProfilePlatform::UnixLike | ShellProfilePlatform::Wsl => ResolvedShellPromptProfile::UnixLike,
226            },
227            Self::UnixLike => ResolvedShellPromptProfile::UnixLike,
228            Self::PowerShell => ResolvedShellPromptProfile::PowerShell,
229        }
230    }
231}
232
233impl fmt::Display for ShellPromptProfile {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        f.write_str(self.as_str())
236    }
237}
238
239impl<'de> Deserialize<'de> for ShellPromptProfile {
240    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241    where
242        D: Deserializer<'de>,
243    {
244        let raw = String::deserialize(deserializer)?;
245        if let Some(parsed) = Self::parse(&raw) {
246            Ok(parsed)
247        } else {
248            Ok(Self::default())
249        }
250    }
251}
252
253/// Platform category used to resolve [`ShellPromptProfile::Auto`].
254#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
256#[serde(rename_all = "snake_case")]
257pub enum ShellProfilePlatform {
258    UnixLike,
259    Wsl,
260    NativeWindows,
261}
262
263impl ShellProfilePlatform {
264    fn current() -> Self {
265        if cfg!(windows) {
266            Self::NativeWindows
267        } else if is_wsl_environment() {
268            Self::Wsl
269        } else {
270            Self::UnixLike
271        }
272    }
273}
274
275/// Prompt profile after automatic platform resolution.
276#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
278#[serde(rename_all = "snake_case")]
279pub enum ResolvedShellPromptProfile {
280    UnixLike,
281    PowerShell,
282}
283
284impl ResolvedShellPromptProfile {
285    fn as_str(self) -> &'static str {
286        match self {
287            Self::UnixLike => "unix_like",
288            Self::PowerShell => "powershell",
289        }
290    }
291}
292
293impl fmt::Display for ResolvedShellPromptProfile {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        f.write_str(self.as_str())
296    }
297}
298
299fn is_wsl_environment() -> bool {
300    if env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some() {
301        return true;
302    }
303
304    for path in ["/proc/sys/kernel/osrelease", "/proc/version"] {
305        if let Ok(contents) = std::fs::read_to_string(path)
306            && contents.to_ascii_lowercase().contains("microsoft")
307        {
308            return true;
309        }
310    }
311
312    false
313}
314
315/// Verbosity level for model output (GPT-5.4-family and compatible models)
316#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
318#[serde(rename_all = "lowercase")]
319#[derive(Default)]
320pub enum VerbosityLevel {
321    Low,
322    #[default]
323    Medium,
324    High,
325}
326
327impl VerbosityLevel {
328    /// Return the textual representation expected by downstream APIs
329    pub fn as_str(self) -> &'static str {
330        match self {
331            Self::Low => "low",
332            Self::Medium => "medium",
333            Self::High => "high",
334        }
335    }
336
337    /// Attempt to parse a verbosity level from user configuration input
338    pub fn parse(value: &str) -> Option<Self> {
339        let normalized = value.trim();
340        if normalized.eq_ignore_ascii_case("low") {
341            Some(Self::Low)
342        } else if normalized.eq_ignore_ascii_case("medium") {
343            Some(Self::Medium)
344        } else if normalized.eq_ignore_ascii_case("high") {
345            Some(Self::High)
346        } else {
347            None
348        }
349    }
350
351    /// Enumerate the allowed configuration values
352    pub fn allowed_values() -> &'static [&'static str] {
353        &["low", "medium", "high"]
354    }
355}
356
357impl fmt::Display for VerbosityLevel {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        f.write_str(self.as_str())
360    }
361}
362
363impl<'de> Deserialize<'de> for VerbosityLevel {
364    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
365    where
366        D: Deserializer<'de>,
367    {
368        let raw = String::deserialize(deserializer)?;
369        if let Some(parsed) = Self::parse(&raw) {
370            Ok(parsed)
371        } else {
372            Ok(Self::default())
373        }
374    }
375}
376
377/// Preferred rendering surface for the interactive chat UI
378#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
380#[serde(rename_all = "lowercase")]
381#[derive(Default)]
382pub enum UiSurfacePreference {
383    Auto,
384    Alternate,
385    #[default]
386    Inline,
387}
388
389impl UiSurfacePreference {
390    /// String representation used in configuration and logging
391    fn as_str(self) -> &'static str {
392        match self {
393            Self::Auto => "auto",
394            Self::Alternate => "alternate",
395            Self::Inline => "inline",
396        }
397    }
398
399    /// Parse a surface preference from configuration input
400    fn parse(value: &str) -> Option<Self> {
401        let normalized = value.trim();
402        if normalized.eq_ignore_ascii_case("auto") {
403            Some(Self::Auto)
404        } else if normalized.eq_ignore_ascii_case("alternate") || normalized.eq_ignore_ascii_case("alt") {
405            Some(Self::Alternate)
406        } else if normalized.eq_ignore_ascii_case("inline") {
407            Some(Self::Inline)
408        } else {
409            None
410        }
411    }
412
413    /// Enumerate the accepted configuration values for validation messaging
414    pub fn allowed_values() -> &'static [&'static str] {
415        &["auto", "alternate", "inline"]
416    }
417}
418
419impl fmt::Display for UiSurfacePreference {
420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421        f.write_str(self.as_str())
422    }
423}
424
425impl<'de> Deserialize<'de> for UiSurfacePreference {
426    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
427    where
428        D: Deserializer<'de>,
429    {
430        let raw = String::deserialize(deserializer)?;
431        if let Some(parsed) = Self::parse(&raw) {
432            Ok(parsed)
433        } else {
434            Ok(Self::default())
435        }
436    }
437}
438
439/// Source describing how the active model was selected
440#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
441pub enum ModelSelectionSource {
442    /// Model provided by workspace configuration
443    #[default]
444    WorkspaceConfig,
445    /// Model provided by CLI override
446    CliOverride,
447}
448
449/// Configuration for the agent
450#[derive(Debug, Clone)]
451pub struct AgentConfig {
452    pub model: String,
453    pub api_key: String,
454    pub provider: String,
455    pub openai_chatgpt_auth: Option<crate::auth::OpenAIChatGptAuthHandle>,
456    pub api_key_env: String,
457    pub workspace: PathBuf,
458    pub verbose: bool,
459    pub quiet: bool,
460    pub theme: String,
461    pub reasoning_effort: ReasoningEffortLevel,
462    pub ui_surface: UiSurfacePreference,
463    pub prompt_cache: PromptCachingConfig,
464    pub model_source: ModelSelectionSource,
465    pub custom_api_keys: BTreeMap<String, String>,
466    pub checkpointing_enabled: bool,
467    pub checkpointing_storage_dir: Option<PathBuf>,
468    pub checkpointing_max_snapshots: usize,
469    pub checkpointing_max_age_days: Option<u64>,
470    pub max_conversation_turns: usize,
471    pub model_behavior: Option<crate::core::ModelConfig>,
472}
473
474/// Workshop agent capability levels
475#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
476pub enum CapabilityLevel {
477    /// Basic chat only
478    Basic,
479    /// Can read files
480    FileReading,
481    /// Can read files and list directories
482    FileListing,
483    /// Can read files, list directories, and run bash commands
484    Bash,
485    /// Can read files, list directories, run bash commands, and edit files
486    Editing,
487    /// Full capabilities including code search
488    CodeSearch,
489}
490
491/// Session information
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct SessionInfo {
494    pub session_id: String,
495    pub start_time: u64,
496    pub total_turns: usize,
497    pub total_decisions: usize,
498    pub error_count: usize,
499}
500
501/// Error information for tracking
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct ErrorInfo {
504    error_type: String,
505    message: String,
506    turn_number: usize,
507    recoverable: bool,
508    timestamp: u64,
509}
510
511/// Performance metrics
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct PerformanceMetrics {
514    pub session_duration_seconds: u64,
515    pub total_api_calls: usize,
516    pub total_tokens_used: Option<usize>,
517    pub average_response_time_ms: f64,
518    pub tool_execution_count: usize,
519    pub error_count: usize,
520    pub recovery_success_rate: f64,
521}
522
523/// Analysis depth for workspace analysis
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub enum AnalysisDepth {
526    Basic,
527    Standard,
528    Deep,
529}
530
531/// Output format for commands
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub enum OutputFormat {
534    Text,
535    Json,
536    Html,
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    #[test]
544    fn test_reasoning_effort_parse_and_allowed_values_include_max() {
545        assert_eq!(ReasoningEffortLevel::parse("max"), Some(ReasoningEffortLevel::Max));
546        assert_eq!(ReasoningEffortLevel::Max.as_str(), "max");
547        assert!(ReasoningEffortLevel::allowed_values().contains(&"max"));
548    }
549
550    #[test]
551    fn shell_prompt_profile_parses_aliases() {
552        assert_eq!(ShellPromptProfile::parse("auto"), Some(ShellPromptProfile::Auto));
553        assert_eq!(ShellPromptProfile::parse("unix-like"), Some(ShellPromptProfile::UnixLike));
554        assert_eq!(ShellPromptProfile::parse("posix"), Some(ShellPromptProfile::UnixLike));
555        assert_eq!(ShellPromptProfile::parse("PowerShell"), Some(ShellPromptProfile::PowerShell));
556        assert_eq!(ShellPromptProfile::parse("unknown"), None);
557    }
558
559    #[test]
560    fn ui_surface_defaults_to_inline() {
561        assert_eq!(UiSurfacePreference::default(), UiSurfacePreference::Inline);
562    }
563
564    #[test]
565    fn auto_shell_prompt_profile_selects_platform_defaults() {
566        let profile = ShellPromptProfile::Auto;
567
568        assert_eq!(profile.resolve_for_platform(ShellProfilePlatform::UnixLike), ResolvedShellPromptProfile::UnixLike);
569        assert_eq!(profile.resolve_for_platform(ShellProfilePlatform::Wsl), ResolvedShellPromptProfile::UnixLike);
570        assert_eq!(
571            profile.resolve_for_platform(ShellProfilePlatform::NativeWindows),
572            ResolvedShellPromptProfile::PowerShell
573        );
574    }
575
576    #[test]
577    fn explicit_shell_prompt_profiles_override_platform_defaults() {
578        assert_eq!(
579            ShellPromptProfile::UnixLike.resolve_for_platform(ShellProfilePlatform::NativeWindows),
580            ResolvedShellPromptProfile::UnixLike
581        );
582        assert_eq!(
583            ShellPromptProfile::PowerShell.resolve_for_platform(ShellProfilePlatform::Wsl),
584            ResolvedShellPromptProfile::PowerShell
585        );
586    }
587}