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")]
118 pub max_input_tokens: Option<u32>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub max_completion_tokens: Option<u32>,
122 pub executor_timeout_secs: u64,
124 pub empty_response_no_retry_after_secs: u64,
126 pub stream_heartbeat_secs: u64,
128}
129
130impl Default for ReasoningConfig {
131 fn default() -> Self {
132 Self {
133 optimizer_model: "anthropic/claude-sonnet-4.6".into(),
134 executor_model: "openai/gpt-5.2".into(),
135 reasoning_effort: None,
136 api_base_url: None,
137 max_input_tokens: None,
138 max_completion_tokens: Some(128_000),
139 executor_timeout_secs: 2700,
140 empty_response_no_retry_after_secs: 600,
141 stream_heartbeat_secs: 30,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
154#[serde(default)]
155pub struct OrchestratorConfig {
156 pub session_deadline_secs: u64,
158 pub inactivity_timeout_secs: u64,
160 pub compaction_threshold: f64,
162}
163
164impl Default for OrchestratorConfig {
165 fn default() -> Self {
166 Self {
167 session_deadline_secs: 3600,
168 inactivity_timeout_secs: 300,
169 compaction_threshold: 0.80,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
182#[serde(default)]
183pub struct WebRetrievalConfig {
184 pub request_timeout_secs: u64,
186 pub default_max_bytes: u64,
188 pub default_search_results: u32,
190 pub max_search_results: u32,
192 pub summarizer: WebSummarizerConfig,
194}
195
196impl Default for WebRetrievalConfig {
197 fn default() -> Self {
198 Self {
199 request_timeout_secs: 30,
200 default_max_bytes: 5 * 1024 * 1024, default_search_results: 8,
202 max_search_results: 20,
203 summarizer: WebSummarizerConfig::default(),
204 }
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
210#[serde(default)]
211pub struct WebSummarizerConfig {
212 pub model: String,
214 pub max_tokens: u32,
216 pub temperature: f64,
218}
219
220impl Default for WebSummarizerConfig {
221 fn default() -> Self {
222 Self {
223 model: "claude-haiku-4-5".into(),
224 max_tokens: 300,
225 temperature: 0.2,
226 }
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
238#[serde(default)]
239pub struct CliToolsConfig {
240 pub ls_page_size: u32,
242 pub grep_default_limit: u32,
244 pub glob_default_limit: u32,
246 pub max_depth: u32,
248 pub pagination_cache_ttl_secs: u64,
250 #[serde(default)]
252 pub extra_ignore_patterns: Vec<String>,
253}
254
255impl Default for CliToolsConfig {
256 fn default() -> Self {
257 Self {
258 ls_page_size: 100,
259 grep_default_limit: 200,
260 glob_default_limit: 500,
261 max_depth: 10,
262 pagination_cache_ttl_secs: 300,
263 extra_ignore_patterns: vec![],
264 }
265 }
266}
267
268#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
276#[serde(default)]
277pub struct ServicesConfig {
278 pub anthropic: AnthropicServiceConfig,
280 pub exa: ExaServiceConfig,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
286#[serde(default)]
287pub struct AnthropicServiceConfig {
288 pub base_url: String,
290}
291
292impl Default for AnthropicServiceConfig {
293 fn default() -> Self {
294 Self {
295 base_url: "https://api.anthropic.com".into(),
296 }
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
302#[serde(default)]
303pub struct ExaServiceConfig {
304 pub base_url: String,
306}
307
308impl Default for ExaServiceConfig {
309 fn default() -> Self {
310 Self {
311 base_url: "https://api.exa.ai".into(),
312 }
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
324#[serde(default)]
325pub struct LoggingConfig {
326 pub level: String,
328
329 pub json: bool,
331}
332
333impl Default for LoggingConfig {
334 fn default() -> Self {
335 Self {
336 level: "info".into(),
337 json: false,
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn test_default_config_serializes() {
348 let config = AgenticConfig::default();
349 let toml_str = toml::to_string_pretty(&config).unwrap();
350 assert!(toml_str.contains("[subagents]"));
351 assert!(toml_str.contains("[reasoning]"));
352 assert!(toml_str.contains("[services.anthropic]"));
354 assert!(toml_str.contains("[services.exa]"));
355 assert!(toml_str.contains("[orchestrator]"));
356 assert!(toml_str.contains("[web_retrieval]"));
357 assert!(toml_str.contains("[cli_tools]"));
358 assert!(toml_str.contains("[logging]"));
359 assert!(!toml_str.contains("[thoughts]"));
361 assert!(!toml_str.contains("[models]"));
362 }
363
364 #[test]
365 fn test_default_models_use_undated_names() {
366 let subagents = SubagentsConfig::default();
367 assert!(!subagents.locator_model.contains("20"));
368 assert!(!subagents.analyzer_model.contains("20"));
369
370 let reasoning = ReasoningConfig::default();
371 assert!(!reasoning.optimizer_model.contains("20"));
372 assert!(!reasoning.executor_model.contains("20"));
373 }
374
375 #[test]
376 fn test_partial_config_deserializes() {
377 let toml_str = r#"
378[subagents]
379locator_model = "custom-model"
380"#;
381 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
382 assert_eq!(config.subagents.locator_model, "custom-model");
383 assert_eq!(config.subagents.analyzer_model, "claude-sonnet-4-6");
385 assert_eq!(
386 config.services.anthropic.base_url,
387 "https://api.anthropic.com"
388 );
389 }
390
391 #[test]
392 fn test_schema_field_optional() {
393 let toml_str = r#""$schema" = "file://./agentic.schema.json""#;
394 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
395 assert_eq!(config.schema, Some("file://./agentic.schema.json".into()));
396 }
397
398 #[test]
400 fn test_web_retrieval_defaults_match_hardcoded() {
401 let cfg = WebRetrievalConfig::default();
402 assert_eq!(cfg.request_timeout_secs, 30);
403 assert_eq!(cfg.default_max_bytes, 5 * 1024 * 1024); assert_eq!(cfg.default_search_results, 8);
405 assert_eq!(cfg.max_search_results, 20);
406 assert_eq!(cfg.summarizer.model, "claude-haiku-4-5");
407 assert_eq!(cfg.summarizer.max_tokens, 300);
408 assert!((cfg.summarizer.temperature - 0.2).abs() < f64::EPSILON);
409 }
410
411 #[test]
412 fn test_cli_tools_defaults_match_hardcoded() {
413 let cfg = CliToolsConfig::default();
414 assert_eq!(cfg.ls_page_size, 100);
415 assert_eq!(cfg.grep_default_limit, 200);
416 assert_eq!(cfg.glob_default_limit, 500);
417 assert_eq!(cfg.max_depth, 10);
418 assert_eq!(cfg.pagination_cache_ttl_secs, 300);
419 assert!(cfg.extra_ignore_patterns.is_empty());
420 }
421
422 #[test]
423 fn test_orchestrator_defaults_match_hardcoded() {
424 let cfg = OrchestratorConfig::default();
425 assert_eq!(cfg.session_deadline_secs, 3600);
426 assert_eq!(cfg.inactivity_timeout_secs, 300);
427 assert!((cfg.compaction_threshold - 0.80).abs() < f64::EPSILON);
428 }
429
430 #[test]
431 fn test_services_defaults_match_hardcoded() {
432 let cfg = ServicesConfig::default();
433
434 assert_eq!(cfg.anthropic.base_url, "https://api.anthropic.com");
436
437 assert_eq!(cfg.exa.base_url, "https://api.exa.ai");
439 }
440}