vtcode_config/core/
agent.rs

1use crate::constants::{defaults, instructions, llm_generation, project_doc, prompts};
2use crate::types::{ReasoningEffortLevel, UiSurfacePreference, VerbosityLevel};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6const DEFAULT_CHECKPOINTS_ENABLED: bool = true;
7const DEFAULT_MAX_SNAPSHOTS: usize = 50;
8const DEFAULT_MAX_AGE_DAYS: u64 = 30;
9
10/// Agent-wide configuration
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct AgentConfig {
14    /// AI provider for single agent mode (gemini, openai, anthropic, openrouter, xai, zai)
15    #[serde(default = "default_provider")]
16    pub provider: String,
17
18    /// Environment variable that stores the API key for the active provider
19    #[serde(default = "default_api_key_env")]
20    pub api_key_env: String,
21
22    /// Default model to use
23    #[serde(default = "default_model")]
24    pub default_model: String,
25
26    /// UI theme identifier controlling ANSI styling
27    #[serde(default = "default_theme")]
28    pub theme: String,
29
30    /// Enable TODO planning workflow integrations (update_plan tool, onboarding hints)
31    #[serde(default = "default_todo_planning_mode")]
32    pub todo_planning_mode: bool,
33
34    /// Preferred rendering surface for the interactive chat UI (auto, alternate, inline)
35    #[serde(default)]
36    pub ui_surface: UiSurfacePreference,
37
38    /// Maximum number of conversation turns before auto-termination
39    #[serde(default = "default_max_conversation_turns")]
40    pub max_conversation_turns: usize,
41
42    /// Reasoning effort level for models that support it (none, low, medium, high)
43    /// Applies to: Claude, GPT-5, GPT-5.1, Gemini, Qwen3, DeepSeek with reasoning capability
44    #[serde(default = "default_reasoning_effort")]
45    pub reasoning_effort: ReasoningEffortLevel,
46
47    /// Verbosity level for output text (low, medium, high)
48    /// Applies to: GPT-5.1 and other models that support verbosity control
49    #[serde(default = "default_verbosity")]
50    pub verbosity: VerbosityLevel,
51
52    /// Temperature for main LLM responses (0.0-1.0)
53    /// Lower values = more deterministic, higher values = more creative
54    /// Recommended: 0.7 for balanced creativity and consistency
55    /// Range: 0.0 (deterministic) to 1.0 (maximum randomness)
56    #[serde(default = "default_temperature")]
57    pub temperature: f32,
58
59    /// Maximum tokens for main LLM generation responses (default: 2000)
60    /// Adjust based on model context window size:
61    /// - 2000 for standard tasks
62    /// - 16384 for models with 128k context
63    /// - 32768 for models with 256k context
64    #[serde(default = "default_max_tokens")]
65    pub max_tokens: u32,
66
67    /// Temperature for prompt refinement (0.0-1.0, default: 0.3)
68    /// Lower values ensure prompt refinement is more deterministic/consistent
69    /// Keep lower than main temperature for stable prompt improvement
70    #[serde(default = "default_refine_temperature")]
71    pub refine_temperature: f32,
72
73    /// Maximum tokens for prompt refinement (default: 800)
74    /// Prompts are typically shorter, so 800 tokens is usually sufficient
75    #[serde(default = "default_refine_max_tokens")]
76    pub refine_max_tokens: u32,
77
78    /// Enable an extra self-review pass to refine final responses
79    #[serde(default = "default_enable_self_review")]
80    pub enable_self_review: bool,
81
82    /// Maximum number of self-review passes
83    #[serde(default = "default_max_review_passes")]
84    pub max_review_passes: usize,
85
86    /// Enable prompt refinement pass before sending to LLM
87    #[serde(default = "default_refine_prompts_enabled")]
88    pub refine_prompts_enabled: bool,
89
90    /// Max refinement passes for prompt writing
91    #[serde(default = "default_refine_max_passes")]
92    pub refine_prompts_max_passes: usize,
93
94    /// Optional model override for the refiner (empty = auto pick efficient sibling)
95    #[serde(default)]
96    pub refine_prompts_model: String,
97
98    /// Small/lightweight model configuration for efficient operations
99    /// Used for tasks like large file reads, parsing, git history, conversation summarization
100    /// Typically 70-80% cheaper than main model; ~50% of Claude Code's calls use this tier
101    #[serde(default)]
102    pub small_model: AgentSmallModelConfig,
103
104    /// Session onboarding and welcome message configuration
105    #[serde(default)]
106    pub onboarding: AgentOnboardingConfig,
107
108    /// Maximum bytes of AGENTS.md content to load from project hierarchy
109    #[serde(default = "default_project_doc_max_bytes")]
110    pub project_doc_max_bytes: usize,
111
112    /// Maximum bytes of instruction content to load from AGENTS.md hierarchy
113    #[serde(
114        default = "default_instruction_max_bytes",
115        alias = "rule_doc_max_bytes"
116    )]
117    pub instruction_max_bytes: usize,
118
119    /// Additional instruction files or globs to merge into the hierarchy
120    #[serde(default, alias = "instruction_paths", alias = "instructions")]
121    pub instruction_files: Vec<String>,
122
123    /// Custom prompt configuration for slash command shortcuts
124    #[serde(default)]
125    pub custom_prompts: AgentCustomPromptsConfig,
126
127    /// Provider-specific API keys captured from interactive configuration flows
128    #[serde(default)]
129    pub custom_api_keys: BTreeMap<String, String>,
130
131    /// Checkpointing configuration for automatic turn snapshots
132    #[serde(default)]
133    pub checkpointing: AgentCheckpointingConfig,
134}
135
136impl Default for AgentConfig {
137    fn default() -> Self {
138        Self {
139            provider: default_provider(),
140            api_key_env: default_api_key_env(),
141            default_model: default_model(),
142            theme: default_theme(),
143            todo_planning_mode: default_todo_planning_mode(),
144            ui_surface: UiSurfacePreference::default(),
145            max_conversation_turns: default_max_conversation_turns(),
146            reasoning_effort: default_reasoning_effort(),
147            verbosity: default_verbosity(),
148            temperature: default_temperature(),
149            max_tokens: default_max_tokens(),
150            refine_temperature: default_refine_temperature(),
151            refine_max_tokens: default_refine_max_tokens(),
152            enable_self_review: default_enable_self_review(),
153            max_review_passes: default_max_review_passes(),
154            refine_prompts_enabled: default_refine_prompts_enabled(),
155            refine_prompts_max_passes: default_refine_max_passes(),
156            refine_prompts_model: String::new(),
157            small_model: AgentSmallModelConfig::default(),
158            onboarding: AgentOnboardingConfig::default(),
159            project_doc_max_bytes: default_project_doc_max_bytes(),
160            instruction_max_bytes: default_instruction_max_bytes(),
161            instruction_files: Vec::new(),
162            custom_prompts: AgentCustomPromptsConfig::default(),
163            custom_api_keys: BTreeMap::new(),
164            checkpointing: AgentCheckpointingConfig::default(),
165        }
166    }
167}
168
169impl AgentConfig {
170    /// Validate LLM generation parameters
171    pub fn validate_llm_params(&self) -> Result<(), String> {
172        // Validate temperature range
173        if !(0.0..=1.0).contains(&self.temperature) {
174            return Err(format!(
175                "temperature must be between 0.0 and 1.0, got {}",
176                self.temperature
177            ));
178        }
179
180        if !(0.0..=1.0).contains(&self.refine_temperature) {
181            return Err(format!(
182                "refine_temperature must be between 0.0 and 1.0, got {}",
183                self.refine_temperature
184            ));
185        }
186
187        // Validate token limits (use static str to avoid allocation)
188        if self.max_tokens == 0 {
189            return Err("max_tokens must be greater than 0".into());
190        }
191
192        if self.refine_max_tokens == 0 {
193            return Err("refine_max_tokens must be greater than 0".into());
194        }
195
196        Ok(())
197    }
198}
199
200// Optimized: Use inline defaults with constants to reduce function call overhead
201#[inline]
202fn default_provider() -> String {
203    defaults::DEFAULT_PROVIDER.into()
204}
205
206#[inline]
207fn default_api_key_env() -> String {
208    defaults::DEFAULT_API_KEY_ENV.into()
209}
210
211#[inline]
212fn default_model() -> String {
213    defaults::DEFAULT_MODEL.into()
214}
215
216#[inline]
217fn default_theme() -> String {
218    defaults::DEFAULT_THEME.into()
219}
220
221#[inline]
222const fn default_todo_planning_mode() -> bool {
223    true
224}
225
226#[inline]
227const fn default_max_conversation_turns() -> usize {
228    150
229}
230
231#[inline]
232fn default_reasoning_effort() -> ReasoningEffortLevel {
233    ReasoningEffortLevel::default()
234}
235
236#[inline]
237fn default_verbosity() -> VerbosityLevel {
238    VerbosityLevel::default()
239}
240
241#[inline]
242const fn default_temperature() -> f32 {
243    llm_generation::DEFAULT_TEMPERATURE
244}
245
246#[inline]
247const fn default_max_tokens() -> u32 {
248    llm_generation::DEFAULT_MAX_TOKENS
249}
250
251#[inline]
252const fn default_refine_temperature() -> f32 {
253    llm_generation::DEFAULT_REFINE_TEMPERATURE
254}
255
256#[inline]
257const fn default_refine_max_tokens() -> u32 {
258    llm_generation::DEFAULT_REFINE_MAX_TOKENS
259}
260
261#[inline]
262const fn default_enable_self_review() -> bool {
263    false
264}
265
266#[inline]
267const fn default_max_review_passes() -> usize {
268    1
269}
270
271#[inline]
272const fn default_refine_prompts_enabled() -> bool {
273    false
274}
275
276#[inline]
277const fn default_refine_max_passes() -> usize {
278    1
279}
280
281#[inline]
282const fn default_project_doc_max_bytes() -> usize {
283    project_doc::DEFAULT_MAX_BYTES
284}
285
286#[inline]
287const fn default_instruction_max_bytes() -> usize {
288    instructions::DEFAULT_MAX_BYTES
289}
290
291#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
292#[derive(Debug, Clone, Deserialize, Serialize)]
293pub struct AgentCustomPromptsConfig {
294    /// Master switch for custom prompt support
295    #[serde(default = "default_custom_prompts_enabled")]
296    pub enabled: bool,
297
298    /// Primary directory for prompt markdown files
299    #[serde(default = "default_custom_prompts_directory")]
300    pub directory: String,
301
302    /// Additional directories to search for prompts
303    #[serde(default)]
304    pub extra_directories: Vec<String>,
305
306    /// Maximum file size (KB) to load for a single prompt
307    #[serde(default = "default_custom_prompts_max_file_size_kb")]
308    pub max_file_size_kb: usize,
309}
310
311impl Default for AgentCustomPromptsConfig {
312    fn default() -> Self {
313        Self {
314            enabled: default_custom_prompts_enabled(),
315            directory: default_custom_prompts_directory(),
316            extra_directories: Vec::new(),
317            max_file_size_kb: default_custom_prompts_max_file_size_kb(),
318        }
319    }
320}
321
322#[inline]
323const fn default_custom_prompts_enabled() -> bool {
324    true
325}
326
327#[inline]
328fn default_custom_prompts_directory() -> String {
329    prompts::DEFAULT_CUSTOM_PROMPTS_DIR.into()
330}
331
332#[inline]
333const fn default_custom_prompts_max_file_size_kb() -> usize {
334    prompts::DEFAULT_CUSTOM_PROMPT_MAX_FILE_SIZE_KB
335}
336
337#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
338#[derive(Debug, Clone, Deserialize, Serialize)]
339pub struct AgentCheckpointingConfig {
340    /// Enable automatic checkpoints after each successful turn
341    #[serde(default = "default_checkpointing_enabled")]
342    pub enabled: bool,
343
344    /// Optional custom directory for storing checkpoints (relative to workspace or absolute)
345    #[serde(default)]
346    pub storage_dir: Option<String>,
347
348    /// Maximum number of checkpoints to retain on disk
349    #[serde(default = "default_checkpointing_max_snapshots")]
350    pub max_snapshots: usize,
351
352    /// Maximum age in days before checkpoints are removed automatically (None disables)
353    #[serde(default = "default_checkpointing_max_age_days")]
354    pub max_age_days: Option<u64>,
355}
356
357impl Default for AgentCheckpointingConfig {
358    fn default() -> Self {
359        Self {
360            enabled: default_checkpointing_enabled(),
361            storage_dir: None,
362            max_snapshots: default_checkpointing_max_snapshots(),
363            max_age_days: default_checkpointing_max_age_days(),
364        }
365    }
366}
367
368#[inline]
369const fn default_checkpointing_enabled() -> bool {
370    DEFAULT_CHECKPOINTS_ENABLED
371}
372
373#[inline]
374const fn default_checkpointing_max_snapshots() -> usize {
375    DEFAULT_MAX_SNAPSHOTS
376}
377
378#[inline]
379const fn default_checkpointing_max_age_days() -> Option<u64> {
380    Some(DEFAULT_MAX_AGE_DAYS)
381}
382
383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
384#[derive(Debug, Clone, Deserialize, Serialize)]
385pub struct AgentOnboardingConfig {
386    /// Toggle onboarding message rendering
387    #[serde(default = "default_onboarding_enabled")]
388    pub enabled: bool,
389
390    /// Introductory text shown at session start
391    #[serde(default = "default_intro_text")]
392    pub intro_text: String,
393
394    /// Whether to include project overview in onboarding message
395    #[serde(default = "default_show_project_overview")]
396    pub include_project_overview: bool,
397
398    /// Whether to include language summary in onboarding message
399    #[serde(default = "default_show_language_summary")]
400    pub include_language_summary: bool,
401
402    /// Whether to include AGENTS.md highlights in onboarding message
403    #[serde(default = "default_show_guideline_highlights")]
404    pub include_guideline_highlights: bool,
405
406    /// Whether to surface usage tips inside the welcome text banner
407    #[serde(default = "default_show_usage_tips_in_welcome")]
408    pub include_usage_tips_in_welcome: bool,
409
410    /// Whether to surface suggested actions inside the welcome text banner
411    #[serde(default = "default_show_recommended_actions_in_welcome")]
412    pub include_recommended_actions_in_welcome: bool,
413
414    /// Maximum number of guideline bullets to surface
415    #[serde(default = "default_guideline_highlight_limit")]
416    pub guideline_highlight_limit: usize,
417
418    /// Tips for collaborating with the agent effectively
419    #[serde(default = "default_usage_tips")]
420    pub usage_tips: Vec<String>,
421
422    /// Recommended follow-up actions to display
423    #[serde(default = "default_recommended_actions")]
424    pub recommended_actions: Vec<String>,
425
426    /// Placeholder suggestion for the chat input bar
427    #[serde(default)]
428    pub chat_placeholder: Option<String>,
429}
430
431impl Default for AgentOnboardingConfig {
432    fn default() -> Self {
433        Self {
434            enabled: default_onboarding_enabled(),
435            intro_text: default_intro_text(),
436            include_project_overview: default_show_project_overview(),
437            include_language_summary: default_show_language_summary(),
438            include_guideline_highlights: default_show_guideline_highlights(),
439            include_usage_tips_in_welcome: default_show_usage_tips_in_welcome(),
440            include_recommended_actions_in_welcome: default_show_recommended_actions_in_welcome(),
441            guideline_highlight_limit: default_guideline_highlight_limit(),
442            usage_tips: default_usage_tips(),
443            recommended_actions: default_recommended_actions(),
444            chat_placeholder: None,
445        }
446    }
447}
448
449#[inline]
450const fn default_onboarding_enabled() -> bool {
451    true
452}
453
454const DEFAULT_INTRO_TEXT: &str =
455    "Let's get oriented. I preloaded workspace context so we can move fast.";
456
457#[inline]
458fn default_intro_text() -> String {
459    DEFAULT_INTRO_TEXT.into()
460}
461
462#[inline]
463const fn default_show_project_overview() -> bool {
464    true
465}
466
467#[inline]
468const fn default_show_language_summary() -> bool {
469    false
470}
471
472#[inline]
473const fn default_show_guideline_highlights() -> bool {
474    true
475}
476
477#[inline]
478const fn default_show_usage_tips_in_welcome() -> bool {
479    false
480}
481
482#[inline]
483const fn default_show_recommended_actions_in_welcome() -> bool {
484    false
485}
486
487#[inline]
488const fn default_guideline_highlight_limit() -> usize {
489    3
490}
491
492const DEFAULT_USAGE_TIPS: &[&str] = &[
493    "Describe your current coding goal or ask for a quick status overview.",
494    "Reference AGENTS.md guidelines when proposing changes.",
495    "Draft or refresh your TODO list with update_plan before coding.",
496    "Prefer asking for targeted file reads or diffs before editing.",
497];
498
499const DEFAULT_RECOMMENDED_ACTIONS: &[&str] = &[
500    "Start the session by outlining a 3–6 step TODO plan via update_plan.",
501    "Review the highlighted guidelines and share the task you want to tackle.",
502    "Ask for a workspace tour if you need more context.",
503];
504
505fn default_usage_tips() -> Vec<String> {
506    DEFAULT_USAGE_TIPS.iter().map(|s| (*s).into()).collect()
507}
508
509fn default_recommended_actions() -> Vec<String> {
510    DEFAULT_RECOMMENDED_ACTIONS
511        .iter()
512        .map(|s| (*s).into())
513        .collect()
514}
515
516/// Small/lightweight model configuration for efficient operations
517///
518/// Following Claude Code's pattern, use a smaller model (e.g., Haiku, GPT-4 Mini) for 50%+ of calls:
519/// - Large file reads and parsing (>50KB)
520/// - Web page summarization and analysis
521/// - Git history and commit message processing
522/// - One-word processing labels and simple classifications
523///
524/// Typically 70-80% cheaper than the main model while maintaining quality for these tasks.
525#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
526#[derive(Debug, Clone, Deserialize, Serialize)]
527pub struct AgentSmallModelConfig {
528    /// Enable small model tier for efficient operations
529    #[serde(default = "default_small_model_enabled")]
530    pub enabled: bool,
531
532    /// Small model to use (e.g., "claude-3-5-haiku", "gpt-4o-mini", "gemini-2.0-flash")
533    /// Leave empty to auto-select a lightweight sibling of the main model
534    #[serde(default)]
535    pub model: String,
536
537    /// Maximum tokens for small model responses
538    #[serde(default = "default_small_model_max_tokens")]
539    pub max_tokens: u32,
540
541    /// Temperature for small model responses
542    #[serde(default = "default_small_model_temperature")]
543    pub temperature: f32,
544
545    /// Enable small model for large file reads (>50KB)
546    #[serde(default = "default_small_model_for_large_reads")]
547    pub use_for_large_reads: bool,
548
549    /// Enable small model for web content summarization
550    #[serde(default = "default_small_model_for_web_summary")]
551    pub use_for_web_summary: bool,
552
553    /// Enable small model for git history processing
554    #[serde(default = "default_small_model_for_git_history")]
555    pub use_for_git_history: bool,
556}
557
558impl Default for AgentSmallModelConfig {
559    fn default() -> Self {
560        Self {
561            enabled: default_small_model_enabled(),
562            model: String::new(),
563            max_tokens: default_small_model_max_tokens(),
564            temperature: default_small_model_temperature(),
565            use_for_large_reads: default_small_model_for_large_reads(),
566            use_for_web_summary: default_small_model_for_web_summary(),
567            use_for_git_history: default_small_model_for_git_history(),
568        }
569    }
570}
571
572#[inline]
573const fn default_small_model_enabled() -> bool {
574    true // Enable by default following Claude Code pattern
575}
576
577#[inline]
578const fn default_small_model_max_tokens() -> u32 {
579    1000 // Smaller responses for summary/parse operations
580}
581
582#[inline]
583const fn default_small_model_temperature() -> f32 {
584    0.3 // More deterministic for parsing/summarization
585}
586
587#[inline]
588const fn default_small_model_for_large_reads() -> bool {
589    true
590}
591
592#[inline]
593const fn default_small_model_for_web_summary() -> bool {
594    true
595}
596
597#[inline]
598const fn default_small_model_for_git_history() -> bool {
599    true
600}