1#![deny(clippy::unwrap_used, clippy::expect_used)]
3#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
4#[cfg(feature = "sqlite-store")]
39use kernex_core::config::MemoryConfig;
40use kernex_core::context::ContextNeeds;
41use kernex_core::error::KernexError;
42use kernex_core::message::{Request, Response};
43use kernex_core::traits::Provider;
44#[cfg(feature = "sqlite-store")]
45use kernex_memory::Store;
46use kernex_skills::{
47 build_skill_prompt, match_skill_toolboxes, match_skill_triggers, Project, Skill,
48};
49
50pub use kernex_core as core;
52#[cfg(feature = "sqlite-store")]
53pub use kernex_memory as memory;
54pub use kernex_pipelines as pipelines;
55pub use kernex_providers as providers;
56pub use kernex_sandbox as sandbox;
57pub use kernex_skills as skills;
58
59pub struct Runtime {
61 #[cfg(feature = "sqlite-store")]
63 pub store: Store,
64 pub skills: Vec<Skill>,
66 pub projects: Vec<Project>,
68 pub data_dir: String,
70 pub system_prompt: String,
72 pub channel: String,
74 pub project: Option<String>,
76}
77
78impl Runtime {
79 pub async fn complete(
85 &self,
86 provider: &dyn Provider,
87 request: &Request,
88 ) -> Result<Response, KernexError> {
89 self.complete_with_needs(provider, request, &ContextNeeds::default())
90 .await
91 }
92
93 pub async fn complete_with_needs(
96 &self,
97 provider: &dyn Provider,
98 request: &Request,
99 #[allow(unused_variables)] needs: &ContextNeeds,
100 ) -> Result<Response, KernexError> {
101 let project_ref = self.project.as_deref();
102
103 let skill_prompt = build_skill_prompt(&self.skills);
105 let full_system_prompt = if skill_prompt.is_empty() {
106 self.system_prompt.clone()
107 } else if self.system_prompt.is_empty() {
108 skill_prompt
109 } else {
110 format!("{}\n\n{}", self.system_prompt, skill_prompt)
111 };
112
113 #[cfg(feature = "sqlite-store")]
115 let mut context = self
116 .store
117 .build_context(
118 &self.channel,
119 request,
120 &full_system_prompt,
121 needs,
122 project_ref,
123 )
124 .await?;
125
126 #[cfg(not(feature = "sqlite-store"))]
127 let mut context = {
128 let mut ctx = kernex_core::context::Context::new(&request.text);
129 ctx.system_prompt = full_system_prompt;
130 ctx
131 };
132
133 let mcp_servers = match_skill_triggers(&self.skills, &request.text);
135 if !mcp_servers.is_empty() {
136 context.mcp_servers = mcp_servers;
137 }
138
139 let toolboxes = match_skill_toolboxes(&self.skills, &request.text);
141 if !toolboxes.is_empty() {
142 context.toolboxes = toolboxes;
143 }
144
145 let response = provider.complete(&context).await?;
147
148 #[allow(unused_variables)]
150 let project_key = project_ref.unwrap_or("default");
151
152 #[cfg(feature = "sqlite-store")]
153 self.store
154 .store_exchange(&self.channel, request, &response, project_key)
155 .await?;
156
157 Ok(response)
158 }
159}
160
161pub struct RuntimeBuilder {
163 data_dir: String,
164 #[cfg(feature = "sqlite-store")]
165 db_path: Option<String>,
166 system_prompt: String,
167 channel: String,
168 project: Option<String>,
169}
170
171impl RuntimeBuilder {
172 pub fn new() -> Self {
174 Self {
175 data_dir: "~/.kernex".to_string(),
176 #[cfg(feature = "sqlite-store")]
177 db_path: None,
178 system_prompt: String::new(),
179 channel: "cli".to_string(),
180 project: None,
181 }
182 }
183
184 pub fn from_env() -> Self {
193 let mut builder = Self::new();
194
195 if let Ok(dir) = std::env::var("KERNEX_DATA_DIR") {
196 builder = builder.data_dir(&dir);
197 }
198 #[cfg(feature = "sqlite-store")]
199 if let Ok(path) = std::env::var("KERNEX_DB_PATH") {
200 builder = builder.db_path(&path);
201 }
202 if let Ok(prompt) = std::env::var("KERNEX_SYSTEM_PROMPT") {
203 builder = builder.system_prompt(&prompt);
204 }
205 if let Ok(channel) = std::env::var("KERNEX_CHANNEL") {
206 builder = builder.channel(&channel);
207 }
208 if let Ok(project) = std::env::var("KERNEX_PROJECT") {
209 builder = builder.project(&project);
210 }
211
212 builder
213 }
214
215 pub fn data_dir(mut self, path: &str) -> Self {
217 self.data_dir = path.to_string();
218 self
219 }
220
221 #[cfg(feature = "sqlite-store")]
223 pub fn db_path(mut self, path: &str) -> Self {
224 self.db_path = Some(path.to_string());
225 self
226 }
227
228 pub fn system_prompt(mut self, prompt: &str) -> Self {
230 self.system_prompt = prompt.to_string();
231 self
232 }
233
234 pub fn channel(mut self, channel: &str) -> Self {
236 self.channel = channel.to_string();
237 self
238 }
239
240 pub fn project(mut self, project: &str) -> Self {
242 self.project = Some(project.to_string());
243 self
244 }
245
246 pub async fn build(self) -> Result<Runtime, KernexError> {
248 let expanded_dir = kernex_core::shellexpand(&self.data_dir);
249
250 tokio::fs::create_dir_all(&expanded_dir)
252 .await
253 .map_err(|e| KernexError::Config(format!("failed to create data dir: {e}")))?;
254
255 #[cfg(feature = "sqlite-store")]
257 let store = {
258 let db_path = self
259 .db_path
260 .unwrap_or_else(|| format!("{expanded_dir}/memory.db"));
261 let mem_config = MemoryConfig {
262 db_path: db_path.clone(),
263 ..Default::default()
264 };
265 Store::new(&mem_config).await?
266 };
267
268 let skills = kernex_skills::load_skills(&self.data_dir);
270 let projects = kernex_skills::load_projects(&self.data_dir);
271
272 tracing::info!(
273 "runtime initialized: {} skills, {} projects",
274 skills.len(),
275 projects.len()
276 );
277
278 Ok(Runtime {
279 #[cfg(feature = "sqlite-store")]
280 store,
281 skills,
282 projects,
283 data_dir: expanded_dir,
284 system_prompt: self.system_prompt,
285 channel: self.channel,
286 project: self.project,
287 })
288 }
289}
290
291impl Default for RuntimeBuilder {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[tokio::test]
302 async fn test_runtime_builder_creates_runtime() {
303 let tmp = std::env::temp_dir().join("__kernex_test_runtime__");
304 let _ = std::fs::remove_dir_all(&tmp);
305
306 let runtime = RuntimeBuilder::new()
307 .data_dir(tmp.to_str().unwrap())
308 .build()
309 .await
310 .unwrap();
311
312 assert!(runtime.skills.is_empty());
313 assert!(runtime.projects.is_empty());
314 assert!(runtime.system_prompt.is_empty());
315 assert_eq!(runtime.channel, "cli");
316 assert!(runtime.project.is_none());
317 assert!(std::path::Path::new(&runtime.data_dir).exists());
318
319 let _ = std::fs::remove_dir_all(&tmp);
320 }
321
322 #[tokio::test]
323 async fn test_runtime_builder_custom_db_path() {
324 let tmp = std::env::temp_dir().join("__kernex_test_runtime_db__");
325 let _ = std::fs::remove_dir_all(&tmp);
326 std::fs::create_dir_all(&tmp).unwrap();
327
328 let db = tmp.join("custom.db");
329 let runtime = RuntimeBuilder::new()
330 .data_dir(tmp.to_str().unwrap())
331 .db_path(db.to_str().unwrap())
332 .build()
333 .await
334 .unwrap();
335
336 assert!(db.exists());
337 drop(runtime);
338 let _ = std::fs::remove_dir_all(&tmp);
339 }
340
341 #[tokio::test]
342 async fn test_runtime_builder_with_config() {
343 let tmp = std::env::temp_dir().join("__kernex_test_runtime_cfg__");
344 let _ = std::fs::remove_dir_all(&tmp);
345
346 let runtime = RuntimeBuilder::new()
347 .data_dir(tmp.to_str().unwrap())
348 .system_prompt("You are helpful.")
349 .channel("api")
350 .project("my-project")
351 .build()
352 .await
353 .unwrap();
354
355 assert_eq!(runtime.system_prompt, "You are helpful.");
356 assert_eq!(runtime.channel, "api");
357 assert_eq!(runtime.project, Some("my-project".to_string()));
358
359 let _ = std::fs::remove_dir_all(&tmp);
360 }
361}