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 crate::mcp::McpServerConfig;
8use crate::memory::MemoryConfig;
9use crate::queue::SessionQueueConfig;
10use crate::task_scheduler::TaskSchedulerConfig;
11use a3s_memory::{PrunePolicy, RelevanceConfig};
12use serde_json::{Map as JsonMap, Value as JsonValue};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16// ============================================================================
17// ACL Parsing Helpers
18// ============================================================================
19
20fn acl_attr<'a>(block: &'a a3s_acl::Block, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
21    keys.iter().find_map(|key| block.attributes.get(*key))
22}
23
24fn acl_string(value: &a3s_acl::Value) -> Option<String> {
25    match value {
26        a3s_acl::Value::String(s) => Some(s.clone()),
27        a3s_acl::Value::Call(name, args) if name == "env" => {
28            let var_name = args.first().and_then(acl_string)?;
29            std::env::var(var_name).ok()
30        }
31        _ => None,
32    }
33}
34
35fn acl_string_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
36    acl_attr(block, keys).and_then(acl_string)
37}
38
39fn acl_label_or_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
40    block
41        .labels
42        .first()
43        .cloned()
44        .or_else(|| acl_string_attr(block, keys))
45}
46
47fn acl_bool_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<bool> {
48    match acl_attr(block, keys) {
49        Some(a3s_acl::Value::Bool(value)) => Some(*value),
50        _ => None,
51    }
52}
53
54fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
55    match acl_attr(block, keys) {
56        Some(a3s_acl::Value::Number(value)) if *value >= 0.0 => Some(*value as usize),
57        _ => None,
58    }
59}
60
61fn acl_f32_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<f32> {
62    match acl_attr(block, keys) {
63        Some(a3s_acl::Value::Number(value)) => Some(*value as f32),
64        _ => None,
65    }
66}
67
68fn parse_auto_delegation_block(
69    block: &a3s_acl::Block,
70    base: &AutoDelegationConfig,
71) -> AutoDelegationConfig {
72    let mut config = base.clone();
73    if let Some(enabled) = acl_bool_attr(block, &["enabled"]) {
74        config.enabled = enabled;
75    }
76    if let Some(auto_parallel) =
77        acl_bool_attr(block, &["auto_parallel", "autoParallel", "parallel"])
78    {
79        config.auto_parallel = auto_parallel;
80    }
81    if let Some(allow_manual_delegation) = acl_bool_attr(
82        block,
83        &[
84            "allow_manual_delegation",
85            "allowManualDelegation",
86            "manual_delegation",
87            "manualDelegation",
88        ],
89    ) {
90        config.allow_manual_delegation = allow_manual_delegation;
91    }
92    if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) {
93        config.min_confidence = min_confidence.clamp(0.0, 1.0);
94    }
95    if let Some(max_tasks) = acl_usize_attr(block, &["max_tasks", "maxTasks"]) {
96        config.max_tasks = max_tasks.max(1);
97    }
98    config
99}
100
101fn parse_memory_block(block: &a3s_acl::Block, base: Option<&MemoryConfig>) -> MemoryConfig {
102    let mut config = base.cloned().unwrap_or_default();
103
104    if let Some(max_short_term) = acl_usize_attr(block, &["max_short_term", "maxShortTerm"]) {
105        config.max_short_term = max_short_term;
106    }
107    if let Some(max_working) = acl_usize_attr(block, &["max_working", "maxWorking"]) {
108        config.max_working = max_working;
109    }
110    if let Some(prune_interval_secs) =
111        acl_usize_attr(block, &["prune_interval_secs", "pruneIntervalSecs"])
112    {
113        config.prune_interval_secs = prune_interval_secs as u64;
114    }
115    if let Some(llm_extraction) = acl_bool_attr(block, &["llm_extraction", "llmExtraction"]) {
116        config.llm_extraction = llm_extraction;
117    }
118    if let Some(max_items) = acl_usize_attr(
119        block,
120        &["llm_extraction_max_items", "llmExtractionMaxItems"],
121    ) {
122        config.llm_extraction_max_items = max_items;
123    }
124    if let Some(max_input_chars) = acl_usize_attr(
125        block,
126        &[
127            "llm_extraction_max_input_chars",
128            "llmExtractionMaxInputChars",
129        ],
130    ) {
131        config.llm_extraction_max_input_chars = max_input_chars;
132    }
133
134    if let Some(relevance) = acl_attr(block, &["relevance"]) {
135        config.relevance = parse_relevance_value(relevance, &config.relevance);
136    }
137
138    if let Some(prune_policy) = acl_attr(block, &["prune", "prune_policy", "prunePolicy"]) {
139        config.prune_policy = Some(parse_prune_policy_value(
140            prune_policy,
141            config.prune_policy.as_ref(),
142        ));
143    }
144
145    for child in &block.blocks {
146        let value = a3s_acl::Value::Object(
147            child
148                .attributes
149                .iter()
150                .map(|(key, value)| (key.clone(), value.clone()))
151                .collect(),
152        );
153        match child.name.as_str() {
154            "relevance" => {
155                config.relevance = parse_relevance_value(&value, &config.relevance);
156            }
157            "prune" | "prune_policy" | "prunePolicy" => {
158                config.prune_policy = Some(parse_prune_policy_value(
159                    &value,
160                    config.prune_policy.as_ref(),
161                ));
162            }
163            _ => {}
164        }
165    }
166
167    config
168}
169
170fn parse_relevance_value(value: &a3s_acl::Value, base: &RelevanceConfig) -> RelevanceConfig {
171    let mut config = base.clone();
172    if let Some(decay_days) = acl_object_f32_attr(value, &["decay_days", "decayDays"]) {
173        config.decay_days = decay_days.max(0.1);
174    }
175    if let Some(importance_weight) =
176        acl_object_f32_attr(value, &["importance_weight", "importanceWeight"])
177    {
178        config.importance_weight = importance_weight.max(0.0);
179    }
180    if let Some(recency_weight) = acl_object_f32_attr(value, &["recency_weight", "recencyWeight"]) {
181        config.recency_weight = recency_weight.max(0.0);
182    }
183    config
184}
185
186fn parse_prune_policy_value(value: &a3s_acl::Value, base: Option<&PrunePolicy>) -> PrunePolicy {
187    let mut policy = base.cloned().unwrap_or_default();
188    if let Some(max_age_days) = acl_object_u32_attr(value, &["max_age_days", "maxAgeDays"]) {
189        policy.max_age_days = max_age_days;
190    }
191    if let Some(min_importance) =
192        acl_object_f32_attr(value, &["min_importance_to_keep", "minImportanceToKeep"])
193    {
194        policy.min_importance_to_keep = min_importance.clamp(0.0, 1.0);
195    }
196    if let Some(max_items) = acl_object_usize_attr(value, &["max_items", "maxItems"]) {
197        policy.max_items = max_items;
198    }
199    policy
200}
201
202fn acl_object_attr<'a>(value: &'a a3s_acl::Value, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
203    match value {
204        a3s_acl::Value::Object(pairs) => keys.iter().find_map(|key| {
205            pairs
206                .iter()
207                .find_map(|(candidate, value)| (candidate == key).then_some(value))
208        }),
209        _ => None,
210    }
211}
212
213fn acl_f32(value: &a3s_acl::Value) -> Option<f32> {
214    match value {
215        a3s_acl::Value::Number(value) => Some(*value as f32),
216        _ => None,
217    }
218}
219
220fn acl_usize(value: &a3s_acl::Value) -> Option<usize> {
221    match value {
222        a3s_acl::Value::Number(value) if *value >= 0.0 => Some(*value as usize),
223        _ => None,
224    }
225}
226
227fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
228    match value {
229        a3s_acl::Value::Number(value) if *value >= 0.0 => {
230            Some((*value as usize).min(u32::MAX as usize) as u32)
231        }
232        _ => None,
233    }
234}
235
236fn acl_object_f32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f32> {
237    acl_object_attr(value, keys).and_then(acl_f32)
238}
239
240fn acl_object_usize_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<usize> {
241    acl_object_attr(value, keys).and_then(acl_usize)
242}
243
244fn acl_object_u32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<u32> {
245    acl_object_attr(value, keys).and_then(acl_u32)
246}
247
248fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
249    let value = acl_attr(block, keys)?;
250    match value {
251        a3s_acl::Value::List(items) => Some(
252            items
253                .iter()
254                .filter_map(acl_string)
255                .map(PathBuf::from)
256                .collect(),
257        ),
258        _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
259    }
260}
261
262fn acl_string_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<String>> {
263    let value = acl_attr(block, keys)?;
264    match value {
265        a3s_acl::Value::List(items) => Some(items.iter().filter_map(acl_string).collect()),
266        _ => acl_string(value).map(|value| vec![value]),
267    }
268}
269
270fn snake_to_camel(value: &str) -> String {
271    let mut output = String::with_capacity(value.len());
272    let mut uppercase_next = false;
273    for ch in value.chars() {
274        if ch == '_' || ch == '-' {
275            uppercase_next = true;
276        } else if uppercase_next {
277            output.extend(ch.to_uppercase());
278            uppercase_next = false;
279        } else {
280            output.push(ch);
281        }
282    }
283    output
284}
285
286fn acl_value_to_json(value: &a3s_acl::Value) -> Option<JsonValue> {
287    match value {
288        a3s_acl::Value::String(value) => Some(JsonValue::String(value.clone())),
289        a3s_acl::Value::Number(value) if value.fract() == 0.0 && *value >= 0.0 => {
290            Some(JsonValue::Number(serde_json::Number::from(*value as u64)))
291        }
292        a3s_acl::Value::Number(value) if value.fract() == 0.0 => {
293            Some(JsonValue::Number(serde_json::Number::from(*value as i64)))
294        }
295        a3s_acl::Value::Number(value) => {
296            serde_json::Number::from_f64(*value).map(JsonValue::Number)
297        }
298        a3s_acl::Value::Bool(value) => Some(JsonValue::Bool(*value)),
299        a3s_acl::Value::List(items) => Some(JsonValue::Array(
300            items.iter().filter_map(acl_value_to_json).collect(),
301        )),
302        a3s_acl::Value::Object(pairs) => {
303            let mut object = JsonMap::new();
304            for (key, value) in pairs {
305                if let Some(value) = acl_value_to_json(value) {
306                    object.insert(key.clone(), value);
307                }
308            }
309            Some(JsonValue::Object(object))
310        }
311        a3s_acl::Value::Null => Some(JsonValue::Null),
312        a3s_acl::Value::Call(name, _) if name == "env" => acl_string(value).map(JsonValue::String),
313        a3s_acl::Value::Call(_, _) => None,
314    }
315}
316
317fn insert_nested_json(object: &mut JsonMap<String, JsonValue>, key: String, value: JsonValue) {
318    match object.remove(&key) {
319        None => {
320            object.insert(key, value);
321        }
322        Some(JsonValue::Array(mut values)) => {
323            values.push(value);
324            object.insert(key, JsonValue::Array(values));
325        }
326        Some(previous) => {
327            object.insert(key, JsonValue::Array(vec![previous, value]));
328        }
329    }
330}
331
332fn acl_block_to_json(block: &a3s_acl::Block) -> JsonValue {
333    let mut object = JsonMap::new();
334    for (key, value) in &block.attributes {
335        if let Some(value) = acl_value_to_json(value) {
336            object.insert(snake_to_camel(key), value);
337        }
338    }
339
340    for child in &block.blocks {
341        let key = snake_to_camel(&child.name);
342        let value = acl_block_to_json(child);
343        if let Some(label) = child.labels.first() {
344            let entry = object
345                .entry(key)
346                .or_insert_with(|| JsonValue::Object(JsonMap::new()));
347            if let JsonValue::Object(entries) = entry {
348                entries.insert(label.clone(), value);
349            }
350        } else {
351            insert_nested_json(&mut object, key, value);
352        }
353    }
354
355    JsonValue::Object(object)
356}
357
358fn normalize_lane_name(value: &str) -> Option<&'static str> {
359    match value.trim().to_ascii_lowercase().as_str() {
360        "control" => Some("Control"),
361        "query" => Some("Query"),
362        "execute" => Some("Execute"),
363        "generate" => Some("Generate"),
364        _ => None,
365    }
366}
367
368fn normalize_lane_map(value: &mut JsonValue, normalize_handler_mode: bool) {
369    let JsonValue::Object(entries) = value else {
370        return;
371    };
372    let previous = std::mem::take(entries);
373    for (name, mut value) in previous {
374        let Some(name) = normalize_lane_name(&name) else {
375            continue;
376        };
377        if normalize_handler_mode {
378            if let JsonValue::Object(handler) = &mut value {
379                rename_json_key(handler, "timeoutMs", "timeout_ms");
380                if let Some(JsonValue::String(mode)) = handler.get_mut("mode") {
381                    *mode = match mode.trim().to_ascii_lowercase().as_str() {
382                        "external" => "External".to_string(),
383                        "hybrid" => "Hybrid".to_string(),
384                        _ => "Internal".to_string(),
385                    };
386                }
387            }
388        }
389        entries.insert(name.to_string(), value);
390    }
391}
392
393fn parse_queue_block(block: &a3s_acl::Block) -> Result<SessionQueueConfig> {
394    let mut value = acl_block_to_json(block);
395    if let Some(lane_handlers) = value.get_mut("laneHandlers") {
396        normalize_lane_map(lane_handlers, true);
397    }
398    if let Some(lane_timeouts) = value.get_mut("laneTimeouts") {
399        normalize_lane_map(lane_timeouts, false);
400    }
401    serde_json::from_value(value)
402        .map_err(|error| CodeError::Config(format!("Invalid queue configuration: {error}")))
403}
404
405fn parse_task_scheduler_block(block: &a3s_acl::Block) -> Result<TaskSchedulerConfig> {
406    let config: TaskSchedulerConfig =
407        serde_json::from_value(acl_block_to_json(block)).map_err(|error| {
408            CodeError::Config(format!("Invalid task scheduler configuration: {error}"))
409        })?;
410    config
411        .validate()
412        .map_err(|error| CodeError::Config(error.to_string()))?;
413    Ok(config)
414}
415
416fn parse_search_block(block: &a3s_acl::Block) -> Result<super::SearchConfig> {
417    serde_json::from_value(acl_block_to_json(block))
418        .map_err(|error| CodeError::Config(format!("Invalid search configuration: {error}")))
419}
420
421fn parse_document_parser_block(block: &a3s_acl::Block) -> Result<super::DocumentParserConfig> {
422    serde_json::from_value(acl_block_to_json(block)).map_err(|error| {
423        CodeError::Config(format!("Invalid document parser configuration: {error}"))
424    })
425}
426
427fn rename_json_key(object: &mut JsonMap<String, JsonValue>, from: &str, to: &str) {
428    if let Some(value) = object.remove(from) {
429        object.insert(to.to_string(), value);
430    }
431}
432
433fn parse_mcp_server_block(block: &a3s_acl::Block) -> Result<McpServerConfig> {
434    let mut value = acl_block_to_json(block);
435    let object = value.as_object_mut().ok_or_else(|| {
436        CodeError::Config("Invalid MCP server configuration: expected an object".to_string())
437    })?;
438    if let Some(label) = block.labels.first() {
439        object.insert("name".to_string(), JsonValue::String(label.clone()));
440    }
441    if let Some(JsonValue::Object(oauth)) = object.get_mut("oauth") {
442        rename_json_key(oauth, "authUrl", "auth_url");
443        rename_json_key(oauth, "tokenUrl", "token_url");
444        rename_json_key(oauth, "clientId", "client_id");
445        rename_json_key(oauth, "clientSecret", "client_secret");
446        rename_json_key(oauth, "redirectUri", "redirect_uri");
447        rename_json_key(oauth, "accessToken", "access_token");
448    }
449    serde_json::from_value(value)
450        .map_err(|error| CodeError::Config(format!("Invalid MCP server configuration: {error}")))
451}
452
453fn acl_string_map(value: &a3s_acl::Value) -> HashMap<String, String> {
454    match value {
455        a3s_acl::Value::Object(pairs) => pairs
456            .iter()
457            .filter_map(|(key, value)| acl_string(value).map(|value| (key.clone(), value)))
458            .collect(),
459        _ => HashMap::new(),
460    }
461}
462
463fn acl_string_list(value: &a3s_acl::Value) -> Vec<String> {
464    match value {
465        a3s_acl::Value::List(items) => items.iter().filter_map(acl_string).collect(),
466        _ => Vec::new(),
467    }
468}
469
470fn acl_object_f64_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f64> {
471    match acl_object_attr(value, keys) {
472        Some(a3s_acl::Value::Number(value)) => Some(*value),
473        _ => None,
474    }
475}
476
477// ============================================================================
478// CodeConfig Implementation
479// ============================================================================
480
481impl CodeConfig {
482    /// Create a new empty configuration
483    pub fn new() -> Self {
484        Self::default()
485    }
486
487    /// Load configuration from an ACL-compatible config file.
488    ///
489    /// `.acl` is the only supported config file extension. JSON and legacy
490    /// `.hcl` config files are not supported.
491    pub fn from_file(path: &Path) -> Result<Self> {
492        let content = std::fs::read_to_string(path).map_err(|e| {
493            CodeError::Config(format!(
494                "Failed to read config file {}: {}",
495                path.display(),
496                e
497            ))
498        })?;
499
500        Self::from_acl(&content).map_err(|e| {
501            CodeError::Config(format!(
502                "Failed to parse ACL config {}: {}",
503                path.display(),
504                e
505            ))
506        })
507    }
508
509    /// Parse configuration from an ACL string.
510    ///
511    /// ACL (Agent Configuration Language) uses labeled blocks like
512    /// `providers "openai" { }`.
513    pub fn from_acl(content: &str) -> Result<Self> {
514        use a3s_acl::parse_acl;
515
516        let doc = parse_acl(content)
517            .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
518
519        let mut config = Self::default();
520
521        for block in doc.blocks {
522            match block.name.as_str() {
523                "default_model" => {
524                    // ACL: default_model = "openai/gpt-4" or just "openai/gpt-4" as label
525                    if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
526                        config.default_model = Some(default_model);
527                    }
528                }
529                "storage_backend" => {
530                    if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
531                        config.storage_backend = match backend.to_ascii_lowercase().as_str() {
532                            "memory" => StorageBackend::Memory,
533                            "custom" => StorageBackend::Custom,
534                            _ => StorageBackend::File,
535                        };
536                    }
537                }
538                "sessions_dir" => {
539                    if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
540                        config.sessions_dir = Some(PathBuf::from(path));
541                    }
542                }
543                "memory_dir" | "memoryDir" => {
544                    if let Some(path) = acl_string_attr(&block, &["memory_dir", "memoryDir"]) {
545                        config.memory_dir = Some(PathBuf::from(path));
546                    }
547                }
548                "memory" => {
549                    config.memory = Some(parse_memory_block(&block, config.memory.as_ref()));
550                }
551                "queue" => {
552                    config.queue = Some(parse_queue_block(&block)?);
553                }
554                "task_scheduler" | "taskScheduler" => {
555                    config.task_scheduler = parse_task_scheduler_block(&block)?;
556                }
557                "search" => {
558                    config.search = Some(parse_search_block(&block)?);
559                }
560                "document_parser" | "documentParser" => {
561                    config.document_parser = Some(parse_document_parser_block(&block)?);
562                }
563                "mcp_servers" | "mcpServers" | "mcp_server" => {
564                    config.mcp_servers.push(parse_mcp_server_block(&block)?);
565                }
566                "storage_url" => {
567                    if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
568                        config.storage_url = Some(storage_url);
569                    }
570                }
571                "skill_dirs" | "skills" => {
572                    if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
573                        config.skill_dirs = paths;
574                    }
575                }
576                "agent_dirs" => {
577                    if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
578                        config.agent_dirs = paths;
579                    }
580                }
581                "user_instructions_dir"
582                | "userInstructionsDir"
583                | "global_instructions_dir"
584                | "globalInstructionsDir" => {
585                    if let Some(path) = acl_string_attr(
586                        &block,
587                        &[
588                            "user_instructions_dir",
589                            "userInstructionsDir",
590                            "global_instructions_dir",
591                            "globalInstructionsDir",
592                        ],
593                    ) {
594                        config.user_instructions_dir = Some(PathBuf::from(path));
595                    }
596                }
597                "project_doc_max_bytes" | "projectDocMaxBytes" => {
598                    if let Some(max_bytes) =
599                        acl_usize_attr(&block, &["project_doc_max_bytes", "projectDocMaxBytes"])
600                    {
601                        config.project_doc_max_bytes = Some(max_bytes);
602                    }
603                }
604                "project_doc_fallback_filenames" | "projectDocFallbackFilenames" => {
605                    if let Some(filenames) = acl_string_list_attr(
606                        &block,
607                        &[
608                            "project_doc_fallback_filenames",
609                            "projectDocFallbackFilenames",
610                        ],
611                    ) {
612                        config.project_doc_fallback_filenames = filenames;
613                    }
614                }
615                "max_tool_rounds" => {
616                    if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
617                        config.max_tool_rounds = Some(max_tool_rounds);
618                    }
619                }
620                "max_parallel_tasks" => {
621                    if let Some(max_parallel_tasks) =
622                        acl_usize_attr(&block, &["max_parallel_tasks"])
623                    {
624                        config.max_parallel_tasks = Some(max_parallel_tasks);
625                    }
626                }
627                "auto_parallel" | "auto_parallel_enabled" => {
628                    if let Some(auto_parallel) =
629                        acl_bool_attr(&block, &["auto_parallel", "auto_parallel_enabled"])
630                    {
631                        config.auto_parallel = Some(auto_parallel);
632                    }
633                }
634                "auto_delegation" => {
635                    config.auto_delegation =
636                        parse_auto_delegation_block(&block, &config.auto_delegation);
637                }
638                "thinking_budget" => {
639                    if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
640                        config.thinking_budget = Some(thinking_budget);
641                    }
642                }
643                "llm_api_timeout_ms" | "api_timeout_ms" | "model_api_timeout_ms" => {
644                    if let Some(timeout_ms) = acl_usize_attr(
645                        &block,
646                        &[
647                            "llm_api_timeout_ms",
648                            "api_timeout_ms",
649                            "model_api_timeout_ms",
650                        ],
651                    ) {
652                        config.llm_api_timeout_ms = Some(timeout_ms as u64);
653                    }
654                }
655                "os" => {
656                    if let Some(address) =
657                        acl_label_or_attr(&block, &["os", "address", "url", "baseUrl", "base_url"])
658                            .map(|value| value.trim().to_string())
659                            .filter(|value| !value.is_empty())
660                    {
661                        config.os = Some(OsConfig { address });
662                    }
663                }
664                "providers" => {
665                    let provider_name = block.labels.first().cloned().ok_or_else(|| {
666                        CodeError::Config(
667                            "providers block requires a label (e.g., providers \"openai\" { ... })"
668                                .into(),
669                        )
670                    })?;
671
672                    let mut provider = ProviderConfig {
673                        name: provider_name.clone(),
674                        api_key: None,
675                        base_url: None,
676                        headers: HashMap::new(),
677                        session_id_header: None,
678                        models: Vec::new(),
679                    };
680
681                    for (key, value) in &block.attributes {
682                        match key.as_str() {
683                            "apiKey" | "api_key" => {
684                                if let Some(api_key) = acl_string(value) {
685                                    provider.api_key = Some(api_key);
686                                }
687                            }
688                            "baseUrl" | "base_url" => {
689                                if let Some(base_url) = acl_string(value) {
690                                    provider.base_url = Some(base_url);
691                                }
692                            }
693                            "sessionIdHeader" | "session_id_header" => {
694                                if let Some(header) = acl_string(value) {
695                                    provider.session_id_header = Some(header);
696                                }
697                            }
698                            "headers" => {
699                                provider.headers = acl_string_map(value);
700                            }
701                            _ => {}
702                        }
703                    }
704
705                    // Process nested models blocks
706                    for model_block in &block.blocks {
707                        if model_block.name == "models" {
708                            let model_name =
709                                model_block.labels.first().cloned().ok_or_else(|| {
710                                    CodeError::Config(
711                                        "models block requires a label (e.g., models \"gpt-4\" { ... })"
712                                            .into(),
713                                    )
714                                })?;
715
716                            let mut model = ModelConfig {
717                                id: model_name.clone(),
718                                name: model_name.clone(),
719                                family: String::new(),
720                                api_key: None,
721                                base_url: None,
722                                headers: HashMap::new(),
723                                session_id_header: None,
724                                attachment: false,
725                                reasoning: false,
726                                tool_call: true,
727                                temperature: true,
728                                release_date: None,
729                                modalities: ModelModalities::default(),
730                                cost: ModelCost::default(),
731                                limit: ModelLimit::default(),
732                            };
733
734                            for (key, value) in &model_block.attributes {
735                                match key.as_str() {
736                                    "name" => {
737                                        if let Some(s) = acl_string(value) {
738                                            model.name = s;
739                                        }
740                                    }
741                                    "family" => {
742                                        if let Some(s) = acl_string(value) {
743                                            model.family = s;
744                                        }
745                                    }
746                                    "apiKey" | "api_key" => {
747                                        if let Some(api_key) = acl_string(value) {
748                                            model.api_key = Some(api_key);
749                                        }
750                                    }
751                                    "baseUrl" | "base_url" => {
752                                        if let Some(base_url) = acl_string(value) {
753                                            model.base_url = Some(base_url);
754                                        }
755                                    }
756                                    "sessionIdHeader" | "session_id_header" => {
757                                        if let Some(header) = acl_string(value) {
758                                            model.session_id_header = Some(header);
759                                        }
760                                    }
761                                    "headers" => {
762                                        model.headers = acl_string_map(value);
763                                    }
764                                    "attachment" => {
765                                        model.attachment =
766                                            acl_bool_attr(model_block, &["attachment"])
767                                                .unwrap_or(model.attachment);
768                                    }
769                                    "reasoning" => {
770                                        model.reasoning =
771                                            acl_bool_attr(model_block, &["reasoning"])
772                                                .unwrap_or(model.reasoning);
773                                    }
774                                    "toolCall" | "tool_call" => {
775                                        model.tool_call =
776                                            acl_bool_attr(model_block, &["toolCall", "tool_call"])
777                                                .unwrap_or(model.tool_call);
778                                    }
779                                    "temperature" => {
780                                        model.temperature =
781                                            acl_bool_attr(model_block, &["temperature"])
782                                                .unwrap_or(model.temperature);
783                                    }
784                                    "releaseDate" | "release_date" => {
785                                        if let Some(release_date) = acl_string(value) {
786                                            model.release_date = Some(release_date);
787                                        }
788                                    }
789                                    "maxTokens" => {
790                                        tracing::warn!(
791                                            provider = %provider.name,
792                                            model = %model.id,
793                                            field = "maxTokens",
794                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
795                                        );
796                                        if let Some(output) = acl_u32(value) {
797                                            model.limit.output = output;
798                                        }
799                                    }
800                                    "contextTokens" => {
801                                        tracing::warn!(
802                                            provider = %provider.name,
803                                            model = %model.id,
804                                            field = "contextTokens",
805                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
806                                        );
807                                        if let Some(context) = acl_u32(value) {
808                                            model.limit.context = context;
809                                        }
810                                    }
811                                    "limit" => {
812                                        if let Some(output) =
813                                            acl_object_u32_attr(value, &["output"])
814                                        {
815                                            model.limit.output = output;
816                                        }
817                                        if let Some(context) =
818                                            acl_object_u32_attr(value, &["context"])
819                                        {
820                                            model.limit.context = context;
821                                        }
822                                    }
823                                    "modalities" => {
824                                        if let Some(input) = acl_object_attr(value, &["input"]) {
825                                            model.modalities.input = acl_string_list(input);
826                                        }
827                                        if let Some(output) = acl_object_attr(value, &["output"]) {
828                                            model.modalities.output = acl_string_list(output);
829                                        }
830                                    }
831                                    "cost" => {
832                                        if let Some(input) = acl_object_f64_attr(value, &["input"])
833                                        {
834                                            model.cost.input = input;
835                                        }
836                                        if let Some(output) =
837                                            acl_object_f64_attr(value, &["output"])
838                                        {
839                                            model.cost.output = output;
840                                        }
841                                        if let Some(cache_read) =
842                                            acl_object_f64_attr(value, &["cache_read", "cacheRead"])
843                                        {
844                                            model.cost.cache_read = cache_read;
845                                        }
846                                        if let Some(cache_write) = acl_object_f64_attr(
847                                            value,
848                                            &["cache_write", "cacheWrite"],
849                                        ) {
850                                            model.cost.cache_write = cache_write;
851                                        }
852                                    }
853                                    _ => {}
854                                }
855                            }
856
857                            provider.models.push(model);
858                        }
859                    }
860
861                    config.providers.push(provider);
862                }
863                _ => {}
864            }
865        }
866
867        if let Some(auto_parallel) = config.auto_parallel {
868            config.auto_delegation.auto_parallel = auto_parallel;
869        }
870
871        Ok(config)
872    }
873
874    /// Find a provider by name
875    pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
876        self.providers.iter().find(|p| p.name == name)
877    }
878
879    /// Get the default provider configuration (parsed from `default_model` "provider/model" format)
880    pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
881        let default = self.default_model.as_ref()?;
882        let (provider_name, _) = default.split_once('/')?;
883        self.find_provider(provider_name)
884    }
885
886    /// Get the default model configuration (parsed from `default_model` "provider/model" format)
887    pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
888        let default = self.default_model.as_ref()?;
889        let (provider_name, model_id) = default.split_once('/')?;
890        let provider = self.find_provider(provider_name)?;
891        let model = provider.find_model(model_id)?;
892        Some((provider, model))
893    }
894
895    /// Get LlmConfig for the default provider and model
896    ///
897    /// Returns None if default provider/model is not configured or API key is missing.
898    pub fn default_llm_config(&self) -> Option<LlmConfig> {
899        let (provider, model) = self.default_model_config()?;
900        let api_key = provider.get_api_key(model)?;
901        let base_url = provider.get_base_url(model);
902        let headers = provider.get_headers(model);
903        let session_id_header = provider.get_session_id_header(model);
904
905        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
906        if let Some(url) = base_url {
907            config = config.with_base_url(url);
908        }
909        if !headers.is_empty() {
910            config = config.with_headers(headers);
911        }
912        if let Some(header_name) = session_id_header {
913            config = config.with_session_id_header(header_name);
914        }
915        if let Some(timeout_ms) = self.llm_api_timeout_ms {
916            config = config.with_api_timeout(timeout_ms);
917        }
918        config = apply_model_caps(config, model, self.thinking_budget);
919        Some(config)
920    }
921
922    /// Get LlmConfig for a specific provider and model
923    ///
924    /// Returns None if provider/model is not found or API key is missing.
925    pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
926        let provider = self.find_provider(provider_name)?;
927        let model = provider.find_model(model_id)?;
928        let api_key = provider.get_api_key(model)?;
929        let base_url = provider.get_base_url(model);
930        let headers = provider.get_headers(model);
931        let session_id_header = provider.get_session_id_header(model);
932
933        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
934        if let Some(url) = base_url {
935            config = config.with_base_url(url);
936        }
937        if !headers.is_empty() {
938            config = config.with_headers(headers);
939        }
940        if let Some(header_name) = session_id_header {
941            config = config.with_session_id_header(header_name);
942        }
943        if let Some(timeout_ms) = self.llm_api_timeout_ms {
944            config = config.with_api_timeout(timeout_ms);
945        }
946        config = apply_model_caps(config, model, self.thinking_budget);
947        Some(config)
948    }
949
950    /// List all available models across all providers
951    pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
952        self.providers
953            .iter()
954            .flat_map(|p| p.models.iter().map(move |m| (p, m)))
955            .collect()
956    }
957
958    /// Add a skill directory
959    pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
960        self.skill_dirs.push(dir.into());
961        self
962    }
963
964    /// Add an agent directory
965    pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
966        self.agent_dirs.push(dir.into());
967        self
968    }
969
970    /// Check if any directories are configured
971    pub fn has_directories(&self) -> bool {
972        !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
973    }
974
975    /// Check if provider configuration is available
976    pub fn has_providers(&self) -> bool {
977        !self.providers.is_empty()
978    }
979}