use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::config::{ApprovalConfig, MemoryConfig, SessionConfigOverride};
use crate::home::enact_home;
use crate::hook_config::HookConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct ChannelBotConfig {
#[serde(default)]
pub bot_name: Option<String>,
#[serde(default)]
pub bot_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct AgentDef {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub workflow: Option<String>,
#[serde(default)]
pub approval: Option<ApprovalConfig>,
#[serde(default)]
pub memory: Option<MemoryConfig>,
#[serde(default)]
pub channels: Vec<String>,
#[serde(default)]
pub telegram: Option<ChannelBotConfig>,
#[serde(default)]
pub whatsapp: Option<ChannelBotConfig>,
#[serde(default)]
pub teams: Option<ChannelBotConfig>,
#[serde(default)]
pub session: Option<SessionConfigOverride>,
#[serde(default)]
pub hooks: Option<Vec<HookConfig>>,
}
fn default_version() -> String {
"1.0.0".to_string()
}
impl Default for AgentDef {
fn default() -> Self {
Self {
name: String::new(),
description: None,
version: default_version(),
model: None,
system_prompt: None,
tools: Vec::new(),
workflow: None,
approval: None,
memory: None,
channels: Vec::new(),
telegram: None,
whatsapp: None,
teams: None,
session: None,
hooks: None,
}
}
}
impl AgentDef {
pub fn agent_dir(home: &Path, name: &str) -> PathBuf {
home.join("agents").join(name)
}
pub fn agent_yaml_path(home: &Path, name: &str) -> PathBuf {
Self::agent_dir(home, name).join("agent.yaml")
}
pub fn sessions_dir(home: &Path, name: &str) -> PathBuf {
Self::agent_dir(home, name).join("sessions")
}
pub fn memory_dir(home: &Path, name: &str) -> PathBuf {
Self::agent_dir(home, name).join("memory")
}
pub fn threads_dir(home: &Path, agent_name: &str) -> PathBuf {
Self::agent_dir(home, agent_name).join("threads")
}
pub fn load(home: &Path, name: &str) -> Result<Option<Self>> {
let path = Self::agent_yaml_path(home, name);
if !path.exists() {
return Ok(None);
}
let s = std::fs::read_to_string(&path).context("Failed to read agent.yaml")?;
let def: AgentDef = serde_yaml::from_str(&s).context("Failed to parse agent.yaml")?;
Ok(Some(def))
}
pub fn save(&self, home: &Path) -> Result<()> {
let dir = Self::agent_dir(home, &self.name);
std::fs::create_dir_all(&dir).context("Failed to create agent directory")?;
let path = dir.join("agent.yaml");
let s = serde_yaml::to_string(self).context("Failed to serialize agent to YAML")?;
std::fs::write(&path, s).context("Failed to write agent.yaml")?;
Ok(())
}
}
pub struct AgentRegistry;
impl AgentRegistry {
pub fn list(home: &Path) -> Result<Vec<String>> {
let agents_dir = home.join("agents");
if !agents_dir.exists() {
return Ok(Vec::new());
}
let mut names = Vec::new();
for e in std::fs::read_dir(agents_dir).context("Failed to read agents directory")? {
let e = e?;
let path = e.path();
if path.is_dir() && path.join("agent.yaml").exists() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
names.push(name.to_string());
}
}
}
names.sort();
Ok(names)
}
pub fn get(home: &Path, name: &str) -> Result<Option<AgentDef>> {
AgentDef::load(home, name)
}
pub fn get_default(name: &str) -> Result<Option<AgentDef>> {
Self::get(&enact_home(), name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_dir_path() {
let home = Path::new("/tmp/.enact");
assert_eq!(
AgentDef::agent_yaml_path(home, "assistant"),
PathBuf::from("/tmp/.enact/agents/assistant/agent.yaml")
);
}
}