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::memory::MemoryConfig;
8use a3s_memory::{PrunePolicy, RelevanceConfig};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12fn acl_attr<'a>(block: &'a a3s_acl::Block, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
17 keys.iter().find_map(|key| block.attributes.get(*key))
18}
19
20fn acl_string(value: &a3s_acl::Value) -> Option<String> {
21 match value {
22 a3s_acl::Value::String(s) => Some(s.clone()),
23 a3s_acl::Value::Call(name, args) if name == "env" => {
24 let var_name = args.first().and_then(acl_string)?;
25 std::env::var(var_name).ok()
26 }
27 _ => None,
28 }
29}
30
31fn acl_string_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
32 acl_attr(block, keys).and_then(acl_string)
33}
34
35fn acl_label_or_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
36 block
37 .labels
38 .first()
39 .cloned()
40 .or_else(|| acl_string_attr(block, keys))
41}
42
43fn acl_bool_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<bool> {
44 match acl_attr(block, keys) {
45 Some(a3s_acl::Value::Bool(value)) => Some(*value),
46 _ => None,
47 }
48}
49
50fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
51 match acl_attr(block, keys) {
52 Some(a3s_acl::Value::Number(value)) if *value >= 0.0 => Some(*value as usize),
53 _ => None,
54 }
55}
56
57fn acl_f32_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<f32> {
58 match acl_attr(block, keys) {
59 Some(a3s_acl::Value::Number(value)) => Some(*value as f32),
60 _ => None,
61 }
62}
63
64fn parse_auto_delegation_block(
65 block: &a3s_acl::Block,
66 base: &AutoDelegationConfig,
67) -> AutoDelegationConfig {
68 let mut config = base.clone();
69 if let Some(enabled) = acl_bool_attr(block, &["enabled"]) {
70 config.enabled = enabled;
71 }
72 if let Some(auto_parallel) =
73 acl_bool_attr(block, &["auto_parallel", "autoParallel", "parallel"])
74 {
75 config.auto_parallel = auto_parallel;
76 }
77 if let Some(allow_manual_delegation) = acl_bool_attr(
78 block,
79 &[
80 "allow_manual_delegation",
81 "allowManualDelegation",
82 "manual_delegation",
83 "manualDelegation",
84 ],
85 ) {
86 config.allow_manual_delegation = allow_manual_delegation;
87 }
88 if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) {
89 config.min_confidence = min_confidence.clamp(0.0, 1.0);
90 }
91 if let Some(max_tasks) = acl_usize_attr(block, &["max_tasks", "maxTasks"]) {
92 config.max_tasks = max_tasks.max(1);
93 }
94 config
95}
96
97fn parse_memory_block(block: &a3s_acl::Block, base: Option<&MemoryConfig>) -> MemoryConfig {
98 let mut config = base.cloned().unwrap_or_default();
99
100 if let Some(max_short_term) = acl_usize_attr(block, &["max_short_term", "maxShortTerm"]) {
101 config.max_short_term = max_short_term;
102 }
103 if let Some(max_working) = acl_usize_attr(block, &["max_working", "maxWorking"]) {
104 config.max_working = max_working;
105 }
106 if let Some(prune_interval_secs) =
107 acl_usize_attr(block, &["prune_interval_secs", "pruneIntervalSecs"])
108 {
109 config.prune_interval_secs = prune_interval_secs as u64;
110 }
111 if let Some(llm_extraction) = acl_bool_attr(block, &["llm_extraction", "llmExtraction"]) {
112 config.llm_extraction = llm_extraction;
113 }
114 if let Some(max_items) = acl_usize_attr(
115 block,
116 &["llm_extraction_max_items", "llmExtractionMaxItems"],
117 ) {
118 config.llm_extraction_max_items = max_items;
119 }
120 if let Some(max_input_chars) = acl_usize_attr(
121 block,
122 &[
123 "llm_extraction_max_input_chars",
124 "llmExtractionMaxInputChars",
125 ],
126 ) {
127 config.llm_extraction_max_input_chars = max_input_chars;
128 }
129
130 if let Some(relevance) = acl_attr(block, &["relevance"]) {
131 config.relevance = parse_relevance_value(relevance, &config.relevance);
132 }
133
134 if let Some(prune_policy) = acl_attr(block, &["prune", "prune_policy", "prunePolicy"]) {
135 config.prune_policy = Some(parse_prune_policy_value(
136 prune_policy,
137 config.prune_policy.as_ref(),
138 ));
139 }
140
141 config
142}
143
144fn parse_relevance_value(value: &a3s_acl::Value, base: &RelevanceConfig) -> RelevanceConfig {
145 let mut config = base.clone();
146 if let Some(decay_days) = acl_object_f32_attr(value, &["decay_days", "decayDays"]) {
147 config.decay_days = decay_days.max(0.1);
148 }
149 if let Some(importance_weight) =
150 acl_object_f32_attr(value, &["importance_weight", "importanceWeight"])
151 {
152 config.importance_weight = importance_weight.max(0.0);
153 }
154 if let Some(recency_weight) = acl_object_f32_attr(value, &["recency_weight", "recencyWeight"]) {
155 config.recency_weight = recency_weight.max(0.0);
156 }
157 config
158}
159
160fn parse_prune_policy_value(value: &a3s_acl::Value, base: Option<&PrunePolicy>) -> PrunePolicy {
161 let mut policy = base.cloned().unwrap_or_default();
162 if let Some(max_age_days) = acl_object_u32_attr(value, &["max_age_days", "maxAgeDays"]) {
163 policy.max_age_days = max_age_days;
164 }
165 if let Some(min_importance) =
166 acl_object_f32_attr(value, &["min_importance_to_keep", "minImportanceToKeep"])
167 {
168 policy.min_importance_to_keep = min_importance.clamp(0.0, 1.0);
169 }
170 if let Some(max_items) = acl_object_usize_attr(value, &["max_items", "maxItems"]) {
171 policy.max_items = max_items;
172 }
173 policy
174}
175
176fn acl_object_attr<'a>(value: &'a a3s_acl::Value, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
177 match value {
178 a3s_acl::Value::Object(pairs) => keys.iter().find_map(|key| {
179 pairs
180 .iter()
181 .find_map(|(candidate, value)| (candidate == key).then_some(value))
182 }),
183 _ => None,
184 }
185}
186
187fn acl_f32(value: &a3s_acl::Value) -> Option<f32> {
188 match value {
189 a3s_acl::Value::Number(value) => Some(*value as f32),
190 _ => None,
191 }
192}
193
194fn acl_usize(value: &a3s_acl::Value) -> Option<usize> {
195 match value {
196 a3s_acl::Value::Number(value) if *value >= 0.0 => Some(*value as usize),
197 _ => None,
198 }
199}
200
201fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
202 match value {
203 a3s_acl::Value::Number(value) if *value >= 0.0 => {
204 Some((*value as usize).min(u32::MAX as usize) as u32)
205 }
206 _ => None,
207 }
208}
209
210fn acl_object_f32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f32> {
211 acl_object_attr(value, keys).and_then(acl_f32)
212}
213
214fn acl_object_usize_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<usize> {
215 acl_object_attr(value, keys).and_then(acl_usize)
216}
217
218fn acl_object_u32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<u32> {
219 acl_object_attr(value, keys).and_then(acl_u32)
220}
221
222fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
223 let value = acl_attr(block, keys)?;
224 match value {
225 a3s_acl::Value::List(items) => Some(
226 items
227 .iter()
228 .filter_map(acl_string)
229 .map(PathBuf::from)
230 .collect(),
231 ),
232 _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
233 }
234}
235
236impl CodeConfig {
241 pub fn new() -> Self {
243 Self::default()
244 }
245
246 pub fn from_file(path: &Path) -> Result<Self> {
251 let content = std::fs::read_to_string(path).map_err(|e| {
252 CodeError::Config(format!(
253 "Failed to read config file {}: {}",
254 path.display(),
255 e
256 ))
257 })?;
258
259 Self::from_acl(&content).map_err(|e| {
260 CodeError::Config(format!(
261 "Failed to parse ACL config {}: {}",
262 path.display(),
263 e
264 ))
265 })
266 }
267
268 pub fn from_acl(content: &str) -> Result<Self> {
273 use a3s_acl::parse_acl;
274
275 let doc = parse_acl(content)
276 .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
277
278 let mut config = Self::default();
279
280 for block in doc.blocks {
281 match block.name.as_str() {
282 "default_model" => {
283 if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
285 config.default_model = Some(default_model);
286 }
287 }
288 "storage_backend" => {
289 if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
290 config.storage_backend = match backend.to_ascii_lowercase().as_str() {
291 "memory" => StorageBackend::Memory,
292 "custom" => StorageBackend::Custom,
293 _ => StorageBackend::File,
294 };
295 }
296 }
297 "sessions_dir" => {
298 if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
299 config.sessions_dir = Some(PathBuf::from(path));
300 }
301 }
302 "memory_dir" | "memoryDir" => {
303 if let Some(path) = acl_string_attr(&block, &["memory_dir", "memoryDir"]) {
304 config.memory_dir = Some(PathBuf::from(path));
305 }
306 }
307 "memory" => {
308 config.memory = Some(parse_memory_block(&block, config.memory.as_ref()));
309 }
310 "storage_url" => {
311 if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
312 config.storage_url = Some(storage_url);
313 }
314 }
315 "skill_dirs" | "skills" => {
316 if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
317 config.skill_dirs = paths;
318 }
319 }
320 "agent_dirs" => {
321 if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
322 config.agent_dirs = paths;
323 }
324 }
325 "max_tool_rounds" => {
326 if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
327 config.max_tool_rounds = Some(max_tool_rounds);
328 }
329 }
330 "max_parallel_tasks" => {
331 if let Some(max_parallel_tasks) =
332 acl_usize_attr(&block, &["max_parallel_tasks"])
333 {
334 config.max_parallel_tasks = Some(max_parallel_tasks);
335 }
336 }
337 "auto_parallel" | "auto_parallel_enabled" => {
338 if let Some(auto_parallel) =
339 acl_bool_attr(&block, &["auto_parallel", "auto_parallel_enabled"])
340 {
341 config.auto_parallel = Some(auto_parallel);
342 }
343 }
344 "auto_delegation" => {
345 config.auto_delegation =
346 parse_auto_delegation_block(&block, &config.auto_delegation);
347 }
348 "thinking_budget" => {
349 if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
350 config.thinking_budget = Some(thinking_budget);
351 }
352 }
353 "llm_api_timeout_ms" | "api_timeout_ms" | "model_api_timeout_ms" => {
354 if let Some(timeout_ms) = acl_usize_attr(
355 &block,
356 &[
357 "llm_api_timeout_ms",
358 "api_timeout_ms",
359 "model_api_timeout_ms",
360 ],
361 ) {
362 config.llm_api_timeout_ms = Some(timeout_ms as u64);
363 }
364 }
365 "os" => {
366 if let Some(address) =
367 acl_label_or_attr(&block, &["os", "address", "url", "baseUrl", "base_url"])
368 .map(|value| value.trim().to_string())
369 .filter(|value| !value.is_empty())
370 {
371 config.os = Some(OsConfig { address });
372 }
373 }
374 "providers" => {
375 let provider_name = block.labels.first().cloned().ok_or_else(|| {
376 CodeError::Config(
377 "providers block requires a label (e.g., providers \"openai\" { ... })"
378 .into(),
379 )
380 })?;
381
382 let mut provider = ProviderConfig {
383 name: provider_name.clone(),
384 api_key: None,
385 base_url: None,
386 headers: HashMap::new(),
387 session_id_header: None,
388 models: Vec::new(),
389 };
390
391 for (key, value) in &block.attributes {
392 match key.as_str() {
393 "apiKey" | "api_key" => {
394 if let Some(api_key) = acl_string(value) {
395 provider.api_key = Some(api_key);
396 }
397 }
398 "baseUrl" | "base_url" => {
399 if let Some(base_url) = acl_string(value) {
400 provider.base_url = Some(base_url);
401 }
402 }
403 "sessionIdHeader" | "session_id_header" => {
404 if let Some(header) = acl_string(value) {
405 provider.session_id_header = Some(header);
406 }
407 }
408 _ => {}
409 }
410 }
411
412 for model_block in &block.blocks {
414 if model_block.name == "models" {
415 let model_name =
416 model_block.labels.first().cloned().ok_or_else(|| {
417 CodeError::Config(
418 "models block requires a label (e.g., models \"gpt-4\" { ... })"
419 .into(),
420 )
421 })?;
422
423 let mut model = ModelConfig {
424 id: model_name.clone(),
425 name: model_name.clone(),
426 family: String::new(),
427 api_key: None,
428 base_url: None,
429 headers: HashMap::new(),
430 session_id_header: None,
431 attachment: false,
432 reasoning: false,
433 tool_call: true,
434 temperature: true,
435 release_date: None,
436 modalities: ModelModalities::default(),
437 cost: ModelCost::default(),
438 limit: ModelLimit::default(),
439 };
440
441 for (key, value) in &model_block.attributes {
442 match key.as_str() {
443 "name" => {
444 if let Some(s) = acl_string(value) {
445 model.name = s;
446 }
447 }
448 "family" => {
449 if let Some(s) = acl_string(value) {
450 model.family = s;
451 }
452 }
453 "apiKey" | "api_key" => {
454 if let Some(api_key) = acl_string(value) {
455 model.api_key = Some(api_key);
456 }
457 }
458 "baseUrl" | "base_url" => {
459 if let Some(base_url) = acl_string(value) {
460 model.base_url = Some(base_url);
461 }
462 }
463 "sessionIdHeader" | "session_id_header" => {
464 if let Some(header) = acl_string(value) {
465 model.session_id_header = Some(header);
466 }
467 }
468 "attachment" => {
469 model.attachment =
470 acl_bool_attr(model_block, &["attachment"])
471 .unwrap_or(model.attachment);
472 }
473 "reasoning" => {
474 model.reasoning =
475 acl_bool_attr(model_block, &["reasoning"])
476 .unwrap_or(model.reasoning);
477 }
478 "toolCall" | "tool_call" => {
479 model.tool_call =
480 acl_bool_attr(model_block, &["toolCall", "tool_call"])
481 .unwrap_or(model.tool_call);
482 }
483 "temperature" => {
484 model.temperature =
485 acl_bool_attr(model_block, &["temperature"])
486 .unwrap_or(model.temperature);
487 }
488 "releaseDate" | "release_date" => {
489 if let Some(release_date) = acl_string(value) {
490 model.release_date = Some(release_date);
491 }
492 }
493 "maxTokens" => {
494 tracing::warn!(
495 provider = %provider.name,
496 model = %model.id,
497 field = "maxTokens",
498 "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
499 );
500 if let Some(output) = acl_u32(value) {
501 model.limit.output = output;
502 }
503 }
504 "contextTokens" => {
505 tracing::warn!(
506 provider = %provider.name,
507 model = %model.id,
508 field = "contextTokens",
509 "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
510 );
511 if let Some(context) = acl_u32(value) {
512 model.limit.context = context;
513 }
514 }
515 "limit" => {
516 if let Some(output) =
517 acl_object_u32_attr(value, &["output"])
518 {
519 model.limit.output = output;
520 }
521 if let Some(context) =
522 acl_object_u32_attr(value, &["context"])
523 {
524 model.limit.context = context;
525 }
526 }
527 _ => {}
528 }
529 }
530
531 provider.models.push(model);
532 }
533 }
534
535 config.providers.push(provider);
536 }
537 _ => {
538 }
541 }
542 }
543
544 if let Some(auto_parallel) = config.auto_parallel {
545 config.auto_delegation.auto_parallel = auto_parallel;
546 }
547
548 Ok(config)
549 }
550
551 pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
553 self.providers.iter().find(|p| p.name == name)
554 }
555
556 pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
558 let default = self.default_model.as_ref()?;
559 let (provider_name, _) = default.split_once('/')?;
560 self.find_provider(provider_name)
561 }
562
563 pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
565 let default = self.default_model.as_ref()?;
566 let (provider_name, model_id) = default.split_once('/')?;
567 let provider = self.find_provider(provider_name)?;
568 let model = provider.find_model(model_id)?;
569 Some((provider, model))
570 }
571
572 pub fn default_llm_config(&self) -> Option<LlmConfig> {
576 let (provider, model) = self.default_model_config()?;
577 let api_key = provider.get_api_key(model)?;
578 let base_url = provider.get_base_url(model);
579 let headers = provider.get_headers(model);
580 let session_id_header = provider.get_session_id_header(model);
581
582 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
583 if let Some(url) = base_url {
584 config = config.with_base_url(url);
585 }
586 if !headers.is_empty() {
587 config = config.with_headers(headers);
588 }
589 if let Some(header_name) = session_id_header {
590 config = config.with_session_id_header(header_name);
591 }
592 if let Some(timeout_ms) = self.llm_api_timeout_ms {
593 config = config.with_api_timeout(timeout_ms);
594 }
595 config = apply_model_caps(config, model, self.thinking_budget);
596 Some(config)
597 }
598
599 pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
603 let provider = self.find_provider(provider_name)?;
604 let model = provider.find_model(model_id)?;
605 let api_key = provider.get_api_key(model)?;
606 let base_url = provider.get_base_url(model);
607 let headers = provider.get_headers(model);
608 let session_id_header = provider.get_session_id_header(model);
609
610 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
611 if let Some(url) = base_url {
612 config = config.with_base_url(url);
613 }
614 if !headers.is_empty() {
615 config = config.with_headers(headers);
616 }
617 if let Some(header_name) = session_id_header {
618 config = config.with_session_id_header(header_name);
619 }
620 if let Some(timeout_ms) = self.llm_api_timeout_ms {
621 config = config.with_api_timeout(timeout_ms);
622 }
623 config = apply_model_caps(config, model, self.thinking_budget);
624 Some(config)
625 }
626
627 pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
629 self.providers
630 .iter()
631 .flat_map(|p| p.models.iter().map(move |m| (p, m)))
632 .collect()
633 }
634
635 pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
637 self.skill_dirs.push(dir.into());
638 self
639 }
640
641 pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
643 self.agent_dirs.push(dir.into());
644 self
645 }
646
647 pub fn has_directories(&self) -> bool {
649 !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
650 }
651
652 pub fn has_providers(&self) -> bool {
654 !self.providers.is_empty()
655 }
656}