Skip to main content

agentic_config/
types.rs

1//! Configuration types for the agentic tools ecosystem.
2//!
3//! The root type is [`AgenticConfig`], which contains namespaced sub-configs
4//! for different concerns: subagents, reasoning, services, orchestrator,
5//! web retrieval, CLI tools, and logging.
6
7use schemars::JsonSchema;
8use serde::Deserialize;
9use serde::Serialize;
10
11/// Root configuration for all agentic tools.
12///
13/// This is the unified configuration that gets loaded from `agentic.toml` files.
14/// All fields use `#[serde(default)]` so partial configs work correctly.
15#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
16#[serde(default)]
17pub struct AgenticConfig {
18    /// Optional JSON Schema URL for IDE autocomplete support.
19    /// In TOML: `"$schema" = "file://./agentic.schema.json"`
20    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
21    pub schema: Option<String>,
22
23    /// Tool-specific config for coding-agent-tools subagents.
24    pub subagents: SubagentsConfig,
25
26    /// Tool-specific config for gpt5-reasoner.
27    pub reasoning: ReasoningConfig,
28
29    /// External service configurations (Anthropic, Exa).
30    pub services: ServicesConfig,
31
32    /// Review tools configuration.
33    pub review: ReviewConfig,
34
35    /// Thoughts tool configuration.
36    pub thoughts: ThoughtsConfig,
37
38    /// Orchestrator session and timing configuration.
39    pub orchestrator: OrchestratorConfig,
40
41    /// Web retrieval tool configuration.
42    pub web_retrieval: WebRetrievalConfig,
43
44    /// CLI tools (grep, glob, ls) configuration.
45    pub cli_tools: CliToolsConfig,
46
47    /// Workspace-local file and todo tools configuration.
48    pub workspace_tools: WorkspaceToolsConfig,
49
50    /// Logging and diagnostics configuration.
51    pub logging: LoggingConfig,
52}
53
54//
55// ─────────────────────────────────────────────────────────────────────────────
56// SUBAGENTS CONFIG
57// ─────────────────────────────────────────────────────────────────────────────
58//
59
60/// Configuration for coding-agent-tools subagents (`ask_agent` tool).
61#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
62#[serde(default)]
63pub struct SubagentsConfig {
64    // TODO(3): Model name handling could be more type-safe:
65    // - Consider documenting supported models in code (enum or const list)
66    // - Standardize approach between anthropic-async, claudecode_rs, and consumers
67    // - Current string-based approach works but lacks IDE completion and validation
68    /// Model for Locator subagent (fast discovery). Uses Claude CLI format.
69    pub locator_model: String,
70    /// Model for Analyzer subagent (deep analysis). Uses Claude CLI format.
71    pub analyzer_model: String,
72    /// Wall-clock timeout for `ask_agent` runtime in seconds. `0` disables the timeout.
73    pub runtime_timeout_secs: u64,
74}
75
76impl Default for SubagentsConfig {
77    fn default() -> Self {
78        Self {
79            locator_model: "claude-haiku-4-5".into(),
80            analyzer_model: "claude-sonnet-4-6".into(),
81            runtime_timeout_secs: 3600,
82        }
83    }
84}
85
86//
87// ─────────────────────────────────────────────────────────────────────────────
88// REASONING CONFIG
89// ─────────────────────────────────────────────────────────────────────────────
90//
91
92/// Schema-only enum for `reasoning_effort` IDE autocomplete.
93/// Runtime storage remains Option<String> for advisory validation semantics.
94#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
95#[serde(rename_all = "lowercase")]
96enum ReasoningEffortLevel {
97    Low,
98    Medium,
99    High,
100    Xhigh,
101}
102
103// Note on external type dependencies: We investigated using model types from the
104// async-openai crate but found they use plain `String` for most model fields (chat
105// completions, embeddings, assistants, fine-tuning, audio transcription). Only image
106// generation (ImageModel) and TTS (SpeechModel) have typed enums, and those include
107// `Other(String)` escape hatches with #[serde(untagged)]. Their Model struct (for
108// listing available models) also uses `id: String`. Copying their types would not
109// improve our type safety since they face the same constraints we do and chose the
110// same approach. See research/pr127-group7-type-safety-external-type-dependencies.md.
111
112/// Configuration for gpt5-reasoner tool.
113#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
114#[serde(default)]
115pub struct ReasoningConfig {
116    /// `OpenRouter` model ID for optimizer step.
117    pub optimizer_model: String,
118    /// `OpenRouter` model ID for executor/reasoner step.
119    pub executor_model: String,
120    /// Optional reasoning effort level: low, medium, high, xhigh.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    #[schemars(with = "Option<ReasoningEffortLevel>")]
123    pub reasoning_effort: Option<String>,
124    /// Optional API base URL override for reasoning service.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub api_base_url: Option<String>,
127    /// Max tokens allowed in the final input prompt after file injection.
128    /// If None, `gpt5_reasoner` enforces its internal default (`250_000`).
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub max_input_tokens: Option<u32>,
131    /// Upper bound for generated completion tokens (visible + reasoning).
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub max_completion_tokens: Option<u32>,
134    /// Executor timeout in seconds.
135    pub executor_timeout_secs: u64,
136    /// Suppress empty-response retry when attempt duration exceeds this threshold.
137    pub empty_response_no_retry_after_secs: u64,
138    /// Heartbeat cadence for executor streaming logs.
139    pub stream_heartbeat_secs: u64,
140}
141
142impl Default for ReasoningConfig {
143    fn default() -> Self {
144        Self {
145            optimizer_model: "anthropic/claude-sonnet-4.6".into(),
146            executor_model: "openai/gpt-5.2".into(),
147            reasoning_effort: None,
148            api_base_url: None,
149            max_input_tokens: None,
150            max_completion_tokens: Some(128_000),
151            executor_timeout_secs: 2700,
152            empty_response_no_retry_after_secs: 600,
153            stream_heartbeat_secs: 30,
154        }
155    }
156}
157
158//
159// ─────────────────────────────────────────────────────────────────────────────
160// ORCHESTRATOR CONFIG
161// ─────────────────────────────────────────────────────────────────────────────
162//
163
164/// Configuration for opencode-orchestrator-mcp.
165#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
166#[serde(default)]
167pub struct OrchestratorConfig {
168    /// Maximum session duration in seconds (default: 3600 = 1 hour).
169    pub session_deadline_secs: u64,
170    /// Inactivity timeout in seconds before session ends (default: 300 = 5 minutes).
171    pub inactivity_timeout_secs: u64,
172    /// Context compaction threshold as fraction 0.0-1.0 (default: 0.80).
173    pub compaction_threshold: f64,
174    /// Command filtering policy for orchestrator-exposed `OpenCode` commands.
175    pub commands: OrchestratorCommandsConfig,
176    /// Agent filtering policy for explicit orchestrator agent listing/selection.
177    pub agents: OrchestratorAgentsConfig,
178}
179
180/// Command filtering policy for orchestrator-exposed `OpenCode` commands.
181#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
182#[serde(default)]
183pub struct OrchestratorCommandsConfig {
184    /// Exact case-sensitive command names to allow. Empty means no allowlist restriction.
185    pub allow: Vec<String>,
186    /// Exact case-sensitive command names to deny. Deny wins over allow.
187    pub deny: Vec<String>,
188}
189
190/// Agent filtering policy for explicit orchestrator agent listing/selection.
191#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
192#[serde(default)]
193pub struct OrchestratorAgentsConfig {
194    /// Exact case-sensitive agent names to allow. Empty means no allowlist restriction.
195    pub allow: Vec<String>,
196    /// Exact case-sensitive agent names to deny. Deny wins over allow.
197    pub deny: Vec<String>,
198}
199
200impl Default for OrchestratorConfig {
201    fn default() -> Self {
202        Self {
203            session_deadline_secs: 3600,
204            inactivity_timeout_secs: 300,
205            compaction_threshold: 0.80,
206            commands: OrchestratorCommandsConfig::default(),
207            agents: OrchestratorAgentsConfig::default(),
208        }
209    }
210}
211
212//
213// ─────────────────────────────────────────────────────────────────────────────
214// WEB RETRIEVAL CONFIG
215// ─────────────────────────────────────────────────────────────────────────────
216//
217
218/// Configuration for web-retrieval tools (`web_fetch`, `web_search`).
219#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
220#[serde(default)]
221pub struct WebRetrievalConfig {
222    /// HTTP request timeout in seconds (default: 30).
223    pub request_timeout_secs: u64,
224    /// Default maximum bytes to fetch (default: 5MB).
225    pub default_max_bytes: u64,
226    /// Default number of search results (default: 8).
227    pub default_search_results: u32,
228    /// Maximum number of search results allowed (default: 20).
229    pub max_search_results: u32,
230    /// Summarizer configuration for Haiku-based summarization.
231    pub summarizer: WebSummarizerConfig,
232}
233
234impl Default for WebRetrievalConfig {
235    fn default() -> Self {
236        Self {
237            request_timeout_secs: 30,
238            default_max_bytes: 5 * 1024 * 1024, // 5MB
239            default_search_results: 8,
240            max_search_results: 20,
241            summarizer: WebSummarizerConfig::default(),
242        }
243    }
244}
245
246/// Configuration for the web summarizer (Haiku).
247#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
248#[serde(default)]
249pub struct WebSummarizerConfig {
250    /// Model to use for summarization (default: claude-haiku-4-5).
251    pub model: String,
252    /// Maximum tokens for summary output (default: 300).
253    pub max_tokens: u32,
254    /// Temperature for summary generation (default: 0.2).
255    pub temperature: f64,
256}
257
258impl Default for WebSummarizerConfig {
259    fn default() -> Self {
260        Self {
261            model: "claude-haiku-4-5".into(),
262            max_tokens: 300,
263            temperature: 0.2,
264        }
265    }
266}
267
268//
269// ─────────────────────────────────────────────────────────────────────────────
270// CLI TOOLS CONFIG
271// ─────────────────────────────────────────────────────────────────────────────
272//
273
274/// Configuration for CLI tools (grep, glob, ls).
275#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
276#[serde(default)]
277pub struct CliToolsConfig {
278    /// Default page size for ls results (default: 100).
279    pub ls_page_size: u32,
280    /// Default `head_limit` for grep results (default: 200).
281    pub grep_default_limit: u32,
282    /// Default `head_limit` for glob results (default: 500).
283    pub glob_default_limit: u32,
284    /// Maximum directory traversal depth (default: 10).
285    pub max_depth: u32,
286    /// Pagination cache TTL in seconds (default: 300 = 5 minutes).
287    pub pagination_cache_ttl_secs: u64,
288    /// Wall-clock timeout for `cli_just_execute` in seconds. `0` disables the timeout.
289    pub just_execute_timeout_secs: u64,
290    /// Wall-clock timeout for `cli_just_search` in seconds. `0` disables the timeout.
291    pub just_search_timeout_secs: u64,
292    /// Additional ignore patterns to append to builtin ignores.
293    #[serde(default)]
294    pub extra_ignore_patterns: Vec<String>,
295}
296
297impl Default for CliToolsConfig {
298    fn default() -> Self {
299        Self {
300            ls_page_size: 100,
301            grep_default_limit: 200,
302            glob_default_limit: 500,
303            max_depth: 10,
304            pagination_cache_ttl_secs: 300,
305            just_execute_timeout_secs: 1800,
306            just_search_timeout_secs: 30,
307            extra_ignore_patterns: vec![],
308        }
309    }
310}
311
312//
313// ─────────────────────────────────────────────────────────────────────────────
314// WORKSPACE TOOLS CONFIG
315// ─────────────────────────────────────────────────────────────────────────────
316//
317
318/// Configuration for workspace-local file and todo tools.
319#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
320#[serde(default)]
321pub struct WorkspaceToolsConfig {
322    /// Enable the `workspace_read` tool.
323    pub workspace_read: bool,
324    /// Enable the `workspace_todowrite` tool.
325    pub workspace_todowrite: bool,
326    /// Enable the `workspace_edit` tool.
327    pub workspace_edit: bool,
328    /// Enable the `workspace_apply_patch` tool.
329    pub workspace_apply_patch: bool,
330}
331
332//
333// ─────────────────────────────────────────────────────────────────────────────
334// SERVICES CONFIG
335// ─────────────────────────────────────────────────────────────────────────────
336//
337
338/// External service configurations.
339#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
340#[serde(default)]
341pub struct ServicesConfig {
342    /// Anthropic API configuration.
343    pub anthropic: AnthropicServiceConfig,
344    /// Exa search API configuration.
345    pub exa: ExaServiceConfig,
346    /// Linear API configuration.
347    pub linear: LinearServiceConfig,
348    /// GitHub API configuration.
349    pub github: GitHubServiceConfig,
350}
351
352/// Anthropic API service configuration.
353#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
354#[serde(default)]
355pub struct AnthropicServiceConfig {
356    /// Base URL for the Anthropic API.
357    pub base_url: String,
358}
359
360impl Default for AnthropicServiceConfig {
361    fn default() -> Self {
362        Self {
363            base_url: "https://api.anthropic.com".into(),
364        }
365    }
366}
367
368/// Exa search API service configuration.
369#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
370#[serde(default)]
371pub struct ExaServiceConfig {
372    /// Base URL for the Exa API.
373    pub base_url: String,
374}
375
376impl Default for ExaServiceConfig {
377    fn default() -> Self {
378        Self {
379            base_url: "https://api.exa.ai".into(),
380        }
381    }
382}
383
384/// Linear API service configuration.
385#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
386#[serde(default)]
387pub struct LinearServiceConfig {
388    /// Base URL for the Linear GraphQL API.
389    pub base_url: String,
390    /// Connection establishment timeout in seconds. `0` disables the timeout.
391    pub connect_timeout_secs: u64,
392    /// Per-request timeout in seconds. `0` disables the timeout.
393    pub request_timeout_secs: u64,
394}
395
396impl Default for LinearServiceConfig {
397    fn default() -> Self {
398        Self {
399            base_url: "https://api.linear.app/graphql".into(),
400            connect_timeout_secs: 10,
401            request_timeout_secs: 60,
402        }
403    }
404}
405
406/// GitHub API service configuration.
407#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
408#[serde(default)]
409pub struct GitHubServiceConfig {
410    /// Base URL for the GitHub API.
411    pub base_url: String,
412    /// Total timeout for multi-request operations in seconds. `0` disables the timeout.
413    pub total_timeout_secs: u64,
414}
415
416impl Default for GitHubServiceConfig {
417    fn default() -> Self {
418        Self {
419            base_url: "https://api.github.com".into(),
420            total_timeout_secs: 120,
421        }
422    }
423}
424
425//
426// ─────────────────────────────────────────────────────────────────────────────
427// REVIEW CONFIG
428// ─────────────────────────────────────────────────────────────────────────────
429//
430
431/// Configuration for review tools.
432#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
433#[serde(default)]
434pub struct ReviewConfig {
435    /// Wall-clock timeout for `review_run` sessions in seconds. `0` disables the timeout.
436    pub run_timeout_secs: u64,
437}
438
439impl Default for ReviewConfig {
440    fn default() -> Self {
441        Self {
442            run_timeout_secs: 1800,
443        }
444    }
445}
446
447//
448// ─────────────────────────────────────────────────────────────────────────────
449// THOUGHTS CONFIG
450// ─────────────────────────────────────────────────────────────────────────────
451//
452
453/// Configuration for thoughts MCP-adjacent operations.
454#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
455#[serde(default)]
456pub struct ThoughtsConfig {
457    /// Wall-clock timeout for `thoughts_add_reference` in seconds. `0` disables the timeout.
458    pub add_reference_timeout_secs: u64,
459}
460
461impl Default for ThoughtsConfig {
462    fn default() -> Self {
463        Self {
464            add_reference_timeout_secs: 600,
465        }
466    }
467}
468
469//
470// ─────────────────────────────────────────────────────────────────────────────
471// LOGGING CONFIG
472// ─────────────────────────────────────────────────────────────────────────────
473//
474
475/// Logging and diagnostics configuration.
476#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
477#[serde(default)]
478pub struct LoggingConfig {
479    /// Log level (trace, debug, info, warn, error).
480    pub level: String,
481
482    /// Whether to enable JSON-formatted logs.
483    pub json: bool,
484}
485
486impl Default for LoggingConfig {
487    fn default() -> Self {
488        Self {
489            level: "info".into(),
490            json: false,
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_default_config_serializes() {
501        let config = AgenticConfig::default();
502        let toml_str = toml::to_string_pretty(&config).unwrap();
503        assert!(toml_str.contains("[subagents]"));
504        assert!(toml_str.contains("[reasoning]"));
505        // Services sections serialize as [services.anthropic], [services.exa], etc.
506        assert!(toml_str.contains("[services.anthropic]"));
507        assert!(toml_str.contains("[services.exa]"));
508        assert!(toml_str.contains("[services.linear]"));
509        assert!(toml_str.contains("[services.github]"));
510        assert!(toml_str.contains("[review]"));
511        assert!(toml_str.contains("[thoughts]"));
512        assert!(toml_str.contains("[orchestrator]"));
513        assert!(toml_str.contains("[orchestrator.commands]"));
514        assert!(toml_str.contains("[orchestrator.agents]"));
515        assert!(toml_str.contains("[web_retrieval]"));
516        assert!(toml_str.contains("[cli_tools]"));
517        assert!(toml_str.contains("[workspace_tools]"));
518        assert!(toml_str.contains("[logging]"));
519        // Ensure old sections are NOT present
520        assert!(!toml_str.contains("[models]"));
521    }
522
523    #[test]
524    fn test_default_models_use_undated_names() {
525        let subagents = SubagentsConfig::default();
526        assert!(!subagents.locator_model.contains("20"));
527        assert!(!subagents.analyzer_model.contains("20"));
528
529        let reasoning = ReasoningConfig::default();
530        assert!(!reasoning.optimizer_model.contains("20"));
531        assert!(!reasoning.executor_model.contains("20"));
532    }
533
534    #[test]
535    fn test_partial_config_deserializes() {
536        let toml_str = r#"
537[subagents]
538locator_model = "custom-model"
539"#;
540        let config: AgenticConfig = toml::from_str(toml_str).unwrap();
541        assert_eq!(config.subagents.locator_model, "custom-model");
542        // Other fields get defaults
543        assert_eq!(config.subagents.analyzer_model, "claude-sonnet-4-6");
544        assert_eq!(config.subagents.runtime_timeout_secs, 3600);
545        assert_eq!(
546            config.services.anthropic.base_url,
547            "https://api.anthropic.com"
548        );
549        assert_eq!(
550            config.services.linear.base_url,
551            "https://api.linear.app/graphql"
552        );
553        assert!(config.orchestrator.commands.allow.is_empty());
554        assert!(config.orchestrator.commands.deny.is_empty());
555        assert!(config.orchestrator.agents.allow.is_empty());
556        assert!(config.orchestrator.agents.deny.is_empty());
557    }
558
559    #[test]
560    fn test_orchestrator_commands_deserialize() {
561        let toml_str = r#"
562[orchestrator.commands]
563allow = ["plan", "research"]
564deny = ["commit"]
565"#;
566
567        let config: AgenticConfig = toml::from_str(toml_str).unwrap();
568
569        assert_eq!(config.orchestrator.commands.allow, ["plan", "research"]);
570        assert_eq!(config.orchestrator.commands.deny, ["commit"]);
571    }
572
573    #[test]
574    fn test_orchestrator_agents_partial_deserializes_to_empty_lists() {
575        let toml_str = r"
576[orchestrator.agents]
577";
578
579        let config: AgenticConfig = toml::from_str(toml_str).unwrap();
580
581        assert!(config.orchestrator.agents.allow.is_empty());
582        assert!(config.orchestrator.agents.deny.is_empty());
583    }
584
585    #[test]
586    fn test_orchestrator_agents_round_trip() {
587        let toml_str = r#"
588[orchestrator.agents]
589allow = ["Plan", "Research"]
590deny = ["Bash"]
591"#;
592
593        let config: AgenticConfig = toml::from_str(toml_str).unwrap();
594
595        assert_eq!(config.orchestrator.agents.allow, ["Plan", "Research"]);
596        assert_eq!(config.orchestrator.agents.deny, ["Bash"]);
597
598        let serialized = toml::to_string_pretty(&config).unwrap();
599        assert!(serialized.contains("[orchestrator.agents]"));
600        assert!(serialized.contains("allow = ["));
601        assert!(serialized.contains("\"Plan\""));
602        assert!(serialized.contains("\"Research\""));
603        assert!(serialized.contains("deny = ["));
604        assert!(serialized.contains("\"Bash\""));
605    }
606
607    #[test]
608    fn test_schema_field_optional() {
609        let toml_str = r#""$schema" = "file://./agentic.schema.json""#;
610        let config: AgenticConfig = toml::from_str(toml_str).unwrap();
611        assert_eq!(config.schema, Some("file://./agentic.schema.json".into()));
612    }
613
614    // Default value assertion tests - ensure defaults match current hardcoded behavior
615    #[test]
616    fn test_web_retrieval_defaults_match_hardcoded() {
617        let cfg = WebRetrievalConfig::default();
618        assert_eq!(cfg.request_timeout_secs, 30);
619        assert_eq!(cfg.default_max_bytes, 5 * 1024 * 1024); // 5MB
620        assert_eq!(cfg.default_search_results, 8);
621        assert_eq!(cfg.max_search_results, 20);
622        assert_eq!(cfg.summarizer.model, "claude-haiku-4-5");
623        assert_eq!(cfg.summarizer.max_tokens, 300);
624        assert!((cfg.summarizer.temperature - 0.2).abs() < f64::EPSILON);
625    }
626
627    #[test]
628    fn test_cli_tools_defaults_match_hardcoded() {
629        let cfg = CliToolsConfig::default();
630        assert_eq!(cfg.ls_page_size, 100);
631        assert_eq!(cfg.grep_default_limit, 200);
632        assert_eq!(cfg.glob_default_limit, 500);
633        assert_eq!(cfg.max_depth, 10);
634        assert_eq!(cfg.pagination_cache_ttl_secs, 300);
635        assert_eq!(cfg.just_execute_timeout_secs, 1800);
636        assert_eq!(cfg.just_search_timeout_secs, 30);
637        assert!(cfg.extra_ignore_patterns.is_empty());
638    }
639
640    #[test]
641    fn test_orchestrator_defaults_match_hardcoded() {
642        let cfg = OrchestratorConfig::default();
643        assert_eq!(cfg.session_deadline_secs, 3600);
644        assert_eq!(cfg.inactivity_timeout_secs, 300);
645        assert!((cfg.compaction_threshold - 0.80).abs() < f64::EPSILON);
646        assert!(cfg.commands.allow.is_empty());
647        assert!(cfg.commands.deny.is_empty());
648        assert!(cfg.agents.allow.is_empty());
649        assert!(cfg.agents.deny.is_empty());
650    }
651
652    #[test]
653    fn test_services_defaults_match_hardcoded() {
654        let cfg = ServicesConfig::default();
655
656        // Anthropic
657        assert_eq!(cfg.anthropic.base_url, "https://api.anthropic.com");
658
659        // Exa
660        assert_eq!(cfg.exa.base_url, "https://api.exa.ai");
661
662        // Linear
663        assert_eq!(cfg.linear.base_url, "https://api.linear.app/graphql");
664        assert_eq!(cfg.linear.connect_timeout_secs, 10);
665        assert_eq!(cfg.linear.request_timeout_secs, 60);
666
667        // GitHub
668        assert_eq!(cfg.github.base_url, "https://api.github.com");
669        assert_eq!(cfg.github.total_timeout_secs, 120);
670    }
671
672    #[test]
673    fn test_new_timeout_defaults_match_plan() {
674        let cfg = AgenticConfig::default();
675
676        assert_eq!(cfg.subagents.runtime_timeout_secs, 3600);
677        assert_eq!(cfg.review.run_timeout_secs, 1800);
678        assert_eq!(cfg.thoughts.add_reference_timeout_secs, 600);
679    }
680
681    #[test]
682    fn test_workspace_tools_defaults_match_plan() {
683        let cfg = AgenticConfig::default();
684
685        assert!(!cfg.workspace_tools.workspace_read);
686        assert!(!cfg.workspace_tools.workspace_todowrite);
687        assert!(!cfg.workspace_tools.workspace_edit);
688        assert!(!cfg.workspace_tools.workspace_apply_patch);
689    }
690}