ceylon_next/runner/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Configuration loaded from ceylon.toml
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct CeylonConfig {
10    pub package: PackageConfig,
11    pub agent: AgentConfig,
12    #[serde(default)]
13    pub tools: ToolsConfig,
14    #[serde(default)]
15    pub memory: MemoryConfig,
16    #[serde(default)]
17    pub build: BuildConfig,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PackageConfig {
22    pub name: String,
23    pub version: String,
24    pub description: Option<String>,
25    pub authors: Option<Vec<String>>,
26    pub license: Option<String>,
27    pub keywords: Option<Vec<String>>,
28    pub repository: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AgentConfig {
33    pub name: String,
34    pub model: String,
35    pub system_prompt_file: Option<String>,
36    #[serde(default = "default_max_retries")]
37    pub max_retries: u32,
38    #[serde(default = "default_timeout")]
39    pub timeout: u64,
40    #[serde(default)]
41    pub analyze_goals: bool,
42    pub temperature: Option<f32>,
43    pub max_tokens: Option<u32>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47pub struct ToolsConfig {
48    #[serde(default)]
49    pub include: Vec<String>,
50    #[serde(default)]
51    pub custom: Vec<String>,
52    #[serde(default)]
53    pub config: HashMap<String, toml::Value>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MemoryConfig {
58    #[serde(default = "default_memory_backend")]
59    pub backend: String,
60    pub path: Option<String>,
61    #[serde(default)]
62    pub vector_enabled: bool,
63    pub vector_provider: Option<String>,
64}
65
66impl Default for MemoryConfig {
67    fn default() -> Self {
68        Self {
69            backend: default_memory_backend(),
70            path: None,
71            vector_enabled: false,
72            vector_provider: None,
73        }
74    }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, Default)]
78pub struct BuildConfig {
79    #[serde(default)]
80    pub compile: bool,
81    pub target_dir: Option<String>,
82    #[serde(default)]
83    pub include: Vec<String>,
84}
85
86fn default_max_retries() -> u32 {
87    3
88}
89
90fn default_timeout() -> u64 {
91    120
92}
93
94fn default_memory_backend() -> String {
95    "in-memory".to_string()
96}
97
98impl CeylonConfig {
99    /// Load configuration from a ceylon.toml file
100    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
101        let content = fs::read_to_string(path.as_ref())
102            .context("Failed to read ceylon.toml file")?;
103
104        let config: CeylonConfig = toml::from_str(&content)
105            .context("Failed to parse ceylon.toml")?;
106
107        Ok(config)
108    }
109
110    /// Load configuration from the current directory
111    pub fn load_from_current_dir() -> Result<Self> {
112        Self::load("./ceylon.toml")
113    }
114
115    /// Load configuration from a project directory
116    pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
117        let config_path = dir.as_ref().join("ceylon.toml");
118        Self::load(config_path)
119    }
120
121    /// Get the absolute path to the system prompt file
122    pub fn system_prompt_path(&self, base_dir: &Path) -> Option<PathBuf> {
123        self.agent.system_prompt_file.as_ref().map(|p| {
124            let path = Path::new(p);
125            if path.is_absolute() {
126                path.to_path_buf()
127            } else {
128                base_dir.join(path)
129            }
130        })
131    }
132
133    /// Read the system prompt from the configured file
134    pub fn read_system_prompt(&self, base_dir: &Path) -> Result<Option<String>> {
135        if let Some(prompt_path) = self.system_prompt_path(base_dir) {
136            let content = fs::read_to_string(&prompt_path)
137                .with_context(|| format!("Failed to read system prompt from {:?}", prompt_path))?;
138            Ok(Some(content))
139        } else {
140            Ok(None)
141        }
142    }
143
144    /// Get the memory path, resolved relative to the base directory
145    pub fn memory_path(&self, base_dir: &Path) -> Option<PathBuf> {
146        self.memory.path.as_ref().map(|p| {
147            let path = Path::new(p);
148            if path.is_absolute() {
149                path.to_path_buf()
150            } else {
151                base_dir.join(path)
152            }
153        })
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_parse_config() {
163        let toml_content = r#"
164[package]
165name = "test-agent"
166version = "1.0.0"
167description = "Test agent"
168
169[agent]
170name = "TestAgent"
171model = "openai::gpt-4"
172system_prompt_file = "./prompts/system.txt"
173max_retries = 5
174timeout = 180
175analyze_goals = true
176
177[tools]
178include = ["calculator"]
179custom = ["csv_analyzer"]
180
181[memory]
182backend = "sqlite"
183path = "./data/memory.db"
184vector_enabled = true
185
186[build]
187compile = true
188        "#;
189
190        let config: CeylonConfig = toml::from_str(toml_content).unwrap();
191
192        assert_eq!(config.package.name, "test-agent");
193        assert_eq!(config.agent.name, "TestAgent");
194        assert_eq!(config.agent.model, "openai::gpt-4");
195        assert_eq!(config.agent.max_retries, 5);
196        assert_eq!(config.agent.timeout, 180);
197        assert_eq!(config.memory.backend, "sqlite");
198        assert!(config.memory.vector_enabled);
199        assert!(config.build.compile);
200    }
201}