1use crate::skills::keybinds::KeybindRegistry;
7use crate::skills::registry::CommandRegistry;
8use crate::{latest_session, resolve_session, Result, Runtime, Session};
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12pub struct EngineOpts {
14 pub continue_session: Option<Option<String>>,
15 pub system: Option<String>,
16 pub prompt_manifest: Option<std::path::PathBuf>,
17 pub profile: Option<String>,
18 pub no_extensions: bool,
19}
20
21pub struct BackgroundTasks {
23 watcher_shutdown: Arc<std::sync::atomic::AtomicBool>,
24 watcher_task: tokio::task::JoinHandle<()>,
25 socket_shutdown: Arc<std::sync::atomic::AtomicBool>,
26 socket_task: tokio::task::JoinHandle<()>,
27 #[allow(dead_code)] session_socket_path: String,
29 session_id: String,
30 #[allow(dead_code)]
36 log_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
37}
38
39impl BackgroundTasks {
40 pub fn shutdown(&self) {
42 self.watcher_shutdown
43 .store(true, std::sync::atomic::Ordering::Release);
44 self.socket_shutdown
45 .store(true, std::sync::atomic::Ordering::Release);
46 crate::events::registry::unregister_session(&self.session_id);
47 }
48}
49
50impl Drop for BackgroundTasks {
51 fn drop(&mut self) {
52 self.watcher_shutdown
53 .store(true, std::sync::atomic::Ordering::Relaxed);
54 self.socket_shutdown
55 .store(true, std::sync::atomic::Ordering::Relaxed);
56 self.watcher_task.abort();
57 self.socket_task.abort();
58 }
59}
60
61pub struct EngineBoot {
63 pub runtime: Runtime,
64 pub config: crate::SynapsConfig,
65 pub no_extensions: bool,
68 pub session: Session,
69 pub api_messages: Vec<crate::SharedMessage>,
70 pub total_input_tokens: u64,
71 pub total_output_tokens: u64,
72 pub session_cost: f64,
73 pub abort_context: Option<String>,
74 pub continued: bool,
75 pub continue_info: Option<ContinueInfo>,
76 pub registry: Arc<CommandRegistry>,
77 pub keybind_registry: Arc<std::sync::RwLock<KeybindRegistry>>,
82 pub mcp_server_count: usize,
83 pub system_prompt_path: std::path::PathBuf,
84 pub ext_manager: Arc<RwLock<crate::extensions::manager::ExtensionManager>>,
85 pub background: BackgroundTasks,
87}
88
89pub struct ContinueInfo {
91 pub session_id: String,
92 pub resolved_via: Option<String>, pub query: String,
94}
95
96pub async fn boot(opts: EngineOpts) -> Result<EngineBoot> {
99 if let Some(ref prof) = opts.profile {
100 crate::config::set_profile(Some(prof.clone()));
101 }
102
103 let log_guard = crate::logging::init_logging();
114 let mut runtime = Runtime::new().await?;
115
116 let config = crate::config::load_config();
118 runtime.apply_config(&config);
119
120 let sb = resolve_or_create_session(&mut runtime, &opts.continue_session)?;
123
124 let legacy_prompt = crate::config::resolve_system_prompt(opts.system.as_deref());
126 if let Some(path) = &opts.prompt_manifest {
127 let raw = std::fs::read_to_string(path)
128 .map_err(|_| crate::RuntimeError::Config("prompt manifest is unavailable".into()))?;
129 let manifest = agent_core::prompt::PromptManifest::parse(&raw)
130 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
131 let registry = manifest
132 .registry(path.parent())
133 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
134 let model = crate::orchestration::canonical_foreground_identity(runtime.model())
135 .map_err(|e| crate::RuntimeError::Config(format!("invalid foreground model: {e}")))?;
136 let context = agent_core::prompt::SelectionContext::new(model.clone(), None)
137 .map_err(|e| crate::RuntimeError::Config(e.to_string()))?;
138 let catalog = crate::orchestration::OrchestrationRuntime::trusted_catalog(
139 &model,
140 manifest.delegation_catalog_candidates(),
141 )
142 .map_err(|error| crate::RuntimeError::Config(error.into()))?;
143 let delegation_policy = manifest
144 .delegation_policy(model.clone(), &catalog)
145 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
146 let delegation_policy_digest = delegation_policy.as_ref().map(|policy| policy.digest());
147 if let Some(policy) = delegation_policy {
148 runtime.install_orchestration(Arc::new(
149 crate::orchestration::OrchestrationRuntime::new(policy),
150 ));
151 } else {
152 runtime.install_orchestration(Arc::new(
153 crate::orchestration::OrchestrationRuntime::baseline(model.clone(), 8, 64)
154 .map_err(|error| crate::RuntimeError::Config(error.into()))?,
155 ));
156 }
157 let user = opts
158 .system
159 .as_ref()
160 .map(|_| {
161 agent_core::prompt::resolved_system_prompt_as_user_module(legacy_prompt.clone())
162 })
163 .transpose()
164 .map_err(|e| crate::RuntimeError::Config(e.to_string()))?;
165 let stack =
166 agent_core::prompt::compile_prompt_stack(&manifest, ®istry, &context, user.clone())
167 .map_err(|e| {
168 crate::RuntimeError::Config(format!("invalid prompt manifest: {e}"))
169 })?;
170 runtime
171 .apply_prompt_stack(stack)
172 .map_err(|e| crate::RuntimeError::Config(format!("invalid prompt manifest: {e}")))?;
173 runtime.retain_prompt_reload_source(path.clone(), context, user, delegation_policy_digest);
174 } else {
175 let foreground = crate::orchestration::canonical_foreground_identity(runtime.model())
176 .map_err(|e| crate::RuntimeError::Config(format!("invalid foreground model: {e}")))?;
177 runtime.install_orchestration(Arc::new(
178 crate::orchestration::OrchestrationRuntime::baseline(foreground, 8, 64)
179 .map_err(|error| crate::RuntimeError::Config(error.into()))?,
180 ));
181 runtime.set_system_prompt(legacy_prompt);
182 }
183
184 let tools_shared = runtime.tools_shared();
186 let (registry, keybind_registry) = crate::skills::register(&tools_shared, &config).await;
187
188 let mcp_server_count =
192 crate::mcp::setup_lazy_mcp(&runtime.tools_shared(), config.progressive_tool_disclosure)
193 .await;
194 if config.progressive_tool_disclosure {
195 runtime.install_mcp_runtime(std::sync::Arc::new(crate::mcp::McpRuntimeManager::new(
199 crate::mcp::lease::config_source_from_disk(),
200 crate::mcp::lease::DEFAULT_IDLE_MAX,
201 )));
202 }
203
204 let system_prompt_path = crate::config::resolve_read_path("system.md");
205
206 let watcher_shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false));
211 let watcher_task = {
212 let inbox_dir = crate::config::base_dir().join("inbox");
213 let event_queue = runtime.event_queue().clone();
214 let shutdown = watcher_shutdown.clone();
215 tokio::spawn(async move {
216 crate::events::watch_inbox(inbox_dir, event_queue, shutdown).await;
217 })
218 };
219
220 let abort_tasks = |ws: &Arc<std::sync::atomic::AtomicBool>,
222 wt: &tokio::task::JoinHandle<()>| {
223 ws.store(true, std::sync::atomic::Ordering::Relaxed);
224 wt.abort();
225 };
226
227 let socket_shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false));
229 let session_socket_path = crate::events::registry::socket_path_for_session(&sb.session.id);
230 let socket_task = crate::events::socket::listen_session_socket(
231 session_socket_path.clone(),
232 runtime.event_queue().clone(),
233 socket_shutdown.clone(),
234 );
235 let session_registration = crate::events::registry::SessionRegistration {
236 session_id: sb.session.id.clone(),
237 name: sb.session.name.clone(),
238 socket_path: session_socket_path.clone(),
239 pid: std::process::id(),
240 started_at: chrono::Utc::now(),
241 };
242 if let Err(e) = crate::events::registry::register_session(&session_registration) {
243 abort_tasks(&watcher_shutdown, &watcher_task);
244 socket_shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
245 socket_task.abort();
246 return Err(crate::core::error::RuntimeError::Session(format!(
251 "failed to register session {}: {}",
252 session_registration.session_id, e
253 )));
254 }
255
256 runtime.set_turn_budget(crate::runtime::budget::TurnBudget::from_config(
259 crate::runtime::budget::TurnRole::Foreground,
260 &config.turn_budgets,
261 ));
262
263 let mut ext_mgr = crate::extensions::manager::ExtensionManager::new_with_tools(
265 Arc::clone(runtime.hook_bus()),
266 runtime.tools_shared(),
267 );
268 ext_mgr.set_progressive_deferral(config.progressive_tool_disclosure);
271 if config.progressive_tool_disclosure {
272 runtime.install_extension_runtime(ext_mgr.extension_runtime());
276 }
277 let ext_manager = Arc::new(RwLock::new(ext_mgr));
278 crate::runtime::openai::set_extension_manager_for_routing(Arc::clone(&ext_manager));
279
280 {
282 let mut index_record =
283 crate::core::session_index::SessionIndexRecord::start(&sb.session.id);
284 index_record.model = Some(sb.session.model.clone());
285 index_record.profile = crate::core::config::get_profile();
286 index_record.cwd = std::env::current_dir().ok();
287 if let Err(err) = crate::core::session_index::append_record(&index_record) {
288 tracing::warn!("failed to append session start index record: {}", err);
289 }
290
291 let hook_event =
292 crate::extensions::hooks::events::HookEvent::on_session_start(&sb.session.id);
293 let _ = runtime.hook_bus().emit(&hook_event).await;
294 }
295
296 if mcp_server_count > 0 {
297 tracing::info!(
298 "{} MCP servers available (use connect_mcp_server to activate)",
299 mcp_server_count
300 );
301 }
302
303 let session_id = sb.session.id.clone();
304
305 Ok(EngineBoot {
306 runtime,
307 config,
308 no_extensions: opts.no_extensions,
309 session: sb.session,
310 api_messages: sb.api_messages,
311 total_input_tokens: sb.total_input_tokens,
312 total_output_tokens: sb.total_output_tokens,
313 session_cost: sb.session_cost,
314 abort_context: sb.abort_context,
315 continued: sb.continued,
316 continue_info: sb.continue_info,
317 registry,
318 keybind_registry,
319 mcp_server_count,
320 system_prompt_path,
321 ext_manager,
322 background: BackgroundTasks {
323 watcher_shutdown,
324 watcher_task,
325 socket_shutdown,
326 socket_task,
327 session_socket_path,
328 session_id,
329 log_guard,
330 },
331 })
332}
333
334struct SessionBootResult {
337 session: Session,
338 api_messages: Vec<crate::SharedMessage>,
339 total_input_tokens: u64,
340 total_output_tokens: u64,
341 session_cost: f64,
342 abort_context: Option<String>,
343 continued: bool,
344 continue_info: Option<ContinueInfo>,
345}
346
347fn resolve_or_create_session(
348 runtime: &mut Runtime,
349 continue_session: &Option<Option<String>>,
350) -> Result<SessionBootResult> {
351 match continue_session {
352 Some(ref maybe_id) => {
353 let session = match maybe_id {
354 Some(ref id) => resolve_session(id).map_err(|e| {
355 crate::error::RuntimeError::Tool(format!(
356 "Failed to load session '{}': {}",
357 id, e
358 ))
359 })?,
360 None => latest_session().map_err(|e| {
361 crate::error::RuntimeError::Tool(format!("No sessions to continue: {}", e))
362 })?,
363 };
364 runtime.set_model(session.model.clone());
365 if let Some(level) =
368 agent_core::reasoning::ReasoningLevel::parse(&session.thinking_level)
369 {
370 runtime.set_reasoning_level_explicit(level);
371 } else if let Some(budget) =
372 crate::models::budget_for_thinking_level(&session.thinking_level)
373 {
374 runtime.set_thinking_budget_explicit(budget);
375 }
376 if let Some(ref sp) = session.system_prompt {
377 runtime.set_system_prompt(sp.clone());
378 }
379
380 let continue_info = maybe_id.as_ref().map(|q| {
381 let resolved_via = if *q != session.id {
382 if crate::chain::load_chain(q).is_ok() {
383 Some("chain".to_string())
384 } else if crate::session::find_session_by_name(q).is_ok() {
385 Some("name".to_string())
386 } else {
387 None
388 }
389 } else {
390 None
391 };
392 ContinueInfo {
393 session_id: session.id.clone(),
394 resolved_via,
395 query: q.clone(),
396 }
397 });
398
399 Ok(SessionBootResult {
400 api_messages: session.api_messages.clone(),
401 total_input_tokens: session.total_input_tokens,
402 total_output_tokens: session.total_output_tokens,
403 session_cost: session.session_cost,
404 abort_context: session.abort_context.clone(),
405 continued: true,
406 continue_info,
407 session,
408 })
409 }
410 None => {
411 let session = Session::new(
412 runtime.model(),
413 runtime.thinking_level(),
414 runtime.system_prompt(),
415 );
416 Ok(SessionBootResult {
417 session,
418 api_messages: Vec::new(),
419 total_input_tokens: 0,
420 total_output_tokens: 0,
421 session_cost: 0.0,
422 abort_context: None,
423 continued: false,
424 continue_info: None,
425 })
426 }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use agent_core::reasoning::ReasoningLevel;
434
435 #[test]
438 fn continue_path_restores_thinking_level_from_session() {
439 let mut runtime = Runtime::new_headless();
440
441 let thinking_level_str = "ultra";
443 if let Some(level) = ReasoningLevel::parse(thinking_level_str) {
444 runtime.set_reasoning_level_explicit(level);
445 }
446
447 assert_eq!(
448 runtime.reasoning_level(),
449 ReasoningLevel::Ultra,
450 "thinking level must be restored from session on --continue"
451 );
452 assert!(
453 runtime.is_reasoning_explicit(),
454 "restored thinking level must be marked explicit so set_model won't overwrite it"
455 );
456 }
457
458 #[test]
459 fn continue_path_restores_max_level() {
460 let mut runtime = Runtime::new_headless();
461 let thinking_level_str = "max";
462 if let Some(level) = ReasoningLevel::parse(thinking_level_str) {
463 runtime.set_reasoning_level_explicit(level);
464 }
465 assert_eq!(runtime.reasoning_level(), ReasoningLevel::Max);
466 }
467
468 #[test]
469 fn continue_path_restores_off_level() {
470 let mut runtime = Runtime::new_headless();
471 let thinking_level_str = "off";
472 if let Some(level) = ReasoningLevel::parse(thinking_level_str) {
473 runtime.set_reasoning_level_explicit(level);
474 }
475 assert_eq!(runtime.reasoning_level(), ReasoningLevel::Off);
476 }
477}