use anyhow::Result;
use std::sync::Arc;
use tokio::sync::mpsc;
use crate::events::ActionExecutionError;
use crate::llm::OllamaClient;
use crate::state::app_state::AppState;
use crate::state::ClientId;
pub fn resolve_client_protocol(protocol: &str) -> Option<String> {
let registry = &crate::protocol::CLIENT_REGISTRY;
if registry.get(protocol).is_some() {
return Some(protocol.to_string());
}
let upper = protocol.to_uppercase();
if registry.get(&upper).is_some() {
return Some(upper);
}
let lower = protocol.to_lowercase();
if registry.get(&lower).is_some() {
return Some(lower);
}
registry
.list_protocols()
.into_iter()
.find(|name| name.eq_ignore_ascii_case(protocol))
.or_else(|| registry.parse_from_str(protocol))
}
fn unknown_client_protocol_error(protocol: &str) -> anyhow::Error {
match crate::protocol::CLIENT_REGISTRY.resolve(protocol) {
Err(e) => anyhow::anyhow!("{e}"),
Ok(_) => anyhow::anyhow!("Unknown client protocol: {}", protocol),
}
}
pub async fn start_client_by_id(
state: &AppState,
client_id: ClientId,
llm_client: &OllamaClient,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<(), ActionExecutionError> {
let client = match state
.with_client_mut(client_id, |current| {
crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
Ok::<_, crate::protocol::StartupParamError>(current.clone())
})
.await
{
Some(Ok(current)) => current,
Some(Err(error)) => return Err(ActionExecutionError::Fatal(error.into())),
None => {
let _ = status_tx.send(format!("[ERROR] Client #{} not found", client_id.as_u32()));
return Ok(());
}
};
let protocol_name = client.protocol_name.clone();
let remote_addr = client.remote_addr.clone();
let msg = format!(
"[CLIENT] Starting client #{} ({}) connecting to {}",
client_id.as_u32(),
protocol_name,
remote_addr
);
let _ = status_tx.send(msg.clone());
use crate::state::client::ClientStatus;
let protocol = match resolve_client_protocol(&protocol_name)
.and_then(|name| crate::protocol::CLIENT_REGISTRY.get(&name))
{
Some(p) => p,
None => return Err(unknown_client_protocol_error(&protocol_name).into()),
};
if let Some(min) = state.get_min_stability().await {
let actual = protocol.metadata().state;
if actual < min {
let msg = crate::cli::server_startup::min_stability_refusal(
"client",
&protocol_name,
actual,
min,
);
state
.update_client_status(client_id, ClientStatus::Error(msg.clone()))
.await;
let _ = status_tx.send(format!("[ERROR] {}", msg));
let _ = status_tx.send("__UPDATE_UI__".to_string());
return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
}
}
let system_caps = state.get_system_capabilities().await;
if let Some(dep) = crate::protocol::dependencies::startup_blocker(
&protocol.startup_dependencies(client.startup_params.as_ref()),
&system_caps,
) {
let msg = format!(
"Cannot start {} client: {}. {}",
protocol_name,
dep.description(),
dep.installation_hint()
);
state
.update_client_status(client_id, ClientStatus::Error(msg.clone()))
.await;
let _ = status_tx.send(format!("[ERROR] {}", msg));
let _ = status_tx.send("__UPDATE_UI__".to_string());
return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
}
let startup_params = if let Some(params_json) = client.startup_params.clone() {
let schema = protocol.get_startup_parameters();
match crate::protocol::StartupParams::new_validated(params_json, schema) {
Ok(p) => Some(p),
Err(e) => {
let msg = format!("Invalid startup_params for {}: {}", protocol_name, e);
state
.update_client_status(client_id, ClientStatus::Error(msg.clone()))
.await;
let _ = status_tx.send(format!("[ERROR] {}", msg));
let _ = status_tx.send("__UPDATE_UI__".to_string());
return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
}
}
} else {
None
};
let connect_ctx = crate::protocol::ConnectContext {
remote_addr: remote_addr.clone(),
llm_client: llm_client.clone(),
state: Arc::new(state.clone()),
status_tx: status_tx.clone(),
client_id,
startup_params,
};
match protocol.connect(connect_ctx).await {
Ok(local_addr) => {
state
.update_client_status(client_id, ClientStatus::Connected)
.await;
let _ = status_tx.send(format!(
"[CLIENT] {} client #{} connected to {} (local: {})",
protocol_name,
client_id.as_u32(),
remote_addr,
local_addr
));
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
Err(e) => {
state
.update_client_status(client_id, ClientStatus::Error(e.to_string()))
.await;
let _ = status_tx.send(format!(
"[ERROR] Failed to connect {} client #{} to {}: {}",
protocol_name,
client_id.as_u32(),
remote_addr,
e
));
let _ = status_tx.send("__UPDATE_UI__".to_string());
return Err(ActionExecutionError::Fatal(e));
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn start_client_from_action(
state: &AppState,
protocol: &str,
remote_addr: &str,
instruction: String,
mut startup_params: Option<serde_json::Value>,
initial_memory: Option<String>,
event_handlers: Option<Vec<serde_json::Value>>,
scheduled_tasks: Option<Vec<crate::llm::actions::common::ServerTaskDefinition>>,
feedback_instructions: Option<String>,
llm_client: OllamaClient,
status_tx: Option<mpsc::UnboundedSender<String>>,
) -> Result<ClientId> {
use crate::state::client::ClientStatus;
crate::protocol::StartupParams::preflight_owned(&mut startup_params)?;
let prepared_tasks = crate::state::task::prepare_tasks(
scheduled_tasks.as_deref(),
crate::state::task::TaskScope::Global,
)?;
let protocol = match resolve_client_protocol(protocol) {
Some(name) => name,
None => return Err(unknown_client_protocol_error(protocol)),
};
let protocol_impl = match crate::protocol::CLIENT_REGISTRY.get(&protocol) {
Some(p) => p,
None => return Err(unknown_client_protocol_error(&protocol)),
};
if let Some(min) = state.get_min_stability().await {
let actual = protocol_impl.metadata().state;
if actual < min {
return Err(anyhow::anyhow!(
crate::cli::server_startup::min_stability_refusal("client", &protocol, actual, min)
));
}
}
let system_caps = state.get_system_capabilities().await;
if let Some(dep) = crate::protocol::dependencies::startup_blocker(
&protocol_impl.startup_dependencies(startup_params.as_ref()),
&system_caps,
) {
return Err(anyhow::anyhow!(
"Cannot start {} client: {}. {}",
protocol,
dep.description(),
dep.installation_hint()
));
}
let startup_params_obj = match startup_params.clone() {
Some(params_json) => {
let schema = protocol_impl.get_startup_parameters();
Some(
crate::protocol::StartupParams::new_validated(params_json, schema).map_err(
|e| anyhow::anyhow!("Invalid startup_params for {}: {}", protocol, e),
)?,
)
}
None => {
crate::protocol::StartupParams::new_validated(
serde_json::json!({}),
protocol_impl.get_startup_parameters(),
)?;
None
}
};
let event_handler_config = match event_handlers {
Some(handlers) if !handlers.is_empty() => Some(
crate::events::handler::EventHandler::parse_event_handlers(handlers)
.map_err(|e| anyhow::anyhow!("Invalid event_handlers for {}: {}", protocol, e))?,
),
_ => None,
};
let client = crate::state::client::ClientInstance {
id: ClientId::new(0), remote_addr: remote_addr.to_string(),
protocol_name: protocol.clone(),
instruction: instruction.clone(),
memory: String::new(),
status: ClientStatus::Connecting,
connection: None,
created_at: crate::utils::clock::Instant::now(),
status_changed_at: crate::utils::clock::Instant::now(),
startup_params: startup_params.clone(),
event_handler_config,
protocol_data: serde_json::Value::Null,
log_files: Default::default(),
feedback_instructions,
feedback_buffer: Vec::new(),
last_feedback_processed: None,
connection_history: Default::default(),
};
let client_id = state.add_client(client).await;
let mut startup_guard = super::startup_guard::StartupGuard::client(state, client_id);
if let Some(mem) = initial_memory {
state
.with_client_mut(client_id, |c| {
c.memory = mem;
})
.await;
}
for mut task in prepared_tasks {
task.scope = crate::state::task::TaskScope::Client(client_id);
state.add_task(task).await;
}
let status_tx = match status_tx {
Some(tx) => tx,
None => {
let (status_tx, mut status_rx) = mpsc::unbounded_channel::<String>();
tokio::spawn(async move {
while let Some(msg) = status_rx.recv().await {
if msg == "__UPDATE_UI__" {
continue;
}
tracing::info!("{}", msg);
}
});
status_tx
}
};
let connect_ctx = crate::protocol::ConnectContext {
remote_addr: remote_addr.to_string(),
llm_client: llm_client.clone(),
state: Arc::new(state.clone()),
status_tx,
client_id,
startup_params: startup_params_obj,
};
match protocol_impl.connect(connect_ctx).await {
Ok(_local_addr) => {
state
.update_client_status(client_id, ClientStatus::Connected)
.await;
startup_guard.disarm();
Ok(client_id)
}
Err(e) => {
state
.update_client_status(client_id, ClientStatus::Error(e.to_string()))
.await;
state.remove_client(client_id).await;
startup_guard.disarm();
Err(e)
}
}
}