ralph_workflow/agents/config/
file.rs1use super::types::{AgentConfigToml, DEFAULT_AGENTS_TOML};
2use crate::agents::ccs_env::CcsEnvVarsError;
3use crate::agents::fallback::FallbackConfig;
4use crate::workspace::{Workspace, WorkspaceFs};
5use serde::Deserialize;
6use std::collections::HashMap;
7use std::io;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Deserialize)]
15pub struct AgentsConfigFile {
16 #[serde(default)]
18 pub agents: HashMap<String, AgentConfigToml>,
19 #[serde(default, rename = "agent_chain")]
21 pub fallback: FallbackConfig,
22}
23
24#[derive(Debug, thiserror::Error)]
26pub enum AgentConfigError {
27 #[error("Failed to read config file: {0}")]
28 Io(#[from] io::Error),
29 #[error("Failed to parse TOML: {0}")]
30 Toml(#[from] toml::de::Error),
31 #[error("Built-in agents.toml template is invalid TOML: {0}")]
32 DefaultTemplateToml(toml::de::Error),
33 #[error("{0}")]
34 CcsEnvVars(#[from] CcsEnvVarsError),
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ConfigInitResult {
40 AlreadyExists,
42 Created,
44}
45
46impl AgentsConfigFile {
47 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Option<Self>, AgentConfigError> {
55 let path = path.as_ref();
56 let workspace = WorkspaceFs::new(PathBuf::from("."));
57
58 if !workspace.exists(path) {
59 return Ok(None);
60 }
61
62 let contents = workspace.read(path)?;
63 let config: Self = toml::from_str(&contents)?;
64 Ok(Some(config))
65 }
66
67 pub fn load_from_file_with_workspace(
79 path: &Path,
80 workspace: &dyn Workspace,
81 ) -> Result<Option<Self>, AgentConfigError> {
82 if !workspace.exists(path) {
83 return Ok(None);
84 }
85
86 let contents = workspace
87 .read(path)
88 .map_err(|e| AgentConfigError::Io(io::Error::other(e)))?;
89 let config: Self = toml::from_str(&contents)?;
90 Ok(Some(config))
91 }
92
93 pub fn ensure_config_exists<P: AsRef<Path>>(path: P) -> io::Result<ConfigInitResult> {
101 let path = path.as_ref();
102 let workspace = WorkspaceFs::new(PathBuf::from("."));
103
104 if workspace.exists(path) {
105 return Ok(ConfigInitResult::AlreadyExists);
106 }
107
108 if let Some(parent) = path.parent() {
110 workspace.create_dir_all(parent)?;
111 }
112
113 workspace.write(path, DEFAULT_AGENTS_TOML)?;
115
116 Ok(ConfigInitResult::Created)
117 }
118
119 pub fn ensure_config_exists_with_workspace(
131 path: &Path,
132 workspace: &dyn Workspace,
133 ) -> io::Result<ConfigInitResult> {
134 if workspace.exists(path) {
135 return Ok(ConfigInitResult::AlreadyExists);
136 }
137
138 if let Some(parent) = path.parent() {
140 workspace.create_dir_all(parent)?;
141 }
142
143 workspace.write(path, DEFAULT_AGENTS_TOML)?;
145
146 Ok(ConfigInitResult::Created)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::workspace::MemoryWorkspace;
154
155 #[test]
156 fn load_from_file_with_workspace_returns_none_when_missing() {
157 let workspace = MemoryWorkspace::new_test();
158 let path = Path::new(".agent/agents.toml");
159
160 let result = AgentsConfigFile::load_from_file_with_workspace(path, &workspace).unwrap();
161 assert!(result.is_none());
162 }
163
164 #[test]
165 fn load_from_file_with_workspace_parses_valid_config() {
166 let workspace =
167 MemoryWorkspace::new_test().with_file(".agent/agents.toml", DEFAULT_AGENTS_TOML);
168 let path = Path::new(".agent/agents.toml");
169
170 let result = AgentsConfigFile::load_from_file_with_workspace(path, &workspace).unwrap();
171 assert!(result.is_some());
172 assert!(result.unwrap().agents.contains_key("claude"));
173 }
174
175 #[test]
176 fn ensure_config_exists_with_workspace_creates_file_when_missing() {
177 let workspace = MemoryWorkspace::new_test();
178 let path = Path::new(".agent/agents.toml");
179
180 let result =
181 AgentsConfigFile::ensure_config_exists_with_workspace(path, &workspace).unwrap();
182 assert!(matches!(result, ConfigInitResult::Created));
183 assert!(workspace.exists(path));
184 assert_eq!(workspace.read(path).unwrap(), DEFAULT_AGENTS_TOML);
185 }
186
187 #[test]
188 fn ensure_config_exists_with_workspace_does_not_overwrite_existing() {
189 let workspace =
190 MemoryWorkspace::new_test().with_file(".agent/agents.toml", "# custom config");
191 let path = Path::new(".agent/agents.toml");
192
193 let result =
194 AgentsConfigFile::ensure_config_exists_with_workspace(path, &workspace).unwrap();
195 assert!(matches!(result, ConfigInitResult::AlreadyExists));
196 assert_eq!(workspace.read(path).unwrap(), "# custom config");
197 }
198}