1use super::provider::{
2 apply_model_caps, ModelConfig, ModelCost, ModelLimit, ModelModalities, ProviderConfig,
3};
4use super::{AutoDelegationConfig, CodeConfig, StorageBackend};
5use crate::error::{CodeError, Result};
6use crate::llm::LlmConfig;
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10fn 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
127impl CodeConfig {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 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 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 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 "providers" => {
237 let provider_name = block.labels.first().cloned().ok_or_else(|| {
238 CodeError::Config(
239 "providers block requires a label (e.g., providers \"openai\" { ... })"
240 .into(),
241 )
242 })?;
243
244 let mut provider = ProviderConfig {
245 name: provider_name.clone(),
246 api_key: None,
247 base_url: None,
248 headers: HashMap::new(),
249 session_id_header: None,
250 models: Vec::new(),
251 };
252
253 for (key, value) in &block.attributes {
254 match key.as_str() {
255 "apiKey" | "api_key" => {
256 if let Some(api_key) = acl_string(value) {
257 provider.api_key = Some(api_key);
258 }
259 }
260 "baseUrl" | "base_url" => {
261 if let Some(base_url) = acl_string(value) {
262 provider.base_url = Some(base_url);
263 }
264 }
265 "sessionIdHeader" | "session_id_header" => {
266 if let Some(header) = acl_string(value) {
267 provider.session_id_header = Some(header);
268 }
269 }
270 _ => {}
271 }
272 }
273
274 for model_block in &block.blocks {
276 if model_block.name == "models" {
277 let model_name =
278 model_block.labels.first().cloned().ok_or_else(|| {
279 CodeError::Config(
280 "models block requires a label (e.g., models \"gpt-4\" { ... })"
281 .into(),
282 )
283 })?;
284
285 let mut model = ModelConfig {
286 id: model_name.clone(),
287 name: model_name.clone(),
288 family: String::new(),
289 api_key: None,
290 base_url: None,
291 headers: HashMap::new(),
292 session_id_header: None,
293 attachment: false,
294 reasoning: false,
295 tool_call: true,
296 temperature: true,
297 release_date: None,
298 modalities: ModelModalities::default(),
299 cost: ModelCost::default(),
300 limit: ModelLimit::default(),
301 };
302
303 for (key, value) in &model_block.attributes {
304 match key.as_str() {
305 "name" => {
306 if let Some(s) = acl_string(value) {
307 model.name = s;
308 }
309 }
310 "family" => {
311 if let Some(s) = acl_string(value) {
312 model.family = s;
313 }
314 }
315 "apiKey" | "api_key" => {
316 if let Some(api_key) = acl_string(value) {
317 model.api_key = Some(api_key);
318 }
319 }
320 "baseUrl" | "base_url" => {
321 if let Some(base_url) = acl_string(value) {
322 model.base_url = Some(base_url);
323 }
324 }
325 "sessionIdHeader" | "session_id_header" => {
326 if let Some(header) = acl_string(value) {
327 model.session_id_header = Some(header);
328 }
329 }
330 "attachment" => {
331 model.attachment =
332 acl_bool_attr(model_block, &["attachment"])
333 .unwrap_or(model.attachment);
334 }
335 "reasoning" => {
336 model.reasoning =
337 acl_bool_attr(model_block, &["reasoning"])
338 .unwrap_or(model.reasoning);
339 }
340 "toolCall" | "tool_call" => {
341 model.tool_call =
342 acl_bool_attr(model_block, &["toolCall", "tool_call"])
343 .unwrap_or(model.tool_call);
344 }
345 "temperature" => {
346 model.temperature =
347 acl_bool_attr(model_block, &["temperature"])
348 .unwrap_or(model.temperature);
349 }
350 "releaseDate" | "release_date" => {
351 if let Some(release_date) = acl_string(value) {
352 model.release_date = Some(release_date);
353 }
354 }
355 "maxTokens" => {
356 tracing::warn!(
357 provider = %provider.name,
358 model = %model.id,
359 field = "maxTokens",
360 "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
361 );
362 if let Some(output) = acl_u32(value) {
363 model.limit.output = output;
364 }
365 }
366 "contextTokens" => {
367 tracing::warn!(
368 provider = %provider.name,
369 model = %model.id,
370 field = "contextTokens",
371 "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
372 );
373 if let Some(context) = acl_u32(value) {
374 model.limit.context = context;
375 }
376 }
377 "limit" => {
378 if let Some(output) = acl_object_u32_attr(value, "output") {
379 model.limit.output = output;
380 }
381 if let Some(context) = acl_object_u32_attr(value, "context")
382 {
383 model.limit.context = context;
384 }
385 }
386 _ => {}
387 }
388 }
389
390 provider.models.push(model);
391 }
392 }
393
394 config.providers.push(provider);
395 }
396 _ => {
397 }
400 }
401 }
402
403 if let Some(auto_parallel) = config.auto_parallel {
404 config.auto_delegation.auto_parallel = auto_parallel;
405 }
406
407 Ok(config)
408 }
409
410 pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
412 self.providers.iter().find(|p| p.name == name)
413 }
414
415 pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
417 let default = self.default_model.as_ref()?;
418 let (provider_name, _) = default.split_once('/')?;
419 self.find_provider(provider_name)
420 }
421
422 pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
424 let default = self.default_model.as_ref()?;
425 let (provider_name, model_id) = default.split_once('/')?;
426 let provider = self.find_provider(provider_name)?;
427 let model = provider.find_model(model_id)?;
428 Some((provider, model))
429 }
430
431 pub fn default_llm_config(&self) -> Option<LlmConfig> {
435 let (provider, model) = self.default_model_config()?;
436 let api_key = provider.get_api_key(model)?;
437 let base_url = provider.get_base_url(model);
438 let headers = provider.get_headers(model);
439 let session_id_header = provider.get_session_id_header(model);
440
441 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
442 if let Some(url) = base_url {
443 config = config.with_base_url(url);
444 }
445 if !headers.is_empty() {
446 config = config.with_headers(headers);
447 }
448 if let Some(header_name) = session_id_header {
449 config = config.with_session_id_header(header_name);
450 }
451 config = apply_model_caps(config, model, self.thinking_budget);
452 Some(config)
453 }
454
455 pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
459 let provider = self.find_provider(provider_name)?;
460 let model = provider.find_model(model_id)?;
461 let api_key = provider.get_api_key(model)?;
462 let base_url = provider.get_base_url(model);
463 let headers = provider.get_headers(model);
464 let session_id_header = provider.get_session_id_header(model);
465
466 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
467 if let Some(url) = base_url {
468 config = config.with_base_url(url);
469 }
470 if !headers.is_empty() {
471 config = config.with_headers(headers);
472 }
473 if let Some(header_name) = session_id_header {
474 config = config.with_session_id_header(header_name);
475 }
476 config = apply_model_caps(config, model, self.thinking_budget);
477 Some(config)
478 }
479
480 pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
482 self.providers
483 .iter()
484 .flat_map(|p| p.models.iter().map(move |m| (p, m)))
485 .collect()
486 }
487
488 pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
490 self.skill_dirs.push(dir.into());
491 self
492 }
493
494 pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
496 self.agent_dirs.push(dir.into());
497 self
498 }
499
500 pub fn has_directories(&self) -> bool {
502 !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
503 }
504
505 pub fn has_providers(&self) -> bool {
507 !self.providers.is_empty()
508 }
509}