crabtalk_daemon/daemon/
builder.rs1use crate::{
9 Daemon, DaemonConfig,
10 config::{ResolvedManifest, resolve_manifests},
11 daemon::event::{DaemonEvent, DaemonEventSender},
12 hook::{self, DaemonHook, skill::loader, system::memory::Memory},
13};
14use anyhow::Result;
15use model::ProviderRegistry;
16use std::{
17 collections::BTreeMap,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21use tokio::sync::RwLock;
22use wcore::{AgentConfig, Runtime, ToolRequest};
23
24fn resolve_package_skills(
30 skills: &mut Vec<String>,
31 package_skill_dirs: &BTreeMap<String, PathBuf>,
32) {
33 let mut resolved = Vec::new();
34 for entry in skills.drain(..) {
35 if entry.contains('/') {
36 if let Some(dir) = package_skill_dirs.get(&entry) {
37 match loader::load_skills_dir(dir) {
38 Ok(registry) => {
39 for skill in registry.skills() {
40 resolved.push(skill.name.clone());
41 }
42 }
43 Err(e) => {
44 tracing::warn!("failed to resolve package skills for '{entry}': {e}");
45 }
46 }
47 } else {
48 tracing::warn!("unknown package skill reference: '{entry}'");
49 }
50 } else {
51 resolved.push(entry);
52 }
53 }
54 *skills = resolved;
55}
56
57const SYSTEM_AGENT: &str = include_str!("../../prompts/crab.md");
58
59impl Daemon {
60 pub(crate) async fn build(
63 config: &DaemonConfig,
64 config_dir: &Path,
65 event_tx: DaemonEventSender,
66 ) -> Result<Self> {
67 let runtime = Self::build_runtime(config, config_dir, &event_tx).await?;
68 Ok(Self {
69 runtime: Arc::new(RwLock::new(Arc::new(runtime))),
70 config_dir: config_dir.to_path_buf(),
71 event_tx,
72 })
73 }
74
75 pub async fn reload(&self) -> Result<()> {
80 let config = DaemonConfig::load(&self.config_dir.join(wcore::paths::CONFIG_FILE))?;
81 let new_runtime = Self::build_runtime(&config, &self.config_dir, &self.event_tx).await?;
82 *self.runtime.write().await = Arc::new(new_runtime);
83 tracing::info!("daemon reloaded");
84 Ok(())
85 }
86
87 async fn build_runtime(
89 config: &DaemonConfig,
90 config_dir: &Path,
91 event_tx: &DaemonEventSender,
92 ) -> Result<Runtime<ProviderRegistry, DaemonHook>> {
93 let manager = Self::build_providers(config)?;
94 let (manifest, _warnings) = resolve_manifests(config_dir);
95 let hook = Self::build_hook(config, config_dir, &manifest, event_tx).await?;
96 let tool_tx = Self::build_tool_sender(event_tx);
97 let mut runtime = Runtime::new(manager, hook, Some(tool_tx)).await;
98 Self::load_agents(&mut runtime, config, &manifest)?;
99 Ok(runtime)
100 }
101
102 fn build_providers(config: &DaemonConfig) -> Result<ProviderRegistry> {
106 let active_model = config
107 .system
108 .crab
109 .model
110 .clone()
111 .ok_or_else(|| anyhow::anyhow!("system.crab.model is required in config.toml"))?;
112 let registry = ProviderRegistry::from_providers(active_model.clone(), &config.provider)?;
113
114 tracing::info!(
115 "provider registry initialized — active model: {}",
116 registry.active_model_name().unwrap_or_default()
117 );
118 Ok(registry)
119 }
120
121 async fn build_hook(
123 config: &DaemonConfig,
124 config_dir: &Path,
125 manifest: &ResolvedManifest,
126 event_tx: &DaemonEventSender,
127 ) -> Result<DaemonHook> {
128 let skills =
129 hook::skill::SkillHandler::load(manifest.skill_dirs.clone()).unwrap_or_else(|e| {
130 tracing::warn!("failed to load skills: {e}");
131 hook::skill::SkillHandler::default()
132 });
133
134 let mcp_servers: Vec<_> = manifest
136 .mcps
137 .values()
138 .map(|mcp| {
139 let mut mcp = mcp.clone();
140 for (k, v) in &config.env {
141 mcp.env.entry(k.clone()).or_insert_with(|| v.clone());
142 }
143 mcp
144 })
145 .collect();
146 let mcp_handler = hook::mcp::McpHandler::load(&mcp_servers).await;
147
148 let memory = Some(Memory::open(
149 config_dir.join("memory"),
150 config.system.memory.clone(),
151 Box::new(crate::hook::system::memory::storage::FsStorage),
152 ));
153
154 let cwd = std::env::current_dir().unwrap_or_else(|_| config_dir.to_path_buf());
155
156 Ok(DaemonHook::new(
157 skills,
158 mcp_handler,
159 cwd,
160 memory,
161 event_tx.clone(),
162 ))
163 }
164
165 fn build_tool_sender(event_tx: &DaemonEventSender) -> wcore::ToolSender {
171 let (tool_tx, mut tool_rx) = tokio::sync::mpsc::unbounded_channel::<ToolRequest>();
172 let event_tx = event_tx.clone();
173 tokio::spawn(async move {
174 while let Some(req) = tool_rx.recv().await {
175 if event_tx.send(DaemonEvent::ToolCall(req)).is_err() {
176 break;
177 }
178 }
179 });
180 tool_tx
181 }
182
183 fn load_agents(
189 runtime: &mut Runtime<ProviderRegistry, DaemonHook>,
190 config: &DaemonConfig,
191 manifest: &ResolvedManifest,
192 ) -> Result<()> {
193 let prompts = crate::config::load_agents_dirs(&manifest.agent_dirs)?;
195 let prompt_map: std::collections::BTreeMap<String, String> = prompts.into_iter().collect();
196
197 let mut crab_config = config.system.crab.clone();
199 crab_config.name = wcore::paths::DEFAULT_AGENT.to_owned();
200 crab_config.system_prompt = runtime
201 .hook
202 .memory
203 .as_ref()
204 .map(|m| m.build_soul())
205 .unwrap_or_else(|| SYSTEM_AGENT.to_owned());
206 runtime.add_agent(crab_config);
207
208 for (name, agent_config) in &manifest.agents {
210 if name == wcore::paths::DEFAULT_AGENT {
211 tracing::warn!(
212 "agents.{name} overrides the built-in system agent and will be ignored — \
213 configure it under [system.crab] instead"
214 );
215 continue;
216 }
217 let Some(prompt) = prompt_map.get(name) else {
218 tracing::warn!("agent '{name}' in manifest has no matching .md file, skipping");
219 continue;
220 };
221 let mut agent = agent_config.clone();
222 agent.name = name.clone();
223 agent.system_prompt = prompt.clone();
224 resolve_package_skills(&mut agent.skills, &manifest.package_skill_dirs);
225 tracing::info!("registered agent '{name}' (thinking={})", agent.thinking);
226 runtime.add_agent(agent);
227 }
228
229 let default_think = config.system.crab.thinking;
231 for (stem, prompt) in &prompt_map {
232 if stem == wcore::paths::DEFAULT_AGENT {
233 tracing::warn!(
234 "agents/{stem}.md shadows the built-in system agent and will be ignored"
235 );
236 continue;
237 }
238 if manifest.agents.contains_key(stem) {
239 continue;
240 }
241 let mut agent = AgentConfig::new(stem.as_str());
242 agent.system_prompt = prompt.clone();
243 agent.thinking = default_think;
244 tracing::info!("registered agent '{stem}' (defaults, thinking={default_think})");
245 runtime.add_agent(agent);
246 }
247
248 for agent_config in runtime.agents() {
250 runtime
251 .hook
252 .register_scope(agent_config.name.clone(), &agent_config);
253 }
254
255 Ok(())
256 }
257}