diff --git a/crates/llm/Cargo.toml b/crates/llm/Cargo.toml new file mode 100644 index 0000000..73de29f --- /dev/null +++ b/crates/llm/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "kon-llm" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs new file mode 100644 index 0000000..cd25547 --- /dev/null +++ b/crates/llm/src/lib.rs @@ -0,0 +1,73 @@ +use std::sync::{Arc, Mutex}; + +struct LlmState { + loaded: bool, +} + +/// Shared handle to the LLM engine. Cheap to clone (Arc). +/// Phase 3 will replace the stub body with a real llama-cpp-2 model. +#[derive(Clone)] +pub struct LlmEngine { + state: Arc>, +} + +impl LlmEngine { + pub fn new() -> Self { + Self { + state: Arc::new(Mutex::new(LlmState { loaded: false })), + } + } + + pub fn is_loaded(&self) -> bool { + self.state.lock().unwrap().loaded + } + + /// Break a task description into 3-7 physical micro-steps. + /// Returns Err if no model is loaded — the caller surfaces this to the UI. + pub fn decompose_task(&self, _task_text: &str) -> Result, String> { + if !self.is_loaded() { + return Err( + "Download an AI model in Settings to break down tasks.".to_string(), + ); + } + // Phase 3: call llama-cpp-2 with GBNF-constrained prompt here. + Err("LLM not yet wired.".to_string()) + } +} + +impl Default for LlmEngine { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decompose_returns_error_when_not_loaded() { + let engine = LlmEngine::new(); + assert!(!engine.is_loaded()); + let result = engine.decompose_task("Write a blog post"); + assert!(result.is_err()); + assert!( + result.unwrap_err().contains("Download an AI model"), + "error message should tell user to download a model" + ); + } + + #[test] + fn default_creates_unloaded_engine() { + let engine = LlmEngine::default(); + assert!(!engine.is_loaded()); + } + + #[test] + fn engine_is_clone_and_shares_state() { + let engine = LlmEngine::new(); + let clone = engine.clone(); + // Both point to the same Arc — neither is loaded + assert!(!clone.is_loaded()); + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 31857a5..3596376 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ kon-ai-formatting = { path = "../crates/ai-formatting" } kon-storage = { path = "../crates/storage" } kon-cloud-providers = { path = "../crates/cloud-providers" } kon-hotkey = { path = "../crates/hotkey" } +kon-llm = { path = "../crates/llm" } # Tauri tauri = { version = "2", features = ["tray-icon"] }