use crate::daemon::Daemon;
use anyhow::{Context, Result};
use crabllm_core::Provider;
use wcore::protocol::message::*;
use wcore::storage::Storage;
pub(super) async fn list<P: Provider + 'static>(node: &Daemon<P>) -> Result<Vec<AgentInfo>> {
let rt = node.runtime.read().await.clone();
Ok(rt
.agents()
.into_iter()
.map(|c| agent_config_to_info(&c))
.collect())
}
pub(super) async fn get<P: Provider + 'static>(
node: &Daemon<P>,
name: String,
) -> Result<AgentInfo> {
let rt = node.runtime.read().await.clone();
let config = rt
.agent(&name)
.ok_or_else(|| anyhow::anyhow!("agent '{name}' not found"))?;
Ok(agent_config_to_info(&config))
}
pub(super) async fn create<P: Provider + 'static>(
node: &Daemon<P>,
req: CreateAgentMsg,
) -> Result<AgentInfo> {
validate_agent_name(&req.name)?;
let mut config: wcore::AgentConfig =
serde_json::from_str(&req.config).context("invalid AgentConfig JSON")?;
if config.id.is_nil() {
config.id = wcore::AgentId::new();
}
config.name = req.name.clone();
let rt = node.runtime.read().await.clone();
let storage = rt.storage();
if storage.load_agent_by_name(&req.name)?.is_some() {
anyhow::bail!("agent '{}' already exists", req.name);
}
storage.upsert_agent(&config, &req.prompt)?;
register_agent_from_disk(node, &req.name).await?;
get(node, req.name).await
}
pub(super) async fn update<P: Provider + 'static>(
node: &Daemon<P>,
req: UpdateAgentMsg,
) -> Result<AgentInfo> {
validate_agent_name(&req.name)?;
let mut config: wcore::AgentConfig =
serde_json::from_str(&req.config).context("invalid AgentConfig JSON")?;
let rt = node.runtime.read().await.clone();
let storage = rt.storage();
let existing = storage.load_agent_by_name(&req.name)?;
if let Some(prev) = &existing {
if config.id.is_nil() {
config.id = prev.id;
}
} else if config.id.is_nil() {
config.id = wcore::AgentId::new();
}
config.name = req.name.clone();
let prompt = if req.prompt.is_empty() {
existing.map(|a| a.system_prompt).unwrap_or_default()
} else {
req.prompt.clone()
};
storage.upsert_agent(&config, &prompt)?;
register_agent_from_disk(node, &req.name).await?;
get(node, req.name).await
}
pub(super) async fn rename<P: Provider + 'static>(
node: &Daemon<P>,
old_name: String,
new_name: String,
) -> Result<AgentInfo> {
validate_agent_name(&new_name)?;
anyhow::ensure!(
old_name != wcore::paths::DEFAULT_AGENT,
"cannot rename the default agent '{old_name}'"
);
if old_name == new_name {
return get(node, old_name).await;
}
let rt = node.runtime.read().await.clone();
let storage = rt.storage();
let existing = storage
.load_agent_by_name(&old_name)?
.ok_or_else(|| anyhow::anyhow!("agent '{old_name}' not found"))?;
storage.rename_agent(&existing.id, &new_name)?;
rt.remove_agent(&old_name);
node.hook.unregister_scope(&old_name);
register_agent_from_disk(node, &new_name).await?;
get(node, new_name).await
}
pub(super) async fn delete<P: Provider + 'static>(node: &Daemon<P>, name: String) -> Result<bool> {
let rt = node.runtime.read().await.clone();
let storage = rt.storage();
let Some(existing) = storage.load_agent_by_name(&name)? else {
return Ok(false);
};
let removed = storage.delete_agent(&existing.id)?;
if removed {
rt.remove_agent(&name);
node.hook.unregister_scope(&name);
}
Ok(removed)
}
async fn register_agent_from_disk<P: Provider + 'static>(
node: &Daemon<P>,
name: &str,
) -> Result<()> {
let rt = node.runtime.read().await.clone();
let agent_config = rt
.storage()
.load_agent_by_name(name)?
.ok_or_else(|| anyhow::anyhow!("agent '{name}' missing from storage after upsert"))?;
let registered = rt.upsert_agent(agent_config);
node.hook.register_scope(name.to_owned(), ®istered);
Ok(())
}
fn validate_agent_name(name: &str) -> Result<()> {
anyhow::ensure!(!name.is_empty(), "agent name cannot be empty");
anyhow::ensure!(
!name.contains('/') && !name.contains('\\') && !name.contains(".."),
"agent name '{name}' contains invalid characters"
);
Ok(())
}
fn agent_config_to_info(config: &wcore::AgentConfig) -> AgentInfo {
let json = serde_json::to_string(config).unwrap_or_default();
AgentInfo {
name: config.name.clone(),
description: config.description.clone(),
config: json,
model: config.model.clone(),
max_iterations: config.max_iterations as u32,
thinking: config.thinking,
members: config.members.clone(),
skills: config.skills.clone(),
mcps: config.mcps.clone(),
compact_threshold: config.compact_threshold.map(|t| t as u32),
compact_tool_max_len: config.compact_tool_max_len as u32,
}
}