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}
119
120impl Default for ReasoningConfig {
121 fn default() -> Self {
122 Self {
123 optimizer_model: "anthropic/claude-sonnet-4.6".into(),
124 executor_model: "openai/gpt-5.2".into(),
125 reasoning_effort: None,
126 api_base_url: None,
127 token_limit: None,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
140#[serde(default)]
141pub struct OrchestratorConfig {
142 pub session_deadline_secs: u64,
144 pub inactivity_timeout_secs: u64,
146 pub compaction_threshold: f64,
148}
149
150impl Default for OrchestratorConfig {
151 fn default() -> Self {
152 Self {
153 session_deadline_secs: 3600,
154 inactivity_timeout_secs: 300,
155 compaction_threshold: 0.80,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
168#[serde(default)]
169pub struct WebRetrievalConfig {
170 pub request_timeout_secs: u64,
172 pub default_max_bytes: u64,
174 pub default_search_results: u32,
176 pub max_search_results: u32,
178 pub summarizer: WebSummarizerConfig,
180}
181
182impl Default for WebRetrievalConfig {
183 fn default() -> Self {
184 Self {
185 request_timeout_secs: 30,
186 default_max_bytes: 5 * 1024 * 1024, default_search_results: 8,
188 max_search_results: 20,
189 summarizer: WebSummarizerConfig::default(),
190 }
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
196#[serde(default)]
197pub struct WebSummarizerConfig {
198 pub model: String,
200 pub max_tokens: u32,
202 pub temperature: f64,
204}
205
206impl Default for WebSummarizerConfig {
207 fn default() -> Self {
208 Self {
209 model: "claude-haiku-4-5".into(),
210 max_tokens: 300,
211 temperature: 0.2,
212 }
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
224#[serde(default)]
225pub struct CliToolsConfig {
226 pub ls_page_size: u32,
228 pub grep_default_limit: u32,
230 pub glob_default_limit: u32,
232 pub max_depth: u32,
234 pub pagination_cache_ttl_secs: u64,
236 #[serde(default)]
238 pub extra_ignore_patterns: Vec<String>,
239}
240
241impl Default for CliToolsConfig {
242 fn default() -> Self {
243 Self {
244 ls_page_size: 100,
245 grep_default_limit: 200,
246 glob_default_limit: 500,
247 max_depth: 10,
248 pagination_cache_ttl_secs: 300,
249 extra_ignore_patterns: vec![],
250 }
251 }
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
262#[serde(default)]
263pub struct ServicesConfig {
264 pub anthropic: AnthropicServiceConfig,
266 pub exa: ExaServiceConfig,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
272#[serde(default)]
273pub struct AnthropicServiceConfig {
274 pub base_url: String,
276}
277
278impl Default for AnthropicServiceConfig {
279 fn default() -> Self {
280 Self {
281 base_url: "https://api.anthropic.com".into(),
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
288#[serde(default)]
289pub struct ExaServiceConfig {
290 pub base_url: String,
292}
293
294impl Default for ExaServiceConfig {
295 fn default() -> Self {
296 Self {
297 base_url: "https://api.exa.ai".into(),
298 }
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
310#[serde(default)]
311pub struct LoggingConfig {
312 pub level: String,
314
315 pub json: bool,
317}
318
319impl Default for LoggingConfig {
320 fn default() -> Self {
321 Self {
322 level: "info".into(),
323 json: false,
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_default_config_serializes() {
334 let config = AgenticConfig::default();
335 let toml_str = toml::to_string_pretty(&config).unwrap();
336 assert!(toml_str.contains("[subagents]"));
337 assert!(toml_str.contains("[reasoning]"));
338 assert!(toml_str.contains("[services.anthropic]"));
340 assert!(toml_str.contains("[services.exa]"));
341 assert!(toml_str.contains("[orchestrator]"));
342 assert!(toml_str.contains("[web_retrieval]"));
343 assert!(toml_str.contains("[cli_tools]"));
344 assert!(toml_str.contains("[logging]"));
345 assert!(!toml_str.contains("[thoughts]"));
347 assert!(!toml_str.contains("[models]"));
348 }
349
350 #[test]
351 fn test_default_models_use_undated_names() {
352 let subagents = SubagentsConfig::default();
353 assert!(!subagents.locator_model.contains("20"));
354 assert!(!subagents.analyzer_model.contains("20"));
355
356 let reasoning = ReasoningConfig::default();
357 assert!(!reasoning.optimizer_model.contains("20"));
358 assert!(!reasoning.executor_model.contains("20"));
359 }
360
361 #[test]
362 fn test_partial_config_deserializes() {
363 let toml_str = r#"
364[subagents]
365locator_model = "custom-model"
366"#;
367 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
368 assert_eq!(config.subagents.locator_model, "custom-model");
369 assert_eq!(config.subagents.analyzer_model, "claude-sonnet-4-6");
371 assert_eq!(
372 config.services.anthropic.base_url,
373 "https://api.anthropic.com"
374 );
375 }
376
377 #[test]
378 fn test_schema_field_optional() {
379 let toml_str = r#""$schema" = "file://./agentic.schema.json""#;
380 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
381 assert_eq!(config.schema, Some("file://./agentic.schema.json".into()));
382 }
383
384 #[test]
386 fn test_web_retrieval_defaults_match_hardcoded() {
387 let cfg = WebRetrievalConfig::default();
388 assert_eq!(cfg.request_timeout_secs, 30);
389 assert_eq!(cfg.default_max_bytes, 5 * 1024 * 1024); assert_eq!(cfg.default_search_results, 8);
391 assert_eq!(cfg.max_search_results, 20);
392 assert_eq!(cfg.summarizer.model, "claude-haiku-4-5");
393 assert_eq!(cfg.summarizer.max_tokens, 300);
394 assert!((cfg.summarizer.temperature - 0.2).abs() < f64::EPSILON);
395 }
396
397 #[test]
398 fn test_cli_tools_defaults_match_hardcoded() {
399 let cfg = CliToolsConfig::default();
400 assert_eq!(cfg.ls_page_size, 100);
401 assert_eq!(cfg.grep_default_limit, 200);
402 assert_eq!(cfg.glob_default_limit, 500);
403 assert_eq!(cfg.max_depth, 10);
404 assert_eq!(cfg.pagination_cache_ttl_secs, 300);
405 assert!(cfg.extra_ignore_patterns.is_empty());
406 }
407
408 #[test]
409 fn test_orchestrator_defaults_match_hardcoded() {
410 let cfg = OrchestratorConfig::default();
411 assert_eq!(cfg.session_deadline_secs, 3600);
412 assert_eq!(cfg.inactivity_timeout_secs, 300);
413 assert!((cfg.compaction_threshold - 0.80).abs() < f64::EPSILON);
414 }
415
416 #[test]
417 fn test_services_defaults_match_hardcoded() {
418 let cfg = ServicesConfig::default();
419
420 assert_eq!(cfg.anthropic.base_url, "https://api.anthropic.com");
422
423 assert_eq!(cfg.exa.base_url, "https://api.exa.ai");
425 }
426}