Skip to main content

agentic_config/
loader.rs

1//! Configuration loader with two-layer merge and env overrides.
2//!
3//! The loading process:
4//! 1. Read global config from `~/.config/agentic/agentic.toml`
5//! 2. Read local config from `./agentic.toml`
6//! 3. Deep merge at TOML Value level (tables merge, arrays/scalars replace)
7//! 4. Deserialize once into typed `AgenticConfig`
8//! 5. Apply env var overrides (highest precedence)
9//! 6. Run advisory validation
10
11use crate::merge::deep_merge;
12use crate::types::AgenticConfig;
13use crate::validation::AdvisoryWarning;
14use anyhow::Context;
15use anyhow::Result;
16use std::path::Path;
17use std::path::PathBuf;
18
19/// Filename for local config (TOML format).
20pub const LOCAL_FILE: &str = "agentic.toml";
21
22/// Directory name under `config_dir` for global config.
23pub const GLOBAL_DIR: &str = "agentic";
24
25/// Filename for global config (TOML format).
26pub const GLOBAL_FILE: &str = "agentic.toml";
27
28/// Resolved paths for config files.
29#[derive(Debug, Clone)]
30pub struct AgenticConfigPaths {
31    /// Path to local config (./agentic.toml).
32    pub local: PathBuf,
33
34    /// Path to global config (~/.config/agentic/agentic.toml).
35    pub global: PathBuf,
36}
37
38/// Result of loading configuration.
39#[derive(Debug)]
40pub struct LoadedAgenticConfig {
41    /// The loaded and merged configuration.
42    pub config: AgenticConfig,
43
44    /// Advisory warnings from validation.
45    pub warnings: Vec<AdvisoryWarning>,
46
47    /// Resolved config file paths.
48    pub paths: AgenticConfigPaths,
49}
50
51/// Get the global config file path.
52///
53/// Returns `~/.config/agentic/agentic.toml` on Unix-like systems.
54///
55/// # Test Hook
56///
57/// Set `__AGENTIC_CONFIG_DIR_FOR_TESTS` to override the base config directory.
58/// This is handled by [`crate::paths::xdg_config_home()`] which implements XDG
59/// path resolution with test hook support.
60pub fn global_config_path() -> Result<PathBuf> {
61    let base = crate::paths::xdg_config_home()?;
62    Ok(base.join(GLOBAL_DIR).join(GLOBAL_FILE))
63}
64
65/// Get the local config file path for a given directory.
66pub fn local_config_path(local_dir: &Path) -> PathBuf {
67    local_dir.join(LOCAL_FILE)
68}
69
70/// Load and merge configuration from global and local files.
71///
72/// # Precedence (lowest to highest)
73/// 1. Default values
74/// 2. Global config (`~/.config/agentic/agentic.toml`)
75/// 3. Local config (`./agentic.toml`)
76/// 4. Environment variables
77pub fn load_merged(local_dir: &Path) -> Result<LoadedAgenticConfig> {
78    let global_path = global_config_path()?;
79    let local_path = local_config_path(local_dir);
80
81    // Collect warnings
82    let mut warnings = Vec::new();
83
84    // Read configs as TOML Values
85    let global_v = read_toml_table_or_empty(&global_path)?;
86    let local_v = read_toml_table_or_empty(&local_path)?;
87
88    // Merge: global as base, local as patch
89    let merged = deep_merge(global_v, local_v);
90
91    // Detect unknown top-level keys
92    warnings.extend(crate::validation::detect_unknown_top_level_keys_toml(
93        &merged,
94    ));
95
96    // Detect deprecated keys from merged config (before deserialization)
97    warnings.extend(crate::validation::detect_deprecated_keys_toml(&merged));
98
99    // Deserialize to typed config using serde_path_to_error for better error messages
100    let cfg: AgenticConfig = {
101        let deserializer = merged;
102        serde_path_to_error::deserialize(deserializer)
103            .with_context(|| "Failed to deserialize merged agentic config")?
104    };
105
106    // Apply env var overrides (highest precedence)
107    let mut cfg = cfg;
108    apply_env_overrides(&mut cfg);
109
110    // Run advisory validation and add to warnings
111    warnings.extend(crate::validation::validate(&cfg));
112
113    Ok(LoadedAgenticConfig {
114        config: cfg,
115        warnings,
116        paths: AgenticConfigPaths {
117            local: local_path,
118            global: global_path,
119        },
120    })
121}
122
123/// Apply environment variable overrides to the config.
124fn apply_env_overrides(cfg: &mut AgenticConfig) {
125    // --- Service URLs ---
126    if let Some(v) = env_trimmed("ANTHROPIC_BASE_URL") {
127        cfg.services.anthropic.base_url = v;
128    }
129    if let Some(v) = env_trimmed("EXA_BASE_URL") {
130        cfg.services.exa.base_url = v;
131    }
132    if let Some(v) = env_trimmed("AGENTIC_SERVICES_LINEAR_BASE_URL") {
133        cfg.services.linear.base_url = v;
134    }
135    if let Some(v) = env_trimmed("AGENTIC_SERVICES_GITHUB_BASE_URL") {
136        cfg.services.github.base_url = v;
137    }
138
139    // --- Subagents model overrides ---
140    if let Some(v) = env_trimmed("AGENTIC_SUBAGENTS_LOCATOR_MODEL") {
141        cfg.subagents.locator_model = v;
142    }
143    if let Some(v) = env_trimmed("AGENTIC_SUBAGENTS_ANALYZER_MODEL") {
144        cfg.subagents.analyzer_model = v;
145    }
146    if let Some(v) = env_trimmed("AGENTIC_SUBAGENTS_RUNTIME_TIMEOUT_SECS")
147        && let Ok(n) = v.parse()
148    {
149        cfg.subagents.runtime_timeout_secs = n;
150    }
151
152    // --- Reasoning model overrides ---
153    if let Some(v) = env_trimmed("AGENTIC_REASONING_OPTIMIZER_MODEL") {
154        cfg.reasoning.optimizer_model = v;
155    }
156    if let Some(v) = env_trimmed("AGENTIC_REASONING_EXECUTOR_MODEL") {
157        cfg.reasoning.executor_model = v;
158    }
159    if let Some(v) = env_trimmed("AGENTIC_REASONING_EFFORT") {
160        cfg.reasoning.reasoning_effort = Some(v);
161    }
162    if let Some(v) = env_trimmed("AGENTIC_REASONING_API_BASE_URL") {
163        cfg.reasoning.api_base_url = Some(v);
164    }
165    if let Some(v) = env_trimmed("AGENTIC_REASONING_MAX_INPUT_TOKENS")
166        && let Ok(n) = v.parse()
167    {
168        cfg.reasoning.max_input_tokens = Some(n);
169    }
170    if let Some(v) = env_trimmed("AGENTIC_REASONING_MAX_COMPLETION_TOKENS")
171        && let Ok(n) = v.parse()
172    {
173        cfg.reasoning.max_completion_tokens = Some(n);
174    }
175    if let Some(v) = env_trimmed("AGENTIC_REASONING_EXECUTOR_TIMEOUT_SECS")
176        && let Ok(n) = v.parse()
177    {
178        cfg.reasoning.executor_timeout_secs = n;
179    }
180    if let Some(v) = env_trimmed("AGENTIC_REASONING_EMPTY_RESPONSE_NO_RETRY_AFTER_SECS")
181        && let Ok(n) = v.parse()
182    {
183        cfg.reasoning.empty_response_no_retry_after_secs = n;
184    }
185    if let Some(v) = env_trimmed("AGENTIC_REASONING_STREAM_HEARTBEAT_SECS")
186        && let Ok(n) = v.parse()
187    {
188        cfg.reasoning.stream_heartbeat_secs = n;
189    }
190
191    // --- CLI tools overrides ---
192    if let Some(v) = env_trimmed("AGENTIC_CLI_TOOLS_JUST_EXECUTE_TIMEOUT_SECS")
193        && let Ok(n) = v.parse()
194    {
195        cfg.cli_tools.just_execute_timeout_secs = n;
196    }
197    if let Some(v) = env_trimmed("AGENTIC_CLI_TOOLS_JUST_SEARCH_TIMEOUT_SECS")
198        && let Ok(n) = v.parse()
199    {
200        cfg.cli_tools.just_search_timeout_secs = n;
201    }
202
203    // --- Service timeout overrides ---
204    if let Some(v) = env_trimmed("AGENTIC_SERVICES_LINEAR_CONNECT_TIMEOUT_SECS")
205        && let Ok(n) = v.parse()
206    {
207        cfg.services.linear.connect_timeout_secs = n;
208    }
209    if let Some(v) = env_trimmed("AGENTIC_SERVICES_LINEAR_REQUEST_TIMEOUT_SECS")
210        && let Ok(n) = v.parse()
211    {
212        cfg.services.linear.request_timeout_secs = n;
213    }
214    if let Some(v) = env_trimmed("AGENTIC_SERVICES_GITHUB_TOTAL_TIMEOUT_SECS")
215        && let Ok(n) = v.parse()
216    {
217        cfg.services.github.total_timeout_secs = n;
218    }
219
220    // --- Review/thoughts overrides ---
221    if let Some(v) = env_trimmed("AGENTIC_REVIEW_RUN_TIMEOUT_SECS")
222        && let Ok(n) = v.parse()
223    {
224        cfg.review.run_timeout_secs = n;
225    }
226    if let Some(v) = env_trimmed("AGENTIC_THOUGHTS_ADD_REFERENCE_TIMEOUT_SECS")
227        && let Ok(n) = v.parse()
228    {
229        cfg.thoughts.add_reference_timeout_secs = n;
230    }
231
232    // --- Logging overrides ---
233    if let Some(v) = env_trimmed("AGENTIC_LOG_LEVEL") {
234        cfg.logging.level = v;
235    }
236    if let Some(v) = env_trimmed("AGENTIC_LOG_JSON") {
237        cfg.logging.json = v.to_lowercase() == "true" || v == "1";
238    }
239}
240
241/// Helper to read and normalize an env var (trim + filter empty).
242fn env_trimmed(name: &str) -> Option<String> {
243    std::env::var(name)
244        .ok()
245        .map(|v| v.trim().to_string())
246        .filter(|v| !v.is_empty())
247}
248
249/// Read a TOML file as a Value, returning empty table if file doesn't exist.
250fn read_toml_table_or_empty(path: &Path) -> Result<toml::Value> {
251    if !path.exists() {
252        return Ok(toml::Value::Table(Default::default()));
253    }
254
255    let raw = std::fs::read_to_string(path)
256        .with_context(|| format!("Failed to read config file {}", path.display()))?;
257
258    let v: toml::Value =
259        toml::from_str(&raw).with_context(|| format!("Invalid TOML in {}", path.display()))?;
260
261    match v {
262        toml::Value::Table(_) => Ok(v),
263        _ => anyhow::bail!("Config root must be a TOML table: {}", path.display()),
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::test_support::EnvGuard;
271    use serial_test::serial;
272    use tempfile::TempDir;
273
274    /// Env var name for test isolation of global config path.
275    const CONFIG_DIR_TEST_VAR: &str = "__AGENTIC_CONFIG_DIR_FOR_TESTS";
276
277    #[test]
278    #[serial]
279    fn test_load_no_files_returns_defaults() {
280        let temp = TempDir::new().unwrap();
281        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
282
283        let loaded = load_merged(temp.path()).unwrap();
284
285        // Should get default values
286        assert_eq!(
287            loaded.config.services.anthropic.base_url,
288            "https://api.anthropic.com"
289        );
290        assert_eq!(loaded.config.orchestrator.session_deadline_secs, 3600);
291        assert_eq!(loaded.config.subagents.runtime_timeout_secs, 3600);
292        assert_eq!(loaded.config.cli_tools.just_execute_timeout_secs, 1800);
293        assert_eq!(loaded.config.review.run_timeout_secs, 1800);
294        assert!(loaded.warnings.is_empty());
295    }
296
297    #[test]
298    #[serial]
299    fn test_load_local_only() {
300        let temp = TempDir::new().unwrap();
301        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
302
303        let local_path = temp.path().join(LOCAL_FILE);
304        std::fs::write(
305            &local_path,
306            r"
307[orchestrator]
308session_deadline_secs = 7200
309",
310        )
311        .unwrap();
312
313        let loaded = load_merged(temp.path()).unwrap();
314        assert_eq!(loaded.config.orchestrator.session_deadline_secs, 7200);
315        // Other fields should be defaults
316        assert_eq!(loaded.config.orchestrator.inactivity_timeout_secs, 300);
317    }
318
319    #[test]
320    #[serial]
321    fn test_local_overrides_global() {
322        let temp = TempDir::new().unwrap();
323
324        // Point global config to our temp directory
325        let global_base = temp.path().join("global_config");
326        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, &global_base);
327
328        // Create global config
329        let global_dir = global_base.join(GLOBAL_DIR);
330        std::fs::create_dir_all(&global_dir).unwrap();
331        std::fs::write(
332            global_dir.join(GLOBAL_FILE),
333            r#"
334[subagents]
335locator_model = "global-model"
336analyzer_model = "global-analyzer"
337"#,
338        )
339        .unwrap();
340
341        // Create local config that overrides locator_model but not analyzer_model
342        let local_dir = temp.path().join("local_repo");
343        std::fs::create_dir_all(&local_dir).unwrap();
344        std::fs::write(
345            local_dir.join(LOCAL_FILE),
346            r#"
347[subagents]
348locator_model = "local-model"
349"#,
350        )
351        .unwrap();
352
353        let loaded = load_merged(&local_dir).unwrap();
354        // Local wins for locator_model
355        assert_eq!(loaded.config.subagents.locator_model, "local-model");
356        // Global value preserved for analyzer_model
357        assert_eq!(loaded.config.subagents.analyzer_model, "global-analyzer");
358    }
359
360    #[test]
361    #[serial]
362    fn test_env_overrides_files() {
363        let temp = TempDir::new().unwrap();
364        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
365        std::fs::write(
366            temp.path().join(LOCAL_FILE),
367            r#"
368[reasoning]
369optimizer_model = "file-model"
370"#,
371        )
372        .unwrap();
373
374        // Set env var via EnvGuard (RAII cleanup)
375        let _env_guard = EnvGuard::set("AGENTIC_REASONING_OPTIMIZER_MODEL", "env-model");
376
377        let loaded = load_merged(temp.path()).unwrap();
378        assert_eq!(loaded.config.reasoning.optimizer_model, "env-model");
379    }
380
381    #[test]
382    #[serial]
383    fn test_reasoning_defaults_include_streaming_recovery_fields() {
384        let temp = TempDir::new().unwrap();
385        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
386
387        let loaded = load_merged(temp.path()).unwrap();
388
389        assert_eq!(loaded.config.reasoning.executor_timeout_secs, 2700);
390        assert_eq!(
391            loaded.config.reasoning.empty_response_no_retry_after_secs,
392            600
393        );
394        assert_eq!(loaded.config.reasoning.stream_heartbeat_secs, 30);
395    }
396
397    #[test]
398    #[serial]
399    fn test_reasoning_streaming_env_overrides_apply() {
400        let temp = TempDir::new().unwrap();
401        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
402        let _g1 = EnvGuard::set("AGENTIC_REASONING_EXECUTOR_TIMEOUT_SECS", "123");
403        let _g2 = EnvGuard::set("AGENTIC_REASONING_EMPTY_RESPONSE_NO_RETRY_AFTER_SECS", "45");
404        let _g3 = EnvGuard::set("AGENTIC_REASONING_STREAM_HEARTBEAT_SECS", "9");
405
406        let loaded = load_merged(temp.path()).unwrap();
407
408        assert_eq!(loaded.config.reasoning.executor_timeout_secs, 123);
409        assert_eq!(
410            loaded.config.reasoning.empty_response_no_retry_after_secs,
411            45
412        );
413        assert_eq!(loaded.config.reasoning.stream_heartbeat_secs, 9);
414    }
415
416    #[test]
417    #[serial]
418    fn test_timeout_env_overrides_apply() {
419        let temp = TempDir::new().unwrap();
420        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
421        let _g1 = EnvGuard::set("AGENTIC_SUBAGENTS_RUNTIME_TIMEOUT_SECS", "123");
422        let _g2 = EnvGuard::set("AGENTIC_CLI_TOOLS_JUST_EXECUTE_TIMEOUT_SECS", "456");
423        let _g3 = EnvGuard::set("AGENTIC_CLI_TOOLS_JUST_SEARCH_TIMEOUT_SECS", "0");
424        let _g4 = EnvGuard::set("AGENTIC_SERVICES_LINEAR_CONNECT_TIMEOUT_SECS", "11");
425        let _g5 = EnvGuard::set("AGENTIC_SERVICES_LINEAR_REQUEST_TIMEOUT_SECS", "22");
426        let _g6 = EnvGuard::set("AGENTIC_SERVICES_GITHUB_TOTAL_TIMEOUT_SECS", "33");
427        let _g7 = EnvGuard::set("AGENTIC_REVIEW_RUN_TIMEOUT_SECS", "44");
428        let _g8 = EnvGuard::set("AGENTIC_THOUGHTS_ADD_REFERENCE_TIMEOUT_SECS", "55");
429
430        let loaded = load_merged(temp.path()).unwrap();
431
432        assert_eq!(loaded.config.subagents.runtime_timeout_secs, 123);
433        assert_eq!(loaded.config.cli_tools.just_execute_timeout_secs, 456);
434        assert_eq!(loaded.config.cli_tools.just_search_timeout_secs, 0);
435        assert_eq!(loaded.config.services.linear.connect_timeout_secs, 11);
436        assert_eq!(loaded.config.services.linear.request_timeout_secs, 22);
437        assert_eq!(loaded.config.services.github.total_timeout_secs, 33);
438        assert_eq!(loaded.config.review.run_timeout_secs, 44);
439        assert_eq!(loaded.config.thoughts.add_reference_timeout_secs, 55);
440    }
441
442    #[test]
443    #[serial]
444    fn test_reasoning_token_limit_toml_is_ignored_without_warnings() {
445        let temp = TempDir::new().unwrap();
446        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
447
448        std::fs::write(
449            temp.path().join(LOCAL_FILE),
450            r"
451[reasoning]
452token_limit = 12345
453",
454        )
455        .unwrap();
456
457        let loaded = load_merged(temp.path()).unwrap();
458        assert_eq!(loaded.config.reasoning.max_input_tokens, None);
459        assert_eq!(loaded.config.reasoning.max_completion_tokens, Some(128_000));
460        assert!(loaded.warnings.is_empty());
461    }
462
463    #[test]
464    #[serial]
465    fn test_reasoning_max_input_tokens_wins_over_token_limit_in_same_toml_layer() {
466        let temp = TempDir::new().unwrap();
467        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
468
469        std::fs::write(
470            temp.path().join(LOCAL_FILE),
471            r"
472[reasoning]
473max_input_tokens = 111
474token_limit = 222
475",
476        )
477        .unwrap();
478
479        let loaded = load_merged(temp.path()).unwrap();
480        assert_eq!(loaded.config.reasoning.max_input_tokens, Some(111));
481        assert!(loaded.warnings.is_empty());
482    }
483
484    #[test]
485    #[serial]
486    fn test_deprecated_env_token_limit_is_ignored_and_new_env_still_wins() {
487        let temp = TempDir::new().unwrap();
488        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
489
490        {
491            let _g_old = EnvGuard::set("AGENTIC_REASONING_TOKEN_LIMIT", "222");
492
493            let loaded = load_merged(temp.path()).unwrap();
494            assert_eq!(loaded.config.reasoning.max_input_tokens, None);
495            assert!(loaded.warnings.is_empty());
496        }
497
498        let _g_old = EnvGuard::set("AGENTIC_REASONING_TOKEN_LIMIT", "222");
499        let _g_new = EnvGuard::set("AGENTIC_REASONING_MAX_INPUT_TOKENS", "111");
500
501        let loaded = load_merged(temp.path()).unwrap();
502        assert_eq!(loaded.config.reasoning.max_input_tokens, Some(111));
503        assert!(loaded.warnings.is_empty());
504    }
505
506    #[test]
507    #[serial]
508    fn test_env_trimmed_ignores_whitespace() {
509        // Use EnvGuard for RAII cleanup
510        let _g1 = EnvGuard::set("TEST_AGENTIC_TRIM", "  value  ");
511        let result = env_trimmed("TEST_AGENTIC_TRIM");
512        assert_eq!(result, Some("value".to_string()));
513
514        let _g2 = EnvGuard::set("TEST_AGENTIC_EMPTY", "   ");
515        let result = env_trimmed("TEST_AGENTIC_EMPTY");
516        assert_eq!(result, None);
517    }
518
519    #[test]
520    #[serial]
521    fn test_invalid_toml_errors() {
522        let temp = TempDir::new().unwrap();
523        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
524        std::fs::write(temp.path().join(LOCAL_FILE), "not valid toml [[[").unwrap();
525
526        let result = load_merged(temp.path());
527        assert!(result.is_err());
528        assert!(result.unwrap_err().to_string().contains("Invalid TOML"));
529    }
530
531    #[test]
532    #[serial]
533    fn test_local_value_overrides_struct_default() {
534        let temp = TempDir::new().unwrap();
535        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
536
537        // Create local config that overrides a nested value
538        std::fs::write(
539            temp.path().join(LOCAL_FILE),
540            r"
541[web_retrieval]
542request_timeout_secs = 60
543",
544        )
545        .unwrap();
546
547        let loaded = load_merged(temp.path()).unwrap();
548        assert_eq!(loaded.config.web_retrieval.request_timeout_secs, 60);
549    }
550
551    #[test]
552    #[serial]
553    fn test_paths_are_set() {
554        let temp = TempDir::new().unwrap();
555        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
556
557        let loaded = load_merged(temp.path()).unwrap();
558
559        assert_eq!(loaded.paths.local, temp.path().join(LOCAL_FILE));
560        // Global path should point to our isolated temp dir
561        assert_eq!(
562            loaded.paths.global,
563            temp.path().join(GLOBAL_DIR).join(GLOBAL_FILE)
564        );
565    }
566
567    #[test]
568    #[serial]
569    fn test_warns_on_unknown_top_level_key() {
570        let temp = TempDir::new().unwrap();
571        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
572
573        std::fs::write(
574            temp.path().join(LOCAL_FILE),
575            r#"
576typo = 1
577unknown_section = "value"
578"#,
579        )
580        .unwrap();
581
582        let loaded = load_merged(temp.path()).unwrap();
583        assert!(
584            loaded
585                .warnings
586                .iter()
587                .any(|w| w.code == "config.unknown_top_level_key" && w.message.contains("typo"))
588        );
589        assert!(
590            loaded
591                .warnings
592                .iter()
593                .any(|w| w.code == "config.unknown_top_level_key"
594                    && w.message.contains("unknown_section"))
595        );
596    }
597
598    #[test]
599    #[serial]
600    fn test_warns_on_deprecated_thoughts_section() {
601        let temp = TempDir::new().unwrap();
602        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
603
604        std::fs::write(
605            temp.path().join(LOCAL_FILE),
606            r"
607[thoughts]
608mount_dirs = {}
609",
610        )
611        .unwrap();
612
613        let loaded = load_merged(temp.path()).unwrap();
614        assert!(
615            loaded
616                .warnings
617                .iter()
618                .any(|w| w.code == "config.deprecated.thoughts.mount_dirs")
619        );
620        assert!(
621            !loaded
622                .warnings
623                .iter()
624                .any(|w| w.code == "config.unknown_top_level_key" && w.message.contains("thoughts"))
625        );
626    }
627}