Skip to main content

kernex_runtime/
lib.rs

1//! kernex-runtime: The facade crate that composes all Kernex components.
2//!
3//! Provides `Runtime` for configuring and running an AI agent runtime
4//! with sandboxed execution, multi-provider support, persistent memory,
5//! skills, and multi-agent pipeline orchestration.
6//!
7//! # Quick Start
8//!
9//! ```rust,ignore
10//! use kernex_runtime::RuntimeBuilder;
11//! use kernex_core::traits::Provider;
12//! use kernex_core::message::Request;
13//! use kernex_providers::ollama::OllamaProvider;
14//!
15//! #[tokio::main]
16//! async fn main() -> anyhow::Result<()> {
17//!     let runtime = RuntimeBuilder::new()
18//!         .data_dir("~/.my-agent")
19//!         .build()
20//!         .await?;
21//!
22//!     let provider = OllamaProvider::from_config(
23//!         "http://localhost:11434".into(),
24//!         "llama3.2".into(),
25//!         None,
26//!     )?;
27//!
28//!     let request = Request::text("user-1", "Hello!");
29//!     let response = runtime.complete(&provider, &request).await?;
30//!     println!("{}", response.text);
31//!
32//!     Ok(())
33//! }
34//! ```
35
36use kernex_core::config::MemoryConfig;
37use kernex_core::context::ContextNeeds;
38use kernex_core::error::KernexError;
39use kernex_core::message::{Request, Response};
40use kernex_core::traits::Provider;
41use kernex_memory::Store;
42use kernex_skills::{
43    build_skill_prompt, match_skill_toolboxes, match_skill_triggers, Project, Skill,
44};
45
46/// Re-export sub-crates for convenience.
47pub use kernex_core as core;
48pub use kernex_memory as memory;
49pub use kernex_pipelines as pipelines;
50pub use kernex_providers as providers;
51pub use kernex_sandbox as sandbox;
52pub use kernex_skills as skills;
53
54/// A configured Kernex runtime with all subsystems initialized.
55pub struct Runtime {
56    /// Persistent memory store.
57    pub store: Store,
58    /// Loaded skills from the data directory.
59    pub skills: Vec<Skill>,
60    /// Loaded projects from the data directory.
61    pub projects: Vec<Project>,
62    /// Data directory path (expanded).
63    pub data_dir: String,
64    /// Base system prompt prepended to every request.
65    pub system_prompt: String,
66    /// Communication channel identifier (e.g. "cli", "api", "slack").
67    pub channel: String,
68    /// Active project key for scoping memory and lessons.
69    pub project: Option<String>,
70}
71
72impl Runtime {
73    /// Send a request through the full runtime pipeline:
74    /// build context from memory → enrich with skills → complete via provider → save exchange.
75    ///
76    /// This is the high-level convenience method that wires together all
77    /// Kernex subsystems in a single call.
78    pub async fn complete(
79        &self,
80        provider: &dyn Provider,
81        request: &Request,
82    ) -> Result<Response, KernexError> {
83        self.complete_with_needs(provider, request, &ContextNeeds::default())
84            .await
85    }
86
87    /// Like [`complete`](Self::complete), but with explicit control over which
88    /// context blocks are loaded from memory.
89    pub async fn complete_with_needs(
90        &self,
91        provider: &dyn Provider,
92        request: &Request,
93        needs: &ContextNeeds,
94    ) -> Result<Response, KernexError> {
95        let project_ref = self.project.as_deref();
96
97        // Build skill prompt and append to base system prompt.
98        let skill_prompt = build_skill_prompt(&self.skills);
99        let full_system_prompt = if skill_prompt.is_empty() {
100            self.system_prompt.clone()
101        } else if self.system_prompt.is_empty() {
102            skill_prompt
103        } else {
104            format!("{}\n\n{}", self.system_prompt, skill_prompt)
105        };
106
107        // Build context from memory (history, recall, facts, lessons, etc).
108        let mut context = self
109            .store
110            .build_context(
111                &self.channel,
112                request,
113                &full_system_prompt,
114                needs,
115                project_ref,
116            )
117            .await?;
118
119        // Enrich context with triggered MCP servers.
120        let mcp_servers = match_skill_triggers(&self.skills, &request.text);
121        if !mcp_servers.is_empty() {
122            context.mcp_servers = mcp_servers;
123        }
124
125        // Enrich context with triggered toolboxes.
126        let toolboxes = match_skill_toolboxes(&self.skills, &request.text);
127        if !toolboxes.is_empty() {
128            context.toolboxes = toolboxes;
129        }
130
131        // Send to provider.
132        let response = provider.complete(&context).await?;
133
134        // Persist exchange in memory.
135        let project_key = project_ref.unwrap_or("default");
136        self.store
137            .store_exchange(&self.channel, request, &response, project_key)
138            .await?;
139
140        Ok(response)
141    }
142}
143
144/// Builder for constructing a `Runtime` with the desired configuration.
145pub struct RuntimeBuilder {
146    data_dir: String,
147    db_path: Option<String>,
148    system_prompt: String,
149    channel: String,
150    project: Option<String>,
151}
152
153impl RuntimeBuilder {
154    /// Create a new builder with default settings.
155    pub fn new() -> Self {
156        Self {
157            data_dir: "~/.kernex".to_string(),
158            db_path: None,
159            system_prompt: String::new(),
160            channel: "cli".to_string(),
161            project: None,
162        }
163    }
164
165    /// Set the data directory (default: `~/.kernex`).
166    pub fn data_dir(mut self, path: &str) -> Self {
167        self.data_dir = path.to_string();
168        self
169    }
170
171    /// Set a custom database path (default: `{data_dir}/memory.db`).
172    pub fn db_path(mut self, path: &str) -> Self {
173        self.db_path = Some(path.to_string());
174        self
175    }
176
177    /// Set the base system prompt.
178    pub fn system_prompt(mut self, prompt: &str) -> Self {
179        self.system_prompt = prompt.to_string();
180        self
181    }
182
183    /// Set the channel identifier (default: `"cli"`).
184    pub fn channel(mut self, channel: &str) -> Self {
185        self.channel = channel.to_string();
186        self
187    }
188
189    /// Set the active project for scoping memory.
190    pub fn project(mut self, project: &str) -> Self {
191        self.project = Some(project.to_string());
192        self
193    }
194
195    /// Build and initialize the runtime.
196    pub async fn build(self) -> Result<Runtime, KernexError> {
197        let expanded_dir = kernex_core::shellexpand(&self.data_dir);
198
199        // Ensure data directory exists.
200        tokio::fs::create_dir_all(&expanded_dir)
201            .await
202            .map_err(|e| KernexError::Config(format!("failed to create data dir: {e}")))?;
203
204        // Initialize store.
205        let db_path = self
206            .db_path
207            .unwrap_or_else(|| format!("{expanded_dir}/memory.db"));
208        let mem_config = MemoryConfig {
209            db_path: db_path.clone(),
210            ..Default::default()
211        };
212        let store = Store::new(&mem_config).await?;
213
214        // Load skills and projects.
215        let skills = kernex_skills::load_skills(&self.data_dir);
216        let projects = kernex_skills::load_projects(&self.data_dir);
217
218        tracing::info!(
219            "runtime initialized: {} skills, {} projects",
220            skills.len(),
221            projects.len()
222        );
223
224        Ok(Runtime {
225            store,
226            skills,
227            projects,
228            data_dir: expanded_dir,
229            system_prompt: self.system_prompt,
230            channel: self.channel,
231            project: self.project,
232        })
233    }
234}
235
236impl Default for RuntimeBuilder {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[tokio::test]
247    async fn test_runtime_builder_creates_runtime() {
248        let tmp = std::env::temp_dir().join("__kernex_test_runtime__");
249        let _ = std::fs::remove_dir_all(&tmp);
250
251        let runtime = RuntimeBuilder::new()
252            .data_dir(tmp.to_str().unwrap())
253            .build()
254            .await
255            .unwrap();
256
257        assert!(runtime.skills.is_empty());
258        assert!(runtime.projects.is_empty());
259        assert!(runtime.system_prompt.is_empty());
260        assert_eq!(runtime.channel, "cli");
261        assert!(runtime.project.is_none());
262        assert!(std::path::Path::new(&runtime.data_dir).exists());
263
264        let _ = std::fs::remove_dir_all(&tmp);
265    }
266
267    #[tokio::test]
268    async fn test_runtime_builder_custom_db_path() {
269        let tmp = std::env::temp_dir().join("__kernex_test_runtime_db__");
270        let _ = std::fs::remove_dir_all(&tmp);
271        std::fs::create_dir_all(&tmp).unwrap();
272
273        let db = tmp.join("custom.db");
274        let runtime = RuntimeBuilder::new()
275            .data_dir(tmp.to_str().unwrap())
276            .db_path(db.to_str().unwrap())
277            .build()
278            .await
279            .unwrap();
280
281        assert!(db.exists());
282        drop(runtime);
283        let _ = std::fs::remove_dir_all(&tmp);
284    }
285
286    #[tokio::test]
287    async fn test_runtime_builder_with_config() {
288        let tmp = std::env::temp_dir().join("__kernex_test_runtime_cfg__");
289        let _ = std::fs::remove_dir_all(&tmp);
290
291        let runtime = RuntimeBuilder::new()
292            .data_dir(tmp.to_str().unwrap())
293            .system_prompt("You are helpful.")
294            .channel("api")
295            .project("my-project")
296            .build()
297            .await
298            .unwrap();
299
300        assert_eq!(runtime.system_prompt, "You are helpful.");
301        assert_eq!(runtime.channel, "api");
302        assert_eq!(runtime.project, Some("my-project".to_string()));
303
304        let _ = std::fs::remove_dir_all(&tmp);
305    }
306}