1use crate::core::PromptCachingConfig;
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::BTreeMap;
6use std::env;
7use std::fmt;
8use std::path::PathBuf;
9
10pub use vtcode_commons::reasoning::ReasoningEffortLevel;
12
13#[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 #[default]
23 Minimal,
24 Lightweight,
27 Default,
30 Specialized,
33}
34
35impl SystemPromptMode {
36 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 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 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#[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 Minimal,
99 #[default]
104 Progressive,
105 Full,
108}
109
110impl ToolDocumentationMode {
111 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 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 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#[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 #[default]
172 Auto,
173 UnixLike,
175 #[serde(rename = "powershell")]
181 PowerShell,
182}
183
184impl ShellPromptProfile {
185 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 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 pub fn allowed_values() -> &'static [&'static str] {
217 &["auto", "unix_like", "powershell"]
218 }
219
220 pub fn resolve_for_current_platform(self) -> ResolvedShellPromptProfile {
222 self.resolve_for_platform(ShellProfilePlatform::current())
223 }
224
225 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#[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#[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 #[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#[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 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 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 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#[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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
449pub enum ModelSelectionSource {
450 #[default]
452 WorkspaceConfig,
453 CliOverride,
455}
456
457#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
484pub enum CapabilityLevel {
485 Basic,
487 FileReading,
489 FileListing,
491 Bash,
493 Editing,
495 CodeSearch,
497}
498
499#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
533pub enum AnalysisDepth {
534 Basic,
535 Standard,
536 Deep,
537}
538
539#[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 #[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 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 #[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}