1use anyhow::Result;
6use std::sync::Arc;
7use tokio::sync::mpsc;
8
9use crate::events::ActionExecutionError;
10use crate::llm::OllamaClient;
11use crate::state::app_state::AppState;
12use crate::state::ClientId;
13
14pub fn resolve_client_protocol(protocol: &str) -> Option<String> {
22 let registry = &crate::protocol::CLIENT_REGISTRY;
23 if registry.get(protocol).is_some() {
24 return Some(protocol.to_string());
25 }
26 let upper = protocol.to_uppercase();
27 if registry.get(&upper).is_some() {
28 return Some(upper);
29 }
30 let lower = protocol.to_lowercase();
31 if registry.get(&lower).is_some() {
32 return Some(lower);
33 }
34 registry
36 .list_protocols()
37 .into_iter()
38 .find(|name| name.eq_ignore_ascii_case(protocol))
39 .or_else(|| registry.parse_from_str(protocol))
40}
41
42fn unknown_client_protocol_error(protocol: &str) -> anyhow::Error {
51 match crate::protocol::CLIENT_REGISTRY.resolve(protocol) {
52 Err(e) => anyhow::anyhow!("{e}"),
53 Ok(_) => anyhow::anyhow!("Unknown client protocol: {}", protocol),
56 }
57}
58
59pub async fn start_client_by_id(
61 state: &AppState,
62 client_id: ClientId,
63 llm_client: &OllamaClient,
64 status_tx: &mpsc::UnboundedSender<String>,
65) -> Result<(), ActionExecutionError> {
66 let client = match state
68 .with_client_mut(client_id, |current| {
69 crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
70 Ok::<_, crate::protocol::StartupParamError>(current.clone())
71 })
72 .await
73 {
74 Some(Ok(current)) => current,
75 Some(Err(error)) => return Err(ActionExecutionError::Fatal(error.into())),
76 None => {
77 let _ = status_tx.send(format!("[ERROR] Client #{} not found", client_id.as_u32()));
78 return Ok(());
79 }
80 };
81
82 let protocol_name = client.protocol_name.clone();
83 let remote_addr = client.remote_addr.clone();
84
85 let msg = format!(
86 "[CLIENT] Starting client #{} ({}) connecting to {}",
87 client_id.as_u32(),
88 protocol_name,
89 remote_addr
90 );
91 let _ = status_tx.send(msg.clone());
92
93 use crate::state::client::ClientStatus;
95
96 let protocol = match resolve_client_protocol(&protocol_name)
98 .and_then(|name| crate::protocol::CLIENT_REGISTRY.get(&name))
99 {
100 Some(p) => p,
101 None => return Err(unknown_client_protocol_error(&protocol_name).into()),
102 };
103
104 if let Some(min) = state.get_min_stability().await {
107 let actual = protocol.metadata().state;
108 if actual < min {
109 let msg = crate::cli::server_startup::min_stability_refusal(
110 "client",
111 &protocol_name,
112 actual,
113 min,
114 );
115 state
116 .update_client_status(client_id, ClientStatus::Error(msg.clone()))
117 .await;
118 let _ = status_tx.send(format!("[ERROR] {}", msg));
119 let _ = status_tx.send("__UPDATE_UI__".to_string());
120 return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
121 }
122 }
123
124 let system_caps = state.get_system_capabilities().await;
128 if let Some(dep) = crate::protocol::dependencies::startup_blocker(
129 &protocol.startup_dependencies(client.startup_params.as_ref()),
130 &system_caps,
131 ) {
132 let msg = format!(
133 "Cannot start {} client: {}. {}",
134 protocol_name,
135 dep.description(),
136 dep.installation_hint()
137 );
138 state
139 .update_client_status(client_id, ClientStatus::Error(msg.clone()))
140 .await;
141 let _ = status_tx.send(format!("[ERROR] {}", msg));
142 let _ = status_tx.send("__UPDATE_UI__".to_string());
143 return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
144 }
145
146 let startup_params = if let Some(params_json) = client.startup_params.clone() {
151 let schema = protocol.get_startup_parameters();
153 match crate::protocol::StartupParams::new_validated(params_json, schema) {
155 Ok(p) => Some(p),
156 Err(e) => {
157 let msg = format!("Invalid startup_params for {}: {}", protocol_name, e);
158 state
159 .update_client_status(client_id, ClientStatus::Error(msg.clone()))
160 .await;
161 let _ = status_tx.send(format!("[ERROR] {}", msg));
162 let _ = status_tx.send("__UPDATE_UI__".to_string());
163 return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
164 }
165 }
166 } else {
167 None
168 };
169
170 let connect_ctx = crate::protocol::ConnectContext {
172 remote_addr: remote_addr.clone(),
173 llm_client: llm_client.clone(),
174 state: Arc::new(state.clone()),
175 status_tx: status_tx.clone(),
176 client_id,
177 startup_params,
178 };
179
180 match protocol.connect(connect_ctx).await {
182 Ok(local_addr) => {
183 state
185 .update_client_status(client_id, ClientStatus::Connected)
186 .await;
187 let _ = status_tx.send(format!(
188 "[CLIENT] {} client #{} connected to {} (local: {})",
189 protocol_name,
190 client_id.as_u32(),
191 remote_addr,
192 local_addr
193 ));
194 let _ = status_tx.send("__UPDATE_UI__".to_string());
195 }
196 Err(e) => {
197 state
199 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
200 .await;
201 let _ = status_tx.send(format!(
202 "[ERROR] Failed to connect {} client #{} to {}: {}",
203 protocol_name,
204 client_id.as_u32(),
205 remote_addr,
206 e
207 ));
208 let _ = status_tx.send("__UPDATE_UI__".to_string());
209 return Err(ActionExecutionError::Fatal(e));
210 }
211 }
212
213 Ok(())
214}
215
216#[allow(clippy::too_many_arguments)]
219pub async fn start_client_from_action(
220 state: &AppState,
221 protocol: &str,
222 remote_addr: &str,
223 instruction: String,
224 mut startup_params: Option<serde_json::Value>,
225 initial_memory: Option<String>,
226 event_handlers: Option<Vec<serde_json::Value>>,
227 scheduled_tasks: Option<Vec<crate::llm::actions::common::ServerTaskDefinition>>,
228 feedback_instructions: Option<String>,
229 llm_client: OllamaClient,
230 status_tx: Option<mpsc::UnboundedSender<String>>,
231) -> Result<ClientId> {
232 use crate::state::client::ClientStatus;
233 crate::protocol::StartupParams::preflight_owned(&mut startup_params)?;
234 let prepared_tasks = crate::state::task::prepare_tasks(
235 scheduled_tasks.as_deref(),
236 crate::state::task::TaskScope::Global,
237 )?;
238
239 let protocol = match resolve_client_protocol(protocol) {
244 Some(name) => name,
245 None => return Err(unknown_client_protocol_error(protocol)),
246 };
247 let protocol_impl = match crate::protocol::CLIENT_REGISTRY.get(&protocol) {
248 Some(p) => p,
249 None => return Err(unknown_client_protocol_error(&protocol)),
250 };
251
252 if let Some(min) = state.get_min_stability().await {
256 let actual = protocol_impl.metadata().state;
257 if actual < min {
258 return Err(anyhow::anyhow!(
259 crate::cli::server_startup::min_stability_refusal("client", &protocol, actual, min)
260 ));
261 }
262 }
263
264 let system_caps = state.get_system_capabilities().await;
269 if let Some(dep) = crate::protocol::dependencies::startup_blocker(
270 &protocol_impl.startup_dependencies(startup_params.as_ref()),
271 &system_caps,
272 ) {
273 return Err(anyhow::anyhow!(
274 "Cannot start {} client: {}. {}",
275 protocol,
276 dep.description(),
277 dep.installation_hint()
278 ));
279 }
280
281 let startup_params_obj = match startup_params.clone() {
287 Some(params_json) => {
288 let schema = protocol_impl.get_startup_parameters();
289 Some(
290 crate::protocol::StartupParams::new_validated(params_json, schema).map_err(
291 |e| anyhow::anyhow!("Invalid startup_params for {}: {}", protocol, e),
292 )?,
293 )
294 }
295 None => {
296 crate::protocol::StartupParams::new_validated(
297 serde_json::json!({}),
298 protocol_impl.get_startup_parameters(),
299 )?;
300 None
301 }
302 };
303
304 let event_handler_config = match event_handlers {
312 Some(handlers) if !handlers.is_empty() => Some(
313 crate::events::handler::EventHandler::parse_event_handlers(handlers)
314 .map_err(|e| anyhow::anyhow!("Invalid event_handlers for {}: {}", protocol, e))?,
315 ),
316 _ => None,
317 };
318
319 let client = crate::state::client::ClientInstance {
321 id: ClientId::new(0), remote_addr: remote_addr.to_string(),
323 protocol_name: protocol.clone(),
324 instruction: instruction.clone(),
325 memory: String::new(),
326 status: ClientStatus::Connecting,
327 connection: None,
328 created_at: crate::utils::clock::Instant::now(),
329 status_changed_at: crate::utils::clock::Instant::now(),
330 startup_params: startup_params.clone(),
331 event_handler_config,
332 protocol_data: serde_json::Value::Null,
333 log_files: Default::default(),
334 feedback_instructions,
335 feedback_buffer: Vec::new(),
336 last_feedback_processed: None,
337 connection_history: Default::default(),
338 };
339
340 let client_id = state.add_client(client).await;
341 let mut startup_guard = super::startup_guard::StartupGuard::client(state, client_id);
342
343 if let Some(mem) = initial_memory {
345 state
346 .with_client_mut(client_id, |c| {
347 c.memory = mem;
348 })
349 .await;
350 }
351
352 for mut task in prepared_tasks {
354 task.scope = crate::state::task::TaskScope::Client(client_id);
355 state.add_task(task).await;
356 }
357
358 let status_tx = match status_tx {
366 Some(tx) => tx,
367 None => {
368 let (status_tx, mut status_rx) = mpsc::unbounded_channel::<String>();
369 tokio::spawn(async move {
370 while let Some(msg) = status_rx.recv().await {
371 if msg == "__UPDATE_UI__" {
372 continue;
373 }
374 tracing::info!("{}", msg);
375 }
376 });
377 status_tx
378 }
379 };
380
381 let connect_ctx = crate::protocol::ConnectContext {
383 remote_addr: remote_addr.to_string(),
384 llm_client: llm_client.clone(),
385 state: Arc::new(state.clone()),
386 status_tx,
387 client_id,
388 startup_params: startup_params_obj,
389 };
390
391 match protocol_impl.connect(connect_ctx).await {
393 Ok(_local_addr) => {
394 state
395 .update_client_status(client_id, ClientStatus::Connected)
396 .await;
397 startup_guard.disarm();
398 Ok(client_id)
399 }
400 Err(e) => {
401 state
402 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
403 .await;
404 state.remove_client(client_id).await;
405 startup_guard.disarm();
406 Err(e)
407 }
408 }
409}