Skip to main content

vtcode_config/core/
agent.rs

1use crate::constants::{defaults, instructions, llm_generation, project_doc};
2use crate::types::{
3    EditingMode, ReasoningEffortLevel, SystemPromptMode, ToolDocumentationMode,
4    UiSurfacePreference, VerbosityLevel,
5};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9const DEFAULT_CHECKPOINTS_ENABLED: bool = true;
10const DEFAULT_MAX_SNAPSHOTS: usize = 50;
11const DEFAULT_MAX_AGE_DAYS: u64 = 30;
12
13/// Agent-wide configuration
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct AgentConfig {
17    /// AI provider for single agent mode (gemini, openai, anthropic, openrouter, zai)
18    #[serde(default = "default_provider")]
19    pub provider: String,
20
21    /// Environment variable that stores the API key for the active provider
22    #[serde(default = "default_api_key_env")]
23    pub api_key_env: String,
24
25    /// Default model to use
26    #[serde(default = "default_model")]
27    pub default_model: String,
28
29    /// UI theme identifier controlling ANSI styling
30    #[serde(default = "default_theme")]
31    pub theme: String,
32
33    /// System prompt mode controlling verbosity and token overhead
34    /// Options: minimal (~500-800 tokens), lightweight (~1-2k), default (~6-7k), specialized (~7-8k)
35    /// Inspired by pi-coding-agent: modern models often perform well with minimal prompts
36    #[serde(default)]
37    pub system_prompt_mode: SystemPromptMode,
38
39    /// Tool documentation mode controlling token overhead for tool definitions
40    /// Options: minimal (~800 tokens), progressive (~1.2k), full (~3k current)
41    /// Progressive: signatures upfront, detailed docs on-demand (recommended)
42    /// Minimal: signatures only, pi-coding-agent style (power users)
43    /// Full: all documentation upfront (current behavior, default)
44    #[serde(default)]
45    pub tool_documentation_mode: ToolDocumentationMode,
46
47    /// Enable split tool results for massive token savings (Phase 4)
48    /// When enabled, tools return dual-channel output:
49    /// - llm_content: Concise summary sent to LLM (token-optimized, 53-95% reduction)
50    /// - ui_content: Rich output displayed to user (full details preserved)
51    ///   Applies to: unified_search, unified_file, unified_exec
52    ///   Default: true (opt-out for compatibility), recommended for production use
53    #[serde(default = "default_enable_split_tool_results")]
54    pub enable_split_tool_results: bool,
55
56    /// Enable TODO planning helper mode for structured task management
57    #[serde(default = "default_todo_planning_mode")]
58    pub todo_planning_mode: bool,
59
60    /// Preferred rendering surface for the interactive chat UI (auto, alternate, inline)
61    #[serde(default)]
62    pub ui_surface: UiSurfacePreference,
63
64    /// Maximum number of conversation turns before auto-termination
65    #[serde(default = "default_max_conversation_turns")]
66    pub max_conversation_turns: usize,
67
68    /// Reasoning effort level for models that support it (none, minimal, low, medium, high, xhigh)
69    /// Applies to: Claude, GPT-5 family, Gemini, Qwen3, DeepSeek with reasoning capability
70    #[serde(default = "default_reasoning_effort")]
71    pub reasoning_effort: ReasoningEffortLevel,
72
73    /// Verbosity level for output text (low, medium, high)
74    /// Applies to: GPT-5.4-family Responses workflows and other models that support verbosity control
75    #[serde(default = "default_verbosity")]
76    pub verbosity: VerbosityLevel,
77
78    /// Temperature for main LLM responses (0.0-1.0)
79    /// Lower values = more deterministic, higher values = more creative
80    /// Recommended: 0.7 for balanced creativity and consistency
81    /// Range: 0.0 (deterministic) to 1.0 (maximum randomness)
82    #[serde(default = "default_temperature")]
83    pub temperature: f32,
84
85    /// Temperature for prompt refinement (0.0-1.0, default: 0.3)
86    /// Lower values ensure prompt refinement is more deterministic/consistent
87    /// Keep lower than main temperature for stable prompt improvement
88    #[serde(default = "default_refine_temperature")]
89    pub refine_temperature: f32,
90
91    /// Enable an extra self-review pass to refine final responses
92    #[serde(default = "default_enable_self_review")]
93    pub enable_self_review: bool,
94
95    /// Maximum number of self-review passes
96    #[serde(default = "default_max_review_passes")]
97    pub max_review_passes: usize,
98
99    /// Enable prompt refinement pass before sending to LLM
100    #[serde(default = "default_refine_prompts_enabled")]
101    pub refine_prompts_enabled: bool,
102
103    /// Max refinement passes for prompt writing
104    #[serde(default = "default_refine_max_passes")]
105    pub refine_prompts_max_passes: usize,
106
107    /// Optional model override for the refiner (empty = auto pick efficient sibling)
108    #[serde(default)]
109    pub refine_prompts_model: String,
110
111    /// Small/lightweight model configuration for efficient operations
112    /// Used for tasks like large file reads, parsing, git history, conversation summarization
113    /// Typically 70-80% cheaper than main model; ~50% of VT Code's calls use this tier
114    #[serde(default)]
115    pub small_model: AgentSmallModelConfig,
116
117    /// Session onboarding and welcome message configuration
118    #[serde(default)]
119    pub onboarding: AgentOnboardingConfig,
120
121    /// Maximum bytes of AGENTS.md content to load from project hierarchy
122    #[serde(default = "default_project_doc_max_bytes")]
123    pub project_doc_max_bytes: usize,
124
125    /// Additional filenames to check when AGENTS.md is absent at a directory level.
126    #[serde(default)]
127    pub project_doc_fallback_filenames: Vec<String>,
128
129    /// Maximum bytes of instruction content to load from AGENTS.md hierarchy
130    #[serde(
131        default = "default_instruction_max_bytes",
132        alias = "rule_doc_max_bytes"
133    )]
134    pub instruction_max_bytes: usize,
135
136    /// Additional instruction files or globs to merge into the hierarchy
137    #[serde(default, alias = "instruction_paths", alias = "instructions")]
138    pub instruction_files: Vec<String>,
139
140    /// Provider-specific API keys captured from interactive configuration flows
141    ///
142    /// Note: Actual API keys are stored securely in the OS keyring.
143    /// This field only tracks which providers have keys stored (for UI/migration purposes).
144    /// The keys themselves are NOT serialized to the config file for security.
145    #[serde(default, skip_serializing)]
146    pub custom_api_keys: BTreeMap<String, String>,
147
148    /// Preferred storage backend for credentials (OAuth tokens, API keys, etc.)
149    ///
150    /// - `keyring`: Use OS-specific secure storage (macOS Keychain, Windows Credential
151    ///   Manager, Linux Secret Service). This is the default as it's the most secure.
152    /// - `file`: Use AES-256-GCM encrypted file with machine-derived key
153    /// - `auto`: Try keyring first, fall back to file if unavailable
154    #[serde(default)]
155    pub credential_storage_mode: crate::auth::AuthCredentialsStoreMode,
156
157    /// Checkpointing configuration for automatic turn snapshots
158    #[serde(default)]
159    pub checkpointing: AgentCheckpointingConfig,
160
161    /// Vibe coding configuration for lazy or vague request support
162    #[serde(default)]
163    pub vibe_coding: AgentVibeCodingConfig,
164
165    /// Maximum number of retries for agent task execution (default: 2)
166    /// When an agent task fails due to retryable errors (timeout, network, 503, etc.),
167    /// it will be retried up to this many times with exponential backoff
168    #[serde(default = "default_max_task_retries")]
169    pub max_task_retries: u32,
170
171    /// Harness configuration for turn-level budgets, telemetry, and execution limits
172    #[serde(default)]
173    pub harness: AgentHarnessConfig,
174
175    /// Include current date/time in system prompt for temporal awareness
176    /// Helps LLM understand context for time-sensitive tasks (default: true)
177    #[serde(default = "default_include_temporal_context")]
178    pub include_temporal_context: bool,
179
180    /// Use UTC instead of local time for temporal context in system prompts
181    #[serde(default)]
182    pub temporal_context_use_utc: bool,
183
184    /// Include current working directory in system prompt (default: true)
185    #[serde(default = "default_include_working_directory")]
186    pub include_working_directory: bool,
187
188    /// Controls inclusion of the structured reasoning tag instructions block.
189    ///
190    /// Behavior:
191    /// - `Some(true)`: always include structured reasoning instructions.
192    /// - `Some(false)`: never include structured reasoning instructions.
193    /// - `None` (default): include only for `default` and `specialized` prompt modes.
194    ///
195    /// This keeps lightweight/minimal prompts smaller by default while allowing
196    /// explicit opt-in when users want tag-based reasoning guidance.
197    #[serde(default)]
198    pub include_structured_reasoning_tags: Option<bool>,
199
200    /// Custom instructions provided by the user via configuration to guide agent behavior
201    #[serde(default)]
202    pub user_instructions: Option<String>,
203
204    /// Default editing mode on startup: "edit" (default) or "plan"
205    /// Codex-inspired: Encourages structured planning before execution.
206    #[serde(default)]
207    pub default_editing_mode: EditingMode,
208
209    /// Require user confirmation before executing a plan generated in plan mode
210    /// When true, exiting plan mode shows the implementation blueprint and
211    /// requires explicit user approval before enabling edit tools.
212    #[serde(default = "default_require_plan_confirmation")]
213    pub require_plan_confirmation: bool,
214
215    /// Enable autonomous mode - auto-approve safe tools with reduced HITL prompts
216    /// When true, the agent operates with fewer confirmation prompts for safe tools.
217    #[serde(default = "default_autonomous_mode")]
218    pub autonomous_mode: bool,
219
220    /// Circuit breaker configuration for resilient tool execution
221    /// Controls when the agent should pause and ask for user guidance due to repeated failures
222    #[serde(default)]
223    pub circuit_breaker: CircuitBreakerConfig,
224
225    /// Open Responses specification compliance configuration
226    /// Enables vendor-neutral LLM API format for interoperable workflows
227    #[serde(default)]
228    pub open_responses: OpenResponsesConfig,
229}
230
231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
232#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))]
233#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
234#[serde(rename_all = "snake_case")]
235pub enum ContinuationPolicy {
236    Off,
237    ExecOnly,
238    #[default]
239    All,
240}
241
242impl ContinuationPolicy {
243    pub fn as_str(&self) -> &'static str {
244        match self {
245            Self::Off => "off",
246            Self::ExecOnly => "exec_only",
247            Self::All => "all",
248        }
249    }
250
251    pub fn parse(value: &str) -> Option<Self> {
252        let normalized = value.trim();
253        if normalized.eq_ignore_ascii_case("off") {
254            Some(Self::Off)
255        } else if normalized.eq_ignore_ascii_case("exec_only")
256            || normalized.eq_ignore_ascii_case("exec-only")
257        {
258            Some(Self::ExecOnly)
259        } else if normalized.eq_ignore_ascii_case("all") {
260            Some(Self::All)
261        } else {
262            None
263        }
264    }
265}
266
267impl<'de> Deserialize<'de> for ContinuationPolicy {
268    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
269    where
270        D: serde::Deserializer<'de>,
271    {
272        let raw = String::deserialize(deserializer)?;
273        Ok(Self::parse(&raw).unwrap_or_default())
274    }
275}
276
277#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
278#[derive(Debug, Clone, Deserialize, Serialize)]
279pub struct AgentHarnessConfig {
280    /// Maximum number of tool calls allowed per turn. Set to `0` to disable the cap.
281    #[serde(default = "default_harness_max_tool_calls_per_turn")]
282    pub max_tool_calls_per_turn: usize,
283    /// Maximum wall clock time (seconds) for tool execution in a turn
284    #[serde(default = "default_harness_max_tool_wall_clock_secs")]
285    pub max_tool_wall_clock_secs: u64,
286    /// Maximum retries for retryable tool errors
287    #[serde(default = "default_harness_max_tool_retries")]
288    pub max_tool_retries: u32,
289    /// Enable automatic context compaction when token pressure crosses threshold.
290    ///
291    /// Disabled by default. When disabled, no automatic compaction is triggered.
292    #[serde(default = "default_harness_auto_compaction_enabled")]
293    pub auto_compaction_enabled: bool,
294    /// Optional absolute compact threshold (tokens) for Responses server-side compaction.
295    ///
296    /// When unset, VT Code derives a threshold from the provider context window.
297    #[serde(default)]
298    pub auto_compaction_threshold_tokens: Option<u64>,
299    /// Controls whether harness-managed continuation loops are enabled.
300    #[serde(default)]
301    pub continuation_policy: ContinuationPolicy,
302    /// Optional JSONL event log path for harness events.
303    /// Defaults to `~/.vtcode/sessions/` when unset.
304    #[serde(default)]
305    pub event_log_path: Option<String>,
306}
307
308impl Default for AgentHarnessConfig {
309    fn default() -> Self {
310        Self {
311            max_tool_calls_per_turn: default_harness_max_tool_calls_per_turn(),
312            max_tool_wall_clock_secs: default_harness_max_tool_wall_clock_secs(),
313            max_tool_retries: default_harness_max_tool_retries(),
314            auto_compaction_enabled: default_harness_auto_compaction_enabled(),
315            auto_compaction_threshold_tokens: None,
316            continuation_policy: ContinuationPolicy::default(),
317            event_log_path: None,
318        }
319    }
320}
321
322#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
323#[derive(Debug, Clone, Deserialize, Serialize)]
324pub struct CircuitBreakerConfig {
325    /// Enable circuit breaker functionality
326    #[serde(default = "default_circuit_breaker_enabled")]
327    pub enabled: bool,
328
329    /// Number of consecutive failures before opening circuit
330    #[serde(default = "default_failure_threshold")]
331    pub failure_threshold: u32,
332
333    /// Pause and ask user when circuit opens (vs auto-backoff)
334    #[serde(default = "default_pause_on_open")]
335    pub pause_on_open: bool,
336
337    /// Number of open circuits before triggering pause
338    #[serde(default = "default_max_open_circuits")]
339    pub max_open_circuits: usize,
340
341    /// Cooldown period between recovery prompts (seconds)
342    #[serde(default = "default_recovery_cooldown")]
343    pub recovery_cooldown: u64,
344}
345
346impl Default for CircuitBreakerConfig {
347    fn default() -> Self {
348        Self {
349            enabled: default_circuit_breaker_enabled(),
350            failure_threshold: default_failure_threshold(),
351            pause_on_open: default_pause_on_open(),
352            max_open_circuits: default_max_open_circuits(),
353            recovery_cooldown: default_recovery_cooldown(),
354        }
355    }
356}
357
358/// Open Responses specification compliance configuration
359///
360/// Enables vendor-neutral LLM API format per the Open Responses specification
361/// (<https://www.openresponses.org/>). When enabled, VT Code emits semantic
362/// streaming events and uses standardized response/item structures.
363#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
364#[derive(Debug, Clone, Deserialize, Serialize)]
365pub struct OpenResponsesConfig {
366    /// Enable Open Responses specification compliance layer
367    /// When true, VT Code emits semantic streaming events alongside internal events
368    /// Default: false (opt-in feature)
369    #[serde(default)]
370    pub enabled: bool,
371
372    /// Emit Open Responses events to the event sink
373    /// When true, streaming events follow Open Responses format
374    /// (response.created, response.output_item.added, response.output_text.delta, etc.)
375    #[serde(default = "default_open_responses_emit_events")]
376    pub emit_events: bool,
377
378    /// Include VT Code extension items (vtcode:file_change, vtcode:web_search, etc.)
379    /// When false, extension items are omitted from the Open Responses output
380    #[serde(default = "default_open_responses_include_extensions")]
381    pub include_extensions: bool,
382
383    /// Map internal tool calls to Open Responses function_call items
384    /// When true, command executions and MCP tool calls are represented as function_call items
385    #[serde(default = "default_open_responses_map_tool_calls")]
386    pub map_tool_calls: bool,
387
388    /// Include reasoning items in Open Responses output
389    /// When true, model reasoning/thinking is exposed as reasoning items
390    #[serde(default = "default_open_responses_include_reasoning")]
391    pub include_reasoning: bool,
392}
393
394impl Default for OpenResponsesConfig {
395    fn default() -> Self {
396        Self {
397            enabled: false, // Opt-in by default
398            emit_events: default_open_responses_emit_events(),
399            include_extensions: default_open_responses_include_extensions(),
400            map_tool_calls: default_open_responses_map_tool_calls(),
401            include_reasoning: default_open_responses_include_reasoning(),
402        }
403    }
404}
405
406#[inline]
407const fn default_open_responses_emit_events() -> bool {
408    true // When enabled, emit events by default
409}
410
411#[inline]
412const fn default_open_responses_include_extensions() -> bool {
413    true // Include VT Code-specific extensions by default
414}
415
416#[inline]
417const fn default_open_responses_map_tool_calls() -> bool {
418    true // Map tool calls to function_call items by default
419}
420
421#[inline]
422const fn default_open_responses_include_reasoning() -> bool {
423    true // Include reasoning items by default
424}
425
426impl Default for AgentConfig {
427    fn default() -> Self {
428        Self {
429            provider: default_provider(),
430            api_key_env: default_api_key_env(),
431            default_model: default_model(),
432            theme: default_theme(),
433            system_prompt_mode: SystemPromptMode::default(),
434            tool_documentation_mode: ToolDocumentationMode::default(),
435            enable_split_tool_results: default_enable_split_tool_results(),
436            todo_planning_mode: default_todo_planning_mode(),
437            ui_surface: UiSurfacePreference::default(),
438            max_conversation_turns: default_max_conversation_turns(),
439            reasoning_effort: default_reasoning_effort(),
440            verbosity: default_verbosity(),
441            temperature: default_temperature(),
442            refine_temperature: default_refine_temperature(),
443            enable_self_review: default_enable_self_review(),
444            max_review_passes: default_max_review_passes(),
445            refine_prompts_enabled: default_refine_prompts_enabled(),
446            refine_prompts_max_passes: default_refine_max_passes(),
447            refine_prompts_model: String::new(),
448            small_model: AgentSmallModelConfig::default(),
449            onboarding: AgentOnboardingConfig::default(),
450            project_doc_max_bytes: default_project_doc_max_bytes(),
451            project_doc_fallback_filenames: Vec::new(),
452            instruction_max_bytes: default_instruction_max_bytes(),
453            instruction_files: Vec::new(),
454            custom_api_keys: BTreeMap::new(),
455            credential_storage_mode: crate::auth::AuthCredentialsStoreMode::default(),
456            checkpointing: AgentCheckpointingConfig::default(),
457            vibe_coding: AgentVibeCodingConfig::default(),
458            max_task_retries: default_max_task_retries(),
459            harness: AgentHarnessConfig::default(),
460            include_temporal_context: default_include_temporal_context(),
461            temporal_context_use_utc: false, // Default to local time
462            include_working_directory: default_include_working_directory(),
463            include_structured_reasoning_tags: None,
464            user_instructions: None,
465            default_editing_mode: EditingMode::default(),
466            require_plan_confirmation: default_require_plan_confirmation(),
467            autonomous_mode: default_autonomous_mode(),
468            circuit_breaker: CircuitBreakerConfig::default(),
469            open_responses: OpenResponsesConfig::default(),
470        }
471    }
472}
473
474impl AgentConfig {
475    /// Determine whether structured reasoning tag instructions should be included.
476    pub fn should_include_structured_reasoning_tags(&self) -> bool {
477        self.include_structured_reasoning_tags.unwrap_or(matches!(
478            self.system_prompt_mode,
479            SystemPromptMode::Default | SystemPromptMode::Specialized
480        ))
481    }
482
483    /// Validate LLM generation parameters
484    pub fn validate_llm_params(&self) -> Result<(), String> {
485        // Validate temperature range
486        if !(0.0..=1.0).contains(&self.temperature) {
487            return Err(format!(
488                "temperature must be between 0.0 and 1.0, got {}",
489                self.temperature
490            ));
491        }
492
493        if !(0.0..=1.0).contains(&self.refine_temperature) {
494            return Err(format!(
495                "refine_temperature must be between 0.0 and 1.0, got {}",
496                self.refine_temperature
497            ));
498        }
499
500        Ok(())
501    }
502}
503
504// Optimized: Use inline defaults with constants to reduce function call overhead
505#[inline]
506fn default_provider() -> String {
507    defaults::DEFAULT_PROVIDER.into()
508}
509
510#[inline]
511fn default_api_key_env() -> String {
512    defaults::DEFAULT_API_KEY_ENV.into()
513}
514
515#[inline]
516fn default_model() -> String {
517    defaults::DEFAULT_MODEL.into()
518}
519
520#[inline]
521fn default_theme() -> String {
522    defaults::DEFAULT_THEME.into()
523}
524
525#[inline]
526const fn default_todo_planning_mode() -> bool {
527    true
528}
529
530#[inline]
531const fn default_enable_split_tool_results() -> bool {
532    true // Default: enabled for production use (84% token savings)
533}
534
535#[inline]
536const fn default_max_conversation_turns() -> usize {
537    defaults::DEFAULT_MAX_CONVERSATION_TURNS
538}
539
540#[inline]
541fn default_reasoning_effort() -> ReasoningEffortLevel {
542    ReasoningEffortLevel::None
543}
544
545#[inline]
546fn default_verbosity() -> VerbosityLevel {
547    VerbosityLevel::default()
548}
549
550#[inline]
551const fn default_temperature() -> f32 {
552    llm_generation::DEFAULT_TEMPERATURE
553}
554
555#[inline]
556const fn default_refine_temperature() -> f32 {
557    llm_generation::DEFAULT_REFINE_TEMPERATURE
558}
559
560#[inline]
561const fn default_enable_self_review() -> bool {
562    false
563}
564
565#[inline]
566const fn default_max_review_passes() -> usize {
567    1
568}
569
570#[inline]
571const fn default_refine_prompts_enabled() -> bool {
572    false
573}
574
575#[inline]
576const fn default_refine_max_passes() -> usize {
577    1
578}
579
580#[inline]
581const fn default_project_doc_max_bytes() -> usize {
582    project_doc::DEFAULT_MAX_BYTES
583}
584
585#[inline]
586const fn default_instruction_max_bytes() -> usize {
587    instructions::DEFAULT_MAX_BYTES
588}
589
590#[inline]
591const fn default_max_task_retries() -> u32 {
592    2 // Retry twice on transient failures
593}
594
595#[inline]
596const fn default_harness_max_tool_calls_per_turn() -> usize {
597    defaults::DEFAULT_MAX_TOOL_CALLS_PER_TURN
598}
599
600#[inline]
601const fn default_harness_max_tool_wall_clock_secs() -> u64 {
602    defaults::DEFAULT_MAX_TOOL_WALL_CLOCK_SECS
603}
604
605#[inline]
606const fn default_harness_max_tool_retries() -> u32 {
607    defaults::DEFAULT_MAX_TOOL_RETRIES
608}
609
610#[inline]
611const fn default_harness_auto_compaction_enabled() -> bool {
612    false
613}
614
615#[inline]
616const fn default_include_temporal_context() -> bool {
617    true // Enable by default - minimal overhead (~20 tokens)
618}
619
620#[inline]
621const fn default_include_working_directory() -> bool {
622    true // Enable by default - minimal overhead (~10 tokens)
623}
624
625#[inline]
626const fn default_require_plan_confirmation() -> bool {
627    true // Default: require confirmation (HITL pattern)
628}
629
630#[inline]
631const fn default_autonomous_mode() -> bool {
632    false // Default: interactive mode with full HITL
633}
634
635#[inline]
636const fn default_circuit_breaker_enabled() -> bool {
637    true // Default: enabled for resilient execution
638}
639
640#[inline]
641const fn default_failure_threshold() -> u32 {
642    5 // Open circuit after 5 consecutive failures
643}
644
645#[inline]
646const fn default_pause_on_open() -> bool {
647    true // Default: ask user for guidance on circuit breaker
648}
649
650#[inline]
651const fn default_max_open_circuits() -> usize {
652    3 // Pause when 3+ tools have open circuits
653}
654
655#[inline]
656const fn default_recovery_cooldown() -> u64 {
657    60 // Cooldown between recovery prompts (seconds)
658}
659
660#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
661#[derive(Debug, Clone, Deserialize, Serialize)]
662pub struct AgentCheckpointingConfig {
663    /// Enable automatic checkpoints after each successful turn
664    #[serde(default = "default_checkpointing_enabled")]
665    pub enabled: bool,
666
667    /// Optional custom directory for storing checkpoints (relative to workspace or absolute)
668    #[serde(default)]
669    pub storage_dir: Option<String>,
670
671    /// Maximum number of checkpoints to retain on disk
672    #[serde(default = "default_checkpointing_max_snapshots")]
673    pub max_snapshots: usize,
674
675    /// Maximum age in days before checkpoints are removed automatically (None disables)
676    #[serde(default = "default_checkpointing_max_age_days")]
677    pub max_age_days: Option<u64>,
678}
679
680impl Default for AgentCheckpointingConfig {
681    fn default() -> Self {
682        Self {
683            enabled: default_checkpointing_enabled(),
684            storage_dir: None,
685            max_snapshots: default_checkpointing_max_snapshots(),
686            max_age_days: default_checkpointing_max_age_days(),
687        }
688    }
689}
690
691#[inline]
692const fn default_checkpointing_enabled() -> bool {
693    DEFAULT_CHECKPOINTS_ENABLED
694}
695
696#[inline]
697const fn default_checkpointing_max_snapshots() -> usize {
698    DEFAULT_MAX_SNAPSHOTS
699}
700
701#[inline]
702const fn default_checkpointing_max_age_days() -> Option<u64> {
703    Some(DEFAULT_MAX_AGE_DAYS)
704}
705
706#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
707#[derive(Debug, Clone, Deserialize, Serialize)]
708pub struct AgentOnboardingConfig {
709    /// Toggle onboarding message rendering
710    #[serde(default = "default_onboarding_enabled")]
711    pub enabled: bool,
712
713    /// Introductory text shown at session start
714    #[serde(default = "default_intro_text")]
715    pub intro_text: String,
716
717    /// Whether to include project overview in onboarding message
718    #[serde(default = "default_show_project_overview")]
719    pub include_project_overview: bool,
720
721    /// Whether to include language summary in onboarding message
722    #[serde(default = "default_show_language_summary")]
723    pub include_language_summary: bool,
724
725    /// Whether to include AGENTS.md highlights in onboarding message
726    #[serde(default = "default_show_guideline_highlights")]
727    pub include_guideline_highlights: bool,
728
729    /// Whether to surface usage tips inside the welcome text banner
730    #[serde(default = "default_show_usage_tips_in_welcome")]
731    pub include_usage_tips_in_welcome: bool,
732
733    /// Whether to surface suggested actions inside the welcome text banner
734    #[serde(default = "default_show_recommended_actions_in_welcome")]
735    pub include_recommended_actions_in_welcome: bool,
736
737    /// Maximum number of guideline bullets to surface
738    #[serde(default = "default_guideline_highlight_limit")]
739    pub guideline_highlight_limit: usize,
740
741    /// Tips for collaborating with the agent effectively
742    #[serde(default = "default_usage_tips")]
743    pub usage_tips: Vec<String>,
744
745    /// Recommended follow-up actions to display
746    #[serde(default = "default_recommended_actions")]
747    pub recommended_actions: Vec<String>,
748
749    /// Placeholder suggestion for the chat input bar
750    #[serde(default)]
751    pub chat_placeholder: Option<String>,
752}
753
754impl Default for AgentOnboardingConfig {
755    fn default() -> Self {
756        Self {
757            enabled: default_onboarding_enabled(),
758            intro_text: default_intro_text(),
759            include_project_overview: default_show_project_overview(),
760            include_language_summary: default_show_language_summary(),
761            include_guideline_highlights: default_show_guideline_highlights(),
762            include_usage_tips_in_welcome: default_show_usage_tips_in_welcome(),
763            include_recommended_actions_in_welcome: default_show_recommended_actions_in_welcome(),
764            guideline_highlight_limit: default_guideline_highlight_limit(),
765            usage_tips: default_usage_tips(),
766            recommended_actions: default_recommended_actions(),
767            chat_placeholder: None,
768        }
769    }
770}
771
772#[inline]
773const fn default_onboarding_enabled() -> bool {
774    true
775}
776
777const DEFAULT_INTRO_TEXT: &str =
778    "Let's get oriented. I preloaded workspace context so we can move fast.";
779
780#[inline]
781fn default_intro_text() -> String {
782    DEFAULT_INTRO_TEXT.into()
783}
784
785#[inline]
786const fn default_show_project_overview() -> bool {
787    true
788}
789
790#[inline]
791const fn default_show_language_summary() -> bool {
792    false
793}
794
795#[inline]
796const fn default_show_guideline_highlights() -> bool {
797    true
798}
799
800#[inline]
801const fn default_show_usage_tips_in_welcome() -> bool {
802    false
803}
804
805#[inline]
806const fn default_show_recommended_actions_in_welcome() -> bool {
807    false
808}
809
810#[inline]
811const fn default_guideline_highlight_limit() -> usize {
812    3
813}
814
815const DEFAULT_USAGE_TIPS: &[&str] = &[
816    "Describe your current coding goal or ask for a quick status overview.",
817    "Reference AGENTS.md guidelines when proposing changes.",
818    "Prefer asking for targeted file reads or diffs before editing.",
819];
820
821const DEFAULT_RECOMMENDED_ACTIONS: &[&str] = &[
822    "Review the highlighted guidelines and share the task you want to tackle.",
823    "Ask for a workspace tour if you need more context.",
824];
825
826fn default_usage_tips() -> Vec<String> {
827    DEFAULT_USAGE_TIPS.iter().map(|s| (*s).into()).collect()
828}
829
830fn default_recommended_actions() -> Vec<String> {
831    DEFAULT_RECOMMENDED_ACTIONS
832        .iter()
833        .map(|s| (*s).into())
834        .collect()
835}
836
837/// Small/lightweight model configuration for efficient operations
838///
839/// Following VT Code's pattern, use a smaller model (e.g., Haiku, GPT-4 Mini) for 50%+ of calls:
840/// - Large file reads and parsing (>50KB)
841/// - Web page summarization and analysis
842/// - Git history and commit message processing
843/// - One-word processing labels and simple classifications
844///
845/// Typically 70-80% cheaper than the main model while maintaining quality for these tasks.
846#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
847#[derive(Debug, Clone, Deserialize, Serialize)]
848pub struct AgentSmallModelConfig {
849    /// Enable small model tier for efficient operations
850    #[serde(default = "default_small_model_enabled")]
851    pub enabled: bool,
852
853    /// Small model to use (e.g., claude-4-5-haiku, "gpt-4-mini", "gemini-2.0-flash")
854    /// Leave empty to auto-select a lightweight sibling of the main model
855    #[serde(default)]
856    pub model: String,
857
858    /// Temperature for small model responses
859    #[serde(default = "default_small_model_temperature")]
860    pub temperature: f32,
861
862    /// Enable small model for large file reads (>50KB)
863    #[serde(default = "default_small_model_for_large_reads")]
864    pub use_for_large_reads: bool,
865
866    /// Enable small model for web content summarization
867    #[serde(default = "default_small_model_for_web_summary")]
868    pub use_for_web_summary: bool,
869
870    /// Enable small model for git history processing
871    #[serde(default = "default_small_model_for_git_history")]
872    pub use_for_git_history: bool,
873}
874
875impl Default for AgentSmallModelConfig {
876    fn default() -> Self {
877        Self {
878            enabled: default_small_model_enabled(),
879            model: String::new(),
880            temperature: default_small_model_temperature(),
881            use_for_large_reads: default_small_model_for_large_reads(),
882            use_for_web_summary: default_small_model_for_web_summary(),
883            use_for_git_history: default_small_model_for_git_history(),
884        }
885    }
886}
887
888#[inline]
889const fn default_small_model_enabled() -> bool {
890    true // Enable by default following VT Code pattern
891}
892
893#[inline]
894const fn default_small_model_temperature() -> f32 {
895    0.3 // More deterministic for parsing/summarization
896}
897
898#[inline]
899const fn default_small_model_for_large_reads() -> bool {
900    true
901}
902
903#[inline]
904const fn default_small_model_for_web_summary() -> bool {
905    true
906}
907
908#[inline]
909const fn default_small_model_for_git_history() -> bool {
910    true
911}
912
913/// Vibe coding configuration for lazy/vague request support
914///
915/// Enables intelligent context gathering and entity resolution to support
916/// casual, imprecise requests like "make it blue" or "decrease by half".
917#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
918#[derive(Debug, Clone, Deserialize, Serialize)]
919pub struct AgentVibeCodingConfig {
920    /// Enable vibe coding support
921    #[serde(default = "default_vibe_coding_enabled")]
922    pub enabled: bool,
923
924    /// Minimum prompt length for refinement (default: 5 chars)
925    #[serde(default = "default_vibe_min_prompt_length")]
926    pub min_prompt_length: usize,
927
928    /// Minimum prompt words for refinement (default: 2 words)
929    #[serde(default = "default_vibe_min_prompt_words")]
930    pub min_prompt_words: usize,
931
932    /// Enable fuzzy entity resolution
933    #[serde(default = "default_vibe_entity_resolution")]
934    pub enable_entity_resolution: bool,
935
936    /// Entity index cache file path (relative to workspace)
937    #[serde(default = "default_vibe_entity_cache")]
938    pub entity_index_cache: String,
939
940    /// Maximum entity matches to return (default: 5)
941    #[serde(default = "default_vibe_max_entity_matches")]
942    pub max_entity_matches: usize,
943
944    /// Track workspace state (file activity, value changes)
945    #[serde(default = "default_vibe_track_workspace")]
946    pub track_workspace_state: bool,
947
948    /// Maximum recent files to track (default: 20)
949    #[serde(default = "default_vibe_max_recent_files")]
950    pub max_recent_files: usize,
951
952    /// Track value history for inference
953    #[serde(default = "default_vibe_track_values")]
954    pub track_value_history: bool,
955
956    /// Enable conversation memory for pronoun resolution
957    #[serde(default = "default_vibe_conversation_memory")]
958    pub enable_conversation_memory: bool,
959
960    /// Maximum conversation turns to remember (default: 50)
961    #[serde(default = "default_vibe_max_memory_turns")]
962    pub max_memory_turns: usize,
963
964    /// Enable pronoun resolution (it, that, this)
965    #[serde(default = "default_vibe_pronoun_resolution")]
966    pub enable_pronoun_resolution: bool,
967
968    /// Enable proactive context gathering
969    #[serde(default = "default_vibe_proactive_context")]
970    pub enable_proactive_context: bool,
971
972    /// Maximum files to gather for context (default: 3)
973    #[serde(default = "default_vibe_max_context_files")]
974    pub max_context_files: usize,
975
976    /// Maximum code snippets per file (default: 20 lines)
977    #[serde(default = "default_vibe_max_snippets_per_file")]
978    pub max_context_snippets_per_file: usize,
979
980    /// Maximum search results to include (default: 5)
981    #[serde(default = "default_vibe_max_search_results")]
982    pub max_search_results: usize,
983
984    /// Enable relative value inference (by half, double, etc.)
985    #[serde(default = "default_vibe_value_inference")]
986    pub enable_relative_value_inference: bool,
987}
988
989impl Default for AgentVibeCodingConfig {
990    fn default() -> Self {
991        Self {
992            enabled: default_vibe_coding_enabled(),
993            min_prompt_length: default_vibe_min_prompt_length(),
994            min_prompt_words: default_vibe_min_prompt_words(),
995            enable_entity_resolution: default_vibe_entity_resolution(),
996            entity_index_cache: default_vibe_entity_cache(),
997            max_entity_matches: default_vibe_max_entity_matches(),
998            track_workspace_state: default_vibe_track_workspace(),
999            max_recent_files: default_vibe_max_recent_files(),
1000            track_value_history: default_vibe_track_values(),
1001            enable_conversation_memory: default_vibe_conversation_memory(),
1002            max_memory_turns: default_vibe_max_memory_turns(),
1003            enable_pronoun_resolution: default_vibe_pronoun_resolution(),
1004            enable_proactive_context: default_vibe_proactive_context(),
1005            max_context_files: default_vibe_max_context_files(),
1006            max_context_snippets_per_file: default_vibe_max_snippets_per_file(),
1007            max_search_results: default_vibe_max_search_results(),
1008            enable_relative_value_inference: default_vibe_value_inference(),
1009        }
1010    }
1011}
1012
1013// Vibe coding default functions
1014#[inline]
1015const fn default_vibe_coding_enabled() -> bool {
1016    false // Conservative default, opt-in
1017}
1018
1019#[inline]
1020const fn default_vibe_min_prompt_length() -> usize {
1021    5
1022}
1023
1024#[inline]
1025const fn default_vibe_min_prompt_words() -> usize {
1026    2
1027}
1028
1029#[inline]
1030const fn default_vibe_entity_resolution() -> bool {
1031    true
1032}
1033
1034#[inline]
1035fn default_vibe_entity_cache() -> String {
1036    ".vtcode/entity_index.json".into()
1037}
1038
1039#[inline]
1040const fn default_vibe_max_entity_matches() -> usize {
1041    5
1042}
1043
1044#[inline]
1045const fn default_vibe_track_workspace() -> bool {
1046    true
1047}
1048
1049#[inline]
1050const fn default_vibe_max_recent_files() -> usize {
1051    20
1052}
1053
1054#[inline]
1055const fn default_vibe_track_values() -> bool {
1056    true
1057}
1058
1059#[inline]
1060const fn default_vibe_conversation_memory() -> bool {
1061    true
1062}
1063
1064#[inline]
1065const fn default_vibe_max_memory_turns() -> usize {
1066    50
1067}
1068
1069#[inline]
1070const fn default_vibe_pronoun_resolution() -> bool {
1071    true
1072}
1073
1074#[inline]
1075const fn default_vibe_proactive_context() -> bool {
1076    true
1077}
1078
1079#[inline]
1080const fn default_vibe_max_context_files() -> usize {
1081    3
1082}
1083
1084#[inline]
1085const fn default_vibe_max_snippets_per_file() -> usize {
1086    20
1087}
1088
1089#[inline]
1090const fn default_vibe_max_search_results() -> usize {
1091    5
1092}
1093
1094#[inline]
1095const fn default_vibe_value_inference() -> bool {
1096    true
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101    use super::*;
1102
1103    #[test]
1104    fn test_continuation_policy_defaults_and_parses() {
1105        assert_eq!(ContinuationPolicy::default(), ContinuationPolicy::All);
1106        assert_eq!(
1107            ContinuationPolicy::parse("off"),
1108            Some(ContinuationPolicy::Off)
1109        );
1110        assert_eq!(
1111            ContinuationPolicy::parse("exec-only"),
1112            Some(ContinuationPolicy::ExecOnly)
1113        );
1114        assert_eq!(
1115            ContinuationPolicy::parse("all"),
1116            Some(ContinuationPolicy::All)
1117        );
1118        assert_eq!(ContinuationPolicy::parse("invalid"), None);
1119    }
1120
1121    #[test]
1122    fn test_harness_config_continuation_policy_deserializes_with_fallback() {
1123        let parsed: AgentHarnessConfig =
1124            toml::from_str("continuation_policy = \"all\"").expect("valid harness config");
1125        assert_eq!(parsed.continuation_policy, ContinuationPolicy::All);
1126
1127        let fallback: AgentHarnessConfig =
1128            toml::from_str("continuation_policy = \"unexpected\"").expect("fallback config");
1129        assert_eq!(fallback.continuation_policy, ContinuationPolicy::All);
1130    }
1131
1132    #[test]
1133    fn test_editing_mode_config_default() {
1134        let config = AgentConfig::default();
1135        assert_eq!(config.default_editing_mode, EditingMode::Edit);
1136        assert!(config.require_plan_confirmation);
1137        assert!(!config.autonomous_mode);
1138    }
1139
1140    #[test]
1141    fn test_structured_reasoning_defaults_follow_prompt_mode() {
1142        let default_mode = AgentConfig {
1143            system_prompt_mode: SystemPromptMode::Default,
1144            ..Default::default()
1145        };
1146        assert!(default_mode.should_include_structured_reasoning_tags());
1147
1148        let specialized_mode = AgentConfig {
1149            system_prompt_mode: SystemPromptMode::Specialized,
1150            ..Default::default()
1151        };
1152        assert!(specialized_mode.should_include_structured_reasoning_tags());
1153
1154        let minimal_mode = AgentConfig {
1155            system_prompt_mode: SystemPromptMode::Minimal,
1156            ..Default::default()
1157        };
1158        assert!(!minimal_mode.should_include_structured_reasoning_tags());
1159
1160        let lightweight_mode = AgentConfig {
1161            system_prompt_mode: SystemPromptMode::Lightweight,
1162            ..Default::default()
1163        };
1164        assert!(!lightweight_mode.should_include_structured_reasoning_tags());
1165    }
1166
1167    #[test]
1168    fn test_structured_reasoning_explicit_override() {
1169        let mut config = AgentConfig {
1170            system_prompt_mode: SystemPromptMode::Minimal,
1171            include_structured_reasoning_tags: Some(true),
1172            ..AgentConfig::default()
1173        };
1174        assert!(config.should_include_structured_reasoning_tags());
1175
1176        config.include_structured_reasoning_tags = Some(false);
1177        assert!(!config.should_include_structured_reasoning_tags());
1178    }
1179}