use kiromi_ai_memory::{Memory, TenantId};
use crate::cli::GlobalArgs;
use crate::config::Config;
use crate::embedder::resolve as resolve_embedder;
use crate::error::{CliError, ExitCode};
use crate::uri::{parse_metadata, parse_storage};
pub(crate) struct Runtime {
pub(crate) mem: Memory,
pub(crate) cfg: Config,
pub(crate) json: bool,
}
impl Runtime {
pub(crate) async fn open(globals: &GlobalArgs) -> Result<Self, CliError> {
let cfg = Config::load(globals)?;
let storage_uri = cfg
.storage
.as_deref()
.ok_or_else(|| missing_field("storage"))?;
let metadata_uri = cfg
.metadata
.as_deref()
.ok_or_else(|| missing_field("metadata"))?;
let scheme = cfg
.scheme
.as_deref()
.ok_or_else(|| missing_field("scheme"))?;
let tenant_str = cfg
.tenant
.as_deref()
.ok_or_else(|| missing_field("tenant"))?;
let tenant = TenantId::new(tenant_str).map_err(|e| CliError {
kind: ExitCode::Config,
source: anyhow::anyhow!("tenant: {e}"),
})?;
let storage = parse_storage(storage_uri).await?;
let metadata = parse_metadata(metadata_uri).await?;
let embedder = if globals.no_embedder {
None
} else {
resolve_embedder(cfg.embedder.as_ref()).await?
};
let mut builder = Memory::builder()
.storage_boxed(storage)
.metadata_boxed(metadata)
.tenant(tenant);
if let Some(e) = embedder {
builder = builder.embedder_boxed(e);
}
if let Some(actor) = &cfg.actor {
builder = builder.actor(actor.clone());
}
let mem = builder
.partition_scheme(scheme)
.map_err(CliError::from)?
.open()
.await
.map_err(CliError::from)?;
Ok(Runtime {
mem,
cfg,
json: globals.json,
})
}
}
fn missing_field(name: &str) -> CliError {
CliError {
kind: ExitCode::Config,
source: anyhow::anyhow!(
"{name} is required (set via --{name}, KIROMI_AI_{}, or config.toml)",
name.to_uppercase()
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::GlobalArgs;
fn empty_globals() -> GlobalArgs {
GlobalArgs {
config: None,
storage: None,
metadata: None,
tenant: None,
scheme: None,
embedder_family: None,
embedder_config: None,
no_embedder: false,
actor: None,
json: false,
verbose: 0,
}
}
fn assert_open_err(err: CliError, expected_substring: &str) {
assert_eq!(err.kind, ExitCode::Config);
let s = err.source.to_string();
assert!(
s.contains(expected_substring),
"expected {expected_substring:?} in {s:?}"
);
}
#[tokio::test]
async fn missing_storage_returns_config_error() {
let g = empty_globals();
match Runtime::open(&g).await {
Ok(_) => panic!("expected config error"),
Err(e) => assert_open_err(e, "storage"),
}
}
#[tokio::test]
async fn missing_metadata_returns_config_error() {
let mut g = empty_globals();
g.storage = Some("memory:".into());
match Runtime::open(&g).await {
Ok(_) => panic!("expected config error"),
Err(e) => assert_open_err(e, "metadata"),
}
}
#[tokio::test]
async fn missing_scheme_returns_config_error() {
let mut g = empty_globals();
g.storage = Some("memory:".into());
g.metadata = Some("sqlite::memory:".into());
match Runtime::open(&g).await {
Ok(_) => panic!("expected config error"),
Err(e) => assert_open_err(e, "scheme"),
}
}
#[tokio::test]
async fn missing_tenant_returns_config_error() {
let mut g = empty_globals();
g.storage = Some("memory:".into());
g.metadata = Some("sqlite::memory:".into());
g.scheme = Some("user={user}".into());
match Runtime::open(&g).await {
Ok(_) => panic!("expected config error"),
Err(e) => assert_open_err(e, "tenant"),
}
}
}