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    /// Orchestrator session and timing configuration.
33    pub orchestrator: OrchestratorConfig,
34
35    /// Web retrieval tool configuration.
36    pub web_retrieval: WebRetrievalConfig,
37
38    /// CLI tools (grep, glob, ls) configuration.
39    pub cli_tools: CliToolsConfig,
40
41    /// Logging and diagnostics configuration.
42    pub logging: LoggingConfig,
43}
44
45//
46// ─────────────────────────────────────────────────────────────────────────────
47// SUBAGENTS CONFIG
48// ─────────────────────────────────────────────────────────────────────────────
49//
50
51/// Configuration for coding-agent-tools subagents (`ask_agent` tool).
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53#[serde(default)]
54pub struct SubagentsConfig {
55    // TODO(3): Model name handling could be more type-safe:
56    // - Consider documenting supported models in code (enum or const list)
57    // - Standardize approach between anthropic-async, claudecode_rs, and consumers
58    // - Current string-based approach works but lacks IDE completion and validation
59    /// Model for Locator subagent (fast discovery). Uses Claude CLI format.
60    pub locator_model: String,
61    /// Model for Analyzer subagent (deep analysis). Uses Claude CLI format.
62    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//
75// ─────────────────────────────────────────────────────────────────────────────
76// REASONING CONFIG
77// ─────────────────────────────────────────────────────────────────────────────
78//
79
80/// Schema-only enum for `reasoning_effort` IDE autocomplete.
81/// Runtime storage remains Option<String> for advisory validation semantics.
82#[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// Note on external type dependencies: We investigated using model types from the
92// async-openai crate but found they use plain `String` for most model fields (chat
93// completions, embeddings, assistants, fine-tuning, audio transcription). Only image
94// generation (ImageModel) and TTS (SpeechModel) have typed enums, and those include
95// `Other(String)` escape hatches with #[serde(untagged)]. Their Model struct (for
96// listing available models) also uses `id: String`. Copying their types would not
97// improve our type safety since they face the same constraints we do and chose the
98// same approach. See research/pr127-group7-type-safety-external-type-dependencies.md.
99
100/// Configuration for gpt5-reasoner tool.
101#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
102#[serde(default)]
103pub struct ReasoningConfig {
104    /// `OpenRouter` model ID for optimizer step.
105    pub optimizer_model: String,
106    /// `OpenRouter` model ID for executor/reasoner step.
107    pub executor_model: String,
108    /// Optional reasoning effort level: low, medium, high, xhigh.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    #[schemars(with = "Option<ReasoningEffortLevel>")]
111    pub reasoning_effort: Option<String>,
112    /// Optional API base URL override for reasoning service.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub api_base_url: Option<String>,
115    /// Optional token limit for reasoning requests.
116    #[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//
133// ─────────────────────────────────────────────────────────────────────────────
134// ORCHESTRATOR CONFIG
135// ─────────────────────────────────────────────────────────────────────────────
136//
137
138/// Configuration for opencode-orchestrator-mcp.
139#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
140#[serde(default)]
141pub struct OrchestratorConfig {
142    /// Maximum session duration in seconds (default: 3600 = 1 hour).
143    pub session_deadline_secs: u64,
144    /// Inactivity timeout in seconds before session ends (default: 300 = 5 minutes).
145    pub inactivity_timeout_secs: u64,
146    /// Context compaction threshold as fraction 0.0-1.0 (default: 0.80).
147    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//
161// ─────────────────────────────────────────────────────────────────────────────
162// WEB RETRIEVAL CONFIG
163// ─────────────────────────────────────────────────────────────────────────────
164//
165
166/// Configuration for web-retrieval tools (`web_fetch`, `web_search`).
167#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
168#[serde(default)]
169pub struct WebRetrievalConfig {
170    /// HTTP request timeout in seconds (default: 30).
171    pub request_timeout_secs: u64,
172    /// Default maximum bytes to fetch (default: 5MB).
173    pub default_max_bytes: u64,
174    /// Default number of search results (default: 8).
175    pub default_search_results: u32,
176    /// Maximum number of search results allowed (default: 20).
177    pub max_search_results: u32,
178    /// Summarizer configuration for Haiku-based summarization.
179    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, // 5MB
187            default_search_results: 8,
188            max_search_results: 20,
189            summarizer: WebSummarizerConfig::default(),
190        }
191    }
192}
193
194/// Configuration for the web summarizer (Haiku).
195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
196#[serde(default)]
197pub struct WebSummarizerConfig {
198    /// Model to use for summarization (default: claude-haiku-4-5).
199    pub model: String,
200    /// Maximum tokens for summary output (default: 300).
201    pub max_tokens: u32,
202    /// Temperature for summary generation (default: 0.2).
203    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//
217// ─────────────────────────────────────────────────────────────────────────────
218// CLI TOOLS CONFIG
219// ─────────────────────────────────────────────────────────────────────────────
220//
221
222/// Configuration for CLI tools (grep, glob, ls).
223#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
224#[serde(default)]
225pub struct CliToolsConfig {
226    /// Default page size for ls results (default: 100).
227    pub ls_page_size: u32,
228    /// Default `head_limit` for grep results (default: 200).
229    pub grep_default_limit: u32,
230    /// Default `head_limit` for glob results (default: 500).
231    pub glob_default_limit: u32,
232    /// Maximum directory traversal depth (default: 10).
233    pub max_depth: u32,
234    /// Pagination cache TTL in seconds (default: 300 = 5 minutes).
235    pub pagination_cache_ttl_secs: u64,
236    /// Additional ignore patterns to append to builtin ignores.
237    #[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//
255// ─────────────────────────────────────────────────────────────────────────────
256// SERVICES CONFIG
257// ─────────────────────────────────────────────────────────────────────────────
258//
259
260/// External service configurations.
261#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
262#[serde(default)]
263pub struct ServicesConfig {
264    /// Anthropic API configuration.
265    pub anthropic: AnthropicServiceConfig,
266    /// Exa search API configuration.
267    pub exa: ExaServiceConfig,
268}
269
270/// Anthropic API service configuration.
271#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
272#[serde(default)]
273pub struct AnthropicServiceConfig {
274    /// Base URL for the Anthropic API.
275    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/// Exa search API service configuration.
287#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
288#[serde(default)]
289pub struct ExaServiceConfig {
290    /// Base URL for the Exa API.
291    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//
303// ─────────────────────────────────────────────────────────────────────────────
304// LOGGING CONFIG
305// ─────────────────────────────────────────────────────────────────────────────
306//
307
308/// Logging and diagnostics configuration.
309#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
310#[serde(default)]
311pub struct LoggingConfig {
312    /// Log level (trace, debug, info, warn, error).
313    pub level: String,
314
315    /// Whether to enable JSON-formatted logs.
316    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        // Services sections serialize as [services.anthropic], [services.exa], etc.
339        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        // Ensure old sections are NOT present
346        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        // Other fields get defaults
370        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    // Default value assertion tests - ensure defaults match current hardcoded behavior
385    #[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); // 5MB
390        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        // Anthropic
421        assert_eq!(cfg.anthropic.base_url, "https://api.anthropic.com");
422
423        // Exa
424        assert_eq!(cfg.exa.base_url, "https://api.exa.ai");
425    }
426}