#![cfg(all(feature = "session-store", not(target_arch = "wasm32")))]
use std::sync::Arc;
use crate::LlmClient;
use meerkat_core::error::AgentError;
use meerkat_core::handles::GeneratedAuthLeaseHandle;
use meerkat_core::service::{SessionError, SessionService};
use meerkat_core::types::SessionId;
use meerkat_core::{
AgentLlmClient, AgentLlmClientDecorator, Config, ConfigRuntime, ModelRegistry,
SessionLlmIdentity, SessionToolVisibilityState,
};
use meerkat_runtime::{
HydratedSessionLlmState, ResolvedSessionLlmReconfigure, RuntimeDriverError,
SessionLlmCapabilitySurface, SessionLlmCapabilitySurfaceStatus, SessionLlmReconfigureHost,
SessionLlmReconfigureRequest,
};
use meerkat_session::PersistentSessionService;
use crate::StagedSessionRegistry;
use crate::factory::AgentFactory;
use crate::service_factory::FactoryAgentBuilder;
use crate::session_runtime::recovery::parse_provider_override;
pub fn session_error_to_runtime_driver(err: SessionError) -> RuntimeDriverError {
match err {
SessionError::NotFound { .. } => RuntimeDriverError::NotReady {
state: meerkat_runtime::RuntimeState::Destroyed,
},
other => RuntimeDriverError::Internal(other.to_string()),
}
}
pub fn runtime_driver_error_to_session_error(err: RuntimeDriverError) -> SessionError {
SessionError::Agent(AgentError::InternalError(err.to_string()))
}
pub fn profile_to_capability_surface(
profile: &meerkat_models::profile::ModelProfile,
) -> SessionLlmCapabilitySurface {
SessionLlmCapabilitySurface {
supports_temperature: profile.supports_temperature,
supports_thinking: profile.supports_thinking,
supports_reasoning: profile.supports_reasoning,
inline_video: profile.inline_video,
vision: profile.vision,
image_input: profile.image_input,
image_tool_results: profile.image_tool_results,
supports_web_search: profile.supports_web_search,
image_generation: profile.image_generation,
realtime: profile.realtime,
call_timeout_secs: profile.call_timeout_secs,
}
}
pub fn registered_model_provider_mismatch_reason(
registry: &ModelRegistry,
provider: meerkat_core::Provider,
model: &str,
) -> Option<String> {
registry.provider_override_mismatch_reason(provider, model)
}
pub struct SessionRuntimeLlmReconfigureHost {
pub service: Arc<PersistentSessionService<FactoryAgentBuilder>>,
pub staged_sessions: Arc<StagedSessionRegistry>,
pub factory: AgentFactory,
pub auth_lease: GeneratedAuthLeaseHandle,
pub default_llm_client: Arc<std::sync::RwLock<Option<Arc<dyn LlmClient>>>>,
pub agent_llm_client_decorator: Arc<std::sync::RwLock<Option<AgentLlmClientDecorator>>>,
pub config_runtime: Arc<std::sync::RwLock<Option<Arc<ConfigRuntime>>>>,
}
impl SessionRuntimeLlmReconfigureHost {
async fn capability_surface_for_identity(
&self,
identity: &SessionLlmIdentity,
) -> Result<
(
Option<SessionLlmCapabilitySurface>,
SessionLlmCapabilitySurfaceStatus,
),
RuntimeDriverError,
> {
let registry = self.model_registry().await?;
Ok(
match registry.profile_for_provider(identity.provider, &identity.model) {
Some(profile) => (
Some(profile_to_capability_surface(&profile)),
SessionLlmCapabilitySurfaceStatus::Resolved,
),
None => (None, SessionLlmCapabilitySurfaceStatus::Unresolved),
},
)
}
async fn hydrate_staged_session_llm_state(
&self,
session_id: &SessionId,
) -> Result<Option<HydratedSessionLlmState>, RuntimeDriverError> {
let Some(current_identity) = self
.staged_sessions
.effective_llm_identity(session_id)
.await
.map_err(|err| RuntimeDriverError::Internal(err.to_string()))?
else {
return Ok(None);
};
let (current_capability_surface, capability_surface_status) = self
.capability_surface_for_identity(¤t_identity)
.await?;
Ok(Some(HydratedSessionLlmState {
current_identity,
current_visibility_state: Default::default(),
current_capability_surface,
capability_surface_status,
base_tool_names: std::collections::BTreeSet::new(),
}))
}
async fn model_registry(&self) -> Result<ModelRegistry, RuntimeDriverError> {
let config_runtime = self
.config_runtime
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let config = if let Some(runtime) = config_runtime {
runtime
.get()
.await
.map(|snapshot| snapshot.config)
.map_err(|e| RuntimeDriverError::Internal(format!("Failed to load config: {e}")))?
} else {
Config::default()
};
config.model_registry().map_err(|e| {
RuntimeDriverError::Internal(format!("Failed to resolve model registry: {e}"))
})
}
pub async fn build_adapter_for_llm_identity(
&self,
identity: &SessionLlmIdentity,
) -> Result<Arc<dyn AgentLlmClient>, RuntimeDriverError> {
let default_llm_client = self
.default_llm_client
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let raw_client = if let Some(default) = default_llm_client {
default
} else {
let config = self.load_config_for_hot_swap().await?;
self.factory
.build_llm_client_for_identity_with_auth_lease(
&config,
identity,
Some(self.auth_lease.clone()),
)
.await
.map_err(|e| {
RuntimeDriverError::Internal(format!(
"Failed to build LLM client for session identity hot-swap: {e}"
))
})?
};
let adapter = self
.factory
.build_llm_adapter(raw_client, identity.model.clone())
.await;
let adapter = Arc::new(adapter) as Arc<dyn AgentLlmClient>;
let decorator = self
.agent_llm_client_decorator
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
Ok(AgentFactory::decorate_agent_llm_client(
adapter,
decorator.as_ref(),
))
}
async fn load_config_for_hot_swap(&self) -> Result<Config, RuntimeDriverError> {
let config_runtime = self
.config_runtime
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(runtime) = config_runtime {
runtime
.get()
.await
.map(|snapshot| snapshot.config)
.map_err(|e| {
RuntimeDriverError::Internal(format!("Failed to load config for hot-swap: {e}"))
})
} else {
Ok(Config::default())
}
}
async fn build_request_policy_for_llm_identity(
&self,
session_id: &SessionId,
identity: &SessionLlmIdentity,
) -> Result<meerkat_core::SessionLlmRequestPolicy, RuntimeDriverError> {
let config = self.load_config_for_hot_swap().await?;
self.factory
.request_policy_for_llm_identity(&config, identity)
.map_err(|e| {
RuntimeDriverError::Internal(format!(
"Failed to build LLM request policy for session {session_id} identity hot-swap: {e}"
))
})
}
pub async fn resolve_target_llm_identity(
&self,
current: &SessionLlmIdentity,
request: &SessionLlmReconfigureRequest,
) -> Result<SessionLlmIdentity, RuntimeDriverError> {
if request.provider.is_some() && request.model.is_none() {
return Err(RuntimeDriverError::ValidationFailed {
reason: "provider override requires model on an existing session".to_string(),
});
}
if request.clear_provider_params && request.provider_params.is_some() {
return Err(RuntimeDriverError::ValidationFailed {
reason: "clear_provider_params cannot be combined with provider_params".to_string(),
});
}
if request.clear_auth_binding && request.auth_binding.is_some() {
return Err(RuntimeDriverError::ValidationFailed {
reason: "clear_auth_binding cannot be combined with auth_binding".to_string(),
});
}
let registry = self.model_registry().await?;
let model = request
.model
.clone()
.unwrap_or_else(|| current.model.clone());
let provider = if let Some(provider_name) = request.provider.as_ref() {
parse_provider_override(provider_name)
.map_err(|reason| RuntimeDriverError::ValidationFailed { reason })?
} else {
current.provider
};
if (request.model.is_some() || request.provider.is_some())
&& let Some(reason) =
registered_model_provider_mismatch_reason(®istry, provider, &model)
{
return Err(RuntimeDriverError::ValidationFailed { reason });
}
let provider_params = if request.clear_provider_params {
None
} else {
request
.provider_params
.clone()
.or_else(|| current.provider_params.clone())
};
let self_hosted_server_id = if provider == meerkat_core::Provider::SelfHosted {
if request.model.is_none() {
current.self_hosted_server_id.clone().or_else(|| {
registry
.entry_for_provider(meerkat_core::Provider::SelfHosted, &model)
.and_then(|entry| entry.self_hosted.as_ref())
.map(|server| server.server_id.clone())
})
} else {
match registry.entry_for_provider(meerkat_core::Provider::SelfHosted, &model) {
Some(entry) => entry
.self_hosted
.as_ref()
.map(|server| server.server_id.clone()),
None => {
return Err(RuntimeDriverError::ValidationFailed {
reason: format!(
"self-hosted provider requires a registered model alias; '{model}' is not configured"
),
});
}
}
}
} else {
None
};
let auth_binding = if request.clear_auth_binding {
None
} else {
request
.auth_binding
.clone()
.or_else(|| current.auth_binding.clone())
};
Ok(SessionLlmIdentity {
model,
provider,
self_hosted_server_id,
provider_params,
auth_binding,
})
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl SessionLlmReconfigureHost for SessionRuntimeLlmReconfigureHost {
async fn hydrate_session_llm_state(
&self,
session_id: &SessionId,
) -> Result<HydratedSessionLlmState, RuntimeDriverError> {
let current_identity = match self.service.live_session_llm_identity(session_id).await {
Ok(identity) => identity,
Err(err) => {
if let Some(hydrated) = self.hydrate_staged_session_llm_state(session_id).await? {
return Ok(hydrated);
}
return Err(session_error_to_runtime_driver(err));
}
};
let session = match self.service.export_live_session(session_id).await {
Ok(session) => session,
Err(err) => {
if let Some(hydrated) = self.hydrate_staged_session_llm_state(session_id).await? {
return Ok(hydrated);
}
return Err(session_error_to_runtime_driver(err));
}
};
let current_visibility_state = session
.try_tool_visibility_state()
.map_err(|err| {
RuntimeDriverError::Internal(format!(
"invalid canonical tool visibility state: {err}"
))
})?
.unwrap_or_default();
let base_tool_names = self
.service
.tool_scope_snapshot(session_id)
.await
.map_err(session_error_to_runtime_driver)?
.ok_or_else(|| {
RuntimeDriverError::Internal(format!(
"session {session_id} missing live tool scope snapshot during llm reconfiguration"
))
})?
.known_base_names
.into_iter()
.collect();
let (current_capability_surface, capability_surface_status) = self
.capability_surface_for_identity(¤t_identity)
.await?;
Ok(HydratedSessionLlmState {
current_identity,
current_visibility_state,
current_capability_surface,
capability_surface_status,
base_tool_names,
})
}
async fn resolve_target_session_llm_identity(
&self,
request: &SessionLlmReconfigureRequest,
current_identity: &SessionLlmIdentity,
) -> Result<ResolvedSessionLlmReconfigure, RuntimeDriverError> {
let target_identity = self
.resolve_target_llm_identity(current_identity, request)
.await?;
let registry = self.model_registry().await?;
let profile = registry
.profile_for_provider(target_identity.provider, &target_identity.model)
.ok_or_else(|| RuntimeDriverError::ValidationFailed {
reason: format!(
"no capability profile is registered for provider '{}' and model '{}'",
target_identity.provider.as_str(),
target_identity.model
),
})?;
Ok(ResolvedSessionLlmReconfigure {
target_identity,
target_capability_surface: profile_to_capability_surface(&profile),
})
}
async fn apply_live_session_llm_identity(
&self,
session_id: &SessionId,
identity: &SessionLlmIdentity,
) -> Result<(), RuntimeDriverError> {
let adapter = self.build_adapter_for_llm_identity(identity).await?;
let request_policy = self
.build_request_policy_for_llm_identity(session_id, identity)
.await?;
self.service
.apply_runtime_session_llm_identity(
session_id,
adapter,
identity.clone(),
request_policy,
)
.await
.map_err(session_error_to_runtime_driver)
}
async fn apply_live_session_tool_visibility_state(
&self,
session_id: &SessionId,
visibility_state: Option<SessionToolVisibilityState>,
) -> Result<(), RuntimeDriverError> {
self.service
.set_session_tool_visibility_state(session_id, visibility_state)
.await
.map_err(session_error_to_runtime_driver)
}
async fn persist_live_session(&self, session_id: &SessionId) -> Result<(), RuntimeDriverError> {
self.service
.persist_live_session_now(session_id)
.await
.map(|_| ())
.map_err(session_error_to_runtime_driver)
}
async fn discard_live_session(&self, session_id: &SessionId) -> Result<(), RuntimeDriverError> {
self.service
.discard_live_session(session_id)
.await
.map_err(session_error_to_runtime_driver)
}
}