Skip to main content

walrus_daemon/daemon/
builder.rs

1//! Daemon construction and lifecycle methods.
2//!
3//! This module provides the [`Daemon`] builder and reload logic as private
4//! `impl Daemon` methods. [`Daemon::build`] constructs a fully-configured
5//! daemon from a [`DaemonConfig`]. [`Daemon::reload`] rebuilds the runtime
6//! in-place from disk without restarting transports.
7
8use crate::{
9    DaemonConfig, config,
10    daemon::event::{DaemonEvent, DaemonEventSender},
11    hook::{self, DaemonHook},
12};
13use anyhow::Result;
14use model::ProviderManager;
15use std::{
16    path::{Path, PathBuf},
17    sync::Arc,
18};
19use tokio::sync::RwLock;
20use wcore::{Runtime, ToolRequest};
21
22use super::Daemon;
23
24const SYSTEM_AGENT: &str = include_str!("../../prompts/system.md");
25
26impl Daemon {
27    /// Build a fully-configured [`Daemon`] from the given config, config
28    /// directory, and event sender.
29    pub(crate) async fn build(
30        config: &DaemonConfig,
31        config_dir: &Path,
32        event_tx: DaemonEventSender,
33    ) -> Result<Self> {
34        let runtime = Self::build_runtime(config, config_dir, &event_tx).await?;
35        Ok(Self {
36            runtime: Arc::new(RwLock::new(Arc::new(runtime))),
37            config_dir: config_dir.to_path_buf(),
38            event_tx,
39        })
40    }
41
42    /// Rebuild the runtime from disk and swap it in atomically.
43    ///
44    /// In-flight requests that already hold a reference to the old runtime
45    /// complete normally. New requests after the swap see the new runtime.
46    pub async fn reload(&self) -> Result<()> {
47        let config = DaemonConfig::load(&self.config_dir.join("walrus.toml"))?;
48        let new_runtime = Self::build_runtime(&config, &self.config_dir, &self.event_tx).await?;
49        *self.runtime.write().await = Arc::new(new_runtime);
50        tracing::info!("daemon reloaded");
51        Ok(())
52    }
53
54    /// Construct a fresh [`Runtime`] from config. Used by both [`build`] and [`reload`].
55    async fn build_runtime(
56        config: &DaemonConfig,
57        config_dir: &Path,
58        event_tx: &DaemonEventSender,
59    ) -> Result<Runtime<ProviderManager, DaemonHook>> {
60        let manager = Self::build_providers(config).await?;
61        let hook = Self::build_hook(config, config_dir).await;
62        let tool_tx = Self::build_tool_sender(event_tx);
63        let mut runtime = Runtime::new(manager, hook, Some(tool_tx)).await;
64        Self::load_agents(&mut runtime, config_dir)?;
65        Ok(runtime)
66    }
67
68    /// Construct the provider manager from config.
69    async fn build_providers(config: &DaemonConfig) -> Result<ProviderManager> {
70        let models = config.model.providers.values().cloned().collect::<Vec<_>>();
71        let manager = ProviderManager::from_configs(&models).await?;
72        tracing::info!(
73            "provider manager initialized — active model: {}",
74            manager.active_model()
75        );
76        Ok(manager)
77    }
78
79    /// Build the daemon hook with all backends (memory, skills, MCP).
80    async fn build_hook(config: &DaemonConfig, config_dir: &Path) -> DaemonHook {
81        let memory = memory::InMemory::new();
82        tracing::info!("using in-memory backend");
83
84        let skills_dir = config_dir.join(config::SKILLS_DIR);
85        let skills = hook::skill::SkillHandler::load(skills_dir).unwrap_or_else(|e| {
86            tracing::warn!("failed to load skills: {e}");
87            hook::skill::SkillHandler::load(PathBuf::new()).expect("empty skill handler")
88        });
89
90        let mcp_servers = config.mcp_servers.values().cloned().collect::<Vec<_>>();
91        let mcp_handler = hook::mcp::McpHandler::load(&mcp_servers).await;
92
93        DaemonHook::new(memory, skills, mcp_handler)
94    }
95
96    /// Build a [`ToolSender`] that forwards [`ToolRequest`]s into the daemon
97    /// event loop as [`DaemonEvent::ToolCall`] variants.
98    ///
99    /// Spawns a lightweight bridge task relaying from the tool channel into
100    /// the main daemon event channel.
101    fn build_tool_sender(event_tx: &DaemonEventSender) -> wcore::ToolSender {
102        let (tool_tx, mut tool_rx) = tokio::sync::mpsc::unbounded_channel::<ToolRequest>();
103        let event_tx = event_tx.clone();
104        tokio::spawn(async move {
105            while let Some(req) = tool_rx.recv().await {
106                if event_tx.send(DaemonEvent::ToolCall(req)).is_err() {
107                    break;
108                }
109            }
110        });
111        tool_tx
112    }
113
114    /// Load agents from markdown files and add them to the runtime.
115    fn load_agents(
116        runtime: &mut Runtime<ProviderManager, DaemonHook>,
117        config_dir: &Path,
118    ) -> Result<()> {
119        let agents = crate::config::load_agents_dir(&config_dir.join(config::AGENTS_DIR))?;
120        runtime.add_agent(wcore::parse_agent_md(SYSTEM_AGENT)?);
121        for agent in agents {
122            tracing::info!("registered agent '{}'", agent.name);
123            runtime.add_agent(agent);
124        }
125        Ok(())
126    }
127}