1use crate::{
12 daemon::event::DaemonEventSender,
13 ext::hub::DownloadRegistry,
14 hook::{
15 mcp::McpHandler,
16 os::PermissionConfig,
17 skill::SkillHandler,
18 system::{memory::BuiltinMemory, task::TaskRegistry},
19 },
20 service::ServiceRegistry,
21};
22use compact_str::CompactString;
23use std::{collections::BTreeMap, sync::Arc, time::Duration};
24use tokio::sync::Mutex;
25use wcore::{AgentConfig, AgentEvent, Hook, ToolRegistry, model::Message};
26
27pub mod mcp;
28pub mod os;
29pub mod skill;
30pub mod system;
31
32#[derive(Default)]
34pub(crate) struct AgentScope {
35 pub(crate) tools: Vec<CompactString>,
36 pub(crate) members: Vec<String>,
37 pub(crate) skills: Vec<String>,
38 pub(crate) mcps: Vec<String>,
39}
40
41pub struct DaemonHook {
42 pub skills: SkillHandler,
43 pub mcp: McpHandler,
44 pub tasks: Arc<Mutex<TaskRegistry>>,
45 pub downloads: Arc<Mutex<DownloadRegistry>>,
46 pub permissions: PermissionConfig,
47 pub sandboxed: bool,
49 pub memory: Option<BuiltinMemory>,
51 pub(crate) event_tx: DaemonEventSender,
53 pub(crate) task_timeout: Duration,
55 pub(crate) scopes: BTreeMap<CompactString, AgentScope>,
57 pub(crate) agent_descriptions: BTreeMap<CompactString, CompactString>,
59 pub(crate) registry: Option<Arc<ServiceRegistry>>,
61}
62
63const BASE_TOOLS: &[&str] = &["read", "write", "edit", "bash"];
66
67const SKILL_TOOLS: &[&str] = &["search_skill", "load_skill", "save_skill"];
69
70const MCP_TOOLS: &[&str] = &["search_mcp", "call_mcp_tool"];
72
73const MEMORY_TOOLS: &[&str] = &["recall", "memory", "user_memory"];
75
76const TASK_TOOLS: &[&str] = &["spawn_task", "check_tasks", "ask_user", "await_tasks"];
78
79impl Hook for DaemonHook {
80 fn on_build_agent(&self, config: AgentConfig) -> AgentConfig {
81 let mut config = match self.registry {
83 Some(ref registry) => registry.on_build_agent(config),
84 None => config,
85 };
86
87 config
89 .system_prompt
90 .push_str(&os::environment_block(self.sandboxed));
91
92 if let Some(ref mem) = self.memory {
94 let prompt = mem.build_prompt();
95 if !prompt.is_empty() {
96 config.system_prompt.push_str(&prompt);
97 }
98 }
99
100 self.apply_scope(&mut config);
102 config
103 }
104
105 fn on_compact(&self, agent: &str, prompt: &mut String) {
106 if let Some(ref registry) = self.registry {
107 registry.on_compact(agent, prompt);
108 }
109 }
110
111 fn on_before_run(
112 &self,
113 agent: &str,
114 history: &[wcore::model::Message],
115 ) -> Vec<wcore::model::Message> {
116 let mut messages = match self.registry {
117 Some(ref registry) => registry.on_before_run(agent, history),
118 None => Vec::new(),
119 };
120 if agent == wcore::paths::DEFAULT_AGENT && !self.agent_descriptions.is_empty() {
121 let mut block = String::from("<agents>\n");
122 for (name, desc) in &self.agent_descriptions {
123 block.push_str(&format!("- {name}: {desc}\n"));
124 }
125 block.push_str("</agents>");
126 messages.push(Message::user(block));
127 }
128 if let Some(ref mem) = self.memory {
129 messages.extend(mem.before_run(history));
130 }
131 messages
132 }
133
134 async fn on_register_tools(&self, tools: &mut ToolRegistry) {
135 self.mcp.on_register_tools(tools).await;
136 tools.insert_all(os::tool::tools());
137 tools.insert_all(skill::tool::tools());
138 tools.insert_all(system::task::tool::tools());
139 if let Some(ref registry) = self.registry {
140 registry.on_register_tools(tools).await;
141 }
142 if self.memory.is_some() && !tools.contains("recall") {
144 tools.insert_all(system::memory::tool::tools());
145 }
146 }
147
148 fn on_after_run(&self, agent: &str, history: &[Message], system_prompt: &str) {
149 if let Some(ref registry) = self.registry {
150 registry.on_after_run(agent, history, system_prompt);
151 }
152 }
153
154 fn on_after_compact(&self, agent: &str, summary: &str) {
155 if let Some(ref registry) = self.registry {
156 registry.on_after_compact(agent, summary);
157 }
158 if let Some(ref mem) = self.memory {
159 mem.after_compact(agent, summary);
160 }
161 }
162
163 fn on_event(&self, agent: &str, event: &AgentEvent) {
164 match event {
165 AgentEvent::TextDelta(text) => {
166 tracing::trace!(%agent, text_len = text.len(), "agent text delta");
167 }
168 AgentEvent::ThinkingDelta(text) => {
169 tracing::trace!(%agent, text_len = text.len(), "agent thinking delta");
170 }
171 AgentEvent::ToolCallsStart(calls) => {
172 tracing::debug!(%agent, count = calls.len(), "agent tool calls started");
173 }
174 AgentEvent::ToolResult { call_id, .. } => {
175 tracing::debug!(%agent, %call_id, "agent tool result");
176 }
177 AgentEvent::ToolCallsComplete => {
178 tracing::debug!(%agent, "agent tool calls complete");
179 }
180 AgentEvent::Compact { summary } => {
181 tracing::info!(%agent, summary_len = summary.len(), "context compacted");
182 self.on_after_compact(agent, summary);
183 }
184 AgentEvent::Done(response) => {
185 tracing::info!(
186 %agent,
187 iterations = response.iterations,
188 stop_reason = ?response.stop_reason,
189 "agent run complete"
190 );
191 let (prompt, completion) = response.steps.iter().fold((0u64, 0u64), |(p, c), s| {
193 (
194 p + u64::from(s.response.usage.prompt_tokens),
195 c + u64::from(s.response.usage.completion_tokens),
196 )
197 });
198 if (prompt > 0 || completion > 0)
199 && let Ok(mut registry) = self.tasks.try_lock()
200 {
201 let tid = registry
202 .list(
203 Some(agent),
204 Some(system::task::TaskStatus::InProgress),
205 None,
206 )
207 .first()
208 .map(|t| t.id);
209 if let Some(tid) = tid {
210 registry.add_tokens(tid, prompt, completion);
211 }
212 }
213 }
214 }
215 }
216}
217
218impl DaemonHook {
219 #[allow(clippy::too_many_arguments)]
221 pub fn new(
222 skills: SkillHandler,
223 mcp: McpHandler,
224 tasks: Arc<Mutex<TaskRegistry>>,
225 downloads: Arc<Mutex<DownloadRegistry>>,
226 permissions: PermissionConfig,
227 sandboxed: bool,
228 memory: Option<BuiltinMemory>,
229 registry: Option<Arc<ServiceRegistry>>,
230 event_tx: DaemonEventSender,
231 task_timeout: Duration,
232 ) -> Self {
233 Self {
234 skills,
235 mcp,
236 tasks,
237 downloads,
238 permissions,
239 sandboxed,
240 memory,
241 event_tx,
242 task_timeout,
243 scopes: BTreeMap::new(),
244 agent_descriptions: BTreeMap::new(),
245 registry,
246 }
247 }
248
249 pub(crate) fn register_scope(&mut self, name: CompactString, config: &AgentConfig) {
251 if name != wcore::paths::DEFAULT_AGENT && !config.description.is_empty() {
252 self.agent_descriptions
253 .insert(name.clone(), config.description.clone());
254 }
255 self.scopes.insert(
256 name,
257 AgentScope {
258 tools: config.tools.clone(),
259 members: config.members.clone(),
260 skills: config.skills.clone(),
261 mcps: config.mcps.clone(),
262 },
263 );
264 }
265
266 fn apply_scope(&self, config: &mut AgentConfig) {
269 let has_scoping =
270 !config.skills.is_empty() || !config.mcps.is_empty() || !config.members.is_empty();
271 if !has_scoping {
272 return;
273 }
274
275 let mut whitelist: Vec<CompactString> =
277 BASE_TOOLS.iter().map(|&s| CompactString::from(s)).collect();
278 if self.memory.is_some() {
279 for &t in MEMORY_TOOLS {
280 whitelist.push(CompactString::from(t));
281 }
282 }
283 if let Some(ref registry) = self.registry {
284 for tool_name in registry.tools.keys() {
285 whitelist.push(CompactString::from(tool_name.as_str()));
286 }
287 }
288 let mut scope_lines = Vec::new();
289
290 if !config.skills.is_empty() {
291 for &t in SKILL_TOOLS {
292 whitelist.push(CompactString::from(t));
293 }
294 scope_lines.push(format!("skills: {}", config.skills.join(", ")));
295 }
296
297 if !config.mcps.is_empty() {
298 for &t in MCP_TOOLS {
299 whitelist.push(CompactString::from(t));
300 }
301 let mcp_servers = tokio::task::block_in_place(|| {
302 tokio::runtime::Handle::current().block_on(self.mcp.list())
303 });
304 let mut mcp_info = Vec::new();
305 for (server_name, tool_names) in &mcp_servers {
306 if config.mcps.iter().any(|m| m == server_name.as_str()) {
307 for tn in tool_names {
308 whitelist.push(tn.clone());
309 }
310 mcp_info.push(format!(
311 " - {}: {}",
312 server_name,
313 tool_names
314 .iter()
315 .map(|t| t.as_str())
316 .collect::<Vec<_>>()
317 .join(", ")
318 ));
319 }
320 }
321 if !mcp_info.is_empty() {
322 scope_lines.push(format!("mcp servers:\n{}", mcp_info.join("\n")));
323 }
324 }
325
326 if !config.members.is_empty() {
327 for &t in TASK_TOOLS {
328 whitelist.push(CompactString::from(t));
329 }
330 scope_lines.push(format!("members: {}", config.members.join(", ")));
331 }
332
333 if !scope_lines.is_empty() {
334 let scope_block = format!("\n\n<scope>\n{}\n</scope>", scope_lines.join("\n"));
335 config.system_prompt.push_str(&scope_block);
336 }
337
338 config.tools = whitelist;
339 }
340
341 async fn check_perm(
344 &self,
345 name: &str,
346 args: &str,
347 agent: &str,
348 task_id: Option<u64>,
349 ) -> Option<String> {
350 if self.sandboxed && BASE_TOOLS.contains(&name) {
352 return None;
353 }
354 use crate::hook::os::ToolPermission;
355 match self.permissions.resolve(agent, name) {
356 ToolPermission::Deny => Some(format!("permission denied: {name}")),
357 ToolPermission::Ask => {
358 if let Some(tid) = task_id {
359 let summary = if args.len() > 200 {
360 format!("{}…", &args[..200])
361 } else {
362 args.to_string()
363 };
364 let question = format!("{name}: {summary}");
365 let rx = self.tasks.lock().await.block(tid, question);
366 if let Some(rx) = rx {
367 match rx.await {
368 Ok(resp) if resp == "denied" => {
369 return Some(format!("permission denied: {name}"));
370 }
371 Err(_) => {
372 return Some(format!("permission denied: {name} (inbox dropped)"));
373 }
374 _ => {} }
376 }
377 }
378 None
380 }
381 ToolPermission::Allow => None,
382 }
383 }
384
385 async fn dispatch_external(
388 &self,
389 name: &str,
390 args: &str,
391 agent: &str,
392 task_id: Option<u64>,
393 ) -> Option<String> {
394 self.registry
395 .as_ref()?
396 .dispatch_tool(name, args, agent, task_id)
397 .await
398 }
399
400 pub async fn dispatch_tool(
406 &self,
407 name: &str,
408 args: &str,
409 agent: &str,
410 task_id: Option<u64>,
411 ) -> String {
412 if let Some(denied) = self.check_perm(name, args, agent, task_id).await {
413 return denied;
414 }
415 if let Some(scope) = self.scopes.get(agent)
417 && !scope.tools.is_empty()
418 && !scope.tools.iter().any(|t| t.as_str() == name)
419 {
420 return format!("tool not available: {name}");
421 }
422 match name {
423 "search_mcp" => self.dispatch_search_mcp(args, agent).await,
424 "call_mcp_tool" => self.dispatch_call_mcp_tool(args, agent).await,
425 "search_skill" => self.dispatch_search_skill(args, agent).await,
426 "load_skill" => self.dispatch_load_skill(args, agent).await,
427 "save_skill" => self.dispatch_save_skill(args).await,
428 "read" => self.dispatch_read(args).await,
429 "write" => self.dispatch_write(args).await,
430 "edit" => self.dispatch_edit(args).await,
431 "bash" => self.dispatch_bash(args).await,
432 "spawn_task" => self.dispatch_spawn_task(args, agent, task_id).await,
433 "check_tasks" => self.dispatch_check_tasks(args).await,
434 "ask_user" => self.dispatch_ask_user(args, task_id).await,
435 "await_tasks" => self.dispatch_await_tasks(args, task_id).await,
436 "recall" => self.dispatch_recall(args).await,
437 "memory" => self.dispatch_memory(args).await,
438 "user_memory" => self.dispatch_user_memory(args).await,
439 name => {
441 if let Some(result) = self.dispatch_external(name, args, agent, task_id).await {
442 return result;
443 }
444 tracing::debug!(tool = name, "forwarding tool to MCP bridge");
445 let bridge = self.mcp.bridge().await;
446 bridge.call(name, args).await
447 }
448 }
449 }
450}