Skip to main content

netget/client/zookeeper/
actions.rs

1//! ZooKeeper client protocol actions
2
3use 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
14// Event type constants
15pub 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
55/// Raised after a write verb (`create_znode`, `set_data`, `delete_znode`) completes.
56///
57/// The read verbs have their own events - `zookeeper_data_received` for `get_data`,
58/// `zookeeper_children_received` for `get_children` - and neither fits a write: there is no
59/// data and no child list to report, only the fact that ZooKeeper accepted the change and
60/// the version it now holds. Without this the three write verbs completed silently and the
61/// model was never told whether its own write landed.
62pub 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
123/// ZooKeeper client protocol implementation
124pub struct ZookeeperClientProtocol;
125
126impl ZookeeperClientProtocol {
127    pub fn new() -> Self {
128        Self
129    }
130}
131
132// Implement Protocol trait (common functionality)
133impl 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            // LLM mode: LLM controls ZooKeeper operations
337            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            // Script mode: Code-based znode operations
344            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            // Static mode: Fixed znode operations
358            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
388// Implement Client trait (client-specific functionality)
389impl 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}