use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use libloading::Library;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::ffi::{CString, c_char, c_uchar};
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Instant;
use super::{decode_non_null_ffi_c_string, decode_provider_last_error_message};
use crate::host::controller::{
LuaRuntimeSpaceControllerBindingIds, LuaRuntimeSpaceControllerBridge,
};
use crate::host::database::{
LuaRuntimeDatabaseCallbackMode, LuaRuntimeDatabaseProviderMode, RuntimeDatabaseBindingContext,
RuntimeDatabaseKind, RuntimeDatabaseProviderCallbacks, RuntimeLanceDbProviderAction,
RuntimeLanceDbProviderRequest, build_runtime_database_binding_plan,
require_database_provider_callback_registration,
};
use crate::lua_skill::{SkillLanceDbLogLevel, SkillLanceDbMeta};
use crate::runtime::path::render_host_visible_path;
use crate::runtime_logging::{info as log_info, warn as log_warn};
use crate::runtime_options::LuaRuntimeHostOptions;
use vldb_controller_client::ControllerLanceDbEnableRequest;
#[repr(C)]
struct VldbLancedbRuntimeHandle {
_private: [u8; 0],
}
#[repr(C)]
struct VldbLancedbEngineHandle {
_private: [u8; 0],
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct VldbLancedbByteBuffer {
data: *mut c_uchar,
len: usize,
cap: usize,
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
struct VldbLancedbRuntimeOptions {
default_db_path: *const c_char,
db_root: *const c_char,
read_consistency_interval_ms: u64,
has_read_consistency_interval: u8,
max_upsert_payload: usize,
max_search_limit: usize,
max_concurrent_requests: usize,
}
type RuntimeOptionsDefaultFn = unsafe extern "C" fn() -> VldbLancedbRuntimeOptions;
type RuntimeCreateFn =
unsafe extern "C" fn(VldbLancedbRuntimeOptions) -> *mut VldbLancedbRuntimeHandle;
type RuntimeDestroyFn = unsafe extern "C" fn(*mut VldbLancedbRuntimeHandle);
type RuntimeOpenDefaultEngineFn =
unsafe extern "C" fn(*mut VldbLancedbRuntimeHandle) -> *mut VldbLancedbEngineHandle;
type RuntimeDatabasePathForNameFn =
unsafe extern "C" fn(*mut VldbLancedbRuntimeHandle, *const c_char) -> *mut c_char;
type EngineCreateTableJsonFn =
unsafe extern "C" fn(*mut VldbLancedbEngineHandle, *const c_char) -> *mut c_char;
type EngineVectorUpsertFn = unsafe extern "C" fn(
*mut VldbLancedbEngineHandle,
*const c_char,
*const u8,
usize,
) -> *mut c_char;
type EngineVectorSearchFn = unsafe extern "C" fn(
*mut VldbLancedbEngineHandle,
*const c_char,
*mut VldbLancedbByteBuffer,
) -> *mut c_char;
type EngineDeleteJsonFn =
unsafe extern "C" fn(*mut VldbLancedbEngineHandle, *const c_char) -> *mut c_char;
type EngineDropTableJsonFn =
unsafe extern "C" fn(*mut VldbLancedbEngineHandle, *const c_char) -> *mut c_char;
type EngineDestroyFn = unsafe extern "C" fn(*mut VldbLancedbEngineHandle);
type BytesFreeFn = unsafe extern "C" fn(VldbLancedbByteBuffer);
type StringFreeFn = unsafe extern "C" fn(*mut c_char);
type LastErrorMessageFn = unsafe extern "C" fn() -> *const c_char;
type ClearLastErrorFn = unsafe extern "C" fn();
struct LoadedLanceDbApi {
_library: Library,
library_path: PathBuf,
runtime_options_default: RuntimeOptionsDefaultFn,
runtime_create: RuntimeCreateFn,
runtime_destroy: RuntimeDestroyFn,
runtime_open_default_engine: RuntimeOpenDefaultEngineFn,
runtime_database_path_for_name: RuntimeDatabasePathForNameFn,
engine_create_table_json: EngineCreateTableJsonFn,
engine_vector_upsert: EngineVectorUpsertFn,
engine_vector_search: EngineVectorSearchFn,
engine_delete_json: EngineDeleteJsonFn,
engine_drop_table_json: EngineDropTableJsonFn,
engine_destroy: EngineDestroyFn,
bytes_free: BytesFreeFn,
string_free: StringFreeFn,
last_error_message: LastErrorMessageFn,
clear_last_error: ClearLastErrorFn,
}
unsafe impl Send for LoadedLanceDbApi {}
unsafe impl Sync for LoadedLanceDbApi {}
impl LoadedLanceDbApi {
fn load(library_path: &Path) -> Result<Self, String> {
if !library_path.exists() {
return Err(format!(
"LanceDB dynamic library path does not exist: {}",
render_host_visible_path(library_path)
));
}
let library = unsafe { Library::new(library_path) }.map_err(|error| {
format!(
"failed to load {}: {}: {}",
render_host_visible_path(library_path),
error,
error
)
})?;
unsafe { Self::from_library(library_path.to_path_buf(), library) }
}
unsafe fn from_library(library_path: PathBuf, library: Library) -> Result<Self, String> {
macro_rules! load_symbol {
($name:literal, $ty:ty) => {{
unsafe {
*library
.get::<$ty>(concat!($name, "\0").as_bytes())
.map_err(|error| {
format!(
"failed to load symbol {} from {}: {}",
$name,
render_host_visible_path(&library_path),
error
)
})?
}
}};
}
Ok(Self {
runtime_options_default: load_symbol!(
"vldb_lancedb_runtime_options_default",
RuntimeOptionsDefaultFn
),
runtime_create: load_symbol!("vldb_lancedb_runtime_create", RuntimeCreateFn),
runtime_destroy: load_symbol!("vldb_lancedb_runtime_destroy", RuntimeDestroyFn),
runtime_open_default_engine: load_symbol!(
"vldb_lancedb_runtime_open_default_engine",
RuntimeOpenDefaultEngineFn
),
runtime_database_path_for_name: load_symbol!(
"vldb_lancedb_runtime_database_path_for_name",
RuntimeDatabasePathForNameFn
),
engine_create_table_json: load_symbol!(
"vldb_lancedb_engine_create_table_json",
EngineCreateTableJsonFn
),
engine_vector_upsert: load_symbol!(
"vldb_lancedb_engine_vector_upsert",
EngineVectorUpsertFn
),
engine_vector_search: load_symbol!(
"vldb_lancedb_engine_vector_search",
EngineVectorSearchFn
),
engine_delete_json: load_symbol!("vldb_lancedb_engine_delete_json", EngineDeleteJsonFn),
engine_drop_table_json: load_symbol!(
"vldb_lancedb_engine_drop_table_json",
EngineDropTableJsonFn
),
engine_destroy: load_symbol!("vldb_lancedb_engine_destroy", EngineDestroyFn),
bytes_free: load_symbol!("vldb_lancedb_bytes_free", BytesFreeFn),
string_free: load_symbol!("vldb_lancedb_string_free", StringFreeFn),
last_error_message: load_symbol!("vldb_lancedb_last_error_message", LastErrorMessageFn),
clear_last_error: load_symbol!("vldb_lancedb_clear_last_error", ClearLastErrorFn),
_library: library,
library_path,
})
}
fn take_last_error_message(&self) -> String {
unsafe {
let ptr = (self.last_error_message)();
let text =
decode_provider_last_error_message(ptr, "LanceDB", "unknown LanceDB host error");
(self.clear_last_error)();
text
}
}
fn take_owned_string(&self, ptr: *mut c_char) -> Result<String, String> {
if ptr.is_null() {
return Err(self.take_last_error_message());
}
unsafe {
let text = decode_non_null_ffi_c_string(ptr);
(self.string_free)(ptr);
text
}
}
fn take_owned_bytes(&self, buffer: VldbLancedbByteBuffer) -> Vec<u8> {
if buffer.data.is_null() || buffer.len == 0 {
return Vec::new();
}
unsafe {
let bytes = std::slice::from_raw_parts(buffer.data, buffer.len).to_vec();
(self.bytes_free)(buffer);
bytes
}
}
}
struct SkillHandleState {
runtime: *mut VldbLancedbRuntimeHandle,
engine: *mut VldbLancedbEngineHandle,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LanceDbBindingMode {
DynamicLibrary,
HostCallback,
SpaceController,
}
unsafe impl Send for SkillHandleState {}
pub struct LanceDbSkillBinding {
api: Option<Arc<LoadedLanceDbApi>>,
skill_name: String,
skill_dir_name: String,
database_path: String,
config: SkillLanceDbMeta,
provider_mode: LanceDbBindingMode,
callback_mode: LuaRuntimeDatabaseCallbackMode,
handles: Option<Mutex<SkillHandleState>>,
controller: Option<Arc<LuaRuntimeSpaceControllerBridge>>,
provider_callbacks: Arc<RuntimeDatabaseProviderCallbacks>,
provider_binding: RuntimeDatabaseBindingContext,
}
impl LanceDbSkillBinding {
pub fn status_json(&self) -> Result<Value, String> {
if self.provider_mode == LanceDbBindingMode::DynamicLibrary && self.api.is_none() {
return Err(self.missing_dynamic_api_error());
}
Ok(json!({
"enabled": true,
"initialized": true,
"skill_name": self.skill_name,
"skill_dir_name": self.skill_dir_name,
"database_path": self.database_path,
"integration_mode": self.integration_mode_name(),
"library_path": lancedb_library_path_value(self.api.as_deref()),
"space_label": self.provider_binding.space_label,
"root_name": self.provider_binding.root_name,
"binding_tag": self.provider_binding.binding_tag,
"space_root": self.provider_binding.space_root,
"default_database_path": self.provider_binding.default_database_path,
"log_level": self.config.log_level.as_str(),
"slow_log_enabled": self.config.slow_log_enabled,
"slow_log_threshold_ms": self.config.slow_log_threshold_ms,
}))
}
pub fn info_json(&self) -> Result<Value, String> {
self.status_json()
}
fn missing_dynamic_api_error(&self) -> String {
format!(
"LanceDB dynamic-library API is unavailable for {} binding",
self.integration_mode_name()
)
}
pub fn create_table_json(&self, input: &Value) -> Result<Value, String> {
if self.is_space_controller_mode() {
let result =
self.run_controller_binding_operation("create_table", None, |bridge, ids| {
let request_json =
serde_json::to_string(input).map_err(|error| error.to_string())?;
bridge.run(move |client| async move {
client
.create_lancedb_table(ids.space_id, ids.binding_id, request_json)
.await
})
})?;
return Ok(json!({ "message": result.message }));
}
if self.is_host_provider_mode() {
return self
.dispatch_host_provider(RuntimeLanceDbProviderAction::CreateTable, input)
.map(|result| result.meta);
}
self.call_json_string("create_table", input, |api, state, input_ptr| unsafe {
(api.engine_create_table_json)(state.engine, input_ptr)
})
}
pub fn vector_upsert_json(&self, input: &Value, data: &[u8]) -> Result<Value, String> {
if self.is_space_controller_mode() {
let result = self.run_controller_binding_operation(
"vector_upsert",
Some(format!("payload_bytes={}", data.len())),
|bridge, ids| {
let request_json =
serde_json::to_string(input).map_err(|error| error.to_string())?;
let payload = data.to_vec();
bridge.run(move |client| async move {
client
.upsert_lancedb(ids.space_id, ids.binding_id, request_json, payload)
.await
})
},
)?;
return Ok(json!({
"message": result.message,
"version": result.version,
"input_rows": result.input_rows,
"inserted_rows": result.inserted_rows,
"updated_rows": result.updated_rows,
"deleted_rows": result.deleted_rows,
}));
}
if self.is_host_provider_mode() {
let mut host_input = input.clone();
if let Some(object) = host_input.as_object_mut() {
object.insert(
"data_base64".to_string(),
Value::String(BASE64_STANDARD.encode(data)),
);
}
return self
.dispatch_host_provider(RuntimeLanceDbProviderAction::VectorUpsert, &host_input)
.map(|result| result.meta);
}
let api = self.api_ref()?;
let input_text = serde_json::to_string(input).map_err(|error| error.to_string())?;
let input_cstr = CString::new(input_text)
.map_err(|_| "input json contains interior NUL bytes".to_string())?;
self.log_info(
"vector_upsert",
Some(format!("payload_bytes={}", data.len())),
);
let started_at = Instant::now();
let guard = self.lock_handles()?;
unsafe {
let response = (api.engine_vector_upsert)(
guard.engine,
input_cstr.as_ptr(),
data.as_ptr(),
data.len(),
);
let text = match api.take_owned_string(response) {
Ok(text) => text,
Err(error) => {
drop(guard);
self.log_warning("vector_upsert", &error);
return Err(error);
}
};
drop(guard);
let value = serde_json::from_str(&text).map_err(|error| {
format!("failed to parse LanceDB upsert response JSON: {}", error)
})?;
self.log_if_slow(
"vector_upsert",
started_at,
Some(format!("payload_bytes={}", data.len())),
);
Ok(value)
}
}
pub fn vector_search_json(&self, input: &Value) -> Result<(Value, Vec<u8>), String> {
if self.is_space_controller_mode() {
self.log_info("vector_search", None);
let started_at = Instant::now();
let (bridge, controller_ids) = self.controller_call_context()?;
let request_json = serde_json::to_string(input).map_err(|error| error.to_string())?;
let result = bridge.run(move |client| async move {
client
.search_lancedb(
controller_ids.space_id,
controller_ids.binding_id,
request_json,
)
.await
})?;
self.log_if_slow(
"vector_search",
started_at,
Some(format!("result_bytes={}", result.data.len())),
);
return Ok((
json!({
"message": result.message,
"format": result.format,
"rows": result.rows,
}),
result.data,
));
}
if self.is_host_provider_mode() {
return self
.dispatch_host_provider(RuntimeLanceDbProviderAction::VectorSearch, input)
.map(|result| (result.meta, result.bytes));
}
let api = self.api_ref()?;
let input_text = serde_json::to_string(input).map_err(|error| error.to_string())?;
let input_cstr = CString::new(input_text)
.map_err(|_| "input json contains interior NUL bytes".to_string())?;
self.log_info("vector_search", None);
let started_at = Instant::now();
let guard = self.lock_handles()?;
let mut buffer = VldbLancedbByteBuffer {
data: ptr::null_mut(),
len: 0,
cap: 0,
};
unsafe {
let response =
(api.engine_vector_search)(guard.engine, input_cstr.as_ptr(), &mut buffer);
let text = match api.take_owned_string(response) {
Ok(text) => text,
Err(error) => {
let _ = api.take_owned_bytes(buffer);
drop(guard);
self.log_warning("vector_search", &error);
return Err(error);
}
};
let bytes = api.take_owned_bytes(buffer);
drop(guard);
let meta: Value = serde_json::from_str(&text).map_err(|error| {
format!("failed to parse LanceDB search response JSON: {}", error)
})?;
self.log_if_slow(
"vector_search",
started_at,
Some(format!("result_bytes={}", bytes.len())),
);
Ok((meta, bytes))
}
}
pub fn delete_json(&self, input: &Value) -> Result<Value, String> {
if self.is_space_controller_mode() {
let result = self.run_controller_binding_operation("delete", None, |bridge, ids| {
let request_json =
serde_json::to_string(input).map_err(|error| error.to_string())?;
bridge.run(move |client| async move {
client
.delete_lancedb(ids.space_id, ids.binding_id, request_json)
.await
})
})?;
return Ok(json!({
"message": result.message,
"version": result.version,
"deleted_rows": result.deleted_rows,
}));
}
if self.is_host_provider_mode() {
return self
.dispatch_host_provider(RuntimeLanceDbProviderAction::Delete, input)
.map(|result| result.meta);
}
self.call_json_string("delete", input, |api, state, input_ptr| unsafe {
(api.engine_delete_json)(state.engine, input_ptr)
})
}
pub fn drop_table_json(&self, input: &Value) -> Result<Value, String> {
if self.is_space_controller_mode() {
let result =
self.run_controller_binding_operation("drop_table", None, |bridge, ids| {
let table_name = require_string_field(input, "table_name")?.to_string();
bridge.run(move |client| async move {
client
.drop_lancedb_table(ids.space_id, ids.binding_id, table_name)
.await
})
})?;
return Ok(json!({ "message": result.message }));
}
if self.is_host_provider_mode() {
return self
.dispatch_host_provider(RuntimeLanceDbProviderAction::DropTable, input)
.map(|result| result.meta);
}
self.call_json_string("drop_table", input, |api, state, input_ptr| unsafe {
(api.engine_drop_table_json)(state.engine, input_ptr)
})
}
fn call_json_string<F>(
&self,
operation: &str,
input: &Value,
invoke: F,
) -> Result<Value, String>
where
F: Fn(&LoadedLanceDbApi, &SkillHandleState, *const c_char) -> *mut c_char,
{
let input_text = serde_json::to_string(input).map_err(|error| error.to_string())?;
let input_cstr = CString::new(input_text)
.map_err(|_| "input json contains interior NUL bytes".to_string())?;
self.log_info(operation, None);
let started_at = Instant::now();
let api = self.api_ref()?;
let guard = self.lock_handles()?;
let response = invoke(api, &guard, input_cstr.as_ptr());
let text = match api.take_owned_string(response) {
Ok(text) => text,
Err(error) => {
drop(guard);
self.log_warning(operation, &error);
return Err(error);
}
};
drop(guard);
let value = serde_json::from_str(&text)
.map_err(|error| format!("failed to parse LanceDB response JSON: {}", error))?;
self.log_if_slow(operation, started_at, None);
Ok(value)
}
fn run_controller_binding_operation<T, F>(
&self,
operation: &str,
slow_extra: Option<String>,
invoke: F,
) -> Result<T, String>
where
F: FnOnce(
&Arc<LuaRuntimeSpaceControllerBridge>,
LuaRuntimeSpaceControllerBindingIds,
) -> Result<T, String>,
{
self.log_info(operation, slow_extra.clone());
let started_at = Instant::now();
let (bridge, controller_ids) = self.controller_call_context()?;
let result = invoke(bridge, controller_ids)?;
self.log_if_slow(operation, started_at, slow_extra);
Ok(result)
}
fn log_info(&self, operation: &str, extra: Option<String>) {
if self.config.log_level == SkillLanceDbLogLevel::Info {
match extra {
Some(extra) => log_info(format!(
"[LanceDb:info] skill={} db={} op={} {}",
self.skill_name, self.skill_dir_name, operation, extra
)),
None => log_info(format!(
"[LanceDb:info] skill={} db={} op={}",
self.skill_name, self.skill_dir_name, operation
)),
}
}
}
fn log_if_slow(&self, operation: &str, started_at: Instant, extra: Option<String>) {
if !self.config.slow_log_enabled {
return;
}
let elapsed_ms = started_at.elapsed().as_millis() as u64;
if elapsed_ms < self.config.slow_log_threshold_ms {
return;
}
match extra {
Some(extra) => log_info(format!(
"[LanceDb:slow] skill={} db={} op={} elapsed_ms={} {}",
self.skill_name, self.skill_dir_name, operation, elapsed_ms, extra
)),
None => log_info(format!(
"[LanceDb:slow] skill={} db={} op={} elapsed_ms={}",
self.skill_name, self.skill_dir_name, operation, elapsed_ms
)),
}
}
fn log_warning(&self, operation: &str, message: &str) {
if matches!(
self.config.log_level,
SkillLanceDbLogLevel::Info | SkillLanceDbLogLevel::Warning
) {
log_warn(format!(
"[LanceDb:warn] skill={} db={} op={} message={}",
self.skill_name, self.skill_dir_name, operation, message
));
}
}
fn is_host_provider_mode(&self) -> bool {
self.provider_mode == LanceDbBindingMode::HostCallback
}
fn is_space_controller_mode(&self) -> bool {
self.provider_mode == LanceDbBindingMode::SpaceController
}
fn api_ref(&self) -> Result<&LoadedLanceDbApi, String> {
self.api.as_deref().ok_or_else(|| {
format!(
"LanceDB dynamic-library API is unavailable for {} binding",
self.integration_mode_name()
)
})
}
fn integration_mode_name(&self) -> &'static str {
match self.provider_mode {
LanceDbBindingMode::DynamicLibrary => "dynamic_library",
LanceDbBindingMode::HostCallback => "host_callback",
LanceDbBindingMode::SpaceController => "space_controller",
}
}
fn dispatch_host_provider(
&self,
action: RuntimeLanceDbProviderAction,
input: &Value,
) -> Result<crate::host::database::RuntimeLanceDbProviderResult, String> {
let request = RuntimeLanceDbProviderRequest {
action,
binding: self.provider_binding.clone(),
input: input.clone(),
};
self.provider_callbacks
.dispatch_lancedb_provider_request(&request, self.callback_mode)
}
fn lock_handles(&self) -> Result<std::sync::MutexGuard<'_, SkillHandleState>, String> {
self.handles
.as_ref()
.ok_or_else(|| {
"LanceDB dynamic-library handles are unavailable in host provider mode".to_string()
})?
.lock()
.map_err(|_| "failed to acquire LanceDB handle lock".to_string())
}
fn controller_bridge(&self) -> Result<&Arc<LuaRuntimeSpaceControllerBridge>, String> {
self.controller
.as_ref()
.ok_or_else(|| "LanceDB space-controller bridge is unavailable".to_string())
}
fn controller_call_context(
&self,
) -> Result<
(
&Arc<LuaRuntimeSpaceControllerBridge>,
LuaRuntimeSpaceControllerBindingIds,
),
String,
> {
let bridge = self.controller_bridge()?;
let ids = bridge.binding_ids_for_binding(&self.provider_binding);
Ok((bridge, ids))
}
}
impl Drop for LanceDbSkillBinding {
fn drop(&mut self) {
let Some(handles) = self.handles.as_ref() else {
return;
};
let Some(api) = self.api.as_ref() else {
return;
};
if let Ok(mut guard) = handles.lock() {
unsafe {
if !guard.engine.is_null() {
(api.engine_destroy)(guard.engine);
guard.engine = ptr::null_mut();
}
if !guard.runtime.is_null() {
(api.runtime_destroy)(guard.runtime);
guard.runtime = ptr::null_mut();
}
}
}
}
}
pub struct LanceDbSkillHost {
api: Option<Arc<LoadedLanceDbApi>>,
controller: Option<Arc<LuaRuntimeSpaceControllerBridge>>,
skills: Mutex<HashMap<String, Arc<LanceDbSkillBinding>>>,
provider_callbacks: Arc<RuntimeDatabaseProviderCallbacks>,
host_options: LuaRuntimeHostOptions,
}
fn resolve_lancedb_skill_host_api(
host_options: &LuaRuntimeHostOptions,
provider_callbacks: &RuntimeDatabaseProviderCallbacks,
) -> Result<Option<Arc<LoadedLanceDbApi>>, String> {
match host_options.lancedb_provider_mode {
LuaRuntimeDatabaseProviderMode::DynamicLibrary => {
let library_path = host_options.lancedb_library_path.clone().ok_or_else(|| {
"LanceDB dynamic-library mode requires host_options.lancedb_library_path"
.to_string()
})?;
Ok(Some(Arc::new(LoadedLanceDbApi::load(&library_path)?)))
}
LuaRuntimeDatabaseProviderMode::HostCallback => {
require_database_provider_callback_registration(
"LanceDB",
host_options.lancedb_callback_mode,
provider_callbacks
.has_lancedb_provider_callback_for_mode(host_options.lancedb_callback_mode),
)?;
Ok(None)
}
LuaRuntimeDatabaseProviderMode::SpaceController => Ok(None),
}
}
fn resolve_lancedb_skill_host_controller(
host_options: &LuaRuntimeHostOptions,
) -> Result<Option<Arc<LuaRuntimeSpaceControllerBridge>>, String> {
match host_options.lancedb_provider_mode {
LuaRuntimeDatabaseProviderMode::SpaceController => Ok(Some(
LuaRuntimeSpaceControllerBridge::new(host_options, "lancedb")?,
)),
_ => Ok(None),
}
}
impl LanceDbSkillHost {
pub fn new(
host_options: LuaRuntimeHostOptions,
provider_callbacks: Arc<RuntimeDatabaseProviderCallbacks>,
) -> Result<Self, String> {
let api = resolve_lancedb_skill_host_api(&host_options, provider_callbacks.as_ref())?;
let controller = resolve_lancedb_skill_host_controller(&host_options)?;
Ok(Self {
api,
controller,
skills: Mutex::new(HashMap::new()),
provider_callbacks,
host_options,
})
}
pub fn register_skill(
&self,
root_name: &str,
skill_name: &str,
skill_dir: &Path,
config: SkillLanceDbMeta,
) -> Result<Arc<LanceDbSkillBinding>, String> {
let mut guard = self.lock_skills();
if let Some(existing) = guard.get(skill_name) {
return Ok(existing.clone());
}
let binding_plan = build_runtime_database_binding_plan(
root_name,
skill_name,
skill_dir,
self.host_options.database_dir_name.as_str(),
RuntimeDatabaseKind::LanceDb,
)?;
let skill_dir_name = binding_plan.skill_dir_name;
let db_path = binding_plan.provider_storage_dir;
let database_path = binding_plan.default_database_path;
let binding_context = binding_plan.context;
let (resolved_path, handles, provider_mode, controller) = if let Some(api) =
self.api.as_ref()
{
std::fs::create_dir_all(&db_path).map_err(|error| {
format!(
"failed to create LanceDB directory {}: {}",
render_host_visible_path(&db_path),
error
)
})?;
let default_path = CString::new(database_path.clone())
.map_err(|_| "database path contains interior NUL bytes".to_string())?;
let mut options = unsafe { (api.runtime_options_default)() };
options.default_db_path = default_path.as_ptr();
options.db_root = ptr::null();
let runtime = unsafe { (api.runtime_create)(options) };
if runtime.is_null() {
return Err(api.take_last_error_message());
}
let engine = unsafe { (api.runtime_open_default_engine)(runtime) };
if engine.is_null() {
unsafe {
(api.runtime_destroy)(runtime);
}
return Err(api.take_last_error_message());
}
let resolved_path = unsafe {
api.take_owned_string((api.runtime_database_path_for_name)(runtime, ptr::null()))
}
.unwrap_or(database_path.clone());
(
resolved_path,
Some(Mutex::new(SkillHandleState { runtime, engine })),
LanceDbBindingMode::DynamicLibrary,
None,
)
} else if matches!(
self.host_options.lancedb_provider_mode,
LuaRuntimeDatabaseProviderMode::SpaceController
) {
let controller = self
.controller
.as_ref()
.ok_or_else(|| "LanceDB space-controller bridge is unavailable".to_string())?
.clone();
let controller_ids = controller.attach_binding_with_ids(&binding_context)?;
let controller_database_path = database_path.clone();
controller.run(move |client| async move {
client
.enable_lancedb(ControllerLanceDbEnableRequest {
space_id: controller_ids.space_id,
binding_id: controller_ids.binding_id,
default_db_path: controller_database_path,
..ControllerLanceDbEnableRequest::default()
})
.await
})?;
(
database_path.clone(),
None,
LanceDbBindingMode::SpaceController,
Some(controller),
)
} else {
(
database_path.clone(),
None,
LanceDbBindingMode::HostCallback,
None,
)
};
let binding = Arc::new(LanceDbSkillBinding {
api: self.api.clone(),
skill_name: skill_name.to_string(),
skill_dir_name,
database_path: resolved_path,
config,
provider_mode,
callback_mode: self.host_options.lancedb_callback_mode,
handles,
controller,
provider_callbacks: self.provider_callbacks.clone(),
provider_binding: binding_context,
});
guard.insert(skill_name.to_string(), binding.clone());
Ok(binding)
}
pub fn binding_for_skill(
&self,
skill_name: &str,
) -> Result<Option<Arc<LanceDbSkillBinding>>, String> {
let skills = self.lock_skills();
Ok(skills.get(skill_name).cloned())
}
fn lock_skills(&self) -> MutexGuard<'_, HashMap<String, Arc<LanceDbSkillBinding>>> {
self.skills
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
pub fn disabled_skill_status_json(skill_name: Option<&str>) -> Value {
json!({
"enabled": false,
"initialized": false,
"skill_name": skill_name.unwrap_or(""),
"integration_mode": "dynamic_library",
"reason": "current skill has not enabled lancedb"
})
}
fn lancedb_library_path_value(api: Option<&LoadedLanceDbApi>) -> Value {
api.map(|api| json!(render_host_visible_path(&api.library_path)))
.unwrap_or(Value::Null)
}
fn require_string_field<'a>(input: &'a Value, field_name: &str) -> Result<&'a str, String> {
input
.get(field_name)
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| format!("missing or empty field `{}`", field_name))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::database::{
LuaRuntimeDatabaseProviderMode, RuntimeDatabaseBindingContext,
RuntimeDatabaseBindingContextSpec, RuntimeDatabaseKind,
};
use crate::runtime::path::render_host_visible_path;
use std::panic::{self, AssertUnwindSafe};
#[test]
fn lancedb_missing_library_error_uses_host_visible_path() {
let library_path = std::env::temp_dir().join(format!(
"luaskills-missing-lancedb-provider-{}.dll",
std::process::id()
));
let error = match LoadedLanceDbApi::load(&library_path) {
Ok(_) => panic!("missing LanceDB library should fail"),
Err(error) => error,
};
let expected_error = format!(
"LanceDB dynamic library path does not exist: {}",
render_host_visible_path(&library_path)
);
assert_eq!(error, expected_error);
}
fn sample_lancedb_binding_context() -> RuntimeDatabaseBindingContext {
RuntimeDatabaseBindingContext::new(RuntimeDatabaseBindingContextSpec {
space_label: "ROOT".to_string(),
skill_id: "lancedb-api-state-skill".to_string(),
root_name: "ROOT".to_string(),
space_root: "D:/runtime-test-root/databases".to_string(),
skill_dir: "D:/runtime-test-root/skills/lancedb-api-state-skill".to_string(),
skill_dir_name: "lancedb-api-state-skill".to_string(),
database_kind: RuntimeDatabaseKind::LanceDb,
default_database_path: "D:/runtime-test-root/databases/lancedb-api-state-skill"
.to_string(),
})
}
fn dynamic_lancedb_binding_without_api() -> LanceDbSkillBinding {
LanceDbSkillBinding {
api: None,
skill_name: "lancedb-api-state-skill".to_string(),
skill_dir_name: "lancedb-api-state-skill".to_string(),
database_path: "D:/runtime-test-root/databases/lancedb-api-state-skill".to_string(),
config: SkillLanceDbMeta::default(),
provider_mode: LanceDbBindingMode::DynamicLibrary,
callback_mode: LuaRuntimeDatabaseCallbackMode::Standard,
handles: None,
controller: None,
provider_callbacks: Arc::new(RuntimeDatabaseProviderCallbacks::default()),
provider_binding: sample_lancedb_binding_context(),
}
}
#[test]
fn lancedb_dynamic_binding_without_api_returns_error() {
let binding = dynamic_lancedb_binding_without_api();
let error = binding
.create_table_json(&json!({ "table_name": "docs" }))
.expect_err("missing dynamic API should return an error");
assert_eq!(
error,
"LanceDB dynamic-library API is unavailable for dynamic_library binding"
);
}
#[test]
fn lancedb_dynamic_status_without_api_returns_error() {
let binding = dynamic_lancedb_binding_without_api();
let error = binding
.status_json()
.expect_err("missing dynamic API should make status fail");
assert_eq!(
error,
"LanceDB dynamic-library API is unavailable for dynamic_library binding"
);
}
#[test]
fn lancedb_skill_binding_registry_recovers_after_poisoned_lock() {
let host_options = LuaRuntimeHostOptions {
lancedb_provider_mode: LuaRuntimeDatabaseProviderMode::HostCallback,
database_dir_name: "databases".to_string(),
..LuaRuntimeHostOptions::default()
};
let host = LanceDbSkillHost {
api: None,
controller: None,
skills: Mutex::new(HashMap::new()),
provider_callbacks: Arc::new(RuntimeDatabaseProviderCallbacks::default()),
host_options,
};
let poison_result = panic::catch_unwind(AssertUnwindSafe(|| {
let _registry_guard = host
.skills
.lock()
.expect("initial lancedb skill binding registry lock");
panic!("poison lancedb skill binding registry for recovery test");
}));
assert!(poison_result.is_err());
let skill_dir = PathBuf::from("D:/runtime-test-root/skills/lancedb-poison-skill");
let binding = host
.register_skill(
"ROOT",
"lancedb-poison-skill",
&skill_dir,
SkillLanceDbMeta::default(),
)
.expect("register lancedb binding after poison");
let fetched = host
.binding_for_skill("lancedb-poison-skill")
.expect("fetch lancedb binding after poison")
.expect("lancedb binding should exist");
assert!(Arc::ptr_eq(&binding, &fetched));
}
}