1use crate::error::{Result, SdkError};
7use feagi_io::{
8 AgentCapabilities, AgentType, MotorCapability, VisionCapability, VisualizationCapability,
9};
10
11#[derive(Debug, Clone)]
13pub struct AgentConfig {
14 pub agent_id: String,
16
17 pub agent_type: AgentType,
19
20 pub capabilities: AgentCapabilities,
22
23 pub registration_endpoint: String,
25
26 pub sensory_endpoint: String,
28
29 pub motor_endpoint: String,
31
32 pub visualization_endpoint: String,
34
35 pub control_endpoint: String,
37
38 pub heartbeat_interval: f64,
40
41 pub connection_timeout_ms: u64,
43
44 pub registration_retries: u32,
46
47 pub retry_backoff_ms: u64,
49
50 pub sensory_send_hwm: i32,
52 pub sensory_linger_ms: i32,
54 pub sensory_immediate: bool,
56}
57
58impl AgentConfig {
59 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 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 sensory_send_hwm: 1,
89 sensory_linger_ms: 0,
90 sensory_immediate: true,
92 }
93 }
94
95 #[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 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 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 pub fn with_registration_endpoint(mut self, endpoint: impl Into<String>) -> Self {
160 self.registration_endpoint = endpoint.into();
161 self
162 }
163
164 pub fn with_sensory_endpoint(mut self, endpoint: impl Into<String>) -> Self {
166 self.sensory_endpoint = endpoint.into();
167 self
168 }
169
170 pub fn with_motor_endpoint(mut self, endpoint: impl Into<String>) -> Self {
172 self.motor_endpoint = endpoint.into();
173 self
174 }
175
176 pub fn with_visualization_endpoint(mut self, endpoint: impl Into<String>) -> Self {
178 self.visualization_endpoint = endpoint.into();
179 self
180 }
181
182 pub fn with_control_endpoint(mut self, endpoint: impl Into<String>) -> Self {
184 self.control_endpoint = endpoint.into();
185 self
186 }
187
188 pub fn with_heartbeat_interval(mut self, interval: f64) -> Self {
190 self.heartbeat_interval = interval;
191 self
192 }
193
194 pub fn with_connection_timeout_ms(mut self, timeout_ms: u64) -> Self {
196 self.connection_timeout_ms = timeout_ms;
197 self
198 }
199
200 pub fn with_registration_retries(mut self, retries: u32) -> Self {
202 self.registration_retries = retries;
203 self
204 }
205
206 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 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 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 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 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 pub fn validate(&self) -> Result<()> {
313 if self.agent_id.is_empty() {
315 return Err(SdkError::InvalidConfig(
316 "agent_id cannot be empty".to_string(),
317 ));
318 }
319
320 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 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 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 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 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 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 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}