Skip to main content

a3s_code_core/config/
loader.rs

1use super::provider::{
2    apply_model_caps, ModelConfig, ModelCost, ModelLimit, ModelModalities, ProviderConfig,
3};
4use super::{AutoDelegationConfig, CodeConfig, OsConfig, StorageBackend};
5use crate::error::{CodeError, Result};
6use crate::llm::LlmConfig;
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10// ============================================================================
11// ACL Parsing Helpers
12// ============================================================================
13
14fn acl_attr<'a>(block: &'a a3s_acl::Block, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
15    keys.iter().find_map(|key| block.attributes.get(*key))
16}
17
18fn acl_string(value: &a3s_acl::Value) -> Option<String> {
19    match value {
20        a3s_acl::Value::String(s) => Some(s.clone()),
21        a3s_acl::Value::Call(name, args) if name == "env" => {
22            let var_name = args.first().and_then(acl_string)?;
23            std::env::var(var_name).ok()
24        }
25        _ => None,
26    }
27}
28
29fn acl_string_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
30    acl_attr(block, keys).and_then(acl_string)
31}
32
33fn acl_label_or_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
34    block
35        .labels
36        .first()
37        .cloned()
38        .or_else(|| acl_string_attr(block, keys))
39}
40
41fn acl_bool_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<bool> {
42    match acl_attr(block, keys) {
43        Some(a3s_acl::Value::Bool(value)) => Some(*value),
44        _ => None,
45    }
46}
47
48fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
49    match acl_attr(block, keys) {
50        Some(a3s_acl::Value::Number(value)) if *value >= 0.0 => Some(*value as usize),
51        _ => None,
52    }
53}
54
55fn acl_f32_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<f32> {
56    match acl_attr(block, keys) {
57        Some(a3s_acl::Value::Number(value)) => Some(*value as f32),
58        _ => None,
59    }
60}
61
62fn parse_auto_delegation_block(
63    block: &a3s_acl::Block,
64    base: &AutoDelegationConfig,
65) -> AutoDelegationConfig {
66    let mut config = base.clone();
67    if let Some(enabled) = acl_bool_attr(block, &["enabled"]) {
68        config.enabled = enabled;
69    }
70    if let Some(auto_parallel) =
71        acl_bool_attr(block, &["auto_parallel", "autoParallel", "parallel"])
72    {
73        config.auto_parallel = auto_parallel;
74    }
75    if let Some(allow_manual_delegation) = acl_bool_attr(
76        block,
77        &[
78            "allow_manual_delegation",
79            "allowManualDelegation",
80            "manual_delegation",
81            "manualDelegation",
82        ],
83    ) {
84        config.allow_manual_delegation = allow_manual_delegation;
85    }
86    if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) {
87        config.min_confidence = min_confidence.clamp(0.0, 1.0);
88    }
89    if let Some(max_tasks) = acl_usize_attr(block, &["max_tasks", "maxTasks"]) {
90        config.max_tasks = max_tasks.max(1);
91    }
92    config
93}
94
95fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
96    match value {
97        a3s_acl::Value::Number(value) if *value >= 0.0 => {
98            Some((*value as usize).min(u32::MAX as usize) as u32)
99        }
100        _ => None,
101    }
102}
103
104fn acl_object_u32_attr(value: &a3s_acl::Value, key: &str) -> Option<u32> {
105    match value {
106        a3s_acl::Value::Object(pairs) => pairs
107            .iter()
108            .find_map(|(candidate, value)| (candidate == key).then(|| acl_u32(value)).flatten()),
109        _ => None,
110    }
111}
112
113fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
114    let value = acl_attr(block, keys)?;
115    match value {
116        a3s_acl::Value::List(items) => Some(
117            items
118                .iter()
119                .filter_map(acl_string)
120                .map(PathBuf::from)
121                .collect(),
122        ),
123        _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
124    }
125}
126
127// ============================================================================
128// CodeConfig Implementation
129// ============================================================================
130
131impl CodeConfig {
132    /// Create a new empty configuration
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Load configuration from an ACL-compatible config file.
138    ///
139    /// `.acl` is the only supported config file extension. JSON and legacy
140    /// `.hcl` config files are not supported.
141    pub fn from_file(path: &Path) -> Result<Self> {
142        let content = std::fs::read_to_string(path).map_err(|e| {
143            CodeError::Config(format!(
144                "Failed to read config file {}: {}",
145                path.display(),
146                e
147            ))
148        })?;
149
150        Self::from_acl(&content).map_err(|e| {
151            CodeError::Config(format!(
152                "Failed to parse ACL config {}: {}",
153                path.display(),
154                e
155            ))
156        })
157    }
158
159    /// Parse configuration from an ACL string.
160    ///
161    /// ACL (Agent Configuration Language) uses labeled blocks like
162    /// `providers "openai" { }`.
163    pub fn from_acl(content: &str) -> Result<Self> {
164        use a3s_acl::parse_acl;
165
166        let doc = parse_acl(content)
167            .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
168
169        let mut config = Self::default();
170
171        for block in doc.blocks {
172            match block.name.as_str() {
173                "default_model" => {
174                    // ACL: default_model = "openai/gpt-4" or just "openai/gpt-4" as label
175                    if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
176                        config.default_model = Some(default_model);
177                    }
178                }
179                "storage_backend" => {
180                    if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
181                        config.storage_backend = match backend.to_ascii_lowercase().as_str() {
182                            "memory" => StorageBackend::Memory,
183                            "custom" => StorageBackend::Custom,
184                            _ => StorageBackend::File,
185                        };
186                    }
187                }
188                "sessions_dir" => {
189                    if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
190                        config.sessions_dir = Some(PathBuf::from(path));
191                    }
192                }
193                "storage_url" => {
194                    if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
195                        config.storage_url = Some(storage_url);
196                    }
197                }
198                "skill_dirs" | "skills" => {
199                    if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
200                        config.skill_dirs = paths;
201                    }
202                }
203                "agent_dirs" => {
204                    if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
205                        config.agent_dirs = paths;
206                    }
207                }
208                "max_tool_rounds" => {
209                    if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
210                        config.max_tool_rounds = Some(max_tool_rounds);
211                    }
212                }
213                "max_parallel_tasks" => {
214                    if let Some(max_parallel_tasks) =
215                        acl_usize_attr(&block, &["max_parallel_tasks"])
216                    {
217                        config.max_parallel_tasks = Some(max_parallel_tasks);
218                    }
219                }
220                "auto_parallel" | "auto_parallel_enabled" => {
221                    if let Some(auto_parallel) =
222                        acl_bool_attr(&block, &["auto_parallel", "auto_parallel_enabled"])
223                    {
224                        config.auto_parallel = Some(auto_parallel);
225                    }
226                }
227                "auto_delegation" => {
228                    config.auto_delegation =
229                        parse_auto_delegation_block(&block, &config.auto_delegation);
230                }
231                "thinking_budget" => {
232                    if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
233                        config.thinking_budget = Some(thinking_budget);
234                    }
235                }
236                "os" => {
237                    if let Some(address) =
238                        acl_label_or_attr(&block, &["os", "address", "url", "baseUrl", "base_url"])
239                            .map(|value| value.trim().to_string())
240                            .filter(|value| !value.is_empty())
241                    {
242                        config.os = Some(OsConfig { address });
243                    }
244                }
245                "providers" => {
246                    let provider_name = block.labels.first().cloned().ok_or_else(|| {
247                        CodeError::Config(
248                            "providers block requires a label (e.g., providers \"openai\" { ... })"
249                                .into(),
250                        )
251                    })?;
252
253                    let mut provider = ProviderConfig {
254                        name: provider_name.clone(),
255                        api_key: None,
256                        base_url: None,
257                        headers: HashMap::new(),
258                        session_id_header: None,
259                        models: Vec::new(),
260                    };
261
262                    for (key, value) in &block.attributes {
263                        match key.as_str() {
264                            "apiKey" | "api_key" => {
265                                if let Some(api_key) = acl_string(value) {
266                                    provider.api_key = Some(api_key);
267                                }
268                            }
269                            "baseUrl" | "base_url" => {
270                                if let Some(base_url) = acl_string(value) {
271                                    provider.base_url = Some(base_url);
272                                }
273                            }
274                            "sessionIdHeader" | "session_id_header" => {
275                                if let Some(header) = acl_string(value) {
276                                    provider.session_id_header = Some(header);
277                                }
278                            }
279                            _ => {}
280                        }
281                    }
282
283                    // Process nested models blocks
284                    for model_block in &block.blocks {
285                        if model_block.name == "models" {
286                            let model_name =
287                                model_block.labels.first().cloned().ok_or_else(|| {
288                                    CodeError::Config(
289                                        "models block requires a label (e.g., models \"gpt-4\" { ... })"
290                                            .into(),
291                                    )
292                                })?;
293
294                            let mut model = ModelConfig {
295                                id: model_name.clone(),
296                                name: model_name.clone(),
297                                family: String::new(),
298                                api_key: None,
299                                base_url: None,
300                                headers: HashMap::new(),
301                                session_id_header: None,
302                                attachment: false,
303                                reasoning: false,
304                                tool_call: true,
305                                temperature: true,
306                                release_date: None,
307                                modalities: ModelModalities::default(),
308                                cost: ModelCost::default(),
309                                limit: ModelLimit::default(),
310                            };
311
312                            for (key, value) in &model_block.attributes {
313                                match key.as_str() {
314                                    "name" => {
315                                        if let Some(s) = acl_string(value) {
316                                            model.name = s;
317                                        }
318                                    }
319                                    "family" => {
320                                        if let Some(s) = acl_string(value) {
321                                            model.family = s;
322                                        }
323                                    }
324                                    "apiKey" | "api_key" => {
325                                        if let Some(api_key) = acl_string(value) {
326                                            model.api_key = Some(api_key);
327                                        }
328                                    }
329                                    "baseUrl" | "base_url" => {
330                                        if let Some(base_url) = acl_string(value) {
331                                            model.base_url = Some(base_url);
332                                        }
333                                    }
334                                    "sessionIdHeader" | "session_id_header" => {
335                                        if let Some(header) = acl_string(value) {
336                                            model.session_id_header = Some(header);
337                                        }
338                                    }
339                                    "attachment" => {
340                                        model.attachment =
341                                            acl_bool_attr(model_block, &["attachment"])
342                                                .unwrap_or(model.attachment);
343                                    }
344                                    "reasoning" => {
345                                        model.reasoning =
346                                            acl_bool_attr(model_block, &["reasoning"])
347                                                .unwrap_or(model.reasoning);
348                                    }
349                                    "toolCall" | "tool_call" => {
350                                        model.tool_call =
351                                            acl_bool_attr(model_block, &["toolCall", "tool_call"])
352                                                .unwrap_or(model.tool_call);
353                                    }
354                                    "temperature" => {
355                                        model.temperature =
356                                            acl_bool_attr(model_block, &["temperature"])
357                                                .unwrap_or(model.temperature);
358                                    }
359                                    "releaseDate" | "release_date" => {
360                                        if let Some(release_date) = acl_string(value) {
361                                            model.release_date = Some(release_date);
362                                        }
363                                    }
364                                    "maxTokens" => {
365                                        tracing::warn!(
366                                            provider = %provider.name,
367                                            model = %model.id,
368                                            field = "maxTokens",
369                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
370                                        );
371                                        if let Some(output) = acl_u32(value) {
372                                            model.limit.output = output;
373                                        }
374                                    }
375                                    "contextTokens" => {
376                                        tracing::warn!(
377                                            provider = %provider.name,
378                                            model = %model.id,
379                                            field = "contextTokens",
380                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
381                                        );
382                                        if let Some(context) = acl_u32(value) {
383                                            model.limit.context = context;
384                                        }
385                                    }
386                                    "limit" => {
387                                        if let Some(output) = acl_object_u32_attr(value, "output") {
388                                            model.limit.output = output;
389                                        }
390                                        if let Some(context) = acl_object_u32_attr(value, "context")
391                                        {
392                                            model.limit.context = context;
393                                        }
394                                    }
395                                    _ => {}
396                                }
397                            }
398
399                            provider.models.push(model);
400                        }
401                    }
402
403                    config.providers.push(provider);
404                }
405                _ => {
406                    // Other top-level blocks are not mapped by the lightweight
407                    // ACL loader yet (queue, search, memory, MCP, etc.).
408                }
409            }
410        }
411
412        if let Some(auto_parallel) = config.auto_parallel {
413            config.auto_delegation.auto_parallel = auto_parallel;
414        }
415
416        Ok(config)
417    }
418
419    /// Find a provider by name
420    pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
421        self.providers.iter().find(|p| p.name == name)
422    }
423
424    /// Get the default provider configuration (parsed from `default_model` "provider/model" format)
425    pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
426        let default = self.default_model.as_ref()?;
427        let (provider_name, _) = default.split_once('/')?;
428        self.find_provider(provider_name)
429    }
430
431    /// Get the default model configuration (parsed from `default_model` "provider/model" format)
432    pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
433        let default = self.default_model.as_ref()?;
434        let (provider_name, model_id) = default.split_once('/')?;
435        let provider = self.find_provider(provider_name)?;
436        let model = provider.find_model(model_id)?;
437        Some((provider, model))
438    }
439
440    /// Get LlmConfig for the default provider and model
441    ///
442    /// Returns None if default provider/model is not configured or API key is missing.
443    pub fn default_llm_config(&self) -> Option<LlmConfig> {
444        let (provider, model) = self.default_model_config()?;
445        let api_key = provider.get_api_key(model)?;
446        let base_url = provider.get_base_url(model);
447        let headers = provider.get_headers(model);
448        let session_id_header = provider.get_session_id_header(model);
449
450        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
451        if let Some(url) = base_url {
452            config = config.with_base_url(url);
453        }
454        if !headers.is_empty() {
455            config = config.with_headers(headers);
456        }
457        if let Some(header_name) = session_id_header {
458            config = config.with_session_id_header(header_name);
459        }
460        config = apply_model_caps(config, model, self.thinking_budget);
461        Some(config)
462    }
463
464    /// Get LlmConfig for a specific provider and model
465    ///
466    /// Returns None if provider/model is not found or API key is missing.
467    pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
468        let provider = self.find_provider(provider_name)?;
469        let model = provider.find_model(model_id)?;
470        let api_key = provider.get_api_key(model)?;
471        let base_url = provider.get_base_url(model);
472        let headers = provider.get_headers(model);
473        let session_id_header = provider.get_session_id_header(model);
474
475        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
476        if let Some(url) = base_url {
477            config = config.with_base_url(url);
478        }
479        if !headers.is_empty() {
480            config = config.with_headers(headers);
481        }
482        if let Some(header_name) = session_id_header {
483            config = config.with_session_id_header(header_name);
484        }
485        config = apply_model_caps(config, model, self.thinking_budget);
486        Some(config)
487    }
488
489    /// List all available models across all providers
490    pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
491        self.providers
492            .iter()
493            .flat_map(|p| p.models.iter().map(move |m| (p, m)))
494            .collect()
495    }
496
497    /// Add a skill directory
498    pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
499        self.skill_dirs.push(dir.into());
500        self
501    }
502
503    /// Add an agent directory
504    pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
505        self.agent_dirs.push(dir.into());
506        self
507    }
508
509    /// Check if any directories are configured
510    pub fn has_directories(&self) -> bool {
511        !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
512    }
513
514    /// Check if provider configuration is available
515    pub fn has_providers(&self) -> bool {
516        !self.providers.is_empty()
517    }
518}