Skip to main content

vtcode_config/core/
agent.rs

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