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 review: ReviewConfig,
34
35 pub thoughts: ThoughtsConfig,
37
38 pub orchestrator: OrchestratorConfig,
40
41 pub web_retrieval: WebRetrievalConfig,
43
44 pub cli_tools: CliToolsConfig,
46
47 pub logging: LoggingConfig,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59#[serde(default)]
60pub struct SubagentsConfig {
61 pub locator_model: String,
67 pub analyzer_model: String,
69 pub runtime_timeout_secs: u64,
71}
72
73impl Default for SubagentsConfig {
74 fn default() -> Self {
75 Self {
76 locator_model: "claude-haiku-4-5".into(),
77 analyzer_model: "claude-sonnet-4-6".into(),
78 runtime_timeout_secs: 3600,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "lowercase")]
93enum ReasoningEffortLevel {
94 Low,
95 Medium,
96 High,
97 Xhigh,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
111#[serde(default)]
112pub struct ReasoningConfig {
113 pub optimizer_model: String,
115 pub executor_model: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 #[schemars(with = "Option<ReasoningEffortLevel>")]
120 pub reasoning_effort: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub api_base_url: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
127 pub max_input_tokens: Option<u32>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub max_completion_tokens: Option<u32>,
131 pub executor_timeout_secs: u64,
133 pub empty_response_no_retry_after_secs: u64,
135 pub stream_heartbeat_secs: u64,
137}
138
139impl Default for ReasoningConfig {
140 fn default() -> Self {
141 Self {
142 optimizer_model: "anthropic/claude-sonnet-4.6".into(),
143 executor_model: "openai/gpt-5.2".into(),
144 reasoning_effort: None,
145 api_base_url: None,
146 max_input_tokens: None,
147 max_completion_tokens: Some(128_000),
148 executor_timeout_secs: 2700,
149 empty_response_no_retry_after_secs: 600,
150 stream_heartbeat_secs: 30,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
163#[serde(default)]
164pub struct OrchestratorConfig {
165 pub session_deadline_secs: u64,
167 pub inactivity_timeout_secs: u64,
169 pub compaction_threshold: f64,
171 pub commands: OrchestratorCommandsConfig,
173}
174
175#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
177#[serde(default)]
178pub struct OrchestratorCommandsConfig {
179 pub allow: Vec<String>,
181 pub deny: Vec<String>,
183}
184
185impl Default for OrchestratorConfig {
186 fn default() -> Self {
187 Self {
188 session_deadline_secs: 3600,
189 inactivity_timeout_secs: 300,
190 compaction_threshold: 0.80,
191 commands: OrchestratorCommandsConfig::default(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
204#[serde(default)]
205pub struct WebRetrievalConfig {
206 pub request_timeout_secs: u64,
208 pub default_max_bytes: u64,
210 pub default_search_results: u32,
212 pub max_search_results: u32,
214 pub summarizer: WebSummarizerConfig,
216}
217
218impl Default for WebRetrievalConfig {
219 fn default() -> Self {
220 Self {
221 request_timeout_secs: 30,
222 default_max_bytes: 5 * 1024 * 1024, default_search_results: 8,
224 max_search_results: 20,
225 summarizer: WebSummarizerConfig::default(),
226 }
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
232#[serde(default)]
233pub struct WebSummarizerConfig {
234 pub model: String,
236 pub max_tokens: u32,
238 pub temperature: f64,
240}
241
242impl Default for WebSummarizerConfig {
243 fn default() -> Self {
244 Self {
245 model: "claude-haiku-4-5".into(),
246 max_tokens: 300,
247 temperature: 0.2,
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
260#[serde(default)]
261pub struct CliToolsConfig {
262 pub ls_page_size: u32,
264 pub grep_default_limit: u32,
266 pub glob_default_limit: u32,
268 pub max_depth: u32,
270 pub pagination_cache_ttl_secs: u64,
272 pub just_execute_timeout_secs: u64,
274 pub just_search_timeout_secs: u64,
276 #[serde(default)]
278 pub extra_ignore_patterns: Vec<String>,
279}
280
281impl Default for CliToolsConfig {
282 fn default() -> Self {
283 Self {
284 ls_page_size: 100,
285 grep_default_limit: 200,
286 glob_default_limit: 500,
287 max_depth: 10,
288 pagination_cache_ttl_secs: 300,
289 just_execute_timeout_secs: 1800,
290 just_search_timeout_secs: 30,
291 extra_ignore_patterns: vec![],
292 }
293 }
294}
295
296#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
304#[serde(default)]
305pub struct ServicesConfig {
306 pub anthropic: AnthropicServiceConfig,
308 pub exa: ExaServiceConfig,
310 pub linear: LinearServiceConfig,
312 pub github: GitHubServiceConfig,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
318#[serde(default)]
319pub struct AnthropicServiceConfig {
320 pub base_url: String,
322}
323
324impl Default for AnthropicServiceConfig {
325 fn default() -> Self {
326 Self {
327 base_url: "https://api.anthropic.com".into(),
328 }
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
334#[serde(default)]
335pub struct ExaServiceConfig {
336 pub base_url: String,
338}
339
340impl Default for ExaServiceConfig {
341 fn default() -> Self {
342 Self {
343 base_url: "https://api.exa.ai".into(),
344 }
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
350#[serde(default)]
351pub struct LinearServiceConfig {
352 pub base_url: String,
354 pub connect_timeout_secs: u64,
356 pub request_timeout_secs: u64,
358}
359
360impl Default for LinearServiceConfig {
361 fn default() -> Self {
362 Self {
363 base_url: "https://api.linear.app/graphql".into(),
364 connect_timeout_secs: 10,
365 request_timeout_secs: 60,
366 }
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
372#[serde(default)]
373pub struct GitHubServiceConfig {
374 pub base_url: String,
376 pub total_timeout_secs: u64,
378}
379
380impl Default for GitHubServiceConfig {
381 fn default() -> Self {
382 Self {
383 base_url: "https://api.github.com".into(),
384 total_timeout_secs: 120,
385 }
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
397#[serde(default)]
398pub struct ReviewConfig {
399 pub run_timeout_secs: u64,
401}
402
403impl Default for ReviewConfig {
404 fn default() -> Self {
405 Self {
406 run_timeout_secs: 1800,
407 }
408 }
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
419#[serde(default)]
420pub struct ThoughtsConfig {
421 pub add_reference_timeout_secs: u64,
423}
424
425impl Default for ThoughtsConfig {
426 fn default() -> Self {
427 Self {
428 add_reference_timeout_secs: 600,
429 }
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
441#[serde(default)]
442pub struct LoggingConfig {
443 pub level: String,
445
446 pub json: bool,
448}
449
450impl Default for LoggingConfig {
451 fn default() -> Self {
452 Self {
453 level: "info".into(),
454 json: false,
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_default_config_serializes() {
465 let config = AgenticConfig::default();
466 let toml_str = toml::to_string_pretty(&config).unwrap();
467 assert!(toml_str.contains("[subagents]"));
468 assert!(toml_str.contains("[reasoning]"));
469 assert!(toml_str.contains("[services.anthropic]"));
471 assert!(toml_str.contains("[services.exa]"));
472 assert!(toml_str.contains("[services.linear]"));
473 assert!(toml_str.contains("[services.github]"));
474 assert!(toml_str.contains("[review]"));
475 assert!(toml_str.contains("[thoughts]"));
476 assert!(toml_str.contains("[orchestrator]"));
477 assert!(toml_str.contains("[orchestrator.commands]"));
478 assert!(toml_str.contains("[web_retrieval]"));
479 assert!(toml_str.contains("[cli_tools]"));
480 assert!(toml_str.contains("[logging]"));
481 assert!(!toml_str.contains("[models]"));
483 }
484
485 #[test]
486 fn test_default_models_use_undated_names() {
487 let subagents = SubagentsConfig::default();
488 assert!(!subagents.locator_model.contains("20"));
489 assert!(!subagents.analyzer_model.contains("20"));
490
491 let reasoning = ReasoningConfig::default();
492 assert!(!reasoning.optimizer_model.contains("20"));
493 assert!(!reasoning.executor_model.contains("20"));
494 }
495
496 #[test]
497 fn test_partial_config_deserializes() {
498 let toml_str = r#"
499[subagents]
500locator_model = "custom-model"
501"#;
502 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
503 assert_eq!(config.subagents.locator_model, "custom-model");
504 assert_eq!(config.subagents.analyzer_model, "claude-sonnet-4-6");
506 assert_eq!(config.subagents.runtime_timeout_secs, 3600);
507 assert_eq!(
508 config.services.anthropic.base_url,
509 "https://api.anthropic.com"
510 );
511 assert_eq!(
512 config.services.linear.base_url,
513 "https://api.linear.app/graphql"
514 );
515 assert!(config.orchestrator.commands.allow.is_empty());
516 assert!(config.orchestrator.commands.deny.is_empty());
517 }
518
519 #[test]
520 fn test_orchestrator_commands_deserialize() {
521 let toml_str = r#"
522[orchestrator.commands]
523allow = ["plan", "research"]
524deny = ["commit"]
525"#;
526
527 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
528
529 assert_eq!(config.orchestrator.commands.allow, ["plan", "research"]);
530 assert_eq!(config.orchestrator.commands.deny, ["commit"]);
531 }
532
533 #[test]
534 fn test_schema_field_optional() {
535 let toml_str = r#""$schema" = "file://./agentic.schema.json""#;
536 let config: AgenticConfig = toml::from_str(toml_str).unwrap();
537 assert_eq!(config.schema, Some("file://./agentic.schema.json".into()));
538 }
539
540 #[test]
542 fn test_web_retrieval_defaults_match_hardcoded() {
543 let cfg = WebRetrievalConfig::default();
544 assert_eq!(cfg.request_timeout_secs, 30);
545 assert_eq!(cfg.default_max_bytes, 5 * 1024 * 1024); assert_eq!(cfg.default_search_results, 8);
547 assert_eq!(cfg.max_search_results, 20);
548 assert_eq!(cfg.summarizer.model, "claude-haiku-4-5");
549 assert_eq!(cfg.summarizer.max_tokens, 300);
550 assert!((cfg.summarizer.temperature - 0.2).abs() < f64::EPSILON);
551 }
552
553 #[test]
554 fn test_cli_tools_defaults_match_hardcoded() {
555 let cfg = CliToolsConfig::default();
556 assert_eq!(cfg.ls_page_size, 100);
557 assert_eq!(cfg.grep_default_limit, 200);
558 assert_eq!(cfg.glob_default_limit, 500);
559 assert_eq!(cfg.max_depth, 10);
560 assert_eq!(cfg.pagination_cache_ttl_secs, 300);
561 assert_eq!(cfg.just_execute_timeout_secs, 1800);
562 assert_eq!(cfg.just_search_timeout_secs, 30);
563 assert!(cfg.extra_ignore_patterns.is_empty());
564 }
565
566 #[test]
567 fn test_orchestrator_defaults_match_hardcoded() {
568 let cfg = OrchestratorConfig::default();
569 assert_eq!(cfg.session_deadline_secs, 3600);
570 assert_eq!(cfg.inactivity_timeout_secs, 300);
571 assert!((cfg.compaction_threshold - 0.80).abs() < f64::EPSILON);
572 assert!(cfg.commands.allow.is_empty());
573 assert!(cfg.commands.deny.is_empty());
574 }
575
576 #[test]
577 fn test_services_defaults_match_hardcoded() {
578 let cfg = ServicesConfig::default();
579
580 assert_eq!(cfg.anthropic.base_url, "https://api.anthropic.com");
582
583 assert_eq!(cfg.exa.base_url, "https://api.exa.ai");
585
586 assert_eq!(cfg.linear.base_url, "https://api.linear.app/graphql");
588 assert_eq!(cfg.linear.connect_timeout_secs, 10);
589 assert_eq!(cfg.linear.request_timeout_secs, 60);
590
591 assert_eq!(cfg.github.base_url, "https://api.github.com");
593 assert_eq!(cfg.github.total_timeout_secs, 120);
594 }
595
596 #[test]
597 fn test_new_timeout_defaults_match_plan() {
598 let cfg = AgenticConfig::default();
599
600 assert_eq!(cfg.subagents.runtime_timeout_secs, 3600);
601 assert_eq!(cfg.review.run_timeout_secs, 1800);
602 assert_eq!(cfg.thoughts.add_reference_timeout_secs, 600);
603 }
604}