1mod agent;
4mod formatter;
5mod permissions;
6
7pub use agent::{AgentConfigInline, ModelVariant};
8pub use formatter::{FormatterConfig, FormatterOverride, FormatterOverrides};
9pub use permissions::{PermissionConfig, ToolPermission};
10
11use permissions::default_true;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15pub(crate) fn default_temperature() -> f64 {
18 0.6
19}
20pub(crate) fn default_max_tokens() -> u32 {
21 16384
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AutoModeConfig {
29 #[serde(default)]
30 pub enabled: bool,
31 #[serde(default = "default_max_operations")]
32 pub max_operations: u32,
33 #[serde(default = "default_require_confirmation_after")]
34 pub require_confirmation_after: u32,
35 #[serde(default = "default_true")]
36 pub dangerous_operations_require_approval: bool,
37}
38
39fn default_max_operations() -> u32 {
40 10
41}
42fn default_require_confirmation_after() -> u32 {
43 5
44}
45
46impl Default for AutoModeConfig {
47 fn default() -> Self {
48 Self {
49 enabled: false,
50 max_operations: 10,
51 require_confirmation_after: 5,
52 dangerous_operations_require_approval: true,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct OperationConfig {
62 #[serde(default = "default_true")]
63 pub show_diffs: bool,
64 #[serde(default = "default_true")]
65 pub backup_before_edit: bool,
66 #[serde(default = "default_max_file_size")]
67 pub max_file_size: u64,
68 #[serde(default)]
69 pub allowed_extensions: Vec<String>,
70}
71
72fn default_max_file_size() -> u64 {
73 1_000_000
74}
75
76impl Default for OperationConfig {
77 fn default() -> Self {
78 Self {
79 show_diffs: true,
80 backup_before_edit: true,
81 max_file_size: 1_000_000,
82 allowed_extensions: Vec::new(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct PlaybookScoringWeights {
92 #[serde(default = "default_effectiveness")]
93 pub effectiveness: f64,
94 #[serde(default = "default_recency")]
95 pub recency: f64,
96 #[serde(default = "default_semantic")]
97 pub semantic: f64,
98}
99
100fn default_effectiveness() -> f64 {
101 0.5
102}
103fn default_recency() -> f64 {
104 0.3
105}
106fn default_semantic() -> f64 {
107 0.2
108}
109
110impl Default for PlaybookScoringWeights {
111 fn default() -> Self {
112 Self {
113 effectiveness: 0.5,
114 recency: 0.3,
115 semantic: 0.2,
116 }
117 }
118}
119
120impl PlaybookScoringWeights {
121 pub fn validate(&self) -> Result<(), String> {
123 for (name, value) in [
124 ("effectiveness", self.effectiveness),
125 ("recency", self.recency),
126 ("semantic", self.semantic),
127 ] {
128 if !(0.0..=1.0).contains(&value) {
129 return Err(format!(
130 "{name} weight must be between 0.0 and 1.0, got {value}"
131 ));
132 }
133 }
134 Ok(())
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct PlaybookConfig {
141 #[serde(default = "default_max_strategies")]
142 pub max_strategies: u32,
143 #[serde(default = "default_true")]
144 pub use_selection: bool,
145 #[serde(default = "default_embedding_model")]
146 pub embedding_model: String,
147 #[serde(default = "default_embedding_provider")]
148 pub embedding_provider: String,
149 #[serde(default)]
150 pub scoring_weights: PlaybookScoringWeights,
151 #[serde(default = "default_true")]
152 pub cache_embeddings: bool,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub cache_file: Option<String>,
155}
156
157fn default_max_strategies() -> u32 {
158 30
159}
160fn default_embedding_model() -> String {
161 "text-embedding-3-small".to_string()
162}
163fn default_embedding_provider() -> String {
164 "openai".to_string()
165}
166
167impl Default for PlaybookConfig {
168 fn default() -> Self {
169 Self {
170 max_strategies: 30,
171 use_selection: true,
172 embedding_model: "text-embedding-3-small".to_string(),
173 embedding_provider: "openai".to_string(),
174 scoring_weights: PlaybookScoringWeights::default(),
175 cache_embeddings: true,
176 cache_file: None,
177 }
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct AppConfig {
186 #[serde(default = "default_model_provider")]
188 pub model_provider: String,
189 #[serde(default = "default_model")]
190 pub model: String,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub model_vlm: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub model_vlm_provider: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub api_key: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub api_base_url: Option<String>,
202 #[serde(default = "default_max_tokens")]
203 pub max_tokens: u32,
204 #[serde(default = "default_temperature")]
205 pub temperature: f64,
206
207 #[serde(default = "default_reasoning_effort")]
209 pub reasoning_effort: String,
210
211 #[serde(default = "default_auto_save_interval")]
213 pub auto_save_interval: u32,
214 #[serde(default = "default_max_context_tokens")]
215 pub max_context_tokens: u64,
216
217 #[serde(default)]
219 pub verbose: bool,
220 #[serde(default)]
221 pub debug_logging: bool,
222 #[serde(default = "default_color_scheme")]
223 pub color_scheme: String,
224 #[serde(default = "default_true")]
225 pub show_token_count: bool,
226 #[serde(default = "default_true")]
227 pub enable_sound: bool,
228
229 #[serde(default)]
231 pub permissions: PermissionConfig,
232
233 #[serde(default = "default_true")]
235 pub enable_bash: bool,
236 #[serde(default = "default_bash_timeout")]
237 pub bash_timeout: u32,
238 #[serde(default)]
239 pub auto_mode: AutoModeConfig,
240 #[serde(default)]
241 pub operation: OperationConfig,
242 #[serde(default = "default_max_undo_history")]
243 pub max_undo_history: u32,
244
245 #[serde(default = "default_true")]
247 pub topic_detection: bool,
248
249 #[serde(default)]
251 pub playbook: PlaybookConfig,
252
253 #[serde(default = "default_plan_mode_workflow")]
255 pub plan_mode_workflow: String,
256 #[serde(default = "default_plan_mode_explore_agent_count")]
257 pub plan_mode_explore_agent_count: u32,
258 #[serde(default = "default_plan_mode_plan_agent_count")]
259 pub plan_mode_plan_agent_count: u32,
260 #[serde(default = "default_plan_mode_explore_variant")]
261 pub plan_mode_explore_variant: String,
262
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub instructions: Vec<String>,
266
267 #[serde(default, skip_serializing_if = "Vec::is_empty")]
269 pub skill_paths: Vec<String>,
270
271 #[serde(default, skip_serializing_if = "Vec::is_empty")]
273 pub skill_urls: Vec<String>,
274
275 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub default_agent: Option<String>,
278
279 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
283 pub agents: HashMap<String, AgentConfigInline>,
284
285 #[serde(default)]
287 pub model_variants: HashMap<String, ModelVariant>,
288
289 #[serde(default, skip_serializing_if = "FormatterConfig::is_default")]
291 pub formatter: FormatterConfig,
292
293 #[serde(default = "default_config_version")]
295 pub config_version: u32,
296}
297
298fn default_config_version() -> u32 {
299 1
300}
301fn default_model_provider() -> String {
302 "fireworks".to_string()
303}
304fn default_model() -> String {
305 "accounts/fireworks/models/kimi-k2-instruct-0905".to_string()
306}
307fn default_auto_save_interval() -> u32 {
308 5
309}
310fn default_max_context_tokens() -> u64 {
311 100_000
312}
313fn default_color_scheme() -> String {
314 "monokai".to_string()
315}
316fn default_bash_timeout() -> u32 {
317 30
318}
319fn default_max_undo_history() -> u32 {
320 50
321}
322fn default_plan_mode_workflow() -> String {
323 "5-phase".to_string()
324}
325fn default_plan_mode_explore_agent_count() -> u32 {
326 3
327}
328fn default_plan_mode_plan_agent_count() -> u32 {
329 1
330}
331fn default_plan_mode_explore_variant() -> String {
332 "enabled".to_string()
333}
334fn default_reasoning_effort() -> String {
335 "medium".to_string()
336}
337
338impl Default for AppConfig {
339 fn default() -> Self {
340 Self {
341 model_provider: default_model_provider(),
342 model: default_model(),
343 model_vlm: None,
344 model_vlm_provider: None,
345 api_key: None,
346 api_base_url: None,
347 max_tokens: 16384,
348 temperature: 0.6,
349 reasoning_effort: "medium".to_string(),
350 auto_save_interval: 5,
351 max_context_tokens: 100_000,
352 verbose: false,
353 debug_logging: false,
354 color_scheme: "monokai".to_string(),
355 show_token_count: true,
356 enable_sound: true,
357 permissions: PermissionConfig::default(),
358 enable_bash: true,
359 bash_timeout: 30,
360 auto_mode: AutoModeConfig::default(),
361 operation: OperationConfig::default(),
362 max_undo_history: 50,
363 topic_detection: true,
364 playbook: PlaybookConfig::default(),
365 plan_mode_workflow: "5-phase".to_string(),
366 plan_mode_explore_agent_count: 3,
367 plan_mode_plan_agent_count: 1,
368 plan_mode_explore_variant: "enabled".to_string(),
369 instructions: Vec::new(),
370 skill_paths: Vec::new(),
371 skill_urls: Vec::new(),
372 default_agent: None,
373 agents: HashMap::new(),
374 model_variants: HashMap::new(),
375 formatter: FormatterConfig::default(),
376 config_version: default_config_version(),
377 }
378 }
379}
380
381impl AppConfig {
382 pub fn resolve_agent_role(&self, role: &str) -> (String, String) {
386 if let Some(agent) = self.agents.get(role) {
387 let model = agent.model.as_deref().unwrap_or(&self.model);
388 let provider = agent.provider.as_deref().unwrap_or(&self.model_provider);
389 (model.to_string(), provider.to_string())
390 } else {
391 (self.model.clone(), self.model_provider.clone())
392 }
393 }
394
395 pub fn get_api_key_with_env(&self, registry_env_var: Option<&str>) -> Result<String, String> {
404 if let Some(env_var) = registry_env_var
406 && !env_var.is_empty()
407 && let Ok(key) = std::env::var(env_var)
408 && !key.is_empty()
409 {
410 return Ok(key);
411 }
412
413 let builtin_env = Self::builtin_env_var(&self.model_provider);
415 if !builtin_env.is_empty()
416 && let Ok(key) = std::env::var(builtin_env)
417 && !key.is_empty()
418 {
419 return Ok(key);
420 }
421
422 let convention_env = Self::convention_env_var(&self.model_provider);
425 if !convention_env.is_empty()
426 && convention_env != builtin_env
427 && registry_env_var != Some(convention_env.as_str())
428 && let Ok(key) = std::env::var(&convention_env)
429 && !key.is_empty()
430 {
431 return Ok(key);
432 }
433
434 if let Some(ref key) = self.api_key {
436 return Ok(key.clone());
437 }
438
439 if builtin_env.is_empty()
441 && registry_env_var.is_none_or(str::is_empty)
442 && let Ok(key) = std::env::var("OPENAI_API_KEY")
443 && !key.is_empty()
444 {
445 return Ok(key);
446 }
447
448 let hint = registry_env_var
449 .filter(|s| !s.is_empty())
450 .or(Some(builtin_env).filter(|s| !s.is_empty()))
451 .or(Some(convention_env.as_str()).filter(|s| !s.is_empty()))
452 .unwrap_or("OPENAI_API_KEY");
453 Err(format!(
454 "No API key found. Set {} environment variable",
455 hint
456 ))
457 }
458
459 pub fn get_api_key(&self) -> Result<String, String> {
461 self.get_api_key_with_env(None)
462 }
463
464 fn builtin_env_var(provider: &str) -> &'static str {
467 match provider {
468 "fireworks" | "fireworks-ai" => "FIREWORKS_API_KEY",
469 "anthropic" => "ANTHROPIC_API_KEY",
470 "openai" => "OPENAI_API_KEY",
471 "azure" => "AZURE_OPENAI_API_KEY",
472 "groq" => "GROQ_API_KEY",
473 "mistral" => "MISTRAL_API_KEY",
474 "deepinfra" => "DEEPINFRA_API_KEY",
475 "openrouter" => "OPENROUTER_API_KEY",
476 "deepseek" => "DEEPSEEK_API_KEY",
477 "cohere" => "COHERE_API_KEY",
478 "togetherai" | "together" => "TOGETHER_API_KEY",
479 "perplexity" | "perplexity-agent" => "PERPLEXITY_API_KEY",
480 "xai" => "XAI_API_KEY",
481 "google" | "gemini" => "GOOGLE_GENERATIVE_AI_API_KEY",
482 _ => "",
483 }
484 }
485
486 fn convention_env_var(provider: &str) -> String {
492 let base = provider
494 .strip_suffix("-coding-plan")
495 .or_else(|| provider.strip_suffix("-cn"))
496 .or_else(|| provider.strip_suffix("-agent"))
497 .unwrap_or(provider);
498 if base.is_empty() {
499 return String::new();
500 }
501 format!("{}_API_KEY", base.to_uppercase().replace('-', "_"))
502 }
503}
504
505#[cfg(test)]
506mod tests;