1use schemars::JsonSchema;
8use serde::Deserialize;
9use serde::Serialize;
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
16#[serde(default)]
17pub struct AgenticConfig {
18 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
21 pub schema: Option<String>,
22
23 pub subagents: SubagentsConfig,
25
26 pub reasoning: ReasoningConfig,
28
29 pub services: ServicesConfig,
31
32 pub orchestrator: OrchestratorConfig,
34
35 pub web_retrieval: WebRetrievalConfig,
37
38 pub cli_tools: CliToolsConfig,
40
41 pub logging: LoggingConfig,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53#[serde(default)]
54pub struct SubagentsConfig {
55 pub locator_model: String,
61 pub analyzer_model: String,
63}
64
65impl Default for SubagentsConfig {
66 fn default() -> Self {
67 Self {
68 locator_model: "claude-haiku-4-5".into(),
69 analyzer_model: "claude-sonnet-4-6".into(),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "lowercase")]
84enum ReasoningEffortLevel {
85 Low,
86 Medium,
87 High,
88 Xhigh,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
102#[serde(default)]
103pub struct ReasoningConfig {
104 pub optimizer_model: String,
106 pub executor_model: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 #[schemars(with = "Option<ReasoningEffortLevel>")]
111 pub reasoning_effort: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub api_base_url: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub token_limit: Option<u32>,
118 pub executor_timeout_secs: u64,
120 pub empty_response_no_retry_after_secs: u64,
122 pub stream_heartbeat_secs: u64,
124}
125
126impl Default for ReasoningConfig {
127 fn default() -> Self {
128 Self {
129 optimizer_model: "anthropic/claude-sonnet-4.6".into(),
130 executor_model: "openai/gpt-5.2".into(),
131 reasoning_effort: None,
132 api_base_url: None,
133 token_limit: None,
134 executor_timeout_secs: 2700,
135 empty_response_no_retry_after_secs: 600,
136 stream_heartbeat_secs: 30,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149#[serde(default)]
150pub struct OrchestratorConfig {
151 pub session_deadline_secs: u64,
153 pub inactivity_timeout_secs: u64,
155 pub compaction_threshold: f64,
157}
158
159impl Default for OrchestratorConfig {
160 fn default() -> Self {
161 Self {
162 session_deadline_secs: 3600,
163 inactivity_timeout_secs: 300,
164 compaction_threshold: 0.80,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
177#[serde(default)]
178pub struct WebRetrievalConfig {
179 pub request_timeout_secs: u64,
181 pub default_max_bytes: u64,
183 pub default_search_results: u32,
185 pub max_search_results: u32,
187 pub summarizer: WebSummarizerConfig,
189}
190
191impl Default for WebRetrievalConfig {
192 fn default() -> Self {
193 Self {
194 request_timeout_secs: 30,
195 default_max_bytes: 5 * 1024 * 1024, default_search_results: 8,
197 max_search_results: 20,
198 summarizer: WebSummarizerConfig::default(),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
205#[serde(default)]
206pub struct WebSummarizerConfig {
207 pub model: String,
209 pub max_tokens: u32,
211 pub temperature: f64,
213}
214
215impl Default for WebSummarizerConfig {
216 fn default() -> Self {
217 Self {
218 model: "claude-haiku-4-5".into(),
219 max_tokens: 300,
220 temperature: 0.2,
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
233#[serde(default)]
234pub struct CliToolsConfig {
235 pub ls_page_size: u32,
237 pub grep_default_limit: u32,
239 pub glob_default_limit: u32,
241 pub max_depth: u32,
243 pub pagination_cache_ttl_secs: u64,
245 #[serde(default)]
247 pub extra_ignore_patterns: Vec<String>,
248}
249
250impl Default for CliToolsConfig {
251 fn default() -> Self {
252 Self {
253 ls_page_size: 100,
254 grep_default_limit: 200,
255 glob_default_limit: 500,
256 max_depth: 10,
257 pagination_cache_ttl_secs: 300,
258 extra_ignore_patterns: vec![],
259 }
260 }
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
271#[serde(default)]
272pub struct ServicesConfig {
273 pub anthropic: AnthropicServiceConfig,
275 pub exa: ExaServiceConfig,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
281#[serde(default)]
282pub struct AnthropicServiceConfig {
283 pub base_url: String,
285}
286
287impl Default for AnthropicServiceConfig {
288 fn default() -> Self {
289 Self {
290 base_url: "https://api.anthropic.com".into(),
291 }
292 }
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
297#[serde(default)]
298pub struct ExaServiceConfig {
299 pub base_url: String,
301}
302
303impl Default for ExaServiceConfig {
304 fn default() -> Self {
305 Self {
306 base_url: "https://api.exa.ai".into(),
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
319#[serde(default)]
320pub struct LoggingConfig {
321 pub level: String,
323
324 pub json: bool,
326}
327
328impl Default for LoggingConfig {
329 fn default() -> Self {
330 Self {
331 level: "info".into(),
332 json: false,
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn test_default_config_serializes() {
343 let config = AgenticConfig::default();
344 let toml_str = toml::to_string_pretty(&config).unwrap();
345 assert!(toml_str.contains("[subagents]"));
346 assert!(toml_str.contains("[reasoning]"));
347 assert!(toml_str.contains("[services.anthropic]"));
349 assert!(toml_str.contains("[services.exa]"));
350 assert!(toml_str.contains("[orchestrator]"));
351 assert!(toml_str.contains("[web_retrieval]"));
352 assert!(toml_str.contains("[cli_tools]"));
353 assert!(toml_str.contains("[logging]"));
354 assert!(!toml_str.contains("[thoughts]"));
356 assert!(!toml_str.contains("[models]"));
357 }
358
359 #[test]
360 fn test_default_models_use_undated_names() {
361 let subagents = SubagentsConfig::default();
362 assert!(!subagents.locator_model.contains("20"));
363 assert!(!subagents.analyzer_model.contains("20"));
364
365 let reasoning = ReasoningConfig::default();
366 assert!(!reasoning.optimizer_model.contains("20"));
367 assert!(!reasoning.executor_model.contains("20"));
368 }
369
370 #[test]
371 fn test_partial_config_deserializes() {
372 let toml_str = r#"
373[subagents]
374locator_model = "custom-model"
375"#;
376 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
377 assert_eq!(config.subagents.locator_model, "custom-model");
378 assert_eq!(config.subagents.analyzer_model, "claude-sonnet-4-6");
380 assert_eq!(
381 config.services.anthropic.base_url,
382 "https://api.anthropic.com"
383 );
384 }
385
386 #[test]
387 fn test_schema_field_optional() {
388 let toml_str = r#""$schema" = "file://./agentic.schema.json""#;
389 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
390 assert_eq!(config.schema, Some("file://./agentic.schema.json".into()));
391 }
392
393 #[test]
395 fn test_web_retrieval_defaults_match_hardcoded() {
396 let cfg = WebRetrievalConfig::default();
397 assert_eq!(cfg.request_timeout_secs, 30);
398 assert_eq!(cfg.default_max_bytes, 5 * 1024 * 1024); assert_eq!(cfg.default_search_results, 8);
400 assert_eq!(cfg.max_search_results, 20);
401 assert_eq!(cfg.summarizer.model, "claude-haiku-4-5");
402 assert_eq!(cfg.summarizer.max_tokens, 300);
403 assert!((cfg.summarizer.temperature - 0.2).abs() < f64::EPSILON);
404 }
405
406 #[test]
407 fn test_cli_tools_defaults_match_hardcoded() {
408 let cfg = CliToolsConfig::default();
409 assert_eq!(cfg.ls_page_size, 100);
410 assert_eq!(cfg.grep_default_limit, 200);
411 assert_eq!(cfg.glob_default_limit, 500);
412 assert_eq!(cfg.max_depth, 10);
413 assert_eq!(cfg.pagination_cache_ttl_secs, 300);
414 assert!(cfg.extra_ignore_patterns.is_empty());
415 }
416
417 #[test]
418 fn test_orchestrator_defaults_match_hardcoded() {
419 let cfg = OrchestratorConfig::default();
420 assert_eq!(cfg.session_deadline_secs, 3600);
421 assert_eq!(cfg.inactivity_timeout_secs, 300);
422 assert!((cfg.compaction_threshold - 0.80).abs() < f64::EPSILON);
423 }
424
425 #[test]
426 fn test_services_defaults_match_hardcoded() {
427 let cfg = ServicesConfig::default();
428
429 assert_eq!(cfg.anthropic.base_url, "https://api.anthropic.com");
431
432 assert_eq!(cfg.exa.base_url, "https://api.exa.ai");
434 }
435}