1use std::sync::{Arc, RwLock};
4
5use khive_db::StorageBackend;
6use khive_gate::{AllowAllGate, GateRef};
7use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
8use khive_types::EdgeEndpointRule;
9use lattice_embed::{
10 CachedEmbeddingService, EmbeddingModel, EmbeddingService, NativeEmbeddingService,
11};
12use tokio::sync::OnceCell;
13
14use crate::error::RuntimeResult;
15
16#[derive(Clone, Debug)]
18pub struct RuntimeConfig {
19 pub db_path: Option<std::path::PathBuf>,
21 pub default_namespace: String,
23 pub embedding_model: Option<EmbeddingModel>,
26 pub gate: GateRef,
30 pub packs: Vec<String>,
36}
37
38pub fn parse_pack_list(s: &str) -> Vec<String> {
42 s.split(|c: char| c == ',' || c.is_whitespace())
43 .map(str::trim)
44 .filter(|s| !s.is_empty())
45 .map(str::to_owned)
46 .collect()
47}
48
49impl Default for RuntimeConfig {
50 fn default() -> Self {
51 let db_path = std::env::var("HOME")
52 .ok()
53 .map(|h| std::path::PathBuf::from(h).join(".khive/khive-graph.db"));
54 let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
55 .ok()
56 .and_then(|s| s.parse().ok())
57 .or(Some(EmbeddingModel::AllMiniLmL6V2));
58 let packs = std::env::var("KHIVE_PACKS")
59 .ok()
60 .map(|s| parse_pack_list(&s))
61 .filter(|v| !v.is_empty())
62 .unwrap_or_else(|| vec!["kg".to_string()]);
63 Self {
64 db_path,
65 default_namespace: "local".to_string(),
66 embedding_model,
67 gate: Arc::new(AllowAllGate),
68 packs,
69 }
70 }
71}
72
73#[derive(Clone)]
78pub struct KhiveRuntime {
79 backend: Arc<StorageBackend>,
80 config: RuntimeConfig,
81 embedder: Arc<OnceCell<Arc<dyn EmbeddingService>>>,
82 edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
87}
88
89impl KhiveRuntime {
90 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
92 let backend = match &config.db_path {
93 Some(path) => {
94 if let Some(parent) = path.parent() {
95 std::fs::create_dir_all(parent).ok();
96 }
97 StorageBackend::sqlite(path)?
98 }
99 None => StorageBackend::memory()?,
100 };
101 Ok(Self {
102 backend: Arc::new(backend),
103 config,
104 embedder: Arc::new(OnceCell::new()),
105 edge_rules: Arc::new(RwLock::new(Vec::new())),
106 })
107 }
108
109 pub fn memory() -> RuntimeResult<Self> {
111 Self::new(RuntimeConfig {
112 db_path: None,
113 default_namespace: "local".to_string(),
114 embedding_model: None,
115 gate: Arc::new(AllowAllGate),
116 packs: vec!["kg".to_string()],
117 })
118 }
119
120 pub fn config(&self) -> &RuntimeConfig {
122 &self.config
123 }
124
125 pub fn backend(&self) -> &StorageBackend {
127 &self.backend
128 }
129
130 pub fn ns<'a>(&'a self, namespace: Option<&'a str>) -> &'a str {
132 namespace.unwrap_or(&self.config.default_namespace)
133 }
134
135 pub fn entities(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn EntityStore>> {
139 Ok(self.backend.entities_for_namespace(self.ns(namespace))?)
140 }
141
142 pub fn graph(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn GraphStore>> {
144 Ok(self.backend.graph_for_namespace(self.ns(namespace))?)
145 }
146
147 pub fn notes(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn NoteStore>> {
149 Ok(self.backend.notes_for_namespace(self.ns(namespace))?)
150 }
151
152 pub fn events(&self, namespace: Option<&str>) -> RuntimeResult<Arc<dyn EventStore>> {
154 Ok(self.backend.events_for_namespace(self.ns(namespace))?)
155 }
156
157 pub fn sql(&self) -> Arc<dyn SqlAccess> {
159 self.backend.sql()
160 }
161
162 pub fn vectors(
166 &self,
167 namespace: Option<&str>,
168 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
169 let model = self
170 .config
171 .embedding_model
172 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?;
173 Ok(self.backend.vectors_for_namespace(
174 &vec_model_key(model),
175 model.dimensions(),
176 self.ns(namespace),
177 )?)
178 }
179
180 pub fn text(
182 &self,
183 namespace: Option<&str>,
184 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
185 let key = format!("entities_{}", sanitize_key(self.ns(namespace)));
186 Ok(self.backend.text(&key)?)
187 }
188
189 pub fn text_for_notes(
191 &self,
192 namespace: Option<&str>,
193 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
194 let key = format!("notes_{}", sanitize_key(self.ns(namespace)));
195 Ok(self.backend.text(&key)?)
196 }
197
198 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
205 if let Ok(mut guard) = self.edge_rules.write() {
206 *guard = rules;
207 }
208 }
209
210 pub(crate) fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
212 self.edge_rules
213 .read()
214 .map(|g| g.clone())
215 .unwrap_or_default()
216 }
217
218 pub async fn embedder(&self) -> RuntimeResult<Arc<dyn EmbeddingService>> {
226 let model = self
227 .config
228 .embedding_model
229 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?;
230 let service = self
231 .embedder
232 .get_or_init(|| async move {
233 let native = Arc::new(NativeEmbeddingService::with_model(model));
234 let cached = CachedEmbeddingService::with_default_cache(native);
235 Arc::new(cached) as Arc<dyn EmbeddingService>
236 })
237 .await
238 .clone();
239 Ok(service)
240 }
241}
242
243pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
246 sanitize_key(&model.to_string())
247}
248
249fn sanitize_key(s: &str) -> String {
250 s.chars()
251 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
252 .collect()
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn memory_runtime_creates_successfully() {
261 let rt = KhiveRuntime::memory().expect("memory runtime should create");
262 assert!(rt.config().db_path.is_none());
263 }
264
265 #[test]
266 fn file_runtime_creates_successfully() {
267 let dir = tempfile::tempdir().unwrap();
268 let path = dir.path().join("test.db");
269 let config = RuntimeConfig {
270 db_path: Some(path.clone()),
271 default_namespace: "test".to_string(),
272 embedding_model: None,
273 gate: Arc::new(AllowAllGate),
274 packs: vec!["kg".to_string()],
275 };
276 let rt = KhiveRuntime::new(config).expect("file runtime should create");
277 assert!(path.exists());
278 assert_eq!(rt.config().default_namespace, "test");
279 }
280
281 #[test]
282 fn ns_defaults_to_config_namespace() {
283 let rt = KhiveRuntime::memory().unwrap();
284 assert_eq!(rt.ns(None), "local");
285 assert_eq!(rt.ns(Some("custom")), "custom");
286 }
287
288 #[test]
289 fn store_accessors_return_ok() {
290 let rt = KhiveRuntime::memory().unwrap();
291 assert!(rt.entities(None).is_ok());
292 assert!(rt.graph(None).is_ok());
293 assert!(rt.notes(None).is_ok());
294 assert!(rt.events(None).is_ok());
295 }
296
297 #[test]
298 fn vectors_returns_unconfigured_without_model() {
299 let rt = KhiveRuntime::memory().unwrap();
300 match rt.vectors(None) {
301 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
302 Err(other) => panic!("expected Unconfigured, got {:?}", other),
303 Ok(_) => panic!("expected Err, got Ok"),
304 }
305 }
306
307 #[test]
308 fn vec_model_key_sanitizes_dots_and_dashes() {
309 assert_eq!(
310 vec_model_key(EmbeddingModel::BgeSmallEnV15),
311 "bge_small_en_v1_5"
312 );
313 assert_eq!(
314 vec_model_key(EmbeddingModel::BgeBaseEnV15),
315 "bge_base_en_v1_5"
316 );
317 assert_eq!(
318 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
319 "all_minilm_l6_v2"
320 );
321 }
322
323 #[test]
324 fn default_config_uses_allow_all_gate() {
325 let cfg = RuntimeConfig::default();
326 assert_eq!(cfg.default_namespace, "local");
329 let _: GateRef = cfg.gate.clone();
332 }
333
334 #[test]
335 fn parse_pack_list_handles_comma_and_whitespace() {
336 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
337 assert_eq!(
338 parse_pack_list("kg,gtd"),
339 vec!["kg".to_string(), "gtd".to_string()]
340 );
341 assert_eq!(
342 parse_pack_list(" kg , gtd "),
343 vec!["kg".to_string(), "gtd".to_string()]
344 );
345 assert_eq!(
346 parse_pack_list("kg gtd"),
347 vec!["kg".to_string(), "gtd".to_string()]
348 );
349 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
350 assert_eq!(parse_pack_list(""), Vec::<String>::new());
351 }
352
353 #[test]
354 fn default_config_packs_falls_back_to_kg() {
355 let prior = std::env::var("KHIVE_PACKS").ok();
356 unsafe {
358 std::env::remove_var("KHIVE_PACKS");
359 }
360 let cfg = RuntimeConfig::default();
361 assert_eq!(cfg.packs, vec!["kg".to_string()]);
362 if let Some(v) = prior {
363 unsafe {
365 std::env::set_var("KHIVE_PACKS", v);
366 }
367 }
368 }
369
370 #[test]
371 fn default_config_uses_minilm_when_env_unset() {
372 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
374 unsafe {
377 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
378 }
379 let cfg = RuntimeConfig::default();
380 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
381 if let Some(v) = prior {
382 unsafe {
384 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
385 }
386 }
387 }
388}