use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::StorageConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpawnerConfig {
#[serde(default)]
pub shared_llms: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shared_storage: Option<StorageConfig>,
#[serde(default)]
pub shared_context: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_agents: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name_prefix: Option<String>,
#[serde(default)]
pub templates: HashMap<String, TemplateSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_tools: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TemplateSource {
File { path: String },
Inline(String),
}
impl TemplateSource {
pub fn is_file(&self) -> bool {
matches!(self, Self::File { .. })
}
pub fn is_inline(&self) -> bool {
matches!(self, Self::Inline(_))
}
}
impl Default for SpawnerConfig {
fn default() -> Self {
Self {
shared_llms: false,
shared_storage: None,
shared_context: HashMap::new(),
max_agents: None,
name_prefix: None,
templates: HashMap::new(),
allowed_tools: None,
}
}
}
impl SpawnerConfig {
pub fn is_configured(&self) -> bool {
self.shared_llms
|| self.shared_storage.is_some()
|| !self.shared_context.is_empty()
|| self.max_agents.is_some()
|| self.name_prefix.is_some()
|| !self.templates.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_is_not_configured() {
let config = SpawnerConfig::default();
assert!(!config.is_configured());
}
#[test]
fn test_deserialize_inline_template() {
let yaml = r#"
shared_llms: true
max_agents: 50
name_prefix: "npc_"
shared_context:
world_name: "Medieval Fantasy"
current_era: "Age of Dragons"
templates:
npc_base: |
name: "{{ name }}"
system_prompt: "You are {{ name }}."
"#;
let config: SpawnerConfig = serde_yaml::from_str(yaml).unwrap();
assert!(config.shared_llms);
assert_eq!(config.max_agents, Some(50));
assert_eq!(config.name_prefix.as_deref(), Some("npc_"));
assert_eq!(config.shared_context.len(), 2);
assert!(config.templates.contains_key("npc_base"));
assert!(config.templates.get("npc_base").unwrap().is_inline());
assert!(config.is_configured());
}
#[test]
fn test_deserialize_file_template() {
let yaml = r#"
templates:
npc_base:
path: ./templates/npc_base.yaml
"#;
let config: SpawnerConfig = serde_yaml::from_str(yaml).unwrap();
match config.templates.get("npc_base") {
Some(TemplateSource::File { path }) => {
assert_eq!(path, "./templates/npc_base.yaml");
}
other => panic!("expected File variant, got {:?}", other),
}
}
#[test]
fn test_deserialize_mixed_templates() {
let yaml = r#"
templates:
inline_one: |
name: "{{ name }}"
file_one:
path: ./templates/npc.yaml
inline_two: "name: test"
"#;
let config: SpawnerConfig = serde_yaml::from_str(yaml).unwrap();
assert!(config.templates.get("inline_one").unwrap().is_inline());
assert!(config.templates.get("file_one").unwrap().is_file());
assert!(config.templates.get("inline_two").unwrap().is_inline());
}
#[test]
fn test_deserialize_absolute_path_template() {
let yaml = r#"
templates:
shared_guard:
path: /opt/game/shared_templates/guard.yaml
"#;
let config: SpawnerConfig = serde_yaml::from_str(yaml).unwrap();
match config.templates.get("shared_guard") {
Some(TemplateSource::File { path }) => {
assert_eq!(path, "/opt/game/shared_templates/guard.yaml");
}
other => panic!("expected File variant, got {:?}", other),
}
}
#[test]
fn test_roundtrip_serde() {
let config = SpawnerConfig {
shared_llms: true,
shared_storage: None,
shared_context: HashMap::new(),
max_agents: Some(100),
name_prefix: Some("test_".to_string()),
templates: HashMap::new(),
allowed_tools: Some(vec!["echo".to_string()]),
};
let yaml = serde_yaml::to_string(&config).unwrap();
let parsed: SpawnerConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(parsed.max_agents, Some(100));
assert_eq!(parsed.name_prefix.as_deref(), Some("test_"));
}
#[test]
fn test_template_source_is_file() {
let file = TemplateSource::File {
path: "./test.yaml".to_string(),
};
let inline = TemplateSource::Inline("content".to_string());
assert!(file.is_file());
assert!(!file.is_inline());
assert!(inline.is_inline());
assert!(!inline.is_file());
}
}