1pub mod actions;
3
4pub use actions::ZookeeperClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::mpsc;
11use tracing::{debug, error, info};
12use zookeeper_async::{Acl, CreateMode, WatchedEvent, ZooKeeper};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::zookeeper::actions::{
16 ZOOKEEPER_CLIENT_CHILDREN_RECEIVED_EVENT, ZOOKEEPER_CLIENT_CONNECTED_EVENT,
17 ZOOKEEPER_CLIENT_DATA_RECEIVED_EVENT, ZOOKEEPER_CLIENT_OPERATION_COMPLETE_EVENT,
18};
19use crate::llm::actions::client_trait::{Client, ClientActionResult};
20use crate::llm::ollama_client::OllamaClient;
21use crate::protocol::{Event, EventType};
22use crate::state::app_state::AppState;
23use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
24use crate::state::{AccessLogOwner, ClientId, ClientStatus};
25
26const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
31
32const SESSION_TIMEOUT: Duration = Duration::from_secs(10);
36
37type SharedZk = Arc<ZooKeeper>;
43
44struct ZkOutcome {
46 detail: String,
47 event_type: &'static EventType,
48 event_data: serde_json::Value,
49}
50
51enum Applied {
54 Ran(String),
56 Disconnect,
58}
59
60pub struct ZookeeperClient;
62
63impl ZookeeperClient {
64 pub async fn connect_with_llm_actions(
66 remote_addr: String,
67 llm_client: OllamaClient,
68 app_state: Arc<AppState>,
69 status_tx: mpsc::UnboundedSender<String>,
70 client_id: ClientId,
71 ) -> Result<SocketAddr> {
72 info!(
73 "ZooKeeper client {} connecting to {}",
74 client_id, remote_addr
75 );
76
77 let zk: SharedZk = Arc::new(
94 tokio::time::timeout(
95 CONNECT_TIMEOUT,
96 ZooKeeper::connect(&remote_addr, SESSION_TIMEOUT, |_ev: WatchedEvent| {}),
97 )
98 .await
99 .with_context(|| {
100 format!(
101 "Timed out after {CONNECT_TIMEOUT:?} connecting to ZooKeeper at \
102 {remote_addr}. The client library retries a connect string indefinitely, \
103 so this bound is what turns an unreachable ensemble into a refusal."
104 )
105 })?
106 .with_context(|| format!("Failed to connect to ZooKeeper at {remote_addr}"))?,
107 );
108
109 info!(
110 "ZooKeeper client {} established a session with {}",
111 client_id, remote_addr
112 );
113
114 app_state
115 .with_client_mut(client_id, |client| {
116 client
117 .set_protocol_field("zookeeper_connected".to_string(), serde_json::json!(true));
118 client.set_protocol_field(
119 "connect_string".to_string(),
120 serde_json::json!(remote_addr),
121 );
122 })
123 .await;
124
125 app_state
127 .update_client_status(client_id, ClientStatus::Connected)
128 .await;
129 let _ = status_tx.send(format!("[CLIENT] ZooKeeper client {} connected", client_id));
130 let _ = status_tx.send("__UPDATE_UI__".to_string());
131
132 let command_rx =
141 crate::client::command_support::register_command_channel(&app_state, client_id).await;
142 let cmd_task = tokio::spawn(Self::command_loop(
143 command_rx,
144 client_id,
145 zk.clone(),
146 app_state.clone(),
147 llm_client.clone(),
148 status_tx.clone(),
149 ));
150 app_state.register_client_task(client_id, cmd_task).await;
151
152 let protocol = Arc::new(ZookeeperClientProtocol::new());
156 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
157 let event = Event::new(
158 &ZOOKEEPER_CLIENT_CONNECTED_EVENT,
159 serde_json::json!({
160 "remote_addr": remote_addr,
161 }),
162 );
163 let memory = app_state
164 .get_memory_for_client(client_id)
165 .await
166 .unwrap_or_default();
167
168 match call_llm_for_client(
169 &llm_client,
170 &app_state,
171 client_id.to_string(),
172 &instruction,
173 &memory,
174 Some(&event),
175 protocol.as_ref(),
176 &status_tx,
177 )
178 .await
179 {
180 Ok(result) => {
181 if let Some(mem) = result.memory_updates {
182 app_state.set_memory_for_client(client_id, mem).await;
183 }
184
185 for action in result.actions {
188 let executed = match protocol.execute_action(action) {
189 Ok(executed) => executed,
190 Err(e) => {
191 error!("ZooKeeper client {} rejected action: {}", client_id, e);
192 continue;
193 }
194 };
195 match Self::apply_action(
196 executed,
197 client_id,
198 &zk,
199 &app_state,
200 &llm_client,
201 &status_tx,
202 )
203 .await
204 {
205 Ok(Applied::Ran(detail)) => {
206 info!("ZooKeeper client {}: {}", client_id, detail)
207 }
208 Ok(Applied::Disconnect) => {
209 info!("ZooKeeper client {} disconnecting after connect", client_id);
210 let _ = zk.close().await;
211 app_state.remove_client_handle(client_id).await;
212 app_state
213 .update_client_status(client_id, ClientStatus::Disconnected)
214 .await;
215 let _ = status_tx.send("__UPDATE_UI__".to_string());
216 break;
217 }
218 Err(e) => {
219 error!("ZooKeeper client {} action failed: {}", client_id, e)
220 }
221 }
222 }
223 }
224 Err(e) => {
225 error!(
226 "ZooKeeper client {} LLM call failed on connect: {}",
227 client_id, e
228 );
229 }
230 }
231 }
232
233 Ok("127.0.0.1:0".parse().unwrap())
236 }
237
238 async fn command_loop(
246 mut command_rx: mpsc::Receiver<ClientCommand>,
247 client_id: ClientId,
248 zk: SharedZk,
249 app_state: Arc<AppState>,
250 llm_client: OllamaClient,
251 status_tx: mpsc::UnboundedSender<String>,
252 ) {
253 use crate::llm::actions::protocol_trait::Protocol;
254
255 let protocol = ZookeeperClientProtocol::new();
256
257 while let Some(command) = command_rx.recv().await {
258 let action = command.action.clone();
259 let outcome = match protocol.execute_action(action.clone()) {
260 Err(e) => Ok(ClientSendOutcome::Rejected {
261 error: e.to_string(),
262 }),
263 Ok(executed) => match Self::apply_action(
264 executed,
265 client_id,
266 &zk,
267 &app_state,
268 &llm_client,
269 &status_tx,
270 )
271 .await
272 {
273 Ok(Applied::Ran(detail)) => Ok(ClientSendOutcome::Executed { detail }),
277 Ok(Applied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
278 Err(e) => Err(e),
279 },
280 };
281
282 let outcome_json = match &outcome {
283 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
284 Err(e) => serde_json::json!({"error": e.to_string()}),
285 };
286 app_state
287 .record_access_log(
288 AccessLogOwner::Client(client_id.as_u32()),
289 protocol.protocol_name(),
290 None,
291 "injected_action",
292 action,
293 vec![outcome_json],
294 )
295 .await;
296
297 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
298 if let Err(e) = &outcome {
299 error!(
300 "ZooKeeper client {} injected action failed: {}",
301 client_id, e
302 );
303 let _ = status_tx.send(format!(
304 "[WARN] Client {} injected action failed: {}",
305 client_id, e
306 ));
307 }
308 let _ = status_tx.send("__UPDATE_UI__".to_string());
309 crate::client::command_support::reply(command, outcome);
310
311 if disconnect {
312 app_state
313 .update_client_status(client_id, ClientStatus::Disconnected)
314 .await;
315 break;
316 }
317 }
318
319 if let Err(e) = zk.close().await {
322 debug!(
323 "ZooKeeper client {} session close returned: {}",
324 client_id, e
325 );
326 }
327 app_state.remove_client_handle(client_id).await;
328 let _ = status_tx.send("__UPDATE_UI__".to_string());
329 info!("ZooKeeper client {} command loop ended", client_id);
330 }
331
332 async fn apply_action(
340 executed: ClientActionResult,
341 client_id: ClientId,
342 zk: &SharedZk,
343 app_state: &Arc<AppState>,
344 llm_client: &OllamaClient,
345 status_tx: &mpsc::UnboundedSender<String>,
346 ) -> Result<Applied> {
347 let outcome = match executed {
348 ClientActionResult::Custom { name, data } if name == "create_znode" => {
349 let path = required_str(&data, "path", "create_znode")?;
350 let value = required_str(&data, "data", "create_znode")?;
351 Self::perform_create(client_id, zk, path, value).await?
352 }
353 ClientActionResult::Custom { name, data } if name == "get_data" => {
354 let path = required_str(&data, "path", "get_data")?;
355 Self::perform_get_data(client_id, zk, path).await?
356 }
357 ClientActionResult::Custom { name, data } if name == "set_data" => {
358 let path = required_str(&data, "path", "set_data")?;
359 let value = required_str(&data, "data", "set_data")?;
360 Self::perform_set_data(client_id, zk, path, value).await?
361 }
362 ClientActionResult::Custom { name, data } if name == "delete_znode" => {
363 let path = required_str(&data, "path", "delete_znode")?;
364 Self::perform_delete(client_id, zk, path).await?
365 }
366 ClientActionResult::Custom { name, data } if name == "get_children" => {
367 let path = required_str(&data, "path", "get_children")?;
368 Self::perform_get_children(client_id, zk, path).await?
369 }
370 ClientActionResult::Disconnect => {
371 info!("ZooKeeper client {} disconnecting", client_id);
372 return Ok(Applied::Disconnect);
373 }
374 ClientActionResult::WaitForMore => {
375 return Ok(Applied::Ran("wait_for_more".to_string()))
376 }
377 ClientActionResult::NoAction => return Ok(Applied::Ran("no_action".to_string())),
378 ClientActionResult::Custom { name, .. } => {
381 return Ok(Applied::Ran(format!(
382 "custom result '{name}' is not handled by the ZooKeeper client"
383 )))
384 }
385 ClientActionResult::SendData(_) => {
386 return Ok(Applied::Ran(
387 "send_data has no meaning for a ZooKeeper client (zookeeper-async owns \
388 the socket)"
389 .to_string(),
390 ))
391 }
392 ClientActionResult::Multiple(_) => {
393 return Ok(Applied::Ran(
394 "multiple results are not produced by the ZooKeeper client".to_string(),
395 ))
396 }
397 };
398
399 let detail = outcome.detail.clone();
400 let state_clone = app_state.clone();
401 let llm_clone = llm_client.clone();
402 let status_clone = status_tx.clone();
403 let notify_handle = tokio::spawn(async move {
404 Self::notify_event(
405 client_id,
406 outcome.event_type,
407 outcome.event_data,
408 state_clone,
409 llm_clone,
410 status_clone,
411 )
412 .await;
413 });
414 app_state
415 .register_client_task(client_id, notify_handle)
416 .await;
417
418 Ok(Applied::Ran(detail))
419 }
420
421 async fn perform_create(
422 client_id: ClientId,
423 zk: &SharedZk,
424 path: String,
425 data: String,
426 ) -> Result<ZkOutcome> {
427 info!("ZooKeeper client {} creating znode {}", client_id, path);
428 let created = zk
429 .create(
430 &path,
431 data.clone().into_bytes(),
432 Acl::open_unsafe().clone(),
433 CreateMode::Persistent,
434 )
435 .await
436 .with_context(|| format!("create of {path} failed"))?;
437
438 Ok(ZkOutcome {
439 detail: format!("create_znode '{path}' -> created '{created}'"),
440 event_type: &ZOOKEEPER_CLIENT_OPERATION_COMPLETE_EVENT,
441 event_data: serde_json::json!({
442 "operation": "create",
443 "path": path,
444 "created_path": created,
445 }),
446 })
447 }
448
449 async fn perform_get_data(
450 client_id: ClientId,
451 zk: &SharedZk,
452 path: String,
453 ) -> Result<ZkOutcome> {
454 info!("ZooKeeper client {} reading znode {}", client_id, path);
455 let (bytes, stat) = zk
458 .get_data(&path, false)
459 .await
460 .with_context(|| format!("get_data of {path} failed"))?;
461 let text = String::from_utf8_lossy(&bytes).to_string();
462
463 Ok(ZkOutcome {
464 detail: format!(
465 "get_data '{}' -> {} byte(s), version {}",
466 path,
467 bytes.len(),
468 stat.version
469 ),
470 event_type: &ZOOKEEPER_CLIENT_DATA_RECEIVED_EVENT,
471 event_data: serde_json::json!({
472 "path": path,
473 "data": text,
474 "version": stat.version,
475 }),
476 })
477 }
478
479 async fn perform_set_data(
480 client_id: ClientId,
481 zk: &SharedZk,
482 path: String,
483 data: String,
484 ) -> Result<ZkOutcome> {
485 info!("ZooKeeper client {} writing znode {}", client_id, path);
486 let stat = zk
489 .set_data(&path, data.clone().into_bytes(), None)
490 .await
491 .with_context(|| format!("set_data of {path} failed"))?;
492
493 Ok(ZkOutcome {
494 detail: format!("set_data '{}' -> version {}", path, stat.version),
495 event_type: &ZOOKEEPER_CLIENT_OPERATION_COMPLETE_EVENT,
496 event_data: serde_json::json!({
497 "operation": "set_data",
498 "path": path,
499 "version": stat.version,
500 }),
501 })
502 }
503
504 async fn perform_delete(client_id: ClientId, zk: &SharedZk, path: String) -> Result<ZkOutcome> {
505 info!("ZooKeeper client {} deleting znode {}", client_id, path);
506 zk.delete(&path, None)
507 .await
508 .with_context(|| format!("delete of {path} failed"))?;
509
510 Ok(ZkOutcome {
511 detail: format!("delete_znode '{path}' -> deleted"),
512 event_type: &ZOOKEEPER_CLIENT_OPERATION_COMPLETE_EVENT,
513 event_data: serde_json::json!({
514 "operation": "delete",
515 "path": path,
516 }),
517 })
518 }
519
520 async fn perform_get_children(
521 client_id: ClientId,
522 zk: &SharedZk,
523 path: String,
524 ) -> Result<ZkOutcome> {
525 info!(
526 "ZooKeeper client {} listing children of {}",
527 client_id, path
528 );
529 let children = zk
530 .get_children(&path, false)
531 .await
532 .with_context(|| format!("get_children of {path} failed"))?;
533
534 Ok(ZkOutcome {
535 detail: format!("get_children '{}' -> {}", path, children.join(", ")),
536 event_type: &ZOOKEEPER_CLIENT_CHILDREN_RECEIVED_EVENT,
537 event_data: serde_json::json!({
538 "path": path,
539 "children": children,
540 }),
541 })
542 }
543
544 async fn notify_event(
546 client_id: ClientId,
547 event_type: &'static EventType,
548 event_data: serde_json::Value,
549 app_state: Arc<AppState>,
550 llm_client: OllamaClient,
551 status_tx: mpsc::UnboundedSender<String>,
552 ) {
553 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
554 return;
555 };
556
557 let event = Event::new(event_type, event_data);
558 let protocol = Arc::new(ZookeeperClientProtocol::new());
559 let memory = app_state
560 .get_memory_for_client(client_id)
561 .await
562 .unwrap_or_default();
563
564 match call_llm_for_client(
565 &llm_client,
566 &app_state,
567 client_id.to_string(),
568 &instruction,
569 &memory,
570 Some(&event),
571 protocol.as_ref(),
572 &status_tx,
573 )
574 .await
575 {
576 Ok(result) => {
577 if let Some(mem) = result.memory_updates {
578 app_state.set_memory_for_client(client_id, mem).await;
579 }
580 for action in result.actions {
585 if let Err(e) = app_state
586 .send_to_client(client_id, action, Duration::from_secs(60))
587 .await
588 {
589 debug!(
590 "ZooKeeper client {} could not apply a follow-up action: {}",
591 client_id, e
592 );
593 }
594 }
595 }
596 Err(e) => {
597 error!("LLM error for ZooKeeper client {}: {}", client_id, e);
598 }
599 }
600 }
601}
602
603fn required_str(data: &serde_json::Value, field: &str, verb: &str) -> Result<String> {
607 data.get(field)
608 .and_then(|v| v.as_str())
609 .map(|s| s.to_string())
610 .with_context(|| format!("Missing '{field}' in {verb}"))
611}