enki_runtime/config/
mesh_config.rs1use super::{AgentConfig, MemoryConfig};
4use crate::core::error::{Error, Result};
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MeshConfig {
35 pub name: String,
37
38 #[serde(default)]
40 pub version: Option<String>,
41
42 #[serde(default)]
45 pub coordinator: Option<String>,
46
47 #[serde(default)]
50 pub auto_coordinator: bool,
51
52 #[serde(default)]
54 pub coordinator_model: Option<String>,
55
56 #[serde(default)]
58 pub memory: Option<MemoryConfig>,
59
60 #[serde(default)]
62 pub agents: Vec<AgentConfig>,
63}
64
65impl Default for MeshConfig {
66 fn default() -> Self {
67 Self {
68 name: "mesh".to_string(),
69 version: None,
70 coordinator: None,
71 auto_coordinator: false,
72 coordinator_model: None,
73 memory: None,
74 agents: Vec::new(),
75 }
76 }
77}
78
79impl MeshConfig {
80 pub fn new(name: impl Into<String>) -> Self {
82 Self {
83 name: name.into(),
84 version: None,
85 coordinator: None,
86 auto_coordinator: false,
87 coordinator_model: None,
88 memory: None,
89 agents: Vec::new(),
90 }
91 }
92
93 pub fn from_toml(toml_str: &str) -> Result<Self> {
113 toml::from_str(toml_str)
114 .map_err(|e| Error::ConfigError(format!("Failed to parse TOML: {}", e)))
115 }
116
117 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
127 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
128 Error::ConfigError(format!(
129 "Failed to read file '{}': {}",
130 path.as_ref().display(),
131 e
132 ))
133 })?;
134 Self::from_toml(&content)
135 }
136
137 pub fn add_agent(mut self, agent: AgentConfig) -> Self {
139 self.agents.push(agent);
140 self
141 }
142
143 pub fn get_agent(&self, name: &str) -> Option<&AgentConfig> {
145 self.agents.iter().find(|a| a.name == name)
146 }
147
148 pub fn get_coordinator(&self) -> Option<&AgentConfig> {
156 if let Some(ref coordinator_name) = self.coordinator {
158 return self.get_agent(coordinator_name);
159 }
160
161 if self.auto_coordinator {
163 if let Some(coord) = self.get_agent("coordinator") {
164 return Some(coord);
165 }
166 }
167
168 self.agents.first()
170 }
171
172 pub fn get_coordinator_name(&self) -> Option<&str> {
174 self.get_coordinator().map(|a| a.name.as_str())
175 }
176
177 pub fn ensure_coordinator(&mut self) {
184 if self.coordinator.is_some() {
186 return;
187 }
188
189 if !self.auto_coordinator {
191 return;
192 }
193
194 if self.get_agent("coordinator").is_some() {
196 return;
197 }
198
199 let model = self
201 .coordinator_model
202 .clone()
203 .or_else(|| self.agents.first().map(|a| a.model.clone()))
204 .unwrap_or_else(|| "ollama::gemma3:latest".to_string());
205
206 let agent_list: Vec<String> = self
208 .agents
209 .iter()
210 .map(|a| {
211 let desc = if a.system_prompt.len() > 100 {
212 format!("{}...", &a.system_prompt[..100])
213 } else {
214 a.system_prompt.clone()
215 };
216 format!("- {}: {}", a.name, desc)
217 })
218 .collect();
219
220 let system_prompt = format!(
221 r#"You are a coordinator agent responsible for orchestrating tasks across multiple specialized agents.
222
223Your role is to:
2241. Understand user requests and break them down into subtasks
2252. Delegate subtasks to the most appropriate specialized agent
2263. Synthesize responses from multiple agents when needed
2274. Provide cohesive, helpful responses to the user
228
229Available agents in this mesh:
230{}
231
232When delegating tasks, consider each agent's specialization and choose the best fit.
233If a task requires multiple agents, coordinate their efforts and combine their outputs."#,
234 agent_list.join("\n")
235 );
236
237 let coordinator = AgentConfig::new("coordinator", model)
238 .with_system_prompt(system_prompt)
239 .with_temperature(0.7);
240
241 self.agents.insert(0, coordinator);
243 }
244
245 pub fn with_coordinator(mut self, name: impl Into<String>) -> Self {
247 self.coordinator = Some(name.into());
248 self
249 }
250
251 pub fn with_auto_coordinator(mut self, model: Option<String>) -> Self {
253 self.auto_coordinator = true;
254 self.coordinator_model = model;
255 self
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_parse_mesh_config() {
265 let toml = r#"
266 name = "test_mesh"
267
268 [[agents]]
269 name = "agent1"
270 model = "ollama::llama2"
271 system_prompt = "First agent"
272
273 [[agents]]
274 name = "agent2"
275 model = "openai::gpt-4"
276 temperature = 0.8
277 "#;
278
279 let config = MeshConfig::from_toml(toml).unwrap();
280 assert_eq!(config.name, "test_mesh");
281 assert_eq!(config.agents.len(), 2);
282 assert_eq!(config.agents[0].name, "agent1");
283 assert_eq!(config.agents[1].name, "agent2");
284 assert_eq!(config.agents[1].temperature, Some(0.8));
285 }
286
287 #[test]
288 fn test_get_agent() {
289 let config = MeshConfig::new("test")
290 .add_agent(AgentConfig::new("agent1", "ollama::llama2"))
291 .add_agent(AgentConfig::new("agent2", "ollama::llama2"));
292
293 assert!(config.get_agent("agent1").is_some());
294 assert!(config.get_agent("agent2").is_some());
295 assert!(config.get_agent("nonexistent").is_none());
296 }
297
298 #[test]
299 fn test_empty_agents() {
300 let toml = r#"
301 name = "empty_mesh"
302 "#;
303
304 let config = MeshConfig::from_toml(toml).unwrap();
305 assert_eq!(config.name, "empty_mesh");
306 assert!(config.agents.is_empty());
307 }
308
309 #[test]
310 fn test_get_coordinator_default_first_agent() {
311 let config = MeshConfig::new("test")
312 .add_agent(AgentConfig::new("agent1", "ollama::llama2"))
313 .add_agent(AgentConfig::new("agent2", "ollama::llama2"));
314
315 let coordinator = config.get_coordinator();
317 assert!(coordinator.is_some());
318 assert_eq!(coordinator.unwrap().name, "agent1");
319 }
320
321 #[test]
322 fn test_get_coordinator_designated() {
323 let config = MeshConfig::new("test")
324 .add_agent(AgentConfig::new("agent1", "ollama::llama2"))
325 .add_agent(AgentConfig::new("agent2", "ollama::llama2"))
326 .with_coordinator("agent2");
327
328 let coordinator = config.get_coordinator();
330 assert!(coordinator.is_some());
331 assert_eq!(coordinator.unwrap().name, "agent2");
332 }
333
334 #[test]
335 fn test_ensure_coordinator_auto() {
336 let mut config = MeshConfig::new("test")
337 .add_agent(AgentConfig::new("worker1", "ollama::llama2"))
338 .add_agent(AgentConfig::new("worker2", "ollama::llama2"))
339 .with_auto_coordinator(None);
340
341 assert_eq!(config.agents.len(), 2);
343
344 config.ensure_coordinator();
346 assert_eq!(config.agents.len(), 3);
347
348 assert_eq!(config.agents[0].name, "coordinator");
350
351 let coordinator = config.get_coordinator();
353 assert!(coordinator.is_some());
354 assert_eq!(coordinator.unwrap().name, "coordinator");
355
356 assert!(config.agents[0].system_prompt.contains("worker1"));
358 assert!(config.agents[0].system_prompt.contains("worker2"));
359 }
360
361 #[test]
362 fn test_ensure_coordinator_skips_if_designated() {
363 let mut config = MeshConfig::new("test")
364 .add_agent(AgentConfig::new("agent1", "ollama::llama2"))
365 .add_agent(AgentConfig::new("agent2", "ollama::llama2"))
366 .with_coordinator("agent1")
367 .with_auto_coordinator(None);
368
369 config.auto_coordinator = true; config.ensure_coordinator();
373 assert_eq!(config.agents.len(), 2); }
375
376 #[test]
377 fn test_parse_coordinator_from_toml() {
378 let toml = r#"
379 name = "test_mesh"
380 coordinator = "agent2"
381
382 [[agents]]
383 name = "agent1"
384 model = "ollama::llama2"
385
386 [[agents]]
387 name = "agent2"
388 model = "ollama::llama2"
389 "#;
390
391 let config = MeshConfig::from_toml(toml).unwrap();
392 assert_eq!(config.coordinator, Some("agent2".to_string()));
393
394 let coordinator = config.get_coordinator();
395 assert_eq!(coordinator.unwrap().name, "agent2");
396 }
397
398 #[test]
399 fn test_parse_auto_coordinator_from_toml() {
400 let toml = r#"
401 name = "test_mesh"
402 auto_coordinator = true
403 coordinator_model = "ollama::gemma3:latest"
404
405 [[agents]]
406 name = "worker1"
407 model = "ollama::llama2"
408 "#;
409
410 let mut config = MeshConfig::from_toml(toml).unwrap();
411 assert!(config.auto_coordinator);
412 assert_eq!(
413 config.coordinator_model,
414 Some("ollama::gemma3:latest".to_string())
415 );
416
417 config.ensure_coordinator();
418 assert_eq!(config.agents.len(), 2);
419 assert_eq!(config.agents[0].name, "coordinator");
420 assert_eq!(config.agents[0].model, "ollama::gemma3:latest");
421 }
422}