1#![allow(
4 missing_docs,
5 dead_code,
6 clippy::field_reassign_with_default,
7 clippy::unwrap_used,
8 clippy::let_and_return,
9 clippy::borrow_interior_mutable_const,
10 clippy::derivable_impls,
11 clippy::new_without_default,
12 unknown_lints
13)]
14pub mod bootstrap;
21pub mod cli;
22pub mod foundation;
23pub mod home_migrate;
24pub mod internal_urls;
25pub mod lsp;
26pub mod main_dispatch;
27pub mod mcp_credentials;
28pub mod oauth_listener;
29pub mod oauth_refresh;
30pub mod print_mode;
31pub mod provider_oauth;
32pub mod services;
33pub mod setup_wizard;
34pub mod store;
35pub(crate) mod app;
37pub(crate) mod context;
38pub mod discovery;
39pub mod extensions; pub(crate) mod infra;
41pub(crate) mod media;
42pub(crate) mod prompt;
43pub mod rpc_mode;
44pub(crate) mod skills;
45pub mod storage; pub use storage::packages::PackageManager;
48pub use storage::packages::ResourceKind;
49pub mod tools;
50pub(crate) mod ui;
51pub(crate) mod util;
52
53pub async fn build_oxicode_engine(
68 embedding_provider: Option<std::sync::Arc<dyn oxicode_sdk::ports::EmbeddingProvider>>,
69 hook_runner: Option<std::sync::Arc<dyn oxicode_sdk::ports::HookRunner>>,
70) -> anyhow::Result<oxicode_sdk::Oxicode> {
71 let paths = services::OxicodePaths::default_paths()?;
72 services::build_oxicode(&paths, embedding_provider, hook_runner).await
73}
74
75pub async fn run_port_check() -> anyhow::Result<()> {
82 let oxicode = build_oxicode_engine(None, None).await?;
83 let ports = oxicode.ports();
84
85 let entries = ports.state.list("").await?;
86 println!("[state] entries: {}", entries.len());
87
88 let providers = ports.auth.list_providers().await?;
90 println!("[auth] providers with credentials: {:?}", providers);
91
92 let keys = ports.config.list()?;
94 println!("[config] keys: {}", keys.len());
95
96 let skills = ports.skills.list().await?;
98 println!("[skills] {} skill(s) discovered", skills.len());
99 for s in &skills {
100 println!(" - {}: {}", s.name, s.description);
101 }
102
103 let _ = ports
105 .event_bus
106 .publish(&"port-check".to_string(), serde_json::json!({"ok": true}))
107 .await;
108 println!("[event-bus] publish ok (noop bus if not registered)");
109
110 println!("\nport check: ok");
111 Ok(())
112}
113
114#[derive(Debug, Clone)]
116pub struct CompactionContext {
117 pub messages_count: usize,
119 pub tokens_before: usize,
121 pub target_tokens: usize,
123 pub strategy: String,
125}
126
127impl CompactionContext {
128 pub fn new(
130 messages_count: usize,
131 tokens_before: usize,
132 target_tokens: usize,
133 strategy: impl Into<String>,
134 ) -> Self {
135 Self {
136 messages_count,
137 tokens_before,
138 target_tokens,
139 strategy: strategy.into(),
140 }
141 }
142
143 pub fn compression_ratio(&self) -> f32 {
145 if self.tokens_before == 0 {
146 return 1.0;
147 }
148 self.target_tokens as f32 / self.tokens_before as f32
149 }
150}
151
152use crate::store::settings::Settings;
154use anyhow::{Error, Result};
155use oxicode_agent::{Agent, AgentConfig, AgentEvent};
156use parking_lot::RwLock;
157use skills::SkillManager;
158use std::collections::VecDeque;
159use std::sync::Arc;
160
161#[derive(Clone)]
172pub struct SessionState {
173 pub should_stop: Arc<std::sync::atomic::AtomicBool>,
177 pub steering: Arc<RwLock<VecDeque<oxicode_sdk::Message>>>,
179 pub follow_up: Arc<RwLock<VecDeque<oxicode_sdk::Message>>>,
181}
182
183impl Default for SessionState {
184 fn default() -> Self {
185 Self {
186 should_stop: Arc::new(std::sync::atomic::AtomicBool::new(false)),
187 steering: Arc::new(RwLock::new(VecDeque::new())),
188 follow_up: Arc::new(RwLock::new(VecDeque::new())),
189 }
190 }
191}
192
193pub struct App {
200 oxicode: oxicode_sdk::Oxicode,
201 agent: Arc<Agent>,
202 settings: Settings,
203 skills: RwLock<SkillManager>,
204 active_skills: RwLock<Vec<String>>,
205 wasm_ext: Option<std::sync::Arc<crate::extensions::WasmExtensionManager>>,
206 ask_bridge: Option<std::sync::Arc<oxicode_agent::tools::ask::AskBridge>>,
207 issue_store: Option<oxicode_sdk::FileIssueStore>,
211 ownership_session_id: String,
216 #[allow(dead_code)]
221 liveness_guard: Option<oxicode_sdk::liveness::AliveGuard>,
222 persona_body: RwLock<Option<String>>,
225 session_state: SessionState,
229}
230fn build_system_prompt(
232 thinking_level: crate::store::settings::ThinkingLevel,
233 skill_contents: &[String],
234 persona_body: Option<&str>,
235) -> String {
236 let skills: Vec<prompt::system_prompt::Skill> = skill_contents
237 .iter()
238 .enumerate()
239 .map(|(i, content)| prompt::system_prompt::Skill {
240 name: format!("skill-{}", i),
241 content: content.clone(),
242 })
243 .collect();
244
245 let options = prompt::system_prompt::BuildSystemPromptOptions {
246 custom_prompt: prompt::system_prompt::thinking_level_prompt(thinking_level),
247 skills,
248 cwd: std::env::current_dir()
249 .map(|p| p.to_string_lossy().to_string())
250 .unwrap_or_default(),
251 persona_prompt: persona_body.map(|s| s.to_string()),
252 ..Default::default()
253 };
254
255 prompt::system_prompt::build_system_prompt(&options)
256}
257
258impl App {
261 pub async fn from_oxicode(
281 oxicode: oxicode_sdk::Oxicode,
282 settings: Settings,
283 ownership_session_id: String,
284 session_state: Option<SessionState>,
285 ) -> Result<Self> {
286 let session_state = session_state.unwrap_or_default();
287 let persona = match oxicode.ports().personas.get("default").await {
292 Ok(Some(p)) if !p.system_prompt.trim().is_empty() => Some(p),
293 Ok(_) => None,
294 Err(e) => {
295 tracing::warn!(error = %e, "default persona lookup failed");
296 None
297 }
298 };
299
300 let model_id = persona
301 .as_ref()
302 .and_then(|p| p.preferred_model.clone())
303 .or_else(|| settings.effective_model(None))
304 .unwrap_or_default();
305 let skills_dir = SkillManager::skills_read_dir().unwrap_or_else(|_| {
309 oxicode_catalog::product_env::home_dir()
310 .unwrap_or_default()
311 .join("skills")
312 });
313 let skills = SkillManager::load_from_dir(&skills_dir).unwrap_or_else(|e| {
314 tracing::debug!("Skills not loaded: {}", e);
315 SkillManager::new()
316 });
317
318 let body_str = persona.as_ref().map(|p| p.system_prompt.clone());
319 let system_prompt = build_system_prompt(settings.thinking_level, &[], body_str.as_deref());
320 let compaction_strategy = if settings.auto_compaction {
321 oxicode_sdk::CompactionStrategy::Threshold(0.8)
322 } else {
323 oxicode_sdk::CompactionStrategy::Disabled
324 };
325
326 let config = AgentConfig {
327 name: "oxicode".to_string(),
328 description: Some("oxicode CLI agent".to_string()),
329 model_id: model_id.clone(),
330 system_prompt: Some(system_prompt),
331 timeout_seconds: settings.tool_timeout_seconds,
332 temperature: settings.effective_temperature(),
333 max_tokens: settings.effective_max_tokens(),
334 compaction_strategy,
335 compaction_instruction: None,
336 context_window: 128_000,
337 workspace_dir: Some(
338 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
339 ),
340 output_mode: None,
341 provider_options: None,
342 session_id: Some(ownership_session_id.clone()),
343 ttsr_engine: None,
344 memory: None,
345 todo: None,
346 agent_pool: None,
347 url_resolver: Some(Arc::new(oxicode_sdk::SdkUrlResolver::new(
348 oxicode.ports().url_router.clone(),
349 ))),
350 lsp: if crate::lsp::manager::default_servers().is_empty() {
356 None
357 } else {
358 Some(Arc::new(crate::lsp::CliLspProvider::with_defaults(
359 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
360 )))
361 },
362 ..Default::default()
363 };
364
365 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
374
375 let ask_timeout = if settings.ask_timeout_secs > 0 {
384 Some(std::time::Duration::from_secs(settings.ask_timeout_secs))
385 } else {
386 None
387 };
388 let bridge = std::sync::Arc::new(oxicode_agent::tools::ask::AskBridge::with_timeout(
389 ask_timeout,
390 ));
391 let mode_handle = bridge.mode_handle();
392
393 let stop_flag = Arc::clone(&session_state.should_stop);
394 let steering = Arc::clone(&session_state.steering);
395 let follow_up = Arc::clone(&session_state.follow_up);
396 let session_hooks = oxicode_sdk::agent_builder::SessionHookClosures {
397 should_stop_after_turn: Arc::new(move |_| {
398 stop_flag.load(std::sync::atomic::Ordering::SeqCst)
399 }),
400 get_steering_messages: Arc::new(move || {
401 let mut msgs: Vec<oxicode_sdk::Message> = steering.write().drain(..).collect();
402 if oxicode_agent::config::Mode::load(&mode_handle).is_auto() {
405 msgs.push(oxicode_sdk::Message::User(oxicode_sdk::UserMessage::new(
406 "Autonomy mode (auto) is active: proceed autonomously to \
407 completion without asking the user questions. Make \
408 reasonable decisions on your own and keep working.",
409 )));
410 }
411 msgs
412 }),
413 get_follow_up_messages: Arc::new(move || follow_up.write().drain(..).collect()),
414 tool_execution: oxicode_agent::config::ToolExecutionMode::Sequential,
415 };
416
417 let agent = oxicode
418 .agent(config)
419 .workspace(cwd)
420 .with_port_hooks()
421 .with_session_hooks(session_hooks)
422 .build()
423 .map_err(|e| Error::msg(format!("agent build failed: {e}")))?;
424 let agent = Arc::new(agent);
425
426 let ask_tool = oxicode_agent::tools::ask::AskTool::new(bridge.clone());
427 agent.tools().register_arc(std::sync::Arc::new(ask_tool));
428 let issue_store = std::env::current_dir()
433 .ok()
434 .map(|cwd| oxicode_sdk::FileIssueStore::open_from_cwd(&cwd))
435 .and_then(|r| {
436 r.map_err(|e| tracing::warn!("issue store unavailable: {e}"))
437 .ok()
438 });
439
440 if let Some(store) = issue_store.clone() {
442 let tool = std::sync::Arc::new(oxicode_sdk::IssueTool::new(store));
443 agent.tools().register_arc(tool);
444 }
445
446 Ok(Self {
447 oxicode,
448 agent,
449 settings,
450 skills: RwLock::new(skills),
451 active_skills: RwLock::new(Vec::new()),
452 wasm_ext: None,
453 ask_bridge: Some(bridge),
454 issue_store,
455 ownership_session_id,
456 liveness_guard: None, persona_body: RwLock::new(persona.as_ref().map(|p| p.system_prompt.clone())),
458 session_state,
459 })
460 .map(|mut app| {
461 app.liveness_guard =
465 acquire_ownership_guard(app.issue_store.as_ref(), &app.ownership_session_id);
466 app
467 })
468 }
469
470 pub fn ownership_session_id(&self) -> &str {
473 &self.ownership_session_id
474 }
475
476 pub fn has_liveness_lock(&self) -> bool {
481 self.liveness_guard.is_some()
482 }
483
484 pub fn settings(&self) -> &Settings {
486 &self.settings
487 }
488
489 pub fn set_wasm_ext(
491 &mut self,
492 ext: Option<std::sync::Arc<crate::extensions::WasmExtensionManager>>,
493 ) {
494 self.wasm_ext = ext;
495 }
496
497 pub fn wasm_ext(&self) -> Option<&std::sync::Arc<crate::extensions::WasmExtensionManager>> {
499 self.wasm_ext.as_ref()
500 }
501
502 pub fn issue_store(&self) -> Option<oxicode_sdk::FileIssueStore> {
504 self.issue_store.clone()
505 }
506
507 pub fn oxicode(&self) -> &oxicode_sdk::Oxicode {
510 &self.oxicode
511 }
512
513 pub fn catalog(&self) -> std::sync::Arc<dyn oxicode_sdk::ports::catalog::ModelCatalog> {
516 std::sync::Arc::clone(self.oxicode.catalog())
517 }
518
519 pub fn agent(&self) -> Arc<Agent> {
521 Arc::clone(&self.agent)
522 }
523
524 pub fn agent_tools(&self) -> Arc<oxicode_agent::ToolRegistry> {
526 self.agent.tools()
527 }
528
529 pub fn ask_bridge(&self) -> Option<&std::sync::Arc<oxicode_agent::tools::ask::AskBridge>> {
531 self.ask_bridge.as_ref()
532 }
533
534 pub fn skills(&self) -> parking_lot::RwLockReadGuard<'_, SkillManager> {
536 self.skills.read()
537 }
538
539 pub fn activate_skill(&self, name: &str) -> Result<(), String> {
541 {
542 let skills = self.skills.read();
543 if skills.get(name).is_none() {
544 return Err(format!("Skill '{}' not found", name));
545 }
546 }
547 let name_lower = name.to_lowercase();
548 {
549 let mut active = self.active_skills.write();
550 if !active.contains(&name_lower) {
551 active.push(name_lower);
552 }
553 }
554 self.rebuild_system_prompt();
555 Ok(())
556 }
557
558 pub fn deactivate_skill(&self, name: &str) {
560 let name_lower = name.to_lowercase();
561 {
562 let mut active = self.active_skills.write();
563 active.retain(|n| n != &name_lower);
564 }
565 self.rebuild_system_prompt();
566 }
567
568 pub fn active_skills(&self) -> Vec<String> {
570 self.active_skills.read().clone()
571 }
572
573 fn rebuild_system_prompt(&self) {
575 let active = self.active_skills.read();
576 let skills = self.skills.read();
577 let contents: Vec<String> = active
578 .iter()
579 .filter_map(|name| skills.get(name).map(|s| s.content.clone()))
580 .collect();
581 let persona = self.persona_body.read().clone();
585 let prompt =
586 build_system_prompt(self.settings.thinking_level, &contents, persona.as_deref());
587 self.agent.set_system_prompt(prompt);
588 }
589
590 pub fn agent_state(&self) -> oxicode_agent::AgentState {
592 self.agent.state()
593 }
594
595 pub async fn run_prompt(&self, prompt: String) -> Result<String> {
597 let (response, _events) = self.agent.run(prompt).await?;
598 Ok(response.content)
599 }
600
601 pub async fn run_prompt_with_events<F>(&self, prompt: String, on_event: F) -> Result<String>
603 where
604 F: FnMut(AgentEvent) + Send + 'static,
605 {
606 self.agent.run_streaming(prompt, on_event).await?;
607 let state = self.agent_state();
608 for msg in state.messages.iter().rev() {
609 if let oxicode_sdk::Message::Assistant(a) = msg {
610 return Ok(a.text_content());
611 }
612 }
613 Ok(String::new())
614 }
615
616 pub fn reset(&self) {
618 self.agent.reset();
619 }
620
621 pub async fn switch_model(&self, model_id: &str) -> anyhow::Result<()> {
627 let _ = self.agent.switch_model(model_id);
628 Ok(())
629 }
630
631 pub fn model_id(&self) -> String {
633 self.agent.model_id()
634 }
635
636 pub fn session_state(&self) -> &SessionState {
641 &self.session_state
642 }
643
644 pub fn should_stop_flag(&self) -> Arc<std::sync::atomic::AtomicBool> {
646 Arc::clone(&self.session_state.should_stop)
647 }
648
649 pub fn steering_queue(&self) -> Arc<RwLock<VecDeque<oxicode_sdk::Message>>> {
651 Arc::clone(&self.session_state.steering)
652 }
653
654 pub fn follow_up_queue(&self) -> Arc<RwLock<VecDeque<oxicode_sdk::Message>>> {
656 Arc::clone(&self.session_state.follow_up)
657 }
658}
659
660pub(crate) fn acquire_ownership_guard(
671 issue_store: Option<&oxicode_sdk::FileIssueStore>,
672 ownership_id: &str,
673) -> Option<oxicode_sdk::liveness::AliveGuard> {
674 let store = issue_store?;
675 if ownership_id.is_empty() {
676 return None;
679 }
680 match oxicode_sdk::liveness::acquire(&store.issues_dir(), ownership_id) {
681 Ok(guard) => Some(guard),
682 Err(e) => {
683 tracing::warn!(
688 ownership_id,
689 error = %e,
690 "issue liveness flock acquisition failed; this session's \
691 ownership claims are contestable while the lock is missing"
692 );
693 None
694 }
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
705 use oxicode_sdk::FileIssueStore;
706 use oxicode_sdk::liveness;
707
708 fn tmp_store() -> (tempfile::TempDir, FileIssueStore) {
709 let tmp = tempfile::tempdir().unwrap();
710 let dir = tmp.path().join(".oxicode").join("issues");
711 std::fs::create_dir_all(&dir).unwrap();
712 (tmp, FileIssueStore::open(dir).unwrap())
713 }
714
715 #[test]
716 fn app_holds_single_liveness_lock() {
717 let (_tmp, store) = tmp_store();
721 let dir = store.issues_dir();
722 let id = "proc-test-app";
723
724 let guard = acquire_ownership_guard(Some(&store), id);
725 assert!(
726 guard.is_some(),
727 "App must acquire the liveness lock for its ownership id"
728 );
729 assert!(
730 liveness::is_session_alive(&dir, id),
731 "after acquire, the session must be live"
732 );
733
734 let second = liveness::acquire(&dir, id);
736 assert!(second.is_err(), "second acquire under same id must fail");
737
738 drop(guard);
739 assert!(
740 !liveness::is_session_alive(&dir, id),
741 "dropping App's guard releases the lock"
742 );
743 }
744
745 #[test]
746 fn acquire_returns_none_without_store() {
747 let dir = tempfile::tempdir().unwrap();
749 let id = "proc-x";
750 assert!(acquire_ownership_guard(None, id).is_none());
751 let _ = dir; }
753
754 #[test]
755 fn acquire_rejects_empty_ownership_id() {
756 let (_tmp, store) = tmp_store();
759 assert!(
760 acquire_ownership_guard(Some(&store), "").is_none(),
761 "empty ownership id must never acquire a lock (#13 guard)"
762 );
763 }
764}
765pub mod symbols;
766pub mod tui_vt;