walrus_daemon/daemon/
builder.rs1use crate::{
9 Daemon, DaemonConfig,
10 daemon::event::{DaemonEvent, DaemonEventSender},
11 ext::hub::DownloadRegistry,
12 hook::{
13 self, DaemonHook,
14 system::{memory::BuiltinMemory, task::TaskRegistry},
15 },
16 service::ServiceManager,
17};
18use anyhow::Result;
19use compact_str::CompactString;
20use model::ProviderRegistry;
21use std::{path::Path, sync::Arc};
22use tokio::sync::{Mutex, RwLock};
23use wcore::{AgentConfig, Runtime, ToolRequest};
24
25const SYSTEM_AGENT: &str = include_str!("../../prompts/walrus.md");
26const SKILL_MASTER_AGENT: &str = include_str!("../../prompts/skill-master.md");
27
28impl Daemon {
29 pub(crate) async fn build(
33 config: &DaemonConfig,
34 config_dir: &Path,
35 event_tx: DaemonEventSender,
36 ) -> Result<(Self, Option<ServiceManager>)> {
37 let (runtime, service_manager) = Self::build_runtime(config, config_dir, &event_tx).await?;
38 Ok((
39 Self {
40 runtime: Arc::new(RwLock::new(Arc::new(runtime))),
41 config_dir: config_dir.to_path_buf(),
42 event_tx,
43 },
44 service_manager,
45 ))
46 }
47
48 pub async fn reload(&self) -> Result<()> {
55 let mut config = DaemonConfig::load(&self.config_dir.join("walrus.toml"))?;
56 config.services.clear();
57 let (new_runtime, _) =
58 Self::build_runtime(&config, &self.config_dir, &self.event_tx).await?;
59 *self.runtime.write().await = Arc::new(new_runtime);
60 tracing::info!("daemon reloaded");
61 Ok(())
62 }
63
64 async fn build_runtime(
67 config: &DaemonConfig,
68 config_dir: &Path,
69 event_tx: &DaemonEventSender,
70 ) -> Result<(
71 Runtime<ProviderRegistry, DaemonHook>,
72 Option<ServiceManager>,
73 )> {
74 let manager = Self::build_providers(config)?;
75 let (hook, service_manager) =
76 Self::build_hook(config, config_dir, event_tx, &manager).await?;
77 let tool_tx = Self::build_tool_sender(event_tx);
78 let mut runtime = Runtime::new(manager, hook, Some(tool_tx)).await;
79 if let Some(ref registry) = runtime.hook.registry {
81 runtime.set_compact_hook(Arc::clone(registry) as Arc<dyn wcore::CompactHook>);
82 }
83 Self::load_agents(&mut runtime, config_dir, config)?;
84 Ok((runtime, service_manager))
85 }
86
87 fn build_providers(config: &DaemonConfig) -> Result<ProviderRegistry> {
91 let active_model = config
92 .system
93 .walrus
94 .model
95 .clone()
96 .ok_or_else(|| anyhow::anyhow!("system.walrus.model is required in walrus.toml"))?;
97 let registry = ProviderRegistry::from_providers(active_model, &config.provider)?;
98
99 tracing::info!(
100 "provider registry initialized — active model: {}",
101 registry.active_model_name().unwrap_or_default()
102 );
103 Ok(registry)
104 }
105
106 async fn build_hook(
110 config: &DaemonConfig,
111 config_dir: &Path,
112 event_tx: &DaemonEventSender,
113 manager: &ProviderRegistry,
114 ) -> Result<(DaemonHook, Option<ServiceManager>)> {
115 let downloads = Arc::new(Mutex::new(DownloadRegistry::new()));
116
117 let skills_dir = config_dir.join(wcore::paths::SKILLS_DIR);
118 let skills = hook::skill::SkillHandler::load(skills_dir).unwrap_or_else(|e| {
119 tracing::warn!("failed to load skills: {e}");
120 hook::skill::SkillHandler::default()
121 });
122
123 let mcp_servers = config.mcps.values().cloned().collect::<Vec<_>>();
124 let mcp_handler = hook::mcp::McpHandler::load(&mcp_servers).await;
125
126 let task_timeout = std::time::Duration::from_secs(config.system.tasks.task_timeout);
127 let tasks = Arc::new(Mutex::new(TaskRegistry::new(
128 config.system.tasks.max_concurrent,
129 config.system.tasks.viewable_window,
130 )));
131
132 let sandboxed = detect_sandbox();
133 if sandboxed {
134 tracing::info!("sandbox mode active — OS tools bypass permission check");
135 }
136
137 let (registry, service_manager) = if config.services.is_empty() {
139 (None, None)
140 } else {
141 let daemon_socket = wcore::paths::SOCKET_PATH.to_path_buf();
142 let mut sm = ServiceManager::new(&config.services, config_dir, daemon_socket);
143 sm.spawn_all().await?;
144 let mut registry = sm.handshake_all().await;
145 registry.set_model(manager.clone());
147 (Some(Arc::new(registry)), Some(sm))
148 };
149
150 let has_ext_memory = registry
152 .as_ref()
153 .is_some_and(|r| r.tools.contains_key("recall"));
154 let memory = if !has_ext_memory {
155 Some(BuiltinMemory::open(
156 config_dir.join("memory"),
157 config.system.memory.clone(),
158 ))
159 } else {
160 None
161 };
162
163 Ok((
164 DaemonHook::new(
165 skills,
166 mcp_handler,
167 tasks,
168 downloads,
169 config.permissions.clone(),
170 sandboxed,
171 memory,
172 registry,
173 event_tx.clone(),
174 task_timeout,
175 ),
176 service_manager,
177 ))
178 }
179
180 fn build_tool_sender(event_tx: &DaemonEventSender) -> wcore::ToolSender {
186 let (tool_tx, mut tool_rx) = tokio::sync::mpsc::unbounded_channel::<ToolRequest>();
187 let event_tx = event_tx.clone();
188 tokio::spawn(async move {
189 while let Some(req) = tool_rx.recv().await {
190 if event_tx.send(DaemonEvent::ToolCall(req)).is_err() {
191 break;
192 }
193 }
194 });
195 tool_tx
196 }
197
198 fn load_agents(
204 runtime: &mut Runtime<ProviderRegistry, DaemonHook>,
205 config_dir: &Path,
206 config: &DaemonConfig,
207 ) -> Result<()> {
208 let prompts = crate::config::load_agents_dir(&config_dir.join(wcore::paths::AGENTS_DIR))?;
210 let prompt_map: std::collections::BTreeMap<String, String> = prompts.into_iter().collect();
211
212 let mut walrus_config = config.system.walrus.clone();
214 walrus_config.name = CompactString::from(wcore::paths::DEFAULT_AGENT);
215 walrus_config.system_prompt = SYSTEM_AGENT.to_owned();
216 runtime.add_agent(walrus_config);
217
218 let mut skill_master = AgentConfig::new("skill-master");
220 skill_master.system_prompt = SKILL_MASTER_AGENT.to_owned();
221 skill_master.description = CompactString::from("Interactive skill recorder");
222 skill_master.thinking = config.system.walrus.thinking;
223 runtime.add_agent(skill_master);
224
225 for (name, agent_config) in &config.agents {
227 let Some(prompt) = prompt_map.get(name) else {
228 tracing::warn!("agent '{name}' in TOML has no matching .md file, skipping");
229 continue;
230 };
231 let mut agent = agent_config.clone();
232 agent.name = CompactString::from(name.as_str());
233 agent.system_prompt = prompt.clone();
234 tracing::info!("registered agent '{name}' (thinking={})", agent.thinking);
235 runtime.add_agent(agent);
236 }
237
238 let default_think = config.system.walrus.thinking;
240 for (stem, prompt) in &prompt_map {
241 if config.agents.contains_key(stem) {
242 continue;
243 }
244 let mut agent = AgentConfig::new(stem.as_str());
245 agent.system_prompt = prompt.clone();
246 agent.thinking = default_think;
247 tracing::info!("registered agent '{stem}' (defaults, thinking={default_think})");
248 runtime.add_agent(agent);
249 }
250
251 for agent_config in runtime.agents() {
253 runtime
254 .hook
255 .register_scope(agent_config.name.clone(), &agent_config);
256 }
257
258 Ok(())
259 }
260}
261
262fn detect_sandbox() -> bool {
265 std::env::var("USER")
266 .or_else(|_| std::env::var("LOGNAME"))
267 .is_ok_and(|u| u == "walrus")
268}