1pub mod actions;
14
15pub use actions::WebSocketClientProtocol;
16
17use anyhow::{Context, Result};
18use futures::stream::StreamExt;
19use futures::SinkExt;
20use std::collections::VecDeque;
21use std::net::SocketAddr;
22use std::sync::Arc;
23use tokio::sync::{mpsc, Mutex};
24use tokio_tungstenite::tungstenite::client::IntoClientRequest;
25use tokio_tungstenite::tungstenite::http::{HeaderName, HeaderValue};
26use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
27use tokio_tungstenite::tungstenite::protocol::CloseFrame;
28use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
29use tokio_tungstenite::tungstenite::Message;
30use tracing::{debug, error, info, trace, warn};
31
32use crate::client::llm_budget::call_llm_for_client;
33use crate::llm::actions::client_trait::{Client, ClientActionResult};
34use crate::llm::ollama_client::OllamaClient;
35use crate::llm::ClientLlmResult;
36use crate::protocol::Event;
37use crate::server::websocket::actions::{encode_inbound_payload, WsOut};
38use crate::state::app_state::AppState;
39use crate::state::{ClientId, ClientStatus};
40use actions::{
41 WEBSOCKET_CLIENT_BINARY_MESSAGE_EVENT, WEBSOCKET_CLIENT_CLOSED_EVENT,
42 WEBSOCKET_CLIENT_CONNECTED_EVENT, WEBSOCKET_CLIENT_TEXT_MESSAGE_EVENT,
43};
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum HandlerState {
47 Idle,
48 Processing,
49 Accumulating,
50}
51
52#[derive(Debug, Clone)]
53enum Inbound {
54 Text(String),
55 Binary(Vec<u8>),
56}
57
58impl Inbound {
59 fn merge(self, next: Inbound) -> std::result::Result<Inbound, (Inbound, Inbound)> {
60 match (self, next) {
61 (Inbound::Text(mut a), Inbound::Text(b)) => {
62 a.push_str(&b);
63 Ok(Inbound::Text(a))
64 }
65 (Inbound::Binary(mut a), Inbound::Binary(b)) => {
66 a.extend_from_slice(&b);
67 Ok(Inbound::Binary(a))
68 }
69 (a, b) => Err((a, b)),
70 }
71 }
72}
73
74impl Inbound {
75 fn len(&self) -> usize {
76 match self {
77 Inbound::Text(t) => t.len(),
78 Inbound::Binary(b) => b.len(),
79 }
80 }
81}
82
83const TURN_QUEUE_CAPACITY: usize = 256;
90
91const MAX_INBOUND_MESSAGE_BYTES: usize = 64 << 20;
94const MAX_INBOUND_FRAME_BYTES: usize = 16 << 20;
96
97pub const DEFAULT_PERMESSAGE_DEFLATE: bool = true;
99
100const TURN_QUEUE_MAX_BYTES: usize = 16 * 1024 * 1024;
107
108enum WsTurn {
110 Connected {
111 remote_addr: String,
112 path: String,
113 subprotocol: String,
114 permessage_deflate: Option<crate::server::websocket::deflate::DeflateParams>,
115 },
116 Message(Inbound),
117 Closed {
118 code: u16,
119 reason: String,
120 },
121}
122
123fn enqueue_inbound(
125 turn_tx: &mpsc::Sender<WsTurn>,
126 queued_bytes: &std::sync::atomic::AtomicUsize,
127 frame: Inbound,
128 client_id: ClientId,
129) {
130 use std::sync::atomic::Ordering;
131 let len = frame.len();
132 let already = queued_bytes.load(Ordering::SeqCst);
133 if already > 0 && already.saturating_add(len) > TURN_QUEUE_MAX_BYTES {
134 warn!(
135 "WebSocket client {} dropped a {}-byte message: {} bytes already wait for the model \
136 decision=turn_queue_full",
137 client_id, len, already
138 );
139 return;
140 }
141 queued_bytes.fetch_add(len, Ordering::SeqCst);
142 match turn_tx.try_send(WsTurn::Message(frame)) {
143 Ok(()) => {}
144 Err(mpsc::error::TrySendError::Full(_)) => {
145 queued_bytes.fetch_sub(len, Ordering::SeqCst);
146 warn!(
147 "WebSocket client {} dropped a {}-byte message: {} messages already wait for \
148 the model decision=turn_queue_full",
149 client_id, len, TURN_QUEUE_CAPACITY
150 );
151 }
152 Err(mpsc::error::TrySendError::Closed(_)) => {
154 queued_bytes.fetch_sub(len, Ordering::SeqCst);
155 }
156 }
157}
158
159struct ClientData {
160 state: HandlerState,
161 queued: VecDeque<Inbound>,
162 pending: Option<Inbound>,
163 memory: String,
164}
165
166pub struct WebSocketClient;
167
168impl WebSocketClient {
169 pub async fn connect_with_llm_actions(
171 ctx: crate::protocol::ConnectContext,
172 ) -> Result<SocketAddr> {
173 let crate::protocol::ConnectContext {
174 remote_addr,
175 llm_client,
176 state: app_state,
177 status_tx,
178 client_id,
179 startup_params,
180 } = ctx;
181
182 let offer_deflate = match startup_params.as_ref() {
184 Some(params) => params
185 .get_optional_bool("permessage_deflate")
186 .map_err(|e| anyhow::anyhow!("WebSocket client parameter error: {e}"))?
187 .unwrap_or(DEFAULT_PERMESSAGE_DEFLATE),
188 None => DEFAULT_PERMESSAGE_DEFLATE,
189 };
190 let (path, subprotocols, extra_headers) = match startup_params.as_ref() {
191 Some(params) => {
192 let path = params
193 .get_optional_string("path")
194 .map_err(|e| anyhow::anyhow!("WebSocket client parameter error: {e}"))?
195 .unwrap_or_else(|| "/".to_string());
196 let subprotocols: Vec<String> = params
197 .get_optional_array("subprotocols")
198 .map_err(|e| anyhow::anyhow!("WebSocket client parameter error: {e}"))?
199 .map(|arr| {
200 arr.iter()
201 .filter_map(|v| v.as_str().map(|s| s.trim().to_string()))
202 .filter(|s| !s.is_empty())
203 .collect()
204 })
205 .unwrap_or_default();
206 let headers = params
207 .get_optional_object("headers")
208 .map_err(|e| anyhow::anyhow!("WebSocket client parameter error: {e}"))?
209 .map(|obj| {
210 obj.iter()
211 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
212 .collect::<Vec<_>>()
213 })
214 .unwrap_or_default();
215 (path, subprotocols, headers)
216 }
217 None => ("/".to_string(), Vec::new(), Vec::new()),
218 };
219
220 let path = if path.starts_with('/') {
221 path
222 } else {
223 format!("/{path}")
224 };
225
226 let url = if remote_addr.starts_with("ws://") {
228 remote_addr.clone()
229 } else if remote_addr.starts_with("wss://") || remote_addr.starts_with("https://") {
230 anyhow::bail!(
231 "wss:// is not supported: the WebSocket client is built without a TLS backend. \
232 Use ws:// (optionally behind a local TLS terminator)."
233 );
234 } else {
235 let host = remote_addr
236 .trim_start_matches("http://")
237 .trim_end_matches('/');
238 format!("ws://{host}{path}")
239 };
240
241 let mut request = url
242 .as_str()
243 .into_client_request()
244 .with_context(|| format!("Invalid WebSocket URL: {url}"))?;
245
246 if !subprotocols.is_empty() {
247 let value = subprotocols.join(", ");
248 request.headers_mut().insert(
249 "sec-websocket-protocol",
250 HeaderValue::from_str(&value)
251 .with_context(|| format!("Invalid subprotocol list: {value:?}"))?,
252 );
253 }
254 for (name, value) in &extra_headers {
255 let header_name = HeaderName::from_bytes(name.to_ascii_lowercase().as_bytes())
256 .with_context(|| format!("Invalid header name: {name:?}"))?;
257 if matches!(
260 header_name.as_str(),
261 "sec-websocket-key"
262 | "sec-websocket-version"
263 | "sec-websocket-accept"
264 | "connection"
265 | "upgrade"
266 | "host"
267 ) {
268 warn!(
269 "WebSocket client ignoring handshake header {:?} supplied in 'headers'",
270 name
271 );
272 continue;
273 }
274 request.headers_mut().insert(
275 header_name,
276 HeaderValue::from_str(value)
277 .with_context(|| format!("Invalid value for header {name:?}"))?,
278 );
279 }
280
281 if offer_deflate {
282 request.headers_mut().insert(
283 "sec-websocket-extensions",
284 HeaderValue::from_static(crate::server::websocket::deflate::CLIENT_OFFER),
285 );
286 }
287
288 let authority = request
292 .uri()
293 .authority()
294 .map(|a| a.to_string())
295 .with_context(|| format!("WebSocket URL has no host: {url}"))?;
296 let dial = if authority.contains(':') {
297 authority
298 } else {
299 format!("{authority}:80")
300 };
301 let tcp = tokio::net::TcpStream::connect(&dial)
302 .await
303 .with_context(|| format!("WebSocket TCP connect to {dial} failed"))?;
304 let local_addr = tcp.local_addr()?;
305 let config = WebSocketConfig {
306 max_message_size: Some(MAX_INBOUND_MESSAGE_BYTES),
307 max_frame_size: Some(MAX_INBOUND_FRAME_BYTES),
308 ..Default::default()
309 };
310 let stream = crate::server::websocket::deflate::DeflateStream::client(
311 tcp,
312 MAX_INBOUND_MESSAGE_BYTES,
313 MAX_INBOUND_FRAME_BYTES,
314 );
315 let (ws_stream, response) =
316 tokio_tungstenite::client_async_with_config(request, stream, Some(config))
317 .await
318 .with_context(|| format!("WebSocket handshake failed against {url}"))?;
319 let permessage_deflate = ws_stream.get_ref().active();
320
321 let negotiated = response
322 .headers()
323 .get("sec-websocket-protocol")
324 .and_then(|v| v.to_str().ok())
325 .map(|s| s.trim().to_string())
326 .unwrap_or_default();
327
328 info!(
329 "WebSocket client {} connected to {} (subprotocol {:?}, permessage-deflate {})",
330 client_id,
331 url,
332 negotiated,
333 permessage_deflate.is_some()
334 );
335 app_state
336 .update_client_status(client_id, ClientStatus::Connected)
337 .await;
338 let _ = status_tx.send(format!(
339 "[CLIENT] WebSocket client {client_id} connected to {url}"
340 ));
341 let _ = status_tx.send("__UPDATE_UI__".to_string());
342
343 let (sink, mut stream) = ws_stream.split();
344 let sink = Arc::new(Mutex::new(sink));
349 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<WsOut>();
350
351 let writer_sink = sink.clone();
354 let writer_handle = tokio::spawn(async move {
355 while let Some(out) = out_rx.recv().await {
356 let closing = matches!(out, WsOut::Close { .. });
357 let message = ws_out_to_message(out);
358 let mut guard = writer_sink.lock().await;
359 if let Err(e) = guard.send(message).await {
360 debug!("WebSocket client write failed: {}", e);
361 break;
362 }
363 if closing {
364 let _ = guard.flush().await;
365 break;
366 }
367 }
368 let _ = writer_sink.lock().await.flush().await;
369 });
370
371 let data = Arc::new(Mutex::new(ClientData {
372 state: HandlerState::Idle,
373 queued: VecDeque::new(),
374 pending: None,
375 memory: String::new(),
376 }));
377
378 let command_rx =
382 crate::client::command_support::register_command_channel(&app_state, client_id).await;
383 let cmd_sink = sink.clone();
384 let cmd_state = app_state.clone();
385 let cmd_status_tx = status_tx.clone();
386 let cmd_task = tokio::spawn(async move {
387 Self::command_loop(command_rx, cmd_sink, client_id, cmd_state, cmd_status_tx).await;
388 });
389 app_state.register_client_task(client_id, cmd_task).await;
390
391 let (turn_tx, turn_rx) = mpsc::channel::<WsTurn>(TURN_QUEUE_CAPACITY);
403 let queued_bytes = Arc::new(std::sync::atomic::AtomicUsize::new(0));
404 let _ = turn_tx.try_send(WsTurn::Connected {
406 remote_addr: remote_addr.clone(),
407 path: path.clone(),
408 subprotocol: negotiated.clone(),
409 permessage_deflate,
410 });
411
412 let turn_abort = app_state
413 .spawn_client_task(
414 client_id,
415 Self::run_model_turns(
416 turn_rx,
417 client_id,
418 llm_client,
419 app_state.clone(),
420 status_tx.clone(),
421 out_tx.clone(),
422 data,
423 queued_bytes.clone(),
424 ),
425 )
426 .await;
427
428 let read_state = app_state.clone();
429 let read_status_tx = status_tx.clone();
430 app_state
431 .spawn_client_task(client_id, async move {
432 let mut clean_close = false;
433 while let Some(message) = stream.next().await {
434 match message {
435 Ok(Message::Text(text)) => {
436 trace!("WebSocket client {} <- text {:?}", client_id, text);
437 enqueue_inbound(
438 &turn_tx,
439 &queued_bytes,
440 Inbound::Text(text),
441 client_id,
442 );
443 }
444 Ok(Message::Binary(bytes)) => {
445 trace!(
446 "WebSocket client {} <- binary {} bytes: {}",
447 client_id,
448 bytes.len(),
449 hex::encode(&bytes)
450 );
451 enqueue_inbound(
452 &turn_tx,
453 &queued_bytes,
454 Inbound::Binary(bytes),
455 client_id,
456 );
457 }
458 Ok(Message::Ping(_)) => {
459 debug!("WebSocket client {} <- ping (pong queued)", client_id);
461 }
462 Ok(Message::Pong(_)) => {
463 debug!("WebSocket client {} <- pong", client_id);
464 }
465 Ok(Message::Close(frame)) => {
466 let (code, reason) = match &frame {
467 Some(f) => (u16::from(f.code), f.reason.to_string()),
468 None => (1005u16, String::new()),
469 };
470 info!(
471 "WebSocket client {} closed by server: code={} reason={:?}",
472 client_id, code, reason
473 );
474 if turn_tx.try_send(WsTurn::Closed { code, reason }).is_err() {
477 warn!(
478 "WebSocket client {} could not queue its close event \
479 decision=turn_queue_full",
480 client_id
481 );
482 }
483 clean_close = true;
484 break;
485 }
486 Ok(Message::Frame(_)) => {}
487 Err(e) => {
488 debug!("WebSocket client {} read ended: {}", client_id, e);
489 break;
490 }
491 }
492 }
493
494 if !clean_close {
495 turn_abort.abort();
498 }
499 read_state
500 .update_client_status(client_id, ClientStatus::Disconnected)
501 .await;
502 read_state.remove_client_handle(client_id).await;
506 let _ =
507 read_status_tx.send(format!("[CLIENT] WebSocket client {client_id} closed"));
508 let _ = read_status_tx.send("__UPDATE_UI__".to_string());
509 })
510 .await;
511
512 drop(writer_handle);
515 drop(out_tx);
516
517 Ok(local_addr)
518 }
519
520 #[allow(clippy::too_many_arguments)]
522 async fn run_model_turns(
523 mut turn_rx: mpsc::Receiver<WsTurn>,
524 client_id: ClientId,
525 llm_client: OllamaClient,
526 app_state: Arc<AppState>,
527 status_tx: mpsc::UnboundedSender<String>,
528 out_tx: mpsc::UnboundedSender<WsOut>,
529 data: Arc<Mutex<ClientData>>,
530 queued_bytes: Arc<std::sync::atomic::AtomicUsize>,
531 ) {
532 while let Some(turn) = turn_rx.recv().await {
533 match turn {
534 WsTurn::Connected {
535 remote_addr,
536 path,
537 subprotocol,
538 permessage_deflate,
539 } => {
540 Self::emit_connected(
541 client_id,
542 remote_addr,
543 path,
544 subprotocol,
545 permessage_deflate,
546 &llm_client,
547 &app_state,
548 &status_tx,
549 &out_tx,
550 &data,
551 )
552 .await;
553 }
554 WsTurn::Message(frame) => {
555 queued_bytes.fetch_sub(frame.len(), std::sync::atomic::Ordering::SeqCst);
556 Self::handle_inbound(
557 frame,
558 client_id,
559 &llm_client,
560 &app_state,
561 &status_tx,
562 &out_tx,
563 &data,
564 )
565 .await;
566 }
567 WsTurn::Closed { code, reason } => {
568 Self::emit_closed(
569 client_id,
570 code,
571 reason,
572 &llm_client,
573 &app_state,
574 &status_tx,
575 &out_tx,
576 &data,
577 )
578 .await;
579 break;
580 }
581 }
582 }
583 }
584
585 #[allow(clippy::too_many_arguments)]
587 async fn emit_connected(
588 client_id: ClientId,
589 remote_addr: String,
590 path: String,
591 subprotocol: String,
592 permessage_deflate: Option<crate::server::websocket::deflate::DeflateParams>,
593 llm_client: &OllamaClient,
594 app_state: &Arc<AppState>,
595 status_tx: &mpsc::UnboundedSender<String>,
596 out_tx: &mpsc::UnboundedSender<WsOut>,
597 data: &Arc<Mutex<ClientData>>,
598 ) {
599 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
600 return;
601 };
602 let event = Event::new(
603 &WEBSOCKET_CLIENT_CONNECTED_EVENT,
604 serde_json::json!({
605 "remote_addr": remote_addr,
606 "path": path,
607 "subprotocol": subprotocol,
608 "permessage_deflate": permessage_deflate.map(|p| p.to_json()),
609 }),
610 );
611 let protocol = WebSocketClientProtocol::for_connection(out_tx.clone());
612 let memory = { data.lock().await.memory.clone() };
613 match call_llm_for_client(
614 llm_client,
615 app_state,
616 client_id.to_string(),
617 &instruction,
618 &memory,
619 Some(&event),
620 &protocol,
621 status_tx,
622 )
623 .await
624 {
625 Ok(ClientLlmResult {
626 actions,
627 memory_updates,
628 }) => {
629 if let Some(mem) = memory_updates {
630 data.lock().await.memory = mem;
631 }
632 for action in actions {
633 if let Err(e) = protocol.execute_action(action) {
634 error!("WebSocket client action failed after connect: {}", e);
635 let _ = status_tx.send(format!("[CLIENT] WebSocket action failed: {e}"));
636 }
637 }
638 }
639 Err(e) => {
640 error!("LLM error on websocket_client_connected: {}", e);
641 let _ = status_tx.send(format!("[CLIENT] WebSocket LLM error: {e}"));
642 }
643 }
644 }
645
646 async fn command_loop<S>(
657 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
658 sink: Arc<Mutex<S>>,
659 client_id: ClientId,
660 app_state: Arc<AppState>,
661 status_tx: mpsc::UnboundedSender<String>,
662 ) where
663 S: futures::Sink<Message> + Unpin + Send,
664 S::Error: std::fmt::Display,
665 {
666 use crate::llm::actions::protocol_trait::Protocol;
667 use crate::state::client_handles::ClientSendOutcome;
668 use crate::state::AccessLogOwner;
669
670 let (frame_tx, mut frame_rx) = mpsc::unbounded_channel::<WsOut>();
673 let protocol = WebSocketClientProtocol::for_connection(frame_tx);
674
675 while let Some(command) = command_rx.recv().await {
676 let action = command.action.clone();
677 let executed = protocol.execute_action(action.clone());
678
679 let mut frames: Vec<WsOut> = Vec::new();
681 while let Ok(frame) = frame_rx.try_recv() {
682 frames.push(frame);
683 }
684
685 let mut bytes_sent = 0usize;
686 let mut write_error: Option<String> = None;
687 for frame in frames {
688 let payload_len = match &frame {
691 WsOut::Text(t) => t.len(),
692 WsOut::Binary(b) => b.len(),
693 WsOut::Ping(p) => p.len(),
694 WsOut::Close { reason, .. } => reason.len(),
695 };
696 let message = ws_out_to_message(frame);
697 let mut guard = sink.lock().await;
698 match guard.send(message).await {
699 Ok(()) => bytes_sent += payload_len,
701 Err(e) => {
702 write_error = Some(e.to_string());
703 break;
704 }
705 }
706 }
707
708 let outcome: anyhow::Result<ClientSendOutcome> = match (&executed, &write_error) {
709 (_, Some(e)) => Err(anyhow::anyhow!("WebSocket write failed: {e}")),
710 (Err(e), None) => Ok(ClientSendOutcome::Rejected {
711 error: e.to_string(),
712 }),
713 (Ok(ClientActionResult::Disconnect), None) => Ok(ClientSendOutcome::Disconnected),
714 (Ok(ClientActionResult::WaitForMore), None) => Ok(ClientSendOutcome::Executed {
715 detail: "wait_for_websocket_data: nothing written".to_string(),
716 }),
717 (Ok(_), None) => {
718 if bytes_sent > 0 {
719 Ok(ClientSendOutcome::Sent { bytes_sent })
720 } else {
721 Ok(ClientSendOutcome::Executed {
722 detail: "executed; the frame carried an empty payload".to_string(),
723 })
724 }
725 }
726 };
727
728 let outcome_json = match &outcome {
729 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
730 Err(e) => serde_json::json!({"error": e.to_string()}),
731 };
732 app_state
733 .record_access_log(
734 AccessLogOwner::Client(client_id.as_u32()),
735 protocol.protocol_name(),
736 None,
737 "injected_action",
738 action,
739 vec![outcome_json],
740 )
741 .await;
742
743 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
744 if let Err(e) = &outcome {
745 error!(
746 "WebSocket client {} injected action failed: {}",
747 client_id, e
748 );
749 let _ = status_tx.send(format!(
750 "[WARN] Client {client_id} injected action failed: {e}"
751 ));
752 }
753 let _ = status_tx.send("__UPDATE_UI__".to_string());
754 crate::client::command_support::reply(command, outcome);
755
756 if disconnect || write_error.is_some() {
757 let _ = sink.lock().await.close().await;
760 app_state.remove_client_handle(client_id).await;
764 let _ = status_tx.send("__UPDATE_UI__".to_string());
765 break;
766 }
767 }
768 }
769
770 #[allow(clippy::too_many_arguments)]
771 async fn handle_inbound(
772 frame: Inbound,
773 client_id: ClientId,
774 llm_client: &OllamaClient,
775 app_state: &Arc<AppState>,
776 status_tx: &mpsc::UnboundedSender<String>,
777 out_tx: &mpsc::UnboundedSender<WsOut>,
778 data: &Arc<Mutex<ClientData>>,
779 ) {
780 {
781 let mut d = data.lock().await;
782 if d.state == HandlerState::Processing {
783 d.queued.push_back(frame);
784 return;
785 }
786 d.state = HandlerState::Processing;
787 match d.pending.take() {
788 Some(pending) => match pending.merge(frame) {
789 Ok(merged) => d.queued.push_front(merged),
790 Err((held, new)) => {
791 d.queued.push_front(new);
792 d.queued.push_front(held);
793 }
794 },
795 None => d.queued.push_front(frame),
796 }
797 }
798
799 loop {
800 let next = {
801 let mut d = data.lock().await;
802 match d.queued.pop_front() {
803 Some(f) => f,
804 None => {
805 d.state = HandlerState::Idle;
806 return;
807 }
808 }
809 };
810
811 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
812 let mut d = data.lock().await;
813 d.state = HandlerState::Idle;
814 return;
815 };
816
817 let event = match &next {
818 Inbound::Text(text) => Event::new(
819 &WEBSOCKET_CLIENT_TEXT_MESSAGE_EVENT,
820 serde_json::json!({ "text": text, "message_bytes": text.len() }),
821 ),
822 Inbound::Binary(bytes) => {
823 let (payload, encoding) = encode_inbound_payload(bytes);
824 Event::new(
825 &WEBSOCKET_CLIENT_BINARY_MESSAGE_EVENT,
826 serde_json::json!({
827 "data": payload,
828 "encoding": encoding,
829 "message_bytes": bytes.len(),
830 }),
831 )
832 }
833 };
834
835 let protocol = WebSocketClientProtocol::for_connection(out_tx.clone());
836 let memory = { data.lock().await.memory.clone() };
837
838 match call_llm_for_client(
839 llm_client,
840 app_state,
841 client_id.to_string(),
842 &instruction,
843 &memory,
844 Some(&event),
845 &protocol,
846 status_tx,
847 )
848 .await
849 {
850 Ok(ClientLlmResult {
851 actions,
852 memory_updates,
853 }) => {
854 if let Some(mem) = memory_updates {
855 data.lock().await.memory = mem;
856 }
857 let mut should_wait = false;
858 let mut should_stop = false;
859 for action in actions {
860 match protocol.execute_action(action) {
861 Ok(ClientActionResult::WaitForMore) => should_wait = true,
862 Ok(ClientActionResult::Disconnect) => should_stop = true,
863 Ok(_) => {}
864 Err(e) => {
865 error!("WebSocket client action failed: {}", e);
866 let _ = status_tx
867 .send(format!("[CLIENT] WebSocket action failed: {e}"));
868 }
869 }
870 }
871 if should_wait {
872 let mut d = data.lock().await;
873 d.pending = Some(next);
874 d.state = HandlerState::Accumulating;
875 return;
876 }
877 if should_stop {
878 let mut d = data.lock().await;
879 d.state = HandlerState::Idle;
880 d.queued.clear();
881 return;
882 }
883 }
884 Err(e) => {
885 error!("LLM error for WebSocket client {}: {}", client_id, e);
886 let _ = status_tx.send(format!("[CLIENT] WebSocket LLM error: {e}"));
887 let mut d = data.lock().await;
888 d.state = HandlerState::Idle;
889 d.queued.clear();
890 return;
891 }
892 }
893 }
894 }
895
896 #[allow(clippy::too_many_arguments)]
897 async fn emit_closed(
898 client_id: ClientId,
899 code: u16,
900 reason: String,
901 llm_client: &OllamaClient,
902 app_state: &Arc<AppState>,
903 status_tx: &mpsc::UnboundedSender<String>,
904 out_tx: &mpsc::UnboundedSender<WsOut>,
905 data: &Arc<Mutex<ClientData>>,
906 ) {
907 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
908 return;
909 };
910 let event = Event::new(
911 &WEBSOCKET_CLIENT_CLOSED_EVENT,
912 serde_json::json!({ "code": code, "reason": reason }),
913 );
914 let protocol = WebSocketClientProtocol::for_connection(out_tx.clone());
915 let memory = { data.lock().await.memory.clone() };
916 match call_llm_for_client(
917 llm_client,
918 app_state,
919 client_id.to_string(),
920 &instruction,
921 &memory,
922 Some(&event),
923 &protocol,
924 status_tx,
925 )
926 .await
927 {
928 Ok(ClientLlmResult {
929 actions,
930 memory_updates,
931 }) => {
932 if !actions.is_empty() {
938 let _ = status_tx.send(format!(
939 "[CLIENT] WebSocket client {} is closed; {} action(s) from \
940 websocket_client_closed were not sent",
941 client_id,
942 actions.len()
943 ));
944 }
945 if let Some(mem) = memory_updates {
946 data.lock().await.memory = mem;
947 }
948 }
949 Err(e) => debug!("WebSocket client close handler failed: {}", e),
950 }
951 }
952}
953
954fn ws_out_to_message(out: WsOut) -> Message {
957 match out {
958 WsOut::Text(t) => Message::Text(t),
959 WsOut::Binary(b) => Message::Binary(b),
960 WsOut::Ping(p) => Message::Ping(p),
961 WsOut::Close { code, reason } => Message::Close(Some(CloseFrame {
962 code: CloseCode::from(code),
963 reason: reason.into(),
964 })),
965 }
966}