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}