crabtalk 0.0.22

Crabtalk library
Documentation
//! Server trait implementation — thin delegates to domain modules.

use crate::system::CrabTalk;
use anyhow::Result;
use crabllm_core::Provider;
use wcore::protocol::api::Server;
use wcore::protocol::message::*;

mod admin;
mod config;
mod conversation;

/// Render an RFC3339 `created_at` string as a human-friendly relative date —
/// "Today" / "Yesterday" / `YYYY-MM-DD`. Returns empty string if parsing fails.
fn format_date_label(created_at: &str) -> String {
    let Ok(ts) = chrono::DateTime::parse_from_rfc3339(created_at) else {
        return String::new();
    };
    let today = chrono::Local::now().date_naive();
    let date = ts.with_timezone(&chrono::Local).date_naive();
    if date == today {
        "Today".to_string()
    } else if date == today - chrono::Duration::days(1) {
        "Yesterday".to_string()
    } else {
        date.format("%Y-%m-%d").to_string()
    }
}

impl<P: Provider + 'static> Server for CrabTalk<P> {
    async fn send(&self, req: SendMsg) -> Result<SendResponse> {
        self.send(req).await
    }

    fn stream(
        &self,
        req: StreamMsg,
    ) -> impl futures_core::Stream<Item = Result<StreamEvent>> + Send {
        self.stream(req)
    }

    async fn compact_conversation(&self, agent: String, sender: String) -> Result<String> {
        let rt = self.runtime.read().await.clone();
        rt.compact_conversation(&agent, &sender).await
    }

    async fn ping(&self) -> Result<()> {
        Ok(())
    }

    async fn list_conversations_active(&self) -> Result<Vec<ActiveConversationInfo>> {
        let rt = self.runtime.read().await.clone();
        Ok(rt.list_active().await)
    }

    async fn kill_conversation(&self, agent: String, sender: String) -> Result<bool> {
        self.kill_conversation(&agent, &sender).await
    }

    fn subscribe_events(&self) -> impl futures_core::Stream<Item = Result<AgentEventMsg>> + Send {
        self.subscribe_events()
    }

    fn subscribe_mcp_events(&self) -> impl futures_core::Stream<Item = Result<McpEventMsg>> + Send {
        self.subscribe_mcp_events()
    }

    async fn reload(&self) -> Result<()> {
        self.reload().await
    }

    async fn get_stats(&self) -> Result<Stats> {
        self.get_stats().await
    }

    async fn subscribe_event(&self, req: SubscribeEventMsg) -> Result<SubscriptionInfo> {
        self.subscribe_event(req).await
    }

    async fn unsubscribe_event(&self, id: u64) -> Result<bool> {
        Ok(self.unsubscribe_event(id))
    }

    async fn list_subscriptions(&self) -> Result<SubscriptionList> {
        Ok(self.list_subscriptions())
    }

    async fn publish_event(&self, req: PublishEventMsg) -> Result<()> {
        self.publish_event(&req.source, &req.payload);
        Ok(())
    }

    async fn reply_to_tool(
        &self,
        conversation_id: u64,
        call_id: String,
        output: String,
        is_error: bool,
    ) -> Result<()> {
        self.reply_to_tool(conversation_id, &call_id, output, is_error)
            .await
    }

    async fn steer_session(&self, req: SteerSessionMsg) -> Result<()> {
        let rt = self.runtime.read().await.clone();
        let sender = if req.sender.is_empty() {
            "user".to_owned()
        } else {
            req.sender
        };
        rt.steer_conversation(&req.agent, &sender, req.content)
            .await
    }

    async fn list_agents(&self) -> Result<Vec<AgentInfo>> {
        let rt = self.runtime.read().await.clone();
        Ok(rt.agents().iter().map(AgentInfo::from).collect())
    }

    async fn get_agent(&self, name: String) -> Result<AgentInfo> {
        let rt = self.runtime.read().await.clone();
        let config = rt
            .agent(&name)
            .ok_or_else(|| anyhow::anyhow!("agent '{name}' not found"))?;
        Ok(AgentInfo::from(&config))
    }

    async fn create_agent(&self, req: CreateAgentMsg) -> Result<AgentInfo> {
        let mut config: wcore::AgentConfig = serde_json::from_str(&req.config)
            .map_err(|e| anyhow::anyhow!("invalid AgentConfig JSON: {e}"))?;
        config.name = req.name;
        let rt = self.runtime.read().await.clone();
        let registered = rt.create_agent(config, &req.prompt).await?;
        Ok(AgentInfo::from(&registered))
    }

    async fn update_agent(&self, req: UpdateAgentMsg) -> Result<AgentInfo> {
        let mut config: wcore::AgentConfig = serde_json::from_str(&req.config)
            .map_err(|e| anyhow::anyhow!("invalid AgentConfig JSON: {e}"))?;
        config.name = req.name;
        let rt = self.runtime.read().await.clone();
        let registered = rt.update_agent(config, &req.prompt).await?;
        Ok(AgentInfo::from(&registered))
    }

    async fn delete_agent(&self, name: String) -> Result<bool> {
        let rt = self.runtime.read().await.clone();
        rt.purge_agent(&name).await
    }

    async fn rename_agent(&self, old_name: String, new_name: String) -> Result<AgentInfo> {
        let rt = self.runtime.read().await.clone();
        let registered = rt.rename_agent(&old_name, &new_name).await?;
        Ok(AgentInfo::from(&registered))
    }

    async fn list_conversations(
        &self,
        agent: String,
        sender: String,
    ) -> Result<Vec<ConversationInfo>> {
        let rt = self.runtime.read().await.clone();
        Ok(rt
            .list_conversations(&agent, &sender)
            .await
            .into_iter()
            .map(|mut c| {
                c.date = format_date_label(&c.date);
                c
            })
            .collect())
    }

    async fn get_conversation_history(&self, file_path: String) -> Result<ConversationHistory> {
        let rt = self.runtime.read().await.clone();
        rt.load_conversation_history(&file_path).await
    }

    async fn delete_conversation(&self, file_path: String) -> Result<()> {
        let rt = self.runtime.read().await.clone();
        rt.delete_conversation(&file_path).await
    }

    async fn list_mcps(&self, req: ListMcpsMsg) -> Result<Vec<McpInfo>> {
        let agent = (!req.agent.is_empty()).then_some(req.agent);
        self.list_mcps(agent).await
    }

    async fn upsert_mcp(&self, req: UpsertMcpMsg) -> Result<McpInfo> {
        self.upsert_mcp(req.agent, req.config).await
    }

    async fn delete_mcp(&self, req: DeleteMcpMsg) -> Result<bool> {
        self.delete_mcp(req.agent, req.name).await
    }

    async fn set_active_model(&self, model: String) -> Result<()> {
        self.set_active_model(model).await
    }

    async fn list_skills(&self) -> Result<Vec<SkillInfo>> {
        Ok(self.list_skills())
    }

    async fn list_models(&self) -> Result<Vec<ModelInfo>> {
        let rt = self.runtime.read().await.clone();
        Ok(rt.list_models().await)
    }
}