feagi_agent/
config.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Configuration for FEAGI Agent SDK
5
6use crate::error::{Result, SdkError};
7use feagi_io::{
8    AgentCapabilities, AgentType, MotorCapability, VisionCapability, VisualizationCapability,
9};
10
11/// Agent configuration builder
12#[derive(Debug, Clone)]
13pub struct AgentConfig {
14    /// Unique agent identifier
15    pub agent_id: String,
16
17    /// Agent type (sensory, motor, both, visualization, or infrastructure)
18    pub agent_type: AgentType,
19
20    /// Agent capabilities
21    pub capabilities: AgentCapabilities,
22
23    /// FEAGI registration endpoint (ZMQ REQ)
24    pub registration_endpoint: String,
25
26    /// FEAGI sensory input endpoint (ZMQ PUSH)
27    pub sensory_endpoint: String,
28
29    /// FEAGI motor output endpoint (ZMQ SUB)
30    pub motor_endpoint: String,
31
32    /// FEAGI visualization stream endpoint (ZMQ SUB)
33    pub visualization_endpoint: String,
34
35    /// FEAGI control/API endpoint (ZMQ REQ - REST over ZMQ)
36    pub control_endpoint: String,
37
38    /// Heartbeat interval in seconds (0 = disabled)
39    pub heartbeat_interval: f64,
40
41    /// Connection timeout in milliseconds
42    pub connection_timeout_ms: u64,
43
44    /// Registration retry attempts
45    pub registration_retries: u32,
46
47    /// Retry backoff base in milliseconds
48    pub retry_backoff_ms: u64,
49
50    /// ZMQ PUSH socket high-water-mark for sensory data
51    pub sensory_send_hwm: i32,
52    /// ZMQ PUSH socket linger period when disconnecting
53    pub sensory_linger_ms: i32,
54    /// Whether to enable ZMQ immediate mode on the sensory socket
55    pub sensory_immediate: bool,
56}
57
58impl AgentConfig {
59    /// Create a new agent configuration
60    ///
61    /// # Arguments
62    /// * `agent_id` - Unique identifier for this agent
63    /// * `agent_type` - Type of agent (Sensory, Motor, or Both)
64    ///
65    /// # Example
66    /// ```
67    /// use feagi_agent::{AgentConfig, AgentType};
68    ///
69    /// let config = AgentConfig::new("my_camera", AgentType::Sensory);
70    /// ```
71    pub fn new(agent_id: impl Into<String>, agent_type: AgentType) -> Self {
72        Self {
73            agent_id: agent_id.into(),
74            agent_type,
75            capabilities: AgentCapabilities::default(),
76            // NO HARDCODED ENDPOINTS - must be set explicitly via builder methods or with_feagi_endpoints()
77            registration_endpoint: String::new(),
78            sensory_endpoint: String::new(),
79            motor_endpoint: String::new(),
80            visualization_endpoint: String::new(),
81            control_endpoint: String::new(),
82            heartbeat_interval: 5.0,
83            connection_timeout_ms: 5000,
84            registration_retries: 3,
85            retry_backoff_ms: 1000,
86            // REAL-TIME: HWM=1 ensures agent doesn't buffer old sensory data
87            // If FEAGI is slow, old frames are dropped (desired behavior for real-time)
88            sensory_send_hwm: 1,
89            sensory_linger_ms: 0,
90            // REAL-TIME: immediate=true disables Nagle's algorithm for lowest latency
91            sensory_immediate: true,
92        }
93    }
94
95    /// Set FEAGI host and ports to derive all endpoints
96    ///
97    /// Note: This method requires explicit port numbers. NO DEFAULTS are provided.
98    /// Ports must match those configured in FEAGI's feagi_configuration.toml
99    ///
100    /// # Example
101    /// ```
102    /// # use feagi_agent::{AgentConfig, AgentType};
103    /// let config = AgentConfig::new("camera", AgentType::Sensory)
104    ///     .with_feagi_endpoints("192.168.1.100", 30001, 5558, 30005, 5562, 5563);
105    /// ```
106    #[deprecated(
107        since = "0.1.0",
108        note = "Use with_feagi_endpoints() instead to explicitly specify all ports"
109    )]
110    pub fn with_feagi_host(mut self, host: impl Into<String>) -> Self {
111        let host = host.into();
112        // @architecture:acceptable - deprecated method, kept for backwards compatibility only
113        // Users should migrate to with_feagi_endpoints() or individual endpoint setters
114        self.registration_endpoint = format!("tcp://{}:30001", host);
115        self.sensory_endpoint = format!("tcp://{}:5558", host);
116        self.motor_endpoint = format!("tcp://{}:30005", host);
117        self.visualization_endpoint = format!("tcp://{}:5562", host);
118        self.control_endpoint = format!("tcp://{}:5563", host);
119        self
120    }
121
122    /// Set FEAGI endpoints with explicit ports (RECOMMENDED)
123    ///
124    /// All ports must be provided explicitly to match FEAGI's configuration.
125    /// No default values are used.
126    ///
127    /// # Example
128    /// ```
129    /// # use feagi_agent::{AgentConfig, AgentType};
130    /// let config = AgentConfig::new("camera", AgentType::Sensory)
131    ///     .with_feagi_endpoints(
132    ///         "192.168.1.100",
133    ///         30001,  // registration_port
134    ///         5558,   // sensory_port
135    ///         30005,  // motor_port
136    ///         5562,   // visualization_port
137    ///         5563    // control_port
138    ///     );
139    /// ```
140    pub fn with_feagi_endpoints(
141        mut self,
142        host: impl Into<String>,
143        registration_port: u16,
144        sensory_port: u16,
145        motor_port: u16,
146        visualization_port: u16,
147        control_port: u16,
148    ) -> Self {
149        let host = host.into();
150        self.registration_endpoint = format!("tcp://{}:{}", host, registration_port);
151        self.sensory_endpoint = format!("tcp://{}:{}", host, sensory_port);
152        self.motor_endpoint = format!("tcp://{}:{}", host, motor_port);
153        self.visualization_endpoint = format!("tcp://{}:{}", host, visualization_port);
154        self.control_endpoint = format!("tcp://{}:{}", host, control_port);
155        self
156    }
157
158    /// Set registration endpoint
159    pub fn with_registration_endpoint(mut self, endpoint: impl Into<String>) -> Self {
160        self.registration_endpoint = endpoint.into();
161        self
162    }
163
164    /// Set sensory input endpoint
165    pub fn with_sensory_endpoint(mut self, endpoint: impl Into<String>) -> Self {
166        self.sensory_endpoint = endpoint.into();
167        self
168    }
169
170    /// Set motor output endpoint
171    pub fn with_motor_endpoint(mut self, endpoint: impl Into<String>) -> Self {
172        self.motor_endpoint = endpoint.into();
173        self
174    }
175
176    /// Set visualization stream endpoint
177    pub fn with_visualization_endpoint(mut self, endpoint: impl Into<String>) -> Self {
178        self.visualization_endpoint = endpoint.into();
179        self
180    }
181
182    /// Set control/API endpoint
183    pub fn with_control_endpoint(mut self, endpoint: impl Into<String>) -> Self {
184        self.control_endpoint = endpoint.into();
185        self
186    }
187
188    /// Set heartbeat interval in seconds (0 to disable)
189    pub fn with_heartbeat_interval(mut self, interval: f64) -> Self {
190        self.heartbeat_interval = interval;
191        self
192    }
193
194    /// Set connection timeout in milliseconds
195    pub fn with_connection_timeout_ms(mut self, timeout_ms: u64) -> Self {
196        self.connection_timeout_ms = timeout_ms;
197        self
198    }
199
200    /// Set registration retry attempts
201    pub fn with_registration_retries(mut self, retries: u32) -> Self {
202        self.registration_retries = retries;
203        self
204    }
205
206    /// Configure sensory socket behaviour (ZMQ PUSH)
207    pub fn with_sensory_socket_config(
208        mut self,
209        send_hwm: i32,
210        linger_ms: i32,
211        immediate: bool,
212    ) -> Self {
213        self.sensory_send_hwm = send_hwm;
214        self.sensory_linger_ms = linger_ms;
215        self.sensory_immediate = immediate;
216        self
217    }
218
219    /// Add vision capability
220    ///
221    /// # Example
222    /// ```
223    /// # use feagi_agent::{AgentConfig, AgentType};
224    /// let config = AgentConfig::new("camera", AgentType::Sensory)
225    ///     .with_vision_capability("camera", (640, 480), 3, "i_vision");
226    /// ```
227    pub fn with_vision_capability(
228        mut self,
229        modality: impl Into<String>,
230        dimensions: (usize, usize),
231        channels: usize,
232        target_cortical_area: impl Into<String>,
233    ) -> Self {
234        self.capabilities.vision = Some(VisionCapability {
235            modality: modality.into(),
236            dimensions,
237            channels,
238            target_cortical_area: target_cortical_area.into(),
239        });
240        self
241    }
242
243    /// Add motor capability
244    ///
245    /// # Example
246    /// ```
247    /// # use feagi_agent::{AgentConfig, AgentType};
248    /// let config = AgentConfig::new("arm", AgentType::Motor)
249    ///     .with_motor_capability("servo", 4, vec!["o_motor".to_string()]);
250    /// ```
251    pub fn with_motor_capability(
252        mut self,
253        modality: impl Into<String>,
254        output_count: usize,
255        source_cortical_areas: Vec<String>,
256    ) -> Self {
257        self.capabilities.motor = Some(MotorCapability {
258            modality: modality.into(),
259            output_count,
260            source_cortical_areas,
261        });
262        self
263    }
264
265    /// Add visualization capability
266    ///
267    /// # Example
268    /// ```
269    /// # use feagi_agent::{AgentConfig, AgentType};
270    /// let config = AgentConfig::new("brain_viz", AgentType::Visualization)
271    ///     .with_visualization_capability("3d_brain", Some((1920, 1080)), Some(30.0), false);
272    /// ```
273    pub fn with_visualization_capability(
274        mut self,
275        visualization_type: impl Into<String>,
276        resolution: Option<(usize, usize)>,
277        refresh_rate: Option<f64>,
278        bridge_proxy: bool,
279    ) -> Self {
280        self.capabilities.visualization = Some(VisualizationCapability {
281            visualization_type: visualization_type.into(),
282            resolution,
283            refresh_rate,
284            bridge_proxy,
285        });
286        self
287    }
288
289    /// Add custom capability
290    ///
291    /// # Example
292    /// ```
293    /// # use feagi_agent::{AgentConfig, AgentType};
294    /// use serde_json::json;
295    ///
296    /// let config = AgentConfig::new("audio", AgentType::Sensory)
297    ///     .with_custom_capability("audio", json!({
298    ///         "sample_rate": 44100,
299    ///         "channels": 2
300    ///     }));
301    /// ```
302    pub fn with_custom_capability(
303        mut self,
304        key: impl Into<String>,
305        value: serde_json::Value,
306    ) -> Self {
307        self.capabilities.custom.insert(key.into(), value);
308        self
309    }
310
311    /// Validate configuration
312    pub fn validate(&self) -> Result<()> {
313        // Agent ID must not be empty
314        if self.agent_id.is_empty() {
315            return Err(SdkError::InvalidConfig(
316                "agent_id cannot be empty".to_string(),
317            ));
318        }
319
320        // Must have at least one capability
321        if self.capabilities.vision.is_none()
322            && self.capabilities.motor.is_none()
323            && self.capabilities.visualization.is_none()
324            && self.capabilities.custom.is_empty()
325        {
326            return Err(SdkError::InvalidConfig(
327                "Agent must have at least one capability".to_string(),
328            ));
329        }
330
331        // Validate agent type matches capabilities
332        match self.agent_type {
333            AgentType::Sensory => {
334                if self.capabilities.vision.is_none() && self.capabilities.custom.is_empty() {
335                    return Err(SdkError::InvalidConfig(
336                        "Sensory agent must have vision or custom input capability".to_string(),
337                    ));
338                }
339            }
340            AgentType::Motor => {
341                if self.capabilities.motor.is_none() {
342                    return Err(SdkError::InvalidConfig(
343                        "Motor agent must have motor capability".to_string(),
344                    ));
345                }
346            }
347            AgentType::Both => {
348                if (self.capabilities.vision.is_none() && self.capabilities.custom.is_empty())
349                    || self.capabilities.motor.is_none()
350                {
351                    return Err(SdkError::InvalidConfig(
352                        "Bidirectional agent must have both input and output capabilities"
353                            .to_string(),
354                    ));
355                }
356            }
357            AgentType::Visualization => {
358                if self.capabilities.visualization.is_none() {
359                    return Err(SdkError::InvalidConfig(
360                        "Visualization agent must have visualization capability".to_string(),
361                    ));
362                }
363            }
364            AgentType::Infrastructure => {
365                // Infrastructure agents can have any combination of capabilities
366                // No strict requirements as they may proxy multiple types
367                if self.capabilities.vision.is_none()
368                    && self.capabilities.motor.is_none()
369                    && self.capabilities.visualization.is_none()
370                    && self.capabilities.custom.is_empty()
371                {
372                    return Err(SdkError::InvalidConfig(
373                        "Infrastructure agent must declare at least one capability".to_string(),
374                    ));
375                }
376            }
377        }
378
379        // Validate endpoints based on agent type
380        // Registration endpoint is always required
381        if self.registration_endpoint.is_empty() {
382            return Err(SdkError::InvalidConfig(
383                "registration_endpoint must be set (use with_registration_endpoint() or with_feagi_endpoints())".to_string()
384            ));
385        }
386        if !self.registration_endpoint.starts_with("tcp://") {
387            return Err(SdkError::InvalidConfig(
388                "registration_endpoint must start with tcp://".to_string(),
389            ));
390        }
391
392        // Validate sensory endpoint for sensory agents
393        if matches!(self.agent_type, AgentType::Sensory | AgentType::Both) {
394            if self.sensory_endpoint.is_empty() {
395                return Err(SdkError::InvalidConfig(
396                    "sensory_endpoint must be set for Sensory/Both agents (use with_sensory_endpoint() or with_feagi_endpoints())".to_string()
397                ));
398            }
399            if !self.sensory_endpoint.starts_with("tcp://") {
400                return Err(SdkError::InvalidConfig(
401                    "sensory_endpoint must start with tcp://".to_string(),
402                ));
403            }
404        }
405
406        // Validate motor endpoint for motor agents
407        if matches!(self.agent_type, AgentType::Motor | AgentType::Both) {
408            if self.motor_endpoint.is_empty() {
409                return Err(SdkError::InvalidConfig(
410                    "motor_endpoint must be set for Motor/Both agents (use with_motor_endpoint() or with_feagi_endpoints())".to_string()
411                ));
412            }
413            if !self.motor_endpoint.starts_with("tcp://") {
414                return Err(SdkError::InvalidConfig(
415                    "motor_endpoint must start with tcp://".to_string(),
416                ));
417            }
418        }
419
420        // Validate visualization endpoint for visualization agents
421        if matches!(self.agent_type, AgentType::Visualization) {
422            if self.visualization_endpoint.is_empty() {
423                return Err(SdkError::InvalidConfig(
424                    "visualization_endpoint must be set for Visualization agents (use with_visualization_endpoint() or with_feagi_endpoints())".to_string()
425                ));
426            }
427            if !self.visualization_endpoint.starts_with("tcp://") {
428                return Err(SdkError::InvalidConfig(
429                    "visualization_endpoint must start with tcp://".to_string(),
430                ));
431            }
432        }
433
434        if self.sensory_send_hwm < 0 {
435            return Err(SdkError::InvalidConfig(
436                "sensory_send_hwm must be >= 0".to_string(),
437            ));
438        }
439
440        Ok(())
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_config_builder() {
450        #[allow(deprecated)]
451        let config = AgentConfig::new("test_agent", AgentType::Sensory)
452            .with_feagi_host("192.168.1.100")
453            .with_vision_capability("camera", (640, 480), 3, "i_vision")
454            .with_heartbeat_interval(10.0);
455
456        assert_eq!(config.agent_id, "test_agent");
457        assert_eq!(config.heartbeat_interval, 10.0);
458        assert_eq!(config.registration_endpoint, "tcp://192.168.1.100:30001");
459        assert!(config.capabilities.vision.is_some());
460    }
461
462    #[test]
463    fn test_config_validation_empty_agent_id() {
464        let config = AgentConfig::new("", AgentType::Sensory);
465        assert!(config.validate().is_err());
466    }
467
468    #[test]
469    fn test_config_validation_no_capabilities() {
470        let config = AgentConfig::new("test", AgentType::Sensory);
471        assert!(config.validate().is_err());
472    }
473
474    #[test]
475    fn test_config_validation_sensory_without_input() {
476        let mut config = AgentConfig::new("test", AgentType::Sensory);
477        config.capabilities.motor = Some(MotorCapability {
478            modality: "servo".to_string(),
479            output_count: 1,
480            source_cortical_areas: vec!["motor".to_string()],
481        });
482        assert!(config.validate().is_err());
483    }
484
485    #[test]
486    fn test_config_validation_valid() {
487        let config = AgentConfig::new("test", AgentType::Sensory)
488            .with_vision_capability("camera", (640, 480), 3, "vision")
489            .with_registration_endpoint("tcp://localhost:8000")
490            .with_sensory_endpoint("tcp://localhost:5558");
491        assert!(config.validate().is_ok());
492    }
493}