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        if !(0.0..=1.0).contains(&self.temperature) {
173            return Err(format!(
174                "temperature must be between 0.0 and 1.0, got {}",
175                self.temperature
176            ));
177        }
178
179        if !(0.0..=1.0).contains(&self.refine_temperature) {
180            return Err(format!(
181                "refine_temperature must be between 0.0 and 1.0, got {}",
182                self.refine_temperature
183            ));
184        }
185
186        if self.max_tokens == 0 {
187            return Err("max_tokens must be greater than 0".to_string());
188        }
189
190        if self.refine_max_tokens == 0 {
191            return Err("refine_max_tokens must be greater than 0".to_string());
192        }
193
194        Ok(())
195    }
196}
197
198fn default_provider() -> String {
199    defaults::DEFAULT_PROVIDER.to_string()
200}
201
202fn default_api_key_env() -> String {
203    defaults::DEFAULT_API_KEY_ENV.to_string()
204}
205fn default_model() -> String {
206    defaults::DEFAULT_MODEL.to_string()
207}
208fn default_theme() -> String {
209    defaults::DEFAULT_THEME.to_string()
210}
211
212fn default_todo_planning_mode() -> bool {
213    true
214}
215fn default_max_conversation_turns() -> usize {
216    150
217}
218fn default_reasoning_effort() -> ReasoningEffortLevel {
219    ReasoningEffortLevel::default()
220}
221
222fn default_verbosity() -> VerbosityLevel {
223    VerbosityLevel::default()
224}
225
226fn default_temperature() -> f32 {
227    llm_generation::DEFAULT_TEMPERATURE
228}
229
230fn default_max_tokens() -> u32 {
231    llm_generation::DEFAULT_MAX_TOKENS
232}
233
234fn default_refine_temperature() -> f32 {
235    llm_generation::DEFAULT_REFINE_TEMPERATURE
236}
237
238fn default_refine_max_tokens() -> u32 {
239    llm_generation::DEFAULT_REFINE_MAX_TOKENS
240}
241
242fn default_enable_self_review() -> bool {
243    false
244}
245
246fn default_max_review_passes() -> usize {
247    1
248}
249
250fn default_refine_prompts_enabled() -> bool {
251    false
252}
253
254fn default_refine_max_passes() -> usize {
255    1
256}
257
258fn default_project_doc_max_bytes() -> usize {
259    project_doc::DEFAULT_MAX_BYTES
260}
261
262fn default_instruction_max_bytes() -> usize {
263    instructions::DEFAULT_MAX_BYTES
264}
265
266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
267#[derive(Debug, Clone, Deserialize, Serialize)]
268pub struct AgentCustomPromptsConfig {
269    /// Master switch for custom prompt support
270    #[serde(default = "default_custom_prompts_enabled")]
271    pub enabled: bool,
272
273    /// Primary directory for prompt markdown files
274    #[serde(default = "default_custom_prompts_directory")]
275    pub directory: String,
276
277    /// Additional directories to search for prompts
278    #[serde(default)]
279    pub extra_directories: Vec<String>,
280
281    /// Maximum file size (KB) to load for a single prompt
282    #[serde(default = "default_custom_prompts_max_file_size_kb")]
283    pub max_file_size_kb: usize,
284}
285
286impl Default for AgentCustomPromptsConfig {
287    fn default() -> Self {
288        Self {
289            enabled: default_custom_prompts_enabled(),
290            directory: default_custom_prompts_directory(),
291            extra_directories: Vec::new(),
292            max_file_size_kb: default_custom_prompts_max_file_size_kb(),
293        }
294    }
295}
296
297fn default_custom_prompts_enabled() -> bool {
298    true
299}
300
301fn default_custom_prompts_directory() -> String {
302    prompts::DEFAULT_CUSTOM_PROMPTS_DIR.to_string()
303}
304
305fn default_custom_prompts_max_file_size_kb() -> usize {
306    prompts::DEFAULT_CUSTOM_PROMPT_MAX_FILE_SIZE_KB
307}
308
309#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
310#[derive(Debug, Clone, Deserialize, Serialize)]
311pub struct AgentCheckpointingConfig {
312    /// Enable automatic checkpoints after each successful turn
313    #[serde(default = "default_checkpointing_enabled")]
314    pub enabled: bool,
315
316    /// Optional custom directory for storing checkpoints (relative to workspace or absolute)
317    #[serde(default)]
318    pub storage_dir: Option<String>,
319
320    /// Maximum number of checkpoints to retain on disk
321    #[serde(default = "default_checkpointing_max_snapshots")]
322    pub max_snapshots: usize,
323
324    /// Maximum age in days before checkpoints are removed automatically (None disables)
325    #[serde(default = "default_checkpointing_max_age_days")]
326    pub max_age_days: Option<u64>,
327}
328
329impl Default for AgentCheckpointingConfig {
330    fn default() -> Self {
331        Self {
332            enabled: default_checkpointing_enabled(),
333            storage_dir: None,
334            max_snapshots: default_checkpointing_max_snapshots(),
335            max_age_days: default_checkpointing_max_age_days(),
336        }
337    }
338}
339
340fn default_checkpointing_enabled() -> bool {
341    DEFAULT_CHECKPOINTS_ENABLED
342}
343
344fn default_checkpointing_max_snapshots() -> usize {
345    DEFAULT_MAX_SNAPSHOTS
346}
347
348fn default_checkpointing_max_age_days() -> Option<u64> {
349    Some(DEFAULT_MAX_AGE_DAYS)
350}
351
352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
353#[derive(Debug, Clone, Deserialize, Serialize)]
354pub struct AgentOnboardingConfig {
355    /// Toggle onboarding message rendering
356    #[serde(default = "default_onboarding_enabled")]
357    pub enabled: bool,
358
359    /// Introductory text shown at session start
360    #[serde(default = "default_intro_text")]
361    pub intro_text: String,
362
363    /// Whether to include project overview in onboarding message
364    #[serde(default = "default_show_project_overview")]
365    pub include_project_overview: bool,
366
367    /// Whether to include language summary in onboarding message
368    #[serde(default = "default_show_language_summary")]
369    pub include_language_summary: bool,
370
371    /// Whether to include AGENTS.md highlights in onboarding message
372    #[serde(default = "default_show_guideline_highlights")]
373    pub include_guideline_highlights: bool,
374
375    /// Whether to surface usage tips inside the welcome text banner
376    #[serde(default = "default_show_usage_tips_in_welcome")]
377    pub include_usage_tips_in_welcome: bool,
378
379    /// Whether to surface suggested actions inside the welcome text banner
380    #[serde(default = "default_show_recommended_actions_in_welcome")]
381    pub include_recommended_actions_in_welcome: bool,
382
383    /// Maximum number of guideline bullets to surface
384    #[serde(default = "default_guideline_highlight_limit")]
385    pub guideline_highlight_limit: usize,
386
387    /// Tips for collaborating with the agent effectively
388    #[serde(default = "default_usage_tips")]
389    pub usage_tips: Vec<String>,
390
391    /// Recommended follow-up actions to display
392    #[serde(default = "default_recommended_actions")]
393    pub recommended_actions: Vec<String>,
394
395    /// Placeholder suggestion for the chat input bar
396    #[serde(default)]
397    pub chat_placeholder: Option<String>,
398}
399
400impl Default for AgentOnboardingConfig {
401    fn default() -> Self {
402        Self {
403            enabled: default_onboarding_enabled(),
404            intro_text: default_intro_text(),
405            include_project_overview: default_show_project_overview(),
406            include_language_summary: default_show_language_summary(),
407            include_guideline_highlights: default_show_guideline_highlights(),
408            include_usage_tips_in_welcome: default_show_usage_tips_in_welcome(),
409            include_recommended_actions_in_welcome: default_show_recommended_actions_in_welcome(),
410            guideline_highlight_limit: default_guideline_highlight_limit(),
411            usage_tips: default_usage_tips(),
412            recommended_actions: default_recommended_actions(),
413            chat_placeholder: None,
414        }
415    }
416}
417
418fn default_onboarding_enabled() -> bool {
419    true
420}
421
422fn default_intro_text() -> String {
423    "Let's get oriented. I preloaded workspace context so we can move fast.".to_string()
424}
425
426fn default_show_project_overview() -> bool {
427    true
428}
429
430fn default_show_language_summary() -> bool {
431    false
432}
433
434fn default_show_guideline_highlights() -> bool {
435    true
436}
437
438fn default_show_usage_tips_in_welcome() -> bool {
439    false
440}
441
442fn default_show_recommended_actions_in_welcome() -> bool {
443    false
444}
445
446fn default_guideline_highlight_limit() -> usize {
447    3
448}
449
450fn default_usage_tips() -> Vec<String> {
451    vec![
452        "Describe your current coding goal or ask for a quick status overview.".to_string(),
453        "Reference AGENTS.md guidelines when proposing changes.".to_string(),
454        "Draft or refresh your TODO list with update_plan before coding.".to_string(),
455        "Prefer asking for targeted file reads or diffs before editing.".to_string(),
456    ]
457}
458
459fn default_recommended_actions() -> Vec<String> {
460    vec![
461        "Start the session by outlining a 3–6 step TODO plan via update_plan.".to_string(),
462        "Review the highlighted guidelines and share the task you want to tackle.".to_string(),
463        "Ask for a workspace tour if you need more context.".to_string(),
464    ]
465}
466
467/// Small/lightweight model configuration for efficient operations
468///
469/// Following Claude Code's pattern, use a smaller model (e.g., Haiku, GPT-4 Mini) for 50%+ of calls:
470/// - Large file reads and parsing (>50KB)
471/// - Web page summarization and analysis
472/// - Git history and commit message processing
473/// - One-word processing labels and simple classifications
474///
475/// Typically 70-80% cheaper than the main model while maintaining quality for these tasks.
476#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
477#[derive(Debug, Clone, Deserialize, Serialize)]
478pub struct AgentSmallModelConfig {
479    /// Enable small model tier for efficient operations
480    #[serde(default = "default_small_model_enabled")]
481    pub enabled: bool,
482
483    /// Small model to use (e.g., "claude-3-5-haiku", "gpt-4o-mini", "gemini-2.0-flash")
484    /// Leave empty to auto-select a lightweight sibling of the main model
485    #[serde(default)]
486    pub model: String,
487
488    /// Maximum tokens for small model responses
489    #[serde(default = "default_small_model_max_tokens")]
490    pub max_tokens: u32,
491
492    /// Temperature for small model responses
493    #[serde(default = "default_small_model_temperature")]
494    pub temperature: f32,
495
496    /// Enable small model for large file reads (>50KB)
497    #[serde(default = "default_small_model_for_large_reads")]
498    pub use_for_large_reads: bool,
499
500    /// Enable small model for web content summarization
501    #[serde(default = "default_small_model_for_web_summary")]
502    pub use_for_web_summary: bool,
503
504    /// Enable small model for git history processing
505    #[serde(default = "default_small_model_for_git_history")]
506    pub use_for_git_history: bool,
507}
508
509impl Default for AgentSmallModelConfig {
510    fn default() -> Self {
511        Self {
512            enabled: default_small_model_enabled(),
513            model: String::new(),
514            max_tokens: default_small_model_max_tokens(),
515            temperature: default_small_model_temperature(),
516            use_for_large_reads: default_small_model_for_large_reads(),
517            use_for_web_summary: default_small_model_for_web_summary(),
518            use_for_git_history: default_small_model_for_git_history(),
519        }
520    }
521}
522
523fn default_small_model_enabled() -> bool {
524    true // Enable by default following Claude Code pattern
525}
526
527fn default_small_model_max_tokens() -> u32 {
528    1000 // Smaller responses for summary/parse operations
529}
530
531fn default_small_model_temperature() -> f32 {
532    0.3 // More deterministic for parsing/summarization
533}
534
535fn default_small_model_for_large_reads() -> bool {
536    true
537}
538
539fn default_small_model_for_web_summary() -> bool {
540    true
541}
542
543fn default_small_model_for_git_history() -> bool {
544    true
545}