1use 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
46pub 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
54pub struct Runtime {
56 pub store: Store,
58 pub skills: Vec<Skill>,
60 pub projects: Vec<Project>,
62 pub data_dir: String,
64 pub system_prompt: String,
66 pub channel: String,
68 pub project: Option<String>,
70}
71
72impl Runtime {
73 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 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 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 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 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 let toolboxes = match_skill_toolboxes(&self.skills, &request.text);
127 if !toolboxes.is_empty() {
128 context.toolboxes = toolboxes;
129 }
130
131 let response = provider.complete(&context).await?;
133
134 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
144pub 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 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 pub fn data_dir(mut self, path: &str) -> Self {
167 self.data_dir = path.to_string();
168 self
169 }
170
171 pub fn db_path(mut self, path: &str) -> Self {
173 self.db_path = Some(path.to_string());
174 self
175 }
176
177 pub fn system_prompt(mut self, prompt: &str) -> Self {
179 self.system_prompt = prompt.to_string();
180 self
181 }
182
183 pub fn channel(mut self, channel: &str) -> Self {
185 self.channel = channel.to_string();
186 self
187 }
188
189 pub fn project(mut self, project: &str) -> Self {
191 self.project = Some(project.to_string());
192 self
193 }
194
195 pub async fn build(self) -> Result<Runtime, KernexError> {
197 let expanded_dir = kernex_core::shellexpand(&self.data_dir);
198
199 tokio::fs::create_dir_all(&expanded_dir)
201 .await
202 .map_err(|e| KernexError::Config(format!("failed to create data dir: {e}")))?;
203
204 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 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}