Skip to main content

netget/client/tcp/
mod.rs

1//! TCP client implementation
2pub mod actions;
3
4pub use actions::TcpClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use tokio::sync::{mpsc, Mutex};
12use tracing::{error, info, trace};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::tcp::actions::{TCP_CLIENT_CONNECTED_EVENT, TCP_CLIENT_DATA_RECEIVED_EVENT};
16use crate::llm::ollama_client::OllamaClient;
17use crate::llm::ClientLlmResult;
18use crate::logging::patterns;
19use crate::protocol::Event;
20use crate::state::app_state::AppState;
21use crate::state::{ClientId, ClientStatus};
22
23/// Connection state for LLM processing
24#[derive(Debug, Clone, PartialEq)]
25enum ConnectionState {
26    Idle,
27    Processing,
28    Accumulating,
29}
30
31/// Per-client data for LLM handling
32struct ClientData {
33    state: ConnectionState,
34    queued_data: Vec<u8>,
35    memory: String,
36}
37
38/// TCP client that connects to a remote TCP server
39pub struct TcpClient;
40
41impl TcpClient {
42    /// Connect to a TCP server with integrated LLM actions
43    pub async fn connect_with_llm_actions(
44        remote_addr: String,
45        llm_client: OllamaClient,
46        app_state: Arc<AppState>,
47        status_tx: mpsc::UnboundedSender<String>,
48        client_id: ClientId,
49    ) -> Result<SocketAddr> {
50        // Resolve and connect
51        let stream = TcpStream::connect(&remote_addr)
52            .await
53            .context(format!("Failed to connect to {}", remote_addr))?;
54
55        let local_addr = stream.local_addr()?;
56        let remote_sock_addr = stream.peer_addr()?;
57
58        info!(
59            "TCP client {} connected to {} (local: {})",
60            client_id, remote_sock_addr, local_addr
61        );
62
63        // Update client state
64        app_state
65            .update_client_status(client_id, ClientStatus::Connected)
66            .await;
67        let _ = status_tx.send(format!("[CLIENT] TCP client {} connected", client_id));
68        let _ = status_tx.send("__UPDATE_UI__".to_string());
69
70        // Split stream
71        let (mut read_half, write_half) = tokio::io::split(stream);
72        let write_half_arc = Arc::new(Mutex::new(write_half));
73        let write_half_for_connected = write_half_arc.clone();
74
75        // Initialize client data
76        let client_data = Arc::new(Mutex::new(ClientData {
77            state: ConnectionState::Idle,
78            queued_data: Vec::new(),
79            memory: String::new(),
80        }));
81
82        // Command channel: lets the dashboard (and any programmatic caller)
83        // inject actions into this loop via AppState::send_to_client.
84        //
85        // Registered BEFORE the connected event is handled: a `manual` routing
86        // rule can park that event at the dashboard for minutes, and until
87        // registration the UI reports "no command channel" — reading as a
88        // protocol limitation when it is only a queue. Registered here, a send
89        // during the park waits in the channel instead of being refused.
90        let mut command_rx =
91            crate::client::command_support::register_command_channel(&app_state, client_id).await;
92
93        // Call LLM with tcp_connected event
94        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
95            let event = Event::new(
96                &TCP_CLIENT_CONNECTED_EVENT,
97                serde_json::json!({
98                    "remote_addr": remote_sock_addr.to_string(),
99                }),
100            );
101
102            match call_llm_for_client(
103                &llm_client,
104                &app_state,
105                client_id.to_string(),
106                &instruction,
107                &client_data.lock().await.memory,
108                Some(&event),
109                &crate::client::tcp::actions::TcpClientProtocol,
110                &status_tx,
111            )
112            .await
113            {
114                Ok(result) => {
115                    // Update memory if provided
116                    if let Some(new_memory) = result.memory_updates {
117                        client_data.lock().await.memory = new_memory;
118                    }
119
120                    // Execute actions from LLM response using the proper execute_action method
121                    use crate::llm::actions::client_trait::Client;
122                    let protocol = crate::client::tcp::actions::TcpClientProtocol::new();
123                    for action in result.actions {
124                        match protocol.execute_action(action) {
125                            Ok(
126                                crate::llm::actions::client_trait::ClientActionResult::SendData(
127                                    bytes,
128                                ),
129                            ) => {
130                                let mut write_guard = write_half_for_connected.lock().await;
131                                if let Err(e) = write_guard.write_all(&bytes).await {
132                                    error!("Failed to send data after connect: {}", e);
133                                } else if let Err(e) = write_guard.flush().await {
134                                    error!("Failed to flush after connect: {}", e);
135                                } else {
136                                    info!("Sent {} {}", bytes.len(), patterns::TCP_CLIENT_SENT);
137                                }
138                            }
139                            Ok(
140                                crate::llm::actions::client_trait::ClientActionResult::Disconnect,
141                            ) => {
142                                info!("LLM requested disconnect after connect");
143                                return Ok(local_addr);
144                            }
145                            Ok(
146                                crate::llm::actions::client_trait::ClientActionResult::WaitForMore,
147                            ) => {
148                                // Just wait for data
149                            }
150                            Ok(_) => {
151                                // Other action results
152                            }
153                            Err(e) => {
154                                error!("Failed to execute action after connect: {}", e);
155                            }
156                        }
157                    }
158                }
159                Err(e) => {
160                    error!("LLM error on tcp_connected event: {}", e);
161                }
162            }
163        }
164
165        // Spawn read loop. The handle is registered with AppState so that
166        // stop_client / remove_client can abort it — dropping a JoinHandle only
167        // detaches the task, leaving the socket open and the LLM being called.
168        let task_registrar = app_state.clone();
169        let handle = tokio::spawn(async move {
170            info!("TCP client {} read loop started", client_id);
171            let mut buffer = vec![0u8; 8192];
172
173            'read_loop: loop {
174                let read_result = tokio::select! {
175                    read = read_half.read(&mut buffer) => read,
176                    Some(cmd) = command_rx.recv() => {
177                        let disconnect = crate::client::command_support::handle_stream_client_command(
178                            &crate::client::tcp::actions::TcpClientProtocol,
179                            &write_half_arc,
180                            cmd,
181                            client_id,
182                            &app_state,
183                            &status_tx,
184                        )
185                        .await;
186                        if disconnect {
187                            app_state
188                                .update_client_status(client_id, ClientStatus::Disconnected)
189                                .await;
190                            let _ = status_tx.send(format!(
191                                "[CLIENT] TCP client {} disconnected (injected action)",
192                                client_id
193                            ));
194                            let _ = status_tx.send("__UPDATE_UI__".to_string());
195                            break;
196                        }
197                        continue;
198                    }
199                };
200                match read_result {
201                    Ok(0) => {
202                        info!(
203                            "TCP client {} {}",
204                            client_id,
205                            patterns::TCP_CLIENT_DISCONNECTED
206                        );
207                        app_state
208                            .update_client_status(client_id, ClientStatus::Disconnected)
209                            .await;
210                        let _ = status_tx
211                            .send(format!("[CLIENT] TCP client {} disconnected", client_id));
212                        let _ = status_tx.send("__UPDATE_UI__".to_string());
213                        break;
214                    }
215                    Ok(n) => {
216                        let data = buffer[..n].to_vec();
217                        info!(
218                            "TCP client {} received {} {}",
219                            client_id,
220                            n,
221                            patterns::TCP_CLIENT_RECEIVED
222                        );
223                        trace!("TCP client {} received {} bytes", client_id, n);
224
225                        // Handle data with LLM
226                        let mut client_data_lock = client_data.lock().await;
227
228                        match client_data_lock.state {
229                            ConnectionState::Idle => {
230                                // Process immediately
231                                client_data_lock.state = ConnectionState::Processing;
232                                drop(client_data_lock);
233
234                                // Call LLM
235                                if let Some(instruction) =
236                                    app_state.get_instruction_for_client(client_id).await
237                                {
238                                    let protocol = Arc::new(
239                                        crate::client::tcp::actions::TcpClientProtocol::new(),
240                                    );
241                                    let event = Event::new(
242                                        &TCP_CLIENT_DATA_RECEIVED_EVENT,
243                                        serde_json::json!({
244                                            "data_hex": hex::encode(&data),
245                                            "data_length": data.len(),
246                                        }),
247                                    );
248
249                                    match call_llm_for_client(
250                                        &llm_client,
251                                        &app_state,
252                                        client_id.to_string(),
253                                        &instruction,
254                                        &client_data.lock().await.memory,
255                                        Some(&event),
256                                        protocol.as_ref(),
257                                        &status_tx,
258                                    )
259                                    .await
260                                    {
261                                        Ok(ClientLlmResult {
262                                            actions,
263                                            memory_updates,
264                                        }) => {
265                                            // Update memory
266                                            if let Some(mem) = memory_updates {
267                                                client_data.lock().await.memory = mem;
268                                            }
269
270                                            // Execute actions
271                                            for action in actions {
272                                                use crate::llm::actions::client_trait::Client;
273                                                match protocol.as_ref().execute_action(action) {
274                                                    Ok(crate::llm::actions::client_trait::ClientActionResult::SendData(bytes)) => {
275                                                        let mut write_guard = write_half_arc.lock().await;
276                                                        if write_guard.write_all(&bytes).await.is_ok() {
277                                                            if write_guard.flush().await.is_ok() {
278                                                                trace!("TCP client {} sent {} bytes", client_id, bytes.len());
279                                                            }
280                                                        }
281                                                    }
282                                                    Ok(crate::llm::actions::client_trait::ClientActionResult::Disconnect) => {
283                                                        info!("TCP client {} disconnecting", client_id);
284                                                        app_state
285                                                            .update_client_status(client_id, ClientStatus::Disconnected)
286                                                            .await;
287                                                        let _ = status_tx.send(format!(
288                                                            "[CLIENT] TCP client {} disconnected",
289                                                            client_id
290                                                        ));
291                                                        let _ = status_tx.send("__UPDATE_UI__".to_string());
292                                                        break 'read_loop;
293                                                    }
294                                                    _ => {}
295                                                }
296                                            }
297                                        }
298                                        Err(e) => {
299                                            error!("LLM error for TCP client {}: {}", client_id, e);
300                                        }
301                                    }
302                                }
303
304                                // Process queued data if any
305                                let mut client_data_lock = client_data.lock().await;
306                                if !client_data_lock.queued_data.is_empty() {
307                                    client_data_lock.queued_data.clear();
308                                }
309                                client_data_lock.state = ConnectionState::Idle;
310                            }
311                            ConnectionState::Processing => {
312                                // Queue data
313                                client_data_lock.queued_data.extend_from_slice(&data);
314                                client_data_lock.state = ConnectionState::Accumulating;
315                            }
316                            ConnectionState::Accumulating => {
317                                // Continue queuing
318                                client_data_lock.queued_data.extend_from_slice(&data);
319                            }
320                        }
321                    }
322                    Err(e) => {
323                        error!("TCP client {} read error: {}", client_id, e);
324                        app_state
325                            .update_client_status(client_id, ClientStatus::Error(e.to_string()))
326                            .await;
327                        let _ = status_tx.send("__UPDATE_UI__".to_string());
328                        break;
329                    }
330                }
331            }
332            // The loop owns the only receiver; dropping the registered handle
333            // makes later send_to_client calls fail fast instead of timing out.
334            app_state.remove_client_handle(client_id).await;
335        });
336        task_registrar.register_client_task(client_id, handle).await;
337
338        Ok(local_addr)
339    }
340}