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