1pub mod actions;
39
40pub mod jetstream;
41pub use actions::NatsClientProtocol;
42
43use anyhow::{Context, Result};
44use std::collections::BTreeMap;
45use std::net::SocketAddr;
46use std::sync::Arc;
47use tokio::io::{AsyncReadExt, AsyncWriteExt};
48use tokio::net::TcpStream;
49use tokio::sync::{mpsc, Mutex};
50use tracing::{debug, error, info, trace, warn};
51
52use crate::client::llm_budget::call_llm_for_client;
53use crate::client::nats::actions::{
54 NatsClientProtocol as Proto, NATS_CLIENT_CONNECTED_EVENT, NATS_CLIENT_ERROR_RECEIVED_EVENT,
55 NATS_CLIENT_MESSAGE_RECEIVED_EVENT, NATS_CLIENT_PERMISSION_ERROR_EVENT,
56};
57use crate::llm::actions::client_trait::{Client, ClientActionResult};
58use crate::llm::ollama_client::OllamaClient;
59use crate::protocol::Event;
60use crate::state::app_state::AppState;
61use crate::state::{ClientId, ClientStatus};
62
63pub const MAX_CONTROL_LINE: usize = 65_536;
69
70pub const DEFAULT_MAX_PAYLOAD: usize = 1_048_576;
74
75pub const MAX_ACCEPTED_PAYLOAD: usize = 64 * 1_048_576;
78
79const MAX_BUFFERED_SLACK: usize = MAX_CONTROL_LINE + 16;
84
85const FRAME_QUEUE_CAPACITY: usize = 256;
91
92const INFO_TIMEOUT_SECS: u64 = 10;
98
99#[derive(Debug, Clone, PartialEq)]
109pub enum ServerFrame {
110 Info(serde_json::Value),
113 Message {
115 subject: String,
116 sid: String,
117 reply_to: Option<String>,
118 headers: BTreeMap<String, String>,
119 payload: Vec<u8>,
120 },
121 Ok,
123 Err(String),
125 Ping,
126 Pong,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ServerFrameError {
132 UnknownProtocolOperation,
133 MaximumControlLineExceeded,
134 MaximumPayloadViolation,
135 InvalidInfo,
136}
137
138impl std::fmt::Display for ServerFrameError {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.write_str(match self {
141 Self::UnknownProtocolOperation => "Unknown Protocol Operation",
142 Self::MaximumControlLineExceeded => "Maximum Control Line Exceeded",
143 Self::MaximumPayloadViolation => "Maximum Payload Violation",
144 Self::InvalidInfo => "Invalid INFO document",
145 })
146 }
147}
148
149fn parse_len(token: &str) -> Result<usize, ServerFrameError> {
150 token
151 .parse::<usize>()
152 .map_err(|_| ServerFrameError::UnknownProtocolOperation)
153}
154
155pub fn parse_server_frame(
164 buf: &[u8],
165 max_payload: usize,
166) -> Result<Option<(ServerFrame, usize)>, ServerFrameError> {
167 let skipped = blank_line_prefix_len(buf);
168 match parse_one_server_frame(&buf[skipped..], max_payload)? {
169 Some((frame, consumed)) => Ok(Some((frame, skipped + consumed))),
170 None => Ok(None),
171 }
172}
173
174pub fn blank_line_prefix_len(buf: &[u8]) -> usize {
186 let mut offset = 0;
187 while let Some(nl) = buf[offset..].iter().position(|&b| b == b'\n') {
188 let line_end = offset + nl;
189 let line = &buf[offset..line_end];
190 let line = line.strip_suffix(b"\r").unwrap_or(line);
191 if !line.iter().all(|b| b.is_ascii_whitespace()) {
192 break;
193 }
194 offset = line_end + 1;
195 }
196 offset
197}
198
199fn parse_one_server_frame(
201 buf: &[u8],
202 max_payload: usize,
203) -> Result<Option<(ServerFrame, usize)>, ServerFrameError> {
204 let Some(nl) = buf.iter().position(|&b| b == b'\n') else {
205 if buf.len() > MAX_CONTROL_LINE {
206 return Err(ServerFrameError::MaximumControlLineExceeded);
207 }
208 return Ok(None);
209 };
210 if nl > MAX_CONTROL_LINE {
211 return Err(ServerFrameError::MaximumControlLineExceeded);
212 }
213
214 let mut line_end = nl;
215 if line_end > 0 && buf[line_end - 1] == b'\r' {
216 line_end -= 1;
217 }
218 let after_line = nl + 1;
219
220 let line = std::str::from_utf8(&buf[..line_end])
221 .map_err(|_| ServerFrameError::UnknownProtocolOperation)?
222 .trim();
223
224 let mut parts = line.split_whitespace();
225 let verb = parts
226 .next()
227 .ok_or(ServerFrameError::UnknownProtocolOperation)?;
228 let args: Vec<&str> = parts.collect();
229
230 if verb.eq_ignore_ascii_case("PING") {
231 return Ok(Some((ServerFrame::Ping, after_line)));
232 }
233 if verb.eq_ignore_ascii_case("PONG") {
234 return Ok(Some((ServerFrame::Pong, after_line)));
235 }
236 if verb.eq_ignore_ascii_case("+OK") {
237 return Ok(Some((ServerFrame::Ok, after_line)));
238 }
239 if verb.eq_ignore_ascii_case("-ERR") {
240 let text = line[verb.len()..].trim();
242 let text = text
243 .strip_prefix('\'')
244 .and_then(|t| t.strip_suffix('\''))
245 .unwrap_or(text);
246 return Ok(Some((ServerFrame::Err(text.to_string()), after_line)));
247 }
248 if verb.eq_ignore_ascii_case("INFO") {
249 let json_start = line
252 .find(char::is_whitespace)
253 .ok_or(ServerFrameError::InvalidInfo)?;
254 let doc: serde_json::Value = serde_json::from_str(line[json_start..].trim())
255 .map_err(|_| ServerFrameError::InvalidInfo)?;
256 if !doc.is_object() {
257 return Err(ServerFrameError::InvalidInfo);
258 }
259 return Ok(Some((ServerFrame::Info(doc), after_line)));
260 }
261
262 let is_hmsg = verb.eq_ignore_ascii_case("HMSG");
263 if is_hmsg || verb.eq_ignore_ascii_case("MSG") {
264 let (subject, sid, reply_to, header_len, total_len) = if is_hmsg {
267 match args.len() {
268 4 => (
269 args[0],
270 args[1],
271 None,
272 parse_len(args[2])?,
273 parse_len(args[3])?,
274 ),
275 5 => (
276 args[0],
277 args[1],
278 Some(args[2].to_string()),
279 parse_len(args[3])?,
280 parse_len(args[4])?,
281 ),
282 _ => return Err(ServerFrameError::UnknownProtocolOperation),
283 }
284 } else {
285 match args.len() {
286 3 => (args[0], args[1], None, 0, parse_len(args[2])?),
287 4 => (
288 args[0],
289 args[1],
290 Some(args[2].to_string()),
291 0,
292 parse_len(args[3])?,
293 ),
294 _ => return Err(ServerFrameError::UnknownProtocolOperation),
295 }
296 };
297
298 if subject.is_empty() || sid.is_empty() {
299 return Err(ServerFrameError::UnknownProtocolOperation);
300 }
301 if header_len > total_len {
302 return Err(ServerFrameError::UnknownProtocolOperation);
303 }
304 if total_len > max_payload {
310 return Err(ServerFrameError::MaximumPayloadViolation);
311 }
312
313 let Some(body_end) = after_line.checked_add(total_len) else {
314 return Err(ServerFrameError::MaximumPayloadViolation);
315 };
316 if buf.len() <= body_end {
317 return Ok(None);
318 }
319 let terminator = match buf[body_end] {
320 b'\r' => {
321 if buf.len() < body_end + 2 {
322 return Ok(None);
323 }
324 if buf[body_end + 1] != b'\n' {
325 return Err(ServerFrameError::UnknownProtocolOperation);
326 }
327 2
328 }
329 b'\n' => 1,
330 _ => return Err(ServerFrameError::UnknownProtocolOperation),
331 };
332
333 let headers = if header_len > 0 {
334 parse_headers(&buf[after_line..after_line + header_len])
335 } else {
336 BTreeMap::new()
337 };
338 let payload = buf[after_line + header_len..body_end].to_vec();
339
340 return Ok(Some((
341 ServerFrame::Message {
342 subject: subject.to_string(),
343 sid: sid.to_string(),
344 reply_to,
345 headers,
346 payload,
347 },
348 body_end + terminator,
349 )));
350 }
351
352 Err(ServerFrameError::UnknownProtocolOperation)
353}
354
355pub fn parse_headers(block: &[u8]) -> BTreeMap<String, String> {
360 let mut out = BTreeMap::new();
361 let text = String::from_utf8_lossy(block);
362 for (index, line) in text.split('\n').enumerate() {
363 let line = line.trim_end_matches('\r');
364 if index == 0 {
365 let mut parts = line.splitn(3, ' ');
367 parts.next();
368 if let Some(code) = parts
369 .next()
370 .filter(|c| c.len() == 3 && c.bytes().all(|b| b.is_ascii_digit()))
371 {
372 out.insert("Status".to_string(), code.to_string());
373 if let Some(text) = parts.next().map(str::trim).filter(|t| !t.is_empty()) {
374 out.insert("Description".to_string(), text.to_string());
375 }
376 }
377 continue;
378 }
379 if line.trim().is_empty() {
380 continue;
381 }
382 if let Some((name, value)) = line.split_once(':') {
383 let name = name.trim();
384 if !name.is_empty() {
385 out.insert(name.to_string(), value.trim().to_string());
386 }
387 }
388 }
389 out
390}
391
392pub fn payload_for_event(payload: &[u8]) -> (String, &'static str) {
398 if payload
399 .iter()
400 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
401 {
402 (String::from_utf8_lossy(payload).to_string(), "utf8")
403 } else {
404 (hex::encode(payload), "hex")
405 }
406}
407
408pub fn classify_permission_error(message: &str) -> (&'static str, Option<String>) {
416 let lower = message.to_ascii_lowercase();
417 let operation = if lower.contains("publish") {
418 "publish"
419 } else if lower.contains("subscription") || lower.contains("subscribe") {
420 "subscription"
421 } else {
422 "unknown"
423 };
424
425 let subject = match (message.find('"'), message.rfind('"')) {
427 (Some(start), Some(end)) if end > start + 1 => Some(message[start + 1..end].to_string()),
428 _ => message
429 .rsplit(' ')
430 .next()
431 .filter(|token| !token.is_empty() && *token != "to")
432 .map(|token| token.trim_matches(['"', '\'']).to_string()),
433 };
434 (operation, subject)
435}
436
437fn is_permission_error(message: &str) -> bool {
440 message
441 .to_ascii_lowercase()
442 .contains("permissions violation")
443}
444
445#[derive(Debug, Clone)]
451pub struct NatsConnectOptions {
452 pub client_name: String,
454 pub verbose: bool,
456 pub subscribe_subjects: Vec<String>,
458}
459
460impl Default for NatsConnectOptions {
461 fn default() -> Self {
462 Self {
463 client_name: "netget".to_string(),
464 verbose: false,
465 subscribe_subjects: Vec::new(),
466 }
467 }
468}
469
470enum Flow {
472 Continue,
473 Disconnect,
474}
475
476pub struct NatsClient;
477
478impl NatsClient {
479 pub async fn connect_with_llm_actions(
485 remote_addr: String,
486 llm_client: OllamaClient,
487 app_state: Arc<AppState>,
488 status_tx: mpsc::UnboundedSender<String>,
489 client_id: ClientId,
490 options: NatsConnectOptions,
491 ) -> Result<SocketAddr> {
492 let stream = TcpStream::connect(&remote_addr)
493 .await
494 .with_context(|| format!("Failed to connect to NATS broker at {}", remote_addr))?;
495 let _ = stream.set_nodelay(true);
498
499 let local_addr = stream.local_addr()?;
500 let remote_sock_addr = stream.peer_addr()?;
501
502 let (mut read_half, write_half) = tokio::io::split(stream);
503 let write_half = Arc::new(Mutex::new(write_half));
504
505 let command_rx =
512 crate::client::command_support::register_command_channel(&app_state, client_id).await;
513
514 let mut buffer: Vec<u8> = Vec::with_capacity(8192);
516 let info = tokio::time::timeout(
517 std::time::Duration::from_secs(INFO_TIMEOUT_SECS),
518 Self::read_info(&mut read_half, &mut buffer),
519 )
520 .await
521 .map_err(|_| {
522 anyhow::anyhow!(
523 "No INFO greeting from {} within {}s. A NATS broker writes INFO immediately on \
524 accept, so this peer is not speaking NATS.",
525 remote_addr,
526 INFO_TIMEOUT_SECS
527 )
528 })??;
529
530 let max_payload = info
531 .get("max_payload")
532 .and_then(|v| v.as_u64())
533 .map(|v| (v as usize).clamp(DEFAULT_MAX_PAYLOAD, MAX_ACCEPTED_PAYLOAD))
534 .unwrap_or(DEFAULT_MAX_PAYLOAD);
535
536 let connect_doc = serde_json::json!({
537 "verbose": options.verbose,
538 "pedantic": false,
539 "tls_required": false,
540 "name": options.client_name,
541 "lang": "rust",
542 "version": env!("CARGO_PKG_VERSION"),
543 "protocol": 1,
544 "headers": info.get("headers").and_then(|v| v.as_bool()).unwrap_or(false),
545 "no_responders": false,
546 "echo": true,
547 });
548
549 let mut handshake = format!("CONNECT {}\r\n", connect_doc).into_bytes();
553 let mut subscriptions = Vec::new();
554 for (index, subject) in options.subscribe_subjects.iter().enumerate() {
555 let sid = (index + 1).to_string();
556 handshake.extend_from_slice(format!("SUB {} {}\r\n", subject, sid).as_bytes());
557 subscriptions.push(serde_json::json!({"subject": subject, "sid": sid}));
558 }
559 let jetstream = info
561 .get("jetstream")
562 .and_then(|v| v.as_bool())
563 .unwrap_or(false);
564 let js_inbox = jetstream::inbox_for(client_id);
565 if jetstream {
566 handshake.extend_from_slice(
567 format!("SUB {js_inbox}.> {}\r\n", jetstream::INBOX_SID).as_bytes(),
568 );
569 }
570 handshake.extend_from_slice(b"PING\r\n");
571 {
572 let mut guard = write_half.lock().await;
573 guard.write_all(&handshake).await?;
574 guard.flush().await?;
575 }
576
577 info!(
578 "NATS client {} connected to {} (local: {}), broker={} version={} max_payload={} \
579 startup_subscriptions={}",
580 client_id,
581 remote_sock_addr,
582 local_addr,
583 info.get("server_name")
584 .and_then(|v| v.as_str())
585 .unwrap_or("?"),
586 info.get("version").and_then(|v| v.as_str()).unwrap_or("?"),
587 max_payload,
588 subscriptions.len(),
589 );
590 app_state
591 .update_client_status(client_id, ClientStatus::Connected)
592 .await;
593 let _ = status_tx.send(format!(
594 "[CLIENT] NATS client {} connected to {} ({} startup subscription(s))",
595 client_id,
596 remote_sock_addr,
597 subscriptions.len()
598 ));
599 let _ = status_tx.send("__UPDATE_UI__".to_string());
600
601 let (frame_tx, frame_rx) = mpsc::channel::<ServerFrame>(FRAME_QUEUE_CAPACITY);
603
604 let reader_state = app_state.clone();
605 let reader_status_tx = status_tx.clone();
606 let reader_write_half = write_half.clone();
607 let reader_handle = tokio::spawn(async move {
608 Self::run_reader(
609 read_half,
610 reader_write_half,
611 buffer,
612 max_payload,
613 frame_tx,
614 client_id,
615 reader_state,
616 reader_status_tx,
617 )
618 .await;
619 });
620 let reader_abort = reader_handle.abort_handle();
621 app_state
622 .register_client_task(client_id, reader_handle)
623 .await;
624
625 let connected_event = Event::new(
627 &NATS_CLIENT_CONNECTED_EVENT,
628 serde_json::json!({
629 "remote_addr": remote_sock_addr.to_string(),
630 "server_name": info.get("server_name").and_then(|v| v.as_str()).unwrap_or(""),
631 "server_id": info.get("server_id").and_then(|v| v.as_str()).unwrap_or(""),
632 "version": info.get("version").and_then(|v| v.as_str()).unwrap_or(""),
633 "max_payload": max_payload,
634 "headers": info.get("headers").and_then(|v| v.as_bool()).unwrap_or(false),
635 "auth_required": info.get("auth_required").and_then(|v| v.as_bool()).unwrap_or(false),
636 "tls_required": info.get("tls_required").and_then(|v| v.as_bool()).unwrap_or(false),
637 "subscriptions": subscriptions,
638 "jetstream": jetstream,
639 "info": info,
640 }),
641 );
642
643 let dispatcher_state = app_state.clone();
644 let dispatcher_handle = tokio::spawn(async move {
645 Self::run_dispatcher(
646 write_half,
647 frame_rx,
648 command_rx,
649 connected_event,
650 client_id,
651 llm_client,
652 dispatcher_state,
653 status_tx,
654 reader_abort,
655 js_inbox,
656 )
657 .await;
658 });
659 app_state
660 .register_client_task(client_id, dispatcher_handle)
661 .await;
662
663 Ok(local_addr)
664 }
665
666 async fn read_info(
672 read_half: &mut tokio::io::ReadHalf<TcpStream>,
673 buffer: &mut Vec<u8>,
674 ) -> Result<serde_json::Value> {
675 let mut chunk = vec![0u8; 8192];
676 loop {
677 match parse_server_frame(buffer, DEFAULT_MAX_PAYLOAD) {
678 Ok(Some((ServerFrame::Info(doc), consumed))) => {
679 buffer.drain(..consumed);
680 return Ok(doc);
681 }
682 Ok(Some((other, _))) => {
683 return Err(anyhow::anyhow!(
684 "Expected INFO as the first NATS frame, got {:?}",
685 other
686 ))
687 }
688 Ok(None) => {}
689 Err(e) => return Err(anyhow::anyhow!("Malformed NATS greeting: {}", e)),
690 }
691
692 let n = read_half.read(&mut chunk).await?;
693 if n == 0 {
694 return Err(anyhow::anyhow!(
695 "Broker closed the connection before sending INFO"
696 ));
697 }
698 buffer.extend_from_slice(&chunk[..n]);
699 }
700 }
701
702 #[allow(clippy::too_many_arguments)]
704 async fn run_reader(
705 mut read_half: tokio::io::ReadHalf<TcpStream>,
706 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
707 mut buffer: Vec<u8>,
708 max_payload: usize,
709 frame_tx: mpsc::Sender<ServerFrame>,
710 client_id: ClientId,
711 app_state: Arc<AppState>,
712 status_tx: mpsc::UnboundedSender<String>,
713 ) {
714 let mut chunk = vec![0u8; 8192];
715 'outer: loop {
716 loop {
718 let blank = blank_line_prefix_len(&buffer);
723 if blank > 0 {
724 buffer.drain(..blank);
725 }
726 if buffer.len() > max_payload.saturating_add(MAX_BUFFERED_SLACK) {
727 error!(
729 "NATS client {} buffered {} bytes with no complete frame; closing",
730 client_id,
731 buffer.len()
732 );
733 let _ = status_tx.send(format!(
734 "[CLIENT] ✖ NATS client {} broker sent an unframeable stream",
735 client_id
736 ));
737 break 'outer;
738 }
739 match parse_server_frame(&buffer, max_payload) {
740 Ok(Some((frame, consumed))) => {
741 buffer.drain(..consumed);
742 match frame {
743 ServerFrame::Ping => {
744 let mut guard = write_half.lock().await;
746 if guard.write_all(b"PONG\r\n").await.is_err()
747 || guard.flush().await.is_err()
748 {
749 warn!(
750 "NATS client {} could not answer PING; connection is \
751 going away",
752 client_id
753 );
754 break 'outer;
755 }
756 trace!("NATS client {} PING -> PONG", client_id);
757 }
758 ServerFrame::Pong => {
759 trace!("NATS client {} received PONG", client_id);
760 }
761 ServerFrame::Ok => {
762 trace!("NATS client {} received +OK", client_id);
763 }
764 ServerFrame::Info(doc) => {
765 debug!("NATS client {} received updated INFO: {}", client_id, doc);
769 }
770 decision => {
771 if frame_tx.send(decision).await.is_err() {
772 debug!(
773 "NATS client {} dispatcher is gone; reader stopping",
774 client_id
775 );
776 break 'outer;
777 }
778 }
779 }
780 }
781 Ok(None) => break,
782 Err(e) => {
783 error!(
784 "NATS client {} could not frame the broker's output ({}); closing",
785 client_id, e
786 );
787 let _ = status_tx.send(format!(
788 "[CLIENT] ✖ NATS client {} protocol error: {}",
789 client_id, e
790 ));
791 break 'outer;
792 }
793 }
794 }
795
796 match read_half.read(&mut chunk).await {
797 Ok(0) => {
798 info!("NATS client {} broker closed the connection", client_id);
799 break;
800 }
801 Ok(n) => buffer.extend_from_slice(&chunk[..n]),
802 Err(e) => {
803 error!("NATS client {} read error: {}", client_id, e);
804 app_state
805 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
806 .await;
807 let _ = status_tx.send("__UPDATE_UI__".to_string());
808 break;
809 }
810 }
811 }
812
813 drop(frame_tx);
815 app_state
816 .update_client_status(client_id, ClientStatus::Disconnected)
817 .await;
818 let _ = status_tx.send(format!("[CLIENT] NATS client {} disconnected", client_id));
819 let _ = status_tx.send("__UPDATE_UI__".to_string());
820 }
821
822 #[allow(clippy::too_many_arguments)]
824 async fn run_dispatcher(
825 write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
826 mut frame_rx: mpsc::Receiver<ServerFrame>,
827 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
828 connected_event: Event,
829 client_id: ClientId,
830 llm_client: OllamaClient,
831 app_state: Arc<AppState>,
832 status_tx: mpsc::UnboundedSender<String>,
833 reader_abort: tokio::task::AbortHandle,
834 js_inbox: String,
835 ) {
836 let protocol = Proto::with_inbox(js_inbox.clone());
837 let mut memory = String::new();
838
839 let mut flow = Self::ask_and_execute(
842 &protocol,
843 &write_half,
844 connected_event,
845 &mut memory,
846 client_id,
847 &llm_client,
848 &app_state,
849 &status_tx,
850 )
851 .await;
852
853 while matches!(flow, Flow::Continue) {
854 tokio::select! {
855 frame = frame_rx.recv() => {
856 let Some(frame) = frame else { break };
857 let Some(event) = Self::event_for(frame, client_id, &js_inbox) else { continue };
858 flow = Self::ask_and_execute(
859 &protocol,
860 &write_half,
861 event,
862 &mut memory,
863 client_id,
864 &llm_client,
865 &app_state,
866 &status_tx,
867 )
868 .await;
869 }
870 command = command_rx.recv() => {
871 let Some(command) = command else { break };
872 let disconnect = crate::client::command_support::handle_stream_client_command(
873 &protocol,
874 &write_half,
875 command,
876 client_id,
877 &app_state,
878 &status_tx,
879 )
880 .await;
881 if disconnect {
882 flow = Flow::Disconnect;
883 }
884 }
885 }
886 }
887
888 {
892 let mut guard = write_half.lock().await;
893 let _ = guard.shutdown().await;
894 }
895 reader_abort.abort();
896
897 app_state
898 .update_client_status(client_id, ClientStatus::Disconnected)
899 .await;
900 app_state.remove_client_handle(client_id).await;
903 let _ = status_tx.send(format!("[CLIENT] NATS client {} session ended", client_id));
904 let _ = status_tx.send("__UPDATE_UI__".to_string());
905 }
906
907 fn event_for(frame: ServerFrame, client_id: ClientId, js_inbox: &str) -> Option<Event> {
912 match frame {
913 ServerFrame::Message {
914 subject,
915 sid,
916 reply_to,
917 headers,
918 payload,
919 } => {
920 if sid == jetstream::INBOX_SID {
921 if let Some(event) = jetstream::event_for(
922 js_inbox,
923 &subject,
924 reply_to.as_deref(),
925 &headers,
926 &payload,
927 ) {
928 return Some(event);
929 }
930 }
931 let (payload, encoding) = payload_for_event(&payload);
932 Some(Event::new(
933 &NATS_CLIENT_MESSAGE_RECEIVED_EVENT,
934 serde_json::json!({
935 "subject": subject,
936 "sid": sid,
937 "reply_to": reply_to,
938 "payload": payload,
939 "payload_encoding": encoding,
940 "headers": headers,
941 }),
942 ))
943 }
944 ServerFrame::Err(message) => {
945 if is_permission_error(&message) {
946 let (operation, subject) = classify_permission_error(&message);
947 warn!(
948 "NATS client {} was denied: {} (operation={})",
949 client_id, message, operation
950 );
951 Some(Event::new(
952 &NATS_CLIENT_PERMISSION_ERROR_EVENT,
953 serde_json::json!({
954 "message": message,
955 "operation": operation,
956 "subject": subject,
957 }),
958 ))
959 } else {
960 warn!("NATS client {} received -ERR: {}", client_id, message);
961 Some(Event::new(
962 &NATS_CLIENT_ERROR_RECEIVED_EVENT,
963 serde_json::json!({
964 "message": message,
965 "fatal": true,
968 }),
969 ))
970 }
971 }
972 other => {
973 debug!(
974 "NATS client {} dispatcher saw a frame the reader owns: {:?}",
975 client_id, other
976 );
977 None
978 }
979 }
980 }
981
982 #[allow(clippy::too_many_arguments)]
989 async fn ask_and_execute(
990 protocol: &Proto,
991 write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
992 event: Event,
993 memory: &mut String,
994 client_id: ClientId,
995 llm_client: &OllamaClient,
996 app_state: &Arc<AppState>,
997 status_tx: &mpsc::UnboundedSender<String>,
998 ) -> Flow {
999 let instruction = app_state
1000 .get_instruction_for_client(client_id)
1001 .await
1002 .unwrap_or_default();
1003
1004 let outcome = call_llm_for_client(
1005 llm_client,
1006 app_state,
1007 client_id.to_string(),
1008 &instruction,
1009 memory,
1010 Some(&event),
1011 protocol,
1012 status_tx,
1013 )
1014 .await;
1015
1016 let result = match outcome {
1017 Ok(result) => result,
1018 Err(e) => {
1019 error!(
1024 "NATS client {} could not answer '{}': {} (decision=fail_closed_llm_error)",
1025 client_id,
1026 event.id(),
1027 e
1028 );
1029 let _ = status_tx.send(format!(
1030 "[CLIENT] ✖ NATS client {} could not answer '{}'",
1031 client_id,
1032 event.id()
1033 ));
1034 return Flow::Continue;
1035 }
1036 };
1037
1038 if let Some(updated) = result.memory_updates {
1039 *memory = updated;
1040 }
1041
1042 if result.actions.is_empty() {
1043 debug!(
1044 "NATS client {} answered '{}' with no actions (decision=model_no_actions)",
1045 client_id,
1046 event.id()
1047 );
1048 }
1049
1050 for action in result.actions {
1051 match Self::apply_action(protocol, write_half, action, client_id).await {
1052 Flow::Disconnect => return Flow::Disconnect,
1053 Flow::Continue => {}
1054 }
1055 }
1056 Flow::Continue
1057 }
1058
1059 async fn apply_action(
1061 protocol: &Proto,
1062 write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
1063 action: serde_json::Value,
1064 client_id: ClientId,
1065 ) -> Flow {
1066 let executed = match protocol.execute_action(action.clone()) {
1067 Ok(executed) => executed,
1068 Err(e) => {
1069 warn!(
1070 "NATS client {} rejected its own model's action {}: {}",
1071 client_id, action, e
1072 );
1073 return Flow::Continue;
1074 }
1075 };
1076
1077 let results = match executed {
1079 ClientActionResult::Multiple(results) => results,
1080 single => vec![single],
1081 };
1082
1083 let mut flow = Flow::Continue;
1084 for result in results {
1085 match result {
1086 ClientActionResult::SendData(bytes) => {
1087 let mut guard = write_half.lock().await;
1088 if let Err(e) = guard.write_all(&bytes).await {
1089 error!("NATS client {} write failed: {}", client_id, e);
1090 return Flow::Disconnect;
1091 }
1092 if let Err(e) = guard.flush().await {
1093 error!("NATS client {} flush failed: {}", client_id, e);
1094 return Flow::Disconnect;
1095 }
1096 trace!(
1097 "NATS client {} sent {} bytes: {}",
1098 client_id,
1099 bytes.len(),
1100 String::from_utf8_lossy(&bytes).escape_debug()
1101 );
1102 }
1103 ClientActionResult::Disconnect => {
1104 info!(
1105 "NATS client {} disconnecting on the model's request",
1106 client_id
1107 );
1108 flow = Flow::Disconnect;
1109 }
1110 ClientActionResult::WaitForMore => {
1111 trace!("NATS client {} is waiting for more traffic", client_id);
1112 }
1113 ClientActionResult::NoAction => {}
1114 ClientActionResult::Custom { name, .. } => {
1115 warn!(
1116 "NATS client {} produced an unexpected custom result '{}'; this \
1117 protocol's actions are all SendData",
1118 client_id, name
1119 );
1120 }
1121 ClientActionResult::Multiple(_) => {
1122 warn!(
1123 "NATS client {} produced a nested Multiple; ignored",
1124 client_id
1125 );
1126 }
1127 }
1128 }
1129 flow
1130 }
1131}