feagi_agent/
client.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! FEAGI Agent Client implementation
5
6use crate::config::AgentConfig;
7use crate::error::{Result, SdkError};
8use crate::heartbeat::HeartbeatService;
9use crate::reconnect::{retry_with_backoff, ReconnectionStrategy};
10use feagi_io::AgentType;
11use std::sync::{Arc, Mutex};
12use tracing::{debug, error, info, warn};
13
14/// Main FEAGI Agent Client
15///
16/// This client handles:
17/// - Registration with FEAGI
18/// - Automatic heartbeat
19/// - Sending sensory data
20/// - Receiving motor data (for motor agents)
21/// - Automatic deregistration on drop
22///
23/// # Example
24/// ```ignore
25/// use feagi_agent::{AgentClient, AgentConfig, AgentType};
26///
27/// let config = AgentConfig::new("my_camera", AgentType::Sensory)
28///     .with_feagi_host("localhost")
29///     .with_vision_capability("camera", (640, 480), 3, "i_vision");
30///
31/// let mut client = AgentClient::new(config)?;
32/// client.connect()?;
33///
34/// // Send sensory data
35/// client.send_sensory_data(vec![(0, 50.0), (1, 75.0)])?;
36///
37/// // Client auto-deregisters on drop
38/// ```
39pub struct AgentClient {
40    /// Configuration
41    config: AgentConfig,
42
43    /// ZMQ context
44    context: zmq::Context,
45
46    /// Registration socket (ZMQ REQ - shared with heartbeat)
47    registration_socket: Option<Arc<Mutex<zmq::Socket>>>,
48
49    /// Sensory data socket (ZMQ PUSH)
50    sensory_socket: Option<zmq::Socket>,
51
52    /// Motor data socket (ZMQ SUB)
53    motor_socket: Option<zmq::Socket>,
54
55    /// Visualization stream socket (ZMQ SUB)
56    viz_socket: Option<zmq::Socket>,
57
58    /// Control/API socket (ZMQ REQ - REST over ZMQ)
59    control_socket: Option<zmq::Socket>,
60
61    /// Heartbeat service
62    heartbeat: Option<HeartbeatService>,
63
64    /// Registration state
65    registered: bool,
66}
67
68impl AgentClient {
69    /// Create a new FEAGI agent client
70    ///
71    /// # Arguments
72    /// * `config` - Agent configuration
73    pub fn new(config: AgentConfig) -> Result<Self> {
74        // Validate configuration
75        config.validate()?;
76
77        let context = zmq::Context::new();
78
79        Ok(Self {
80            config,
81            context,
82            registration_socket: None,
83            sensory_socket: None,
84            motor_socket: None,
85            viz_socket: None,
86            control_socket: None,
87            heartbeat: None,
88            registered: false,
89        })
90    }
91
92    /// Connect to FEAGI and register the agent
93    ///
94    /// This will:
95    /// 1. Create ZMQ sockets
96    /// 2. Register with FEAGI
97    /// 3. Start heartbeat service (ONLY after successful registration)
98    ///
99    /// IMPORTANT: Background threads are ONLY started AFTER successful registration.
100    /// This prevents thread interference with GUI event loops (e.g., MuJoCo, Godot).
101    /// If registration fails, NO threads are spawned.
102    pub fn connect(&mut self) -> Result<()> {
103        if self.registered {
104            return Err(SdkError::AlreadyConnected);
105        }
106
107        info!(
108            "[CLIENT] Connecting to FEAGI: {}",
109            self.config.registration_endpoint
110        );
111
112        // Step 1: Create sockets with retry
113        let mut socket_strategy = ReconnectionStrategy::new(
114            self.config.retry_backoff_ms,
115            self.config.registration_retries,
116        );
117        retry_with_backoff(
118            || self.create_sockets(),
119            &mut socket_strategy,
120            "Socket creation",
121        )?;
122
123        // Step 2: Register with FEAGI with retry
124        let mut reg_strategy = ReconnectionStrategy::new(
125            self.config.retry_backoff_ms,
126            self.config.registration_retries,
127        );
128        retry_with_backoff(|| self.register(), &mut reg_strategy, "Registration")?;
129
130        // Step 3: Start heartbeat service (ONLY after successful registration)
131        // This is critical: threads are only spawned AFTER we know FEAGI is reachable
132        if self.config.heartbeat_interval > 0.0 {
133            debug!("[CLIENT] Starting heartbeat service (post-registration)");
134            self.start_heartbeat()?;
135        } else {
136            debug!("[CLIENT] Heartbeat disabled (interval = 0)");
137        }
138
139        info!(
140            "[CLIENT] ✓ Connected and registered as: {}",
141            self.config.agent_id
142        );
143        Ok(())
144    }
145
146    /// Create ZMQ sockets
147    fn create_sockets(&mut self) -> Result<()> {
148        // Registration socket (REQ - for registration and heartbeat)
149        let reg_socket = self.context.socket(zmq::REQ)?;
150        reg_socket.set_rcvtimeo(self.config.connection_timeout_ms as i32)?;
151        reg_socket.set_sndtimeo(self.config.connection_timeout_ms as i32)?;
152        reg_socket.connect(&self.config.registration_endpoint)?;
153        self.registration_socket = Some(Arc::new(Mutex::new(reg_socket)));
154
155        // Sensory socket (PUSH - for sending data to FEAGI)
156        let sensory_socket = self.context.socket(zmq::PUSH)?;
157        sensory_socket.set_sndhwm(self.config.sensory_send_hwm)?;
158        sensory_socket.set_linger(self.config.sensory_linger_ms)?;
159        sensory_socket.set_immediate(self.config.sensory_immediate)?;
160        sensory_socket.connect(&self.config.sensory_endpoint)?;
161        self.sensory_socket = Some(sensory_socket);
162
163        // Motor socket (SUB - for receiving motor commands from FEAGI)
164        if matches!(self.config.agent_type, AgentType::Motor | AgentType::Both) {
165            info!(
166                "[SDK-CONNECT] 🎮 Initializing motor socket for agent '{}' (type: {:?})",
167                self.config.agent_id, self.config.agent_type
168            );
169            info!(
170                "[SDK-CONNECT] 🎮 Motor endpoint: {}",
171                self.config.motor_endpoint
172            );
173
174            let motor_socket = self.context.socket(zmq::SUB)?;
175            motor_socket.connect(&self.config.motor_endpoint)?;
176            info!("[SDK-CONNECT] ✅ Motor socket connected");
177
178            // Subscribe to messages for this agent
179            info!(
180                "[SDK-CONNECT] 🎮 Subscribing to topic: '{}'",
181                String::from_utf8_lossy(self.config.agent_id.as_bytes())
182            );
183            motor_socket.set_subscribe(self.config.agent_id.as_bytes())?;
184            info!("[SDK-CONNECT] ✅ Motor subscription set");
185
186            self.motor_socket = Some(motor_socket);
187            info!("[SDK-CONNECT] ✅ Motor socket initialized successfully");
188        } else {
189            info!(
190                "[SDK-CONNECT] ⚠️ Motor socket NOT initialized (agent type: {:?})",
191                self.config.agent_type
192            );
193        }
194
195        // Visualization socket (SUB - for receiving neural activity stream from FEAGI)
196        if matches!(
197            self.config.agent_type,
198            AgentType::Visualization | AgentType::Infrastructure
199        ) {
200            let viz_socket = self.context.socket(zmq::SUB)?;
201            viz_socket.connect(&self.config.visualization_endpoint)?;
202
203            // Subscribe to all visualization messages
204            viz_socket.set_subscribe(b"")?;
205            self.viz_socket = Some(viz_socket);
206            debug!("[CLIENT] ✓ Visualization socket created");
207        }
208
209        // Control socket (REQ - for REST API requests over ZMQ)
210        if matches!(self.config.agent_type, AgentType::Infrastructure) {
211            let control_socket = self.context.socket(zmq::REQ)?;
212            control_socket.set_rcvtimeo(self.config.connection_timeout_ms as i32)?;
213            control_socket.set_sndtimeo(self.config.connection_timeout_ms as i32)?;
214            control_socket.connect(&self.config.control_endpoint)?;
215            self.control_socket = Some(control_socket);
216            debug!("[CLIENT] ✓ Control/API socket created");
217        }
218
219        debug!("[CLIENT] ✓ ZMQ sockets created");
220        Ok(())
221    }
222
223    /// Register with FEAGI
224    fn register(&mut self) -> Result<()> {
225        let registration_msg = serde_json::json!({
226            "method": "POST",
227            "path": "/v1/agent/register",
228            "body": {
229                "agent_id": self.config.agent_id,
230                "agent_type": match self.config.agent_type {
231                    AgentType::Sensory => "sensory",
232                    AgentType::Motor => "motor",
233                    AgentType::Both => "both",
234                    AgentType::Visualization => "visualization",
235                    AgentType::Infrastructure => "infrastructure",
236                },
237                "capabilities": self.config.capabilities,
238            }
239        });
240
241        let socket = self
242            .registration_socket
243            .as_ref()
244            .ok_or_else(|| SdkError::Other("Registration socket not initialized".to_string()))?;
245
246        // Send registration request and get response
247        let response = {
248            let socket = socket
249                .lock()
250                .map_err(|e| SdkError::ThreadError(format!("Failed to lock socket: {}", e)))?;
251
252            debug!(
253                "[CLIENT] Sending registration request for: {}",
254                self.config.agent_id
255            );
256            socket.send(registration_msg.to_string().as_bytes(), 0)?;
257
258            // Wait for response
259            let response_bytes = socket.recv_bytes(0)?;
260            serde_json::from_slice::<serde_json::Value>(&response_bytes)?
261        }; // Lock is dropped here
262
263        // Check response status (REST format: {"status": 200, "body": {...}})
264        let status_code = response
265            .get("status")
266            .and_then(|s| s.as_u64())
267            .unwrap_or(500);
268        if status_code == 200 {
269            self.registered = true;
270            info!("[CLIENT] ✓ Registration successful: {:?}", response);
271            Ok(())
272        } else {
273            let empty_body = serde_json::json!({});
274            let body = response.get("body").unwrap_or(&empty_body);
275            let message = body
276                .get("error")
277                .and_then(|m| m.as_str())
278                .unwrap_or("Unknown error");
279
280            // Check if already registered - try deregistration
281            if message.contains("already registered") {
282                warn!("[CLIENT] ⚠ Agent already registered - attempting deregistration first");
283                self.deregister()?;
284                Err(SdkError::RegistrationFailed(
285                    "Retry after deregistration".to_string(),
286                ))
287            } else {
288                error!("[CLIENT] ✗ Registration failed: {}", message);
289                Err(SdkError::RegistrationFailed(message.to_string()))
290            }
291        }
292    }
293
294    /// Deregister from FEAGI
295    fn deregister(&mut self) -> Result<()> {
296        if !self.registered && self.registration_socket.is_none() {
297            return Ok(()); // Nothing to deregister
298        }
299
300        info!("[CLIENT] Deregistering agent: {}", self.config.agent_id);
301
302        let deregistration_msg = serde_json::json!({
303            "method": "DELETE",
304            "path": "/v1/agent/deregister",
305            "body": {
306                "agent_id": self.config.agent_id,
307            }
308        });
309
310        if let Some(socket) = &self.registration_socket {
311            let socket = socket
312                .lock()
313                .map_err(|e| SdkError::ThreadError(format!("Failed to lock socket: {}", e)))?;
314
315            // Send deregistration request
316            if let Err(e) = socket.send(deregistration_msg.to_string().as_bytes(), 0) {
317                warn!("[CLIENT] ⚠ Failed to send deregistration: {}", e);
318                return Ok(()); // Don't fail on deregistration error
319            }
320
321            // Wait for response (with timeout)
322            match socket.recv_bytes(0) {
323                Ok(response_bytes) => {
324                    let response: serde_json::Value = serde_json::from_slice(&response_bytes)?;
325                    if response.get("status").and_then(|s| s.as_str()) == Some("success") {
326                        info!("[CLIENT] ✓ Deregistration successful");
327                    } else {
328                        warn!("[CLIENT] ⚠ Deregistration returned: {:?}", response);
329                    }
330                }
331                Err(e) => {
332                    warn!("[CLIENT] ⚠ Deregistration timeout/error: {}", e);
333                }
334            }
335        }
336
337        self.registered = false;
338        Ok(())
339    }
340
341    /// Start heartbeat service
342    fn start_heartbeat(&mut self) -> Result<()> {
343        if self.heartbeat.is_some() {
344            return Ok(());
345        }
346
347        let socket = self
348            .registration_socket
349            .as_ref()
350            .ok_or_else(|| SdkError::Other("Registration socket not initialized".to_string()))?;
351
352        let mut heartbeat = HeartbeatService::new(
353            self.config.agent_id.clone(),
354            Arc::clone(socket),
355            self.config.heartbeat_interval,
356        );
357
358        heartbeat.start()?;
359        self.heartbeat = Some(heartbeat);
360
361        debug!(
362            "[CLIENT] ✓ Heartbeat service started (interval: {}s)",
363            self.config.heartbeat_interval
364        );
365        Ok(())
366    }
367
368    /// Send sensory data to FEAGI
369    ///
370    /// # Arguments
371    /// * `neuron_pairs` - Vector of (neuron_id, potential) pairs
372    ///
373    /// # Example
374    /// ```ignore
375    /// client.send_sensory_data(vec![
376    ///     (0, 50.0),
377    ///     (1, 75.0),
378    ///     (2, 30.0),
379    /// ])?;
380    /// ```
381    pub fn send_sensory_data(&self, neuron_pairs: Vec<(i32, f64)>) -> Result<()> {
382        if !self.registered {
383            return Err(SdkError::NotRegistered);
384        }
385
386        let socket = self
387            .sensory_socket
388            .as_ref()
389            .ok_or_else(|| SdkError::Other("Sensory socket not initialized".to_string()))?;
390
391        // ARCHITECTURE COMPLIANCE: Use binary XYZP format, NOT JSON
392        // This serializes data using feagi_data_structures for cross-platform compatibility
393        use feagi_structures::genomic::cortical_area::CorticalID;
394        use feagi_structures::neuron_voxels::xyzp::{
395            CorticalMappedXYZPNeuronVoxels, NeuronVoxelXYZPArrays,
396        };
397
398        // Get cortical area and dimensions from vision capability
399        let vision_cap = self
400            .config
401            .capabilities
402            .vision
403            .as_ref()
404            .ok_or_else(|| SdkError::Other("No vision capability configured".to_string()))?;
405
406        let (width, _height) = vision_cap.dimensions;
407        let cortical_area = &vision_cap.target_cortical_area;
408
409        // Create CorticalID from area name
410        let mut bytes = [b' '; 8];
411        let name_bytes = cortical_area.as_bytes();
412        let copy_len = name_bytes.len().min(8);
413        bytes[..copy_len].copy_from_slice(&name_bytes[..copy_len]);
414        let cortical_id = CorticalID::try_from_bytes(&bytes).map_err(|e| {
415            SdkError::Other(format!("Invalid cortical ID '{}': {:?}", cortical_area, e))
416        })?;
417
418        // Convert flat neuron IDs to XYZP format
419        let mut x_coords = Vec::with_capacity(neuron_pairs.len());
420        let mut y_coords = Vec::with_capacity(neuron_pairs.len());
421        let mut z_coords = Vec::with_capacity(neuron_pairs.len());
422        let mut potentials = Vec::with_capacity(neuron_pairs.len());
423
424        for (neuron_id, potential) in neuron_pairs {
425            let neuron_id = neuron_id as u32;
426            x_coords.push(neuron_id % (width as u32));
427            y_coords.push(neuron_id / (width as u32));
428            z_coords.push(0); // Single channel grayscale
429            potentials.push(potential as f32);
430        }
431
432        let _neuron_count = x_coords.len(); // Reserved for future validation
433
434        // Create neuron arrays from vectors
435        let neuron_arrays =
436            NeuronVoxelXYZPArrays::new_from_vectors(x_coords, y_coords, z_coords, potentials)
437                .map_err(|e| SdkError::Other(format!("Failed to create neuron arrays: {:?}", e)))?;
438
439        // Create cortical mapped data
440        let mut cortical_mapped = CorticalMappedXYZPNeuronVoxels::new();
441        cortical_mapped.insert(cortical_id, neuron_arrays);
442
443        // Serialize to binary using FeagiByteContainer (version 2 container format)
444        let mut byte_container = feagi_serialization::FeagiByteContainer::new_empty();
445        byte_container
446            .overwrite_byte_data_with_single_struct_data(&cortical_mapped, 0)
447            .map_err(|e| SdkError::Other(format!("Failed to serialize to container: {:?}", e)))?;
448
449        let buffer = byte_container.get_byte_ref().to_vec();
450
451        // Send binary XYZP data (version 2 container format)
452        socket.send(&buffer, 0)?;
453
454        debug!(
455            "[CLIENT] Sent {} bytes XYZP binary to {}",
456            buffer.len(),
457            cortical_area
458        );
459        Ok(())
460    }
461
462    /// Receive motor data from FEAGI (non-blocking)
463    ///
464    /// Returns None if no data is available.
465    /// Motor data is in binary XYZP format (CorticalMappedXYZPNeuronVoxels).
466    ///
467    /// # Example
468    /// ```ignore
469    /// use feagi_structures::neuron_voxels::xyzp::CorticalMappedXYZPNeuronVoxels;
470    ///
471    /// if let Some(motor_data) = client.receive_motor_data()? {
472    ///     // Process binary motor data
473    ///     for (cortical_id, neurons) in motor_data.iter() {
474    ///         println!("Motor area {:?}: {} neurons", cortical_id, neurons.len());
475    ///     }
476    /// }
477    /// ```
478    pub fn receive_motor_data(
479        &self,
480    ) -> Result<Option<feagi_structures::neuron_voxels::xyzp::CorticalMappedXYZPNeuronVoxels>> {
481        use feagi_structures::neuron_voxels::xyzp::CorticalMappedXYZPNeuronVoxels;
482
483        if !self.registered {
484            return Err(SdkError::NotRegistered);
485        }
486
487        let socket = self.motor_socket.as_ref().ok_or_else(|| {
488            info!("[CLIENT] ❌ receive_motor_data() called but motor_socket is None!");
489            SdkError::Other("Motor socket not initialized (not a motor agent?)".to_string())
490        })?;
491
492        // Non-blocking receive (multipart: [topic/agent_id, data])
493        // First frame is the topic (agent_id), second frame is the motor data
494        match socket.recv_bytes(zmq::DONTWAIT) {
495            Ok(topic) => {
496                info!(
497                    "[CLIENT] 📥 Received first frame: {} bytes: '{}'",
498                    topic.len(),
499                    String::from_utf8_lossy(&topic)
500                );
501
502                // Verify topic matches our agent_id (redundant due to SUB filter, but safe)
503                if topic != self.config.agent_id.as_bytes() {
504                    info!(
505                        "[CLIENT] ⚠️ Received motor data for different agent: expected '{}', got '{}'",
506                        self.config.agent_id,
507                        String::from_utf8_lossy(&topic)
508                    );
509                    return Ok(None);
510                }
511
512                // Check if more frames are available (should be for multipart)
513                let data = if socket.get_rcvmore().map_err(SdkError::Zmq)? {
514                    info!(
515                        "[CLIENT] 📥 More frames available, receiving second frame (motor data)..."
516                    );
517                    // Receive second frame (actual motor data)
518                    let data = socket.recv_bytes(0).map_err(|e| {
519                        info!("[CLIENT] ❌ Failed to receive second frame: {}", e);
520                        SdkError::Zmq(e)
521                    })?;
522                    info!(
523                        "[CLIENT] 📥 Received motor data frame: {} bytes",
524                        data.len()
525                    );
526                    data
527                } else {
528                    info!("[CLIENT] ⚠️ NO MORE FRAMES! Old FEAGI (single-part message)");
529                    info!(
530                        "[CLIENT] 📥 Using first frame as motor data ({} bytes)",
531                        topic.len()
532                    );
533                    // Fallback: treat first frame as data (backward compatibility with old FEAGI)
534                    topic
535                };
536
537                // ARCHITECTURE COMPLIANCE: Deserialize binary XYZP motor data using FeagiByteContainer
538                let mut byte_container = feagi_serialization::FeagiByteContainer::new_empty();
539                let mut data_vec = data.to_vec();
540
541                // Load bytes into container
542                byte_container
543                    .try_write_data_to_container_and_verify(&mut |bytes| {
544                        std::mem::swap(bytes, &mut data_vec);
545                        Ok(())
546                    })
547                    .map_err(|e| {
548                        SdkError::Other(format!("Failed to load motor data bytes: {:?}", e))
549                    })?;
550
551                // Get number of structures (should be 1 for motor data)
552                let num_structures = byte_container
553                    .try_get_number_contained_structures()
554                    .map_err(|e| {
555                        SdkError::Other(format!("Failed to get structure count: {:?}", e))
556                    })?;
557
558                if num_structures == 0 {
559                    return Ok(None);
560                }
561
562                // Extract first structure
563                let boxed_struct =
564                    byte_container
565                        .try_create_new_struct_from_index(0)
566                        .map_err(|e| {
567                            SdkError::Other(format!("Failed to extract motor structure: {:?}", e))
568                        })?;
569
570                // Downcast to CorticalMappedXYZPNeuronVoxels
571                let motor_data = boxed_struct
572                    .as_any()
573                    .downcast_ref::<CorticalMappedXYZPNeuronVoxels>()
574                    .ok_or_else(|| {
575                        SdkError::Other(
576                            "Motor data is not CorticalMappedXYZPNeuronVoxels".to_string(),
577                        )
578                    })?
579                    .clone();
580
581                debug!(
582                    "[CLIENT] ✓ Received motor data ({} bytes, {} areas)",
583                    data.len(),
584                    motor_data.len()
585                );
586                Ok(Some(motor_data))
587            }
588            Err(zmq::Error::EAGAIN) => {
589                // No data available (FEAGI not publishing OR slow joiner syndrome)
590                Ok(None)
591            }
592            Err(e) => {
593                error!("[CLIENT] ❌ ZMQ error on motor receive: {}", e);
594                Err(SdkError::Zmq(e))
595            }
596        }
597    }
598
599    /// Receive visualization data from FEAGI (non-blocking)
600    ///
601    /// Returns None if no data is available.
602    ///
603    /// # Example
604    /// ```ignore
605    /// if let Some(viz_data) = client.receive_visualization_data()? {
606    ///     // Process neural activity data
607    ///     println!("Visualization data size: {} bytes", viz_data.len());
608    /// }
609    /// ```
610    pub fn receive_visualization_data(&self) -> Result<Option<Vec<u8>>> {
611        if !self.registered {
612            return Err(SdkError::NotRegistered);
613        }
614
615        let socket = self.viz_socket.as_ref().ok_or_else(|| {
616            SdkError::Other(
617                "Visualization socket not initialized (not a visualization/infrastructure agent?)"
618                    .to_string(),
619            )
620        })?;
621
622        // Non-blocking receive
623        match socket.recv_bytes(zmq::DONTWAIT) {
624            Ok(data) => {
625                debug!(
626                    "[CLIENT] ✓ Received visualization data ({} bytes)",
627                    data.len()
628                );
629                Ok(Some(data))
630            }
631            Err(zmq::Error::EAGAIN) => Ok(None), // No data available
632            Err(e) => Err(SdkError::Zmq(e)),
633        }
634    }
635
636    /// Make a REST API request to FEAGI over ZMQ
637    ///
638    /// # Arguments
639    /// * `method` - HTTP method (GET, POST, PUT, DELETE)
640    /// * `route` - API route (e.g., "/v1/system/health_check")
641    /// * `data` - Optional request body for POST/PUT requests
642    ///
643    /// # Example
644    /// ```ignore
645    /// // GET request
646    /// let health = client.control_request("GET", "/v1/system/health_check", None)?;
647    ///
648    /// // POST request
649    /// let data = serde_json::json!({"key": "value"});
650    /// let response = client.control_request("POST", "/v1/some/endpoint", Some(data))?;
651    /// ```
652    pub fn control_request(
653        &self,
654        method: &str,
655        route: &str,
656        data: Option<serde_json::Value>,
657    ) -> Result<serde_json::Value> {
658        if !self.registered {
659            return Err(SdkError::NotRegistered);
660        }
661
662        let socket = self.control_socket.as_ref().ok_or_else(|| {
663            SdkError::Other(
664                "Control socket not initialized (not an infrastructure agent?)".to_string(),
665            )
666        })?;
667
668        // Prepare REST-over-ZMQ request
669        let mut request = serde_json::json!({
670            "method": method,
671            "route": route,
672            "headers": {"content-type": "application/json"},
673        });
674
675        if let Some(body) = data {
676            request["body"] = body;
677        }
678
679        // Send request
680        socket.send(request.to_string().as_bytes(), 0)?;
681
682        // Wait for response
683        let response_bytes = socket.recv_bytes(0)?;
684        let response: serde_json::Value = serde_json::from_slice(&response_bytes)?;
685
686        debug!("[CLIENT] ✓ Control request {} {} completed", method, route);
687        Ok(response)
688    }
689
690    /// Check if agent is registered
691    pub fn is_registered(&self) -> bool {
692        self.registered
693    }
694
695    /// Get agent ID
696    pub fn agent_id(&self) -> &str {
697        &self.config.agent_id
698    }
699}
700
701impl Drop for AgentClient {
702    fn drop(&mut self) {
703        debug!("[CLIENT] Dropping AgentClient: {}", self.config.agent_id);
704
705        // Step 1: Stop heartbeat service first (this stops background threads)
706        if let Some(mut heartbeat) = self.heartbeat.take() {
707            debug!("[CLIENT] Stopping heartbeat service before cleanup");
708            heartbeat.stop();
709            debug!("[CLIENT] Heartbeat service stopped");
710        }
711
712        // Step 2: Deregister from FEAGI (after threads stopped)
713        if self.registered {
714            debug!("[CLIENT] Deregistering agent: {}", self.config.agent_id);
715            if let Err(e) = self.deregister() {
716                warn!("[CLIENT] Deregistration failed during drop: {}", e);
717                // Continue cleanup even if deregistration fails
718            }
719        }
720
721        // Step 3: Sockets will be dropped automatically
722        debug!(
723            "[CLIENT] AgentClient dropped cleanly: {}",
724            self.config.agent_id
725        );
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use feagi_io::AgentType;
733
734    #[test]
735    fn test_client_creation() {
736        let config = AgentConfig::new("test_agent", AgentType::Sensory)
737            .with_vision_capability("camera", (640, 480), 3, "i_vision")
738            .with_registration_endpoint("tcp://localhost:8000")
739            .with_sensory_endpoint("tcp://localhost:5558");
740
741        let client = AgentClient::new(config);
742        assert!(client.is_ok());
743
744        let client = client.unwrap();
745        assert!(!client.is_registered());
746        assert_eq!(client.agent_id(), "test_agent");
747    }
748
749    // Note: Full integration tests require a running FEAGI instance
750    // and should be in separate integration test files
751}