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