use std::sync::Arc;
use khive_db::StorageBackend;
use khive_gate::{ActorRef, AllowAllGate, GateRef};
use khive_types::Namespace;
use lattice_embed::EmbeddingModel;
use crate::error::RuntimeResult;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BackendId(pub String);
impl BackendId {
pub const MAIN: &'static str = "main";
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn main() -> Self {
Self(Self::MAIN.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for BackendId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
mod private {
#[derive(Clone, Debug)]
pub(crate) struct Sealed;
}
#[derive(Clone, Debug)]
pub struct NamespaceToken {
namespace: Namespace,
actor: ActorRef,
_sealed: private::Sealed,
}
impl NamespaceToken {
pub(crate) fn mint_authorized(namespace: Namespace, actor: ActorRef) -> Self {
Self {
namespace,
actor,
_sealed: private::Sealed,
}
}
#[allow(dead_code)]
pub(crate) fn local() -> Self {
Self::mint_authorized(Namespace::local(), ActorRef::anonymous())
}
#[allow(dead_code)]
pub(crate) fn for_namespace(ns: Namespace) -> Self {
Self::mint_authorized(ns, ActorRef::anonymous())
}
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
pub fn actor(&self) -> &ActorRef {
&self.actor
}
pub fn with_namespace(&self, ns: Namespace) -> Self {
Self::mint_authorized(ns, self.actor.clone())
}
}
#[derive(Clone, Debug)]
pub struct RuntimeConfig {
pub db_path: Option<std::path::PathBuf>,
pub default_namespace: Namespace,
pub embedding_model: Option<EmbeddingModel>,
pub additional_embedding_models: Vec<EmbeddingModel>,
pub gate: GateRef,
pub packs: Vec<String>,
pub backend_id: BackendId,
pub brain_profile: Option<String>,
}
pub fn parse_pack_list(s: &str) -> Vec<String> {
s.split(|c: char| c == ',' || c.is_whitespace())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
impl Default for RuntimeConfig {
fn default() -> Self {
let db_path = std::env::var("HOME")
.ok()
.map(|h| std::path::PathBuf::from(h).join(".khive/khive.db"));
let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
.ok()
.and_then(|s| s.parse().ok())
.or(Some(EmbeddingModel::AllMiniLmL6V2));
let additional_embedding_models = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS")
.ok()
.map(|s| parse_embedding_model_list(&s))
.unwrap_or_else(|| vec![EmbeddingModel::ParaphraseMultilingualMiniLmL12V2]);
let packs = std::env::var("KHIVE_PACKS")
.ok()
.map(|s| parse_pack_list(&s))
.filter(|v| !v.is_empty())
.unwrap_or_else(|| {
vec![
"kg",
"gtd",
"memory",
"brain",
"comm",
"schedule",
"knowledge",
]
.into_iter()
.map(String::from)
.collect()
});
let brain_profile = std::env::var("KHIVE_BRAIN_PROFILE")
.ok()
.filter(|s| !s.trim().is_empty());
Self {
db_path,
default_namespace: Namespace::local(),
embedding_model,
additional_embedding_models,
gate: Arc::new(AllowAllGate),
packs,
backend_id: BackendId::main(),
brain_profile,
}
}
}
pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
sanitize_key(&model.to_string())
}
pub(crate) fn sanitize_key(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
pub(crate) fn build_embedder_registry(
config: &RuntimeConfig,
) -> (crate::embedder_registry::EmbedderRegistry, Arc<str>) {
use crate::embedder_registry::{EmbedderRegistry, LatticeEmbedderProvider};
let mut registry = EmbedderRegistry::new();
for model in configured_embedding_models(config) {
registry.register(LatticeEmbedderProvider::new(model));
}
let default_embedder_name = config
.embedding_model
.map(|model| Arc::<str>::from(model.to_string()))
.unwrap_or_else(|| Arc::<str>::from(""));
(registry, default_embedder_name)
}
fn configured_embedding_models(config: &RuntimeConfig) -> Vec<EmbeddingModel> {
let mut models = Vec::new();
if let Some(model) = config.embedding_model {
models.push(model);
}
models.extend(config.additional_embedding_models.iter().copied());
models.sort_by_key(|model| model.to_string());
models.dedup();
models
}
pub(crate) fn register_configured_embedding_models(
backend: &StorageBackend,
config: &RuntimeConfig,
) -> RuntimeResult<()> {
for model in configured_embedding_models(config) {
backend.register_embedding_model(
&model.to_string(),
model.model_id(),
model.key_version(),
model.dimensions() as u32,
)?;
}
Ok(())
}
pub fn runtime_config_from_khive_config(
khive_cfg: &crate::engine_config::KhiveConfig,
base: RuntimeConfig,
) -> RuntimeConfig {
let default_namespace = match khive_cfg.actor.id.as_deref() {
Some(id) if !id.is_empty() => match Namespace::parse(id) {
Ok(ns) => {
tracing::debug!(actor_id = id, "actor.id from config sets default_namespace");
ns
}
Err(e) => {
panic!(
"actor.id {id:?} passed validation but Namespace::parse failed: {e}; \
this is a bug — KhiveConfig must be validated before calling \
runtime_config_from_khive_config"
);
}
},
_ => base.default_namespace.clone(),
};
let brain_profile = base.brain_profile.clone().or_else(|| {
khive_cfg
.runtime
.brain_profile
.clone()
.filter(|s| !s.trim().is_empty())
});
if khive_cfg.engines.is_empty() {
return RuntimeConfig {
default_namespace,
brain_profile,
..base
};
}
let mut embedding_model: Option<EmbeddingModel> = None;
let mut additional: Vec<EmbeddingModel> = Vec::new();
for engine in &khive_cfg.engines {
match parse_embedding_model_alias(&engine.model) {
Some(model) => {
if engine.default {
embedding_model = Some(model);
} else {
additional.push(model);
}
}
None => {
tracing::warn!(
engine = %engine.name,
model = %engine.model,
"engine config: unknown model name; engine will be skipped"
);
}
}
}
RuntimeConfig {
embedding_model,
additional_embedding_models: additional,
default_namespace,
brain_profile,
..base
}
}
fn parse_embedding_model_list(s: &str) -> Vec<EmbeddingModel> {
parse_pack_list(s)
.into_iter()
.filter_map(|raw| {
let parsed = parse_embedding_model_alias(&raw);
if parsed.is_none() && !raw.trim().is_empty() {
tracing::warn!(
model = %raw,
"KHIVE_ADDITIONAL_EMBEDDING_MODELS contains unknown model name; ignored. \
Valid forms: short alias like 'paraphrase' or a fully-qualified key \
from lattice_embed::EmbeddingModel::from_str."
);
}
parsed
})
.collect()
}
pub(crate) fn parse_embedding_model_alias(name: &str) -> Option<EmbeddingModel> {
let normalized = name.trim().to_ascii_lowercase().replace('_', "-");
match normalized.as_str() {
"paraphrase" => Some(EmbeddingModel::ParaphraseMultilingualMiniLmL12V2),
_ => normalized.parse().ok(),
}
}