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> {
49 let path = path.as_ref();
50 let workspace = WorkspaceFs::new(PathBuf::from("."));
51
52 if !workspace.exists(path) {
53 return Ok(None);
54 }
55
56 let contents = workspace.read(path)?;
57 let config: Self = toml::from_str(&contents)?;
58 Ok(Some(config))
59 }
60
61 pub fn load_from_file_with_workspace(
67 path: &Path,
68 workspace: &dyn Workspace,
69 ) -> Result<Option<Self>, AgentConfigError> {
70 if !workspace.exists(path) {
71 return Ok(None);
72 }
73
74 let contents = workspace
75 .read(path)
76 .map_err(|e| AgentConfigError::Io(io::Error::other(e)))?;
77 let config: Self = toml::from_str(&contents)?;
78 Ok(Some(config))
79 }
80
81 pub fn ensure_config_exists<P: AsRef<Path>>(path: P) -> io::Result<ConfigInitResult> {
83 let path = path.as_ref();
84 let workspace = WorkspaceFs::new(PathBuf::from("."));
85
86 if workspace.exists(path) {
87 return Ok(ConfigInitResult::AlreadyExists);
88 }
89
90 if let Some(parent) = path.parent() {
92 workspace.create_dir_all(parent)?;
93 }
94
95 workspace.write(path, DEFAULT_AGENTS_TOML)?;
97
98 Ok(ConfigInitResult::Created)
99 }
100
101 pub fn ensure_config_exists_with_workspace(
107 path: &Path,
108 workspace: &dyn Workspace,
109 ) -> io::Result<ConfigInitResult> {
110 if workspace.exists(path) {
111 return Ok(ConfigInitResult::AlreadyExists);
112 }
113
114 if let Some(parent) = path.parent() {
116 workspace.create_dir_all(parent)?;
117 }
118
119 workspace.write(path, DEFAULT_AGENTS_TOML)?;
121
122 Ok(ConfigInitResult::Created)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::workspace::MemoryWorkspace;
130
131 #[test]
132 fn load_from_file_with_workspace_returns_none_when_missing() {
133 let workspace = MemoryWorkspace::new_test();
134 let path = Path::new(".agent/agents.toml");
135
136 let result = AgentsConfigFile::load_from_file_with_workspace(path, &workspace).unwrap();
137 assert!(result.is_none());
138 }
139
140 #[test]
141 fn load_from_file_with_workspace_parses_valid_config() {
142 let workspace =
143 MemoryWorkspace::new_test().with_file(".agent/agents.toml", DEFAULT_AGENTS_TOML);
144 let path = Path::new(".agent/agents.toml");
145
146 let result = AgentsConfigFile::load_from_file_with_workspace(path, &workspace).unwrap();
147 assert!(result.is_some());
148 assert!(result.unwrap().agents.contains_key("claude"));
149 }
150
151 #[test]
152 fn ensure_config_exists_with_workspace_creates_file_when_missing() {
153 let workspace = MemoryWorkspace::new_test();
154 let path = Path::new(".agent/agents.toml");
155
156 let result =
157 AgentsConfigFile::ensure_config_exists_with_workspace(path, &workspace).unwrap();
158 assert!(matches!(result, ConfigInitResult::Created));
159 assert!(workspace.exists(path));
160 assert_eq!(workspace.read(path).unwrap(), DEFAULT_AGENTS_TOML);
161 }
162
163 #[test]
164 fn ensure_config_exists_with_workspace_does_not_overwrite_existing() {
165 let workspace =
166 MemoryWorkspace::new_test().with_file(".agent/agents.toml", "# custom config");
167 let path = Path::new(".agent/agents.toml");
168
169 let result =
170 AgentsConfigFile::ensure_config_exists_with_workspace(path, &workspace).unwrap();
171 assert!(matches!(result, ConfigInitResult::AlreadyExists));
172 assert_eq!(workspace.read(path).unwrap(), "# custom config");
173 }
174}