1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{anyhow, Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static ZOOKEEPER_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "zookeeper_connected",
18 "ZooKeeper client connected to server",
19 json!({
20 "type": "wait_for_more"
21 }),
22 )
23});
24
25pub static ZOOKEEPER_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
26 EventType::new(
27 "zookeeper_data_received",
28 "ZooKeeper client received data from server",
29 json!({
30 "type": "wait_for_more"
31 }),
32 )
33 .with_parameters(vec![
34 Parameter {
35 name: "path".to_string(),
36 type_hint: "string".to_string(),
37 description: "ZNode path".to_string(),
38 required: true,
39 },
40 Parameter {
41 name: "data".to_string(),
42 type_hint: "string".to_string(),
43 description: "ZNode data".to_string(),
44 required: true,
45 },
46 Parameter {
47 name: "version".to_string(),
48 type_hint: "integer".to_string(),
49 description: "Data version".to_string(),
50 required: true,
51 },
52 ])
53});
54
55pub static ZOOKEEPER_CLIENT_OPERATION_COMPLETE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
63 EventType::new(
64 "zookeeper_operation_complete",
65 "A ZooKeeper write (create/set_data/delete) completed",
66 json!({
67 "type": "wait_for_more"
68 }),
69 )
70 .with_parameters(vec![
71 Parameter {
72 name: "operation".to_string(),
73 type_hint: "string".to_string(),
74 description: "The verb that completed: create, set_data or delete".to_string(),
75 required: true,
76 },
77 Parameter {
78 name: "path".to_string(),
79 type_hint: "string".to_string(),
80 description: "ZNode path the operation was requested for".to_string(),
81 required: true,
82 },
83 Parameter {
84 name: "created_path".to_string(),
85 type_hint: "string".to_string(),
86 description: "Path ZooKeeper actually created (create only; a sequential node gets a suffix)"
87 .to_string(),
88 required: false,
89 },
90 Parameter {
91 name: "version".to_string(),
92 type_hint: "integer".to_string(),
93 description: "Data version after the write (set_data only)".to_string(),
94 required: false,
95 },
96 ])
97});
98
99pub static ZOOKEEPER_CLIENT_CHILDREN_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
100 EventType::new(
101 "zookeeper_children_received",
102 "ZooKeeper client received children list",
103 json!({
104 "type": "disconnect"
105 }),
106 )
107 .with_parameters(vec![
108 Parameter {
109 name: "path".to_string(),
110 type_hint: "string".to_string(),
111 description: "ZNode path".to_string(),
112 required: true,
113 },
114 Parameter {
115 name: "children".to_string(),
116 type_hint: "array".to_string(),
117 description: "Array of child node names".to_string(),
118 required: true,
119 },
120 ])
121});
122
123pub struct ZookeeperClientProtocol;
125
126impl ZookeeperClientProtocol {
127 pub fn new() -> Self {
128 Self
129 }
130}
131
132impl Protocol for ZookeeperClientProtocol {
134 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
135 vec![
136 ActionDefinition {
137 name: "create_znode".to_string(),
138 description: "Create a ZNode at the specified path".to_string(),
139 parameters: vec![
140 Parameter {
141 name: "path".to_string(),
142 type_hint: "string".to_string(),
143 description: "ZNode path".to_string(),
144 required: true,
145 },
146 Parameter {
147 name: "data".to_string(),
148 type_hint: "string".to_string(),
149 description: "ZNode data".to_string(),
150 required: true,
151 },
152 ],
153 example: json!({
154 "type": "create_znode",
155 "path": "/myapp/config",
156 "data": "configuration_data"
157 }),
158 log_template: None,
159 },
160 ActionDefinition {
161 name: "get_data".to_string(),
162 description: "Get data from a ZNode".to_string(),
163 parameters: vec![Parameter {
164 name: "path".to_string(),
165 type_hint: "string".to_string(),
166 description: "ZNode path".to_string(),
167 required: true,
168 }],
169 example: json!({
170 "type": "get_data",
171 "path": "/myapp/config"
172 }),
173 log_template: None,
174 },
175 ActionDefinition {
176 name: "set_data".to_string(),
177 description: "Set data for a ZNode".to_string(),
178 parameters: vec![
179 Parameter {
180 name: "path".to_string(),
181 type_hint: "string".to_string(),
182 description: "ZNode path".to_string(),
183 required: true,
184 },
185 Parameter {
186 name: "data".to_string(),
187 type_hint: "string".to_string(),
188 description: "New ZNode data".to_string(),
189 required: true,
190 },
191 ],
192 example: json!({
193 "type": "set_data",
194 "path": "/myapp/config",
195 "data": "new_configuration_data"
196 }),
197 log_template: None,
198 },
199 ActionDefinition {
200 name: "delete_znode".to_string(),
201 description: "Delete a ZNode at the given path. ZooKeeper refuses to delete a \
202 node that still has children, so remove those first; watches \
203 registered on the node fire as a result."
204 .to_string(),
205 parameters: vec![Parameter {
206 name: "path".to_string(),
207 type_hint: "string".to_string(),
208 description: "ZNode path".to_string(),
209 required: true,
210 }],
211 example: json!({
212 "type": "delete_znode",
213 "path": "/myapp/config"
214 }),
215 log_template: None,
216 },
217 ActionDefinition {
218 name: "get_children".to_string(),
219 description: "Get children of a ZNode".to_string(),
220 parameters: vec![Parameter {
221 name: "path".to_string(),
222 type_hint: "string".to_string(),
223 description: "ZNode path".to_string(),
224 required: true,
225 }],
226 example: json!({
227 "type": "get_children",
228 "path": "/myapp"
229 }),
230 log_template: None,
231 },
232 ]
233 }
234
235 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
236 vec![
237 ActionDefinition {
238 name: "wait_for_more".to_string(),
239 description: "Wait for more events from ZooKeeper".to_string(),
240 parameters: vec![],
241 example: json!({
242 "type": "wait_for_more"
243 }),
244 log_template: None,
245 },
246 ActionDefinition {
247 name: "disconnect".to_string(),
248 description: "Disconnect from the ZooKeeper server".to_string(),
249 parameters: vec![],
250 example: json!({
251 "type": "disconnect"
252 }),
253 log_template: None,
254 },
255 ]
256 }
257
258 fn protocol_name(&self) -> &'static str {
259 "ZooKeeper"
260 }
261
262 fn get_event_types(&self) -> Vec<EventType> {
263 vec![
264 EventType::new(
265 "zookeeper_connected",
266 "Triggered when ZooKeeper client connects",
267 json!({"type": "placeholder", "event_id": "zookeeper_connected"}),
268 ),
269 EventType::new(
270 "zookeeper_data_received",
271 "Triggered when ZooKeeper client receives data",
272 json!({"type": "placeholder", "event_id": "zookeeper_data_received"}),
273 ),
274 EventType::new(
275 "zookeeper_children_received",
276 "Triggered when ZooKeeper client receives children list",
277 json!({"type": "placeholder", "event_id": "zookeeper_children_received"}),
278 ),
279 EventType::new(
280 "zookeeper_operation_complete",
281 "Triggered when a ZooKeeper write (create/set_data/delete) completes",
282 json!({"type": "placeholder", "event_id": "zookeeper_operation_complete"}),
283 ),
284 ]
285 }
286
287 fn stack_name(&self) -> &'static str {
288 "ETH>IP>TCP>ZooKeeper"
289 }
290
291 fn keywords(&self) -> Vec<&'static str> {
292 vec![
293 "zookeeper",
294 "zk",
295 "zookeeper client",
296 "connect to zookeeper",
297 ]
298 }
299
300 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
301 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
302
303 ProtocolMetadataV2::builder()
304 .state(DevelopmentState::Experimental)
305 .implementation("zookeeper-async v5.0 client library")
306 .llm_control("ZNode operations (create, get, set, delete, getChildren)")
307 .e2e_testing(
308 "tests/client/zookeeper/command_channel_test.rs drives a real zookeeper-async \
309 session against NetGet's own ZooKeeper server on 127.0.0.1",
310 )
311 .notes(
312 "Real zookeeper-async session: create/get_data/set_data/delete/get_children \
313 run against the server and raise events. No watch mechanism - watches are \
314 registered as `false` on every call, so the client polls rather than being \
315 notified.",
316 )
317 .build()
318 }
319
320 fn description(&self) -> &'static str {
321 "ZooKeeper client for distributed coordination"
322 }
323
324 fn example_prompt(&self) -> &'static str {
325 "Connect to ZooKeeper at localhost:2181 and read /config/database"
326 }
327
328 fn group_name(&self) -> &'static str {
329 "Database"
330 }
331 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
332 use crate::llm::actions::StartupExamples;
333 use serde_json::json;
334
335 StartupExamples::new(
336 json!({
338 "type": "open_client",
339 "remote_addr": "localhost:2181",
340 "base_stack": "zookeeper",
341 "instruction": "Read configuration from /myapp/config and list its children"
342 }),
343 json!({
345 "type": "open_client",
346 "remote_addr": "localhost:2181",
347 "base_stack": "zookeeper",
348 "event_handlers": [{
349 "event_pattern": "zookeeper_data_received",
350 "handler": {
351 "type": "script",
352 "language": "python",
353 "code": "<zookeeper_client_handler>"
354 }
355 }]
356 }),
357 json!({
359 "type": "open_client",
360 "remote_addr": "localhost:2181",
361 "base_stack": "zookeeper",
362 "event_handlers": [
363 {
364 "event_pattern": "zookeeper_connected",
365 "handler": {
366 "type": "static",
367 "actions": [{
368 "type": "get_data",
369 "path": "/myapp/config"
370 }]
371 }
372 },
373 {
374 "event_pattern": "zookeeper_data_received",
375 "handler": {
376 "type": "static",
377 "actions": [{
378 "type": "disconnect"
379 }]
380 }
381 }
382 ]
383 }),
384 )
385 }
386}
387
388impl Client for ZookeeperClientProtocol {
390 fn connect(
391 &self,
392 ctx: crate::protocol::ConnectContext,
393 ) -> std::pin::Pin<
394 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
395 > {
396 Box::pin(async move {
397 use crate::client::zookeeper::ZookeeperClient;
398
399 ZookeeperClient::connect_with_llm_actions(
400 ctx.remote_addr,
401 ctx.llm_client,
402 ctx.state,
403 ctx.status_tx,
404 ctx.client_id,
405 )
406 .await
407 })
408 }
409
410 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
411 let action_type = action
412 .get("type")
413 .and_then(|v| v.as_str())
414 .context("Missing 'type' field in action")?;
415
416 match action_type {
417 "create_znode" => {
418 let path = action
419 .get("path")
420 .and_then(|v| v.as_str())
421 .context("Missing path")?;
422 let data = action
423 .get("data")
424 .and_then(|v| v.as_str())
425 .context("Missing data")?;
426
427 Ok(ClientActionResult::Custom {
428 name: "create_znode".to_string(),
429 data: json!({
430 "path": path,
431 "data": data
432 }),
433 })
434 }
435 "get_data" => {
436 let path = action
437 .get("path")
438 .and_then(|v| v.as_str())
439 .context("Missing path")?;
440
441 Ok(ClientActionResult::Custom {
442 name: "get_data".to_string(),
443 data: json!({
444 "path": path
445 }),
446 })
447 }
448 "set_data" => {
449 let path = action
450 .get("path")
451 .and_then(|v| v.as_str())
452 .context("Missing path")?;
453 let data = action
454 .get("data")
455 .and_then(|v| v.as_str())
456 .context("Missing data")?;
457
458 Ok(ClientActionResult::Custom {
459 name: "set_data".to_string(),
460 data: json!({
461 "path": path,
462 "data": data
463 }),
464 })
465 }
466 "delete_znode" => {
467 let path = action
468 .get("path")
469 .and_then(|v| v.as_str())
470 .context("Missing path")?;
471
472 Ok(ClientActionResult::Custom {
473 name: "delete_znode".to_string(),
474 data: json!({
475 "path": path
476 }),
477 })
478 }
479 "get_children" => {
480 let path = action
481 .get("path")
482 .and_then(|v| v.as_str())
483 .context("Missing path")?;
484
485 Ok(ClientActionResult::Custom {
486 name: "get_children".to_string(),
487 data: json!({
488 "path": path
489 }),
490 })
491 }
492 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
493 "disconnect" => Ok(ClientActionResult::Disconnect),
494 _ => Err(anyhow!("Unknown action type: {}", action_type)),
495 }
496 }
497}