1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum AgentType {
12 Sensory,
14 Motor,
16 Both,
18 Visualization,
20 Infrastructure,
22}
23
24impl std::fmt::Display for AgentType {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 AgentType::Sensory => write!(f, "sensory"),
28 AgentType::Motor => write!(f, "motor"),
29 AgentType::Both => write!(f, "both"),
30 AgentType::Visualization => write!(f, "visualization"),
31 AgentType::Infrastructure => write!(f, "infrastructure"),
32 }
33 }
34}
35
36impl std::str::FromStr for AgentType {
37 type Err = String;
38
39 fn from_str(s: &str) -> Result<Self, Self::Err> {
40 match s.to_lowercase().as_str() {
41 "sensory" => Ok(AgentType::Sensory),
42 "motor" => Ok(AgentType::Motor),
43 "both" => Ok(AgentType::Both),
44 "visualization" => Ok(AgentType::Visualization),
45 "infrastructure" => Ok(AgentType::Infrastructure),
46 _ => Err(format!("Invalid agent type: {}", s)),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct VisionCapability {
54 pub modality: String,
56 pub dimensions: (usize, usize),
58 pub channels: usize,
60 pub target_cortical_area: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
67 pub unit: Option<SensoryUnit>,
68 #[serde(skip_serializing_if = "Option::is_none")]
73 pub group: Option<u8>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct MotorCapability {
79 pub modality: String,
81 pub output_count: usize,
83 pub source_cortical_areas: Vec<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub unit: Option<MotorUnit>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub group: Option<u8>,
91 #[serde(skip_serializing_if = "Option::is_none")]
96 pub source_units: Option<Vec<MotorUnitSpec>>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
101pub struct MotorUnitSpec {
102 pub unit: MotorUnit,
103 pub group: u8,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum SensoryUnit {
113 Infrared,
114 Proximity,
115 Shock,
116 Battery,
117 Servo,
118 AnalogGpio,
119 DigitalGpio,
120 MiscData,
121 TextEnglishInput,
122 CountInput,
123 Vision,
124 SegmentedVision,
125 Accelerometer,
126 Gyroscope,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132pub enum MotorUnit {
133 RotaryMotor,
134 PositionalServo,
135 Gaze,
136 MiscData,
137 TextEnglishOutput,
138 CountOutput,
139 ObjectSegmentation,
140 SimpleVisionOutput,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct VisualizationCapability {
146 pub visualization_type: String,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub resolution: Option<(usize, usize)>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub refresh_rate: Option<f64>,
154 #[serde(default)]
156 pub bridge_proxy: bool,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SensoryCapability {
162 pub rate_hz: f64,
163 pub shm_path: Option<String>,
164}
165
166#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct AgentCapabilities {
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub vision: Option<VisionCapability>,
172
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub motor: Option<MotorCapability>,
176
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub visualization: Option<VisualizationCapability>,
180
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub sensory: Option<SensoryCapability>,
184
185 #[serde(flatten)]
187 pub custom: serde_json::Map<String, serde_json::Value>,
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "lowercase")]
193pub enum AgentTransport {
194 Zmq,
195 Shm,
196 Hybrid, }
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AgentInfo {
202 pub agent_id: String,
204
205 pub agent_type: AgentType,
207
208 pub capabilities: AgentCapabilities,
210
211 pub chosen_transport: Option<String>, pub registered_at: u64,
216
217 pub last_seen: u64,
219
220 pub transport: AgentTransport,
222
223 #[serde(default)]
225 pub metadata: serde_json::Map<String, serde_json::Value>,
226}
227
228impl AgentInfo {
229 pub fn new(
231 agent_id: String,
232 agent_type: AgentType,
233 capabilities: AgentCapabilities,
234 transport: AgentTransport,
235 ) -> Self {
236 let now = std::time::SystemTime::now()
237 .duration_since(std::time::UNIX_EPOCH)
238 .unwrap()
239 .as_millis() as u64;
240
241 Self {
242 agent_id,
243 agent_type,
244 capabilities,
245 chosen_transport: None, registered_at: now,
247 last_seen: now,
248 transport,
249 metadata: serde_json::Map::new(),
250 }
251 }
252
253 pub fn update_activity(&mut self) {
255 self.last_seen = std::time::SystemTime::now()
256 .duration_since(std::time::UNIX_EPOCH)
257 .unwrap()
258 .as_millis() as u64;
259 }
260
261 pub fn is_inactive(&self, timeout_ms: u64) -> bool {
263 let now = std::time::SystemTime::now()
264 .duration_since(std::time::UNIX_EPOCH)
265 .unwrap()
266 .as_millis() as u64;
267
268 now - self.last_seen > timeout_ms
269 }
270}
271
272pub struct AgentRegistry {
274 agents: std::collections::HashMap<String, AgentInfo>,
275 max_agents: usize,
276 timeout_ms: u64,
277}
278
279impl AgentRegistry {
280 pub fn new(max_agents: usize, timeout_ms: u64) -> Self {
286 tracing::info!(
287 "🦀 [REGISTRY] Initialized (max_agents={}, timeout_ms={})",
288 max_agents,
289 timeout_ms
290 );
291 Self {
292 agents: std::collections::HashMap::new(),
293 max_agents,
294 timeout_ms,
295 }
296 }
297
298 pub fn with_defaults() -> Self {
300 Self::new(100, 60000)
301 }
302
303 pub fn register(&mut self, agent_info: AgentInfo) -> Result<(), String> {
305 let agent_id = agent_info.agent_id.clone();
306
307 self.validate_agent_config(&agent_id, &agent_info.agent_type, &agent_info.capabilities)?;
309
310 let is_reregistration = self.agents.contains_key(&agent_id);
312 if is_reregistration {
313 tracing::warn!(
314 "⚠️ [REGISTRY] Agent re-registering (updating existing entry): {}",
315 agent_id
316 );
317 } else {
318 if self.agents.len() >= self.max_agents {
320 return Err(format!(
321 "Registry full ({}/{})",
322 self.agents.len(),
323 self.max_agents
324 ));
325 }
326 }
327
328 tracing::info!(
329 "🦀 [REGISTRY] Registered agent: {} (type: {}, total: {})",
330 agent_id,
331 agent_info.agent_type,
332 self.agents.len() + if is_reregistration { 0 } else { 1 }
333 );
334 self.agents.insert(agent_id, agent_info);
335 Ok(())
336 }
337
338 pub fn deregister(&mut self, agent_id: &str) -> Result<(), String> {
340 if self.agents.remove(agent_id).is_some() {
341 tracing::info!(
342 "🦀 [REGISTRY] Deregistered agent: {} (total: {})",
343 agent_id,
344 self.agents.len()
345 );
346 Ok(())
347 } else {
348 Err(format!("Agent {} not found", agent_id))
349 }
350 }
351
352 pub fn heartbeat(&mut self, agent_id: &str) -> Result<(), String> {
354 use tracing::debug;
355
356 if let Some(agent) = self.agents.get_mut(agent_id) {
357 let old_last_seen = agent.last_seen;
358 agent.update_activity();
359 let new_last_seen = agent.last_seen;
360
361 debug!(
362 "💓 [REGISTRY] Heartbeat updated for '{}': last_seen {} -> {} (+{}ms)",
363 agent_id,
364 old_last_seen,
365 new_last_seen,
366 new_last_seen.saturating_sub(old_last_seen)
367 );
368
369 Ok(())
370 } else {
371 Err(format!("Agent {} not found", agent_id))
372 }
373 }
374
375 pub fn get(&self, agent_id: &str) -> Option<&AgentInfo> {
377 self.agents.get(agent_id)
378 }
379
380 pub fn get_all(&self) -> Vec<&AgentInfo> {
382 self.agents.values().collect()
383 }
384
385 pub fn get_stale_agents(&self) -> Vec<String> {
387 self.agents
388 .iter()
389 .filter(|(_, info)| info.is_inactive(self.timeout_ms))
390 .map(|(id, _)| id.clone())
391 .collect()
392 }
393
394 pub fn prune_inactive_agents(&mut self) -> usize {
399 let inactive: Vec<String> = self
400 .agents
401 .iter()
402 .filter(|(_, info)| info.is_inactive(self.timeout_ms))
403 .map(|(id, _)| id.clone())
404 .collect();
405
406 let count = inactive.len();
407 for agent_id in &inactive {
408 self.agents.remove(agent_id);
409 tracing::info!("🦀 [REGISTRY] Pruned inactive agent: {}", agent_id);
410 }
411
412 if count > 0 {
413 tracing::info!(
414 "🦀 [REGISTRY] Pruned {} inactive agents (total: {})",
415 count,
416 self.agents.len()
417 );
418 }
419
420 count
421 }
422
423 pub fn count(&self) -> usize {
425 self.agents.len()
426 }
427
428 pub fn has_sensory_agents(&self) -> bool {
430 self.agents.values().any(|agent| {
431 agent.capabilities.sensory.is_some() || agent.capabilities.vision.is_some()
432 })
433 }
434
435 pub fn has_motor_agents(&self) -> bool {
437 self.agents
438 .values()
439 .any(|agent| agent.capabilities.motor.is_some())
440 }
441
442 pub fn has_visualization_agents(&self) -> bool {
444 self.agents
445 .values()
446 .any(|agent| agent.capabilities.visualization.is_some())
447 }
448
449 pub fn count_sensory_agents(&self) -> usize {
451 self.agents
452 .values()
453 .filter(|agent| {
454 agent.capabilities.sensory.is_some() || agent.capabilities.vision.is_some()
455 })
456 .count()
457 }
458
459 pub fn count_motor_agents(&self) -> usize {
461 self.agents
462 .values()
463 .filter(|agent| agent.capabilities.motor.is_some())
464 .count()
465 }
466
467 pub fn count_visualization_agents(&self) -> usize {
469 self.agents
470 .values()
471 .filter(|agent| agent.capabilities.visualization.is_some())
472 .count()
473 }
474
475 pub fn get_timeout_ms(&self) -> u64 {
477 self.timeout_ms
478 }
479
480 fn validate_agent_config(
482 &self,
483 agent_id: &str,
484 agent_type: &AgentType,
485 capabilities: &AgentCapabilities,
486 ) -> Result<(), String> {
487 if agent_id.is_empty() {
489 return Err("Agent ID cannot be empty".to_string());
490 }
491
492 match agent_type {
494 AgentType::Sensory => {
495 if capabilities.vision.is_none()
496 && capabilities.sensory.is_none()
497 && capabilities.custom.is_empty()
498 {
499 return Err("Sensory agent must have at least one input capability".to_string());
500 }
501 }
502 AgentType::Motor => {
503 if capabilities.motor.is_none() {
504 return Err("Motor agent must have motor capability".to_string());
505 }
506 }
507 AgentType::Both => {
508 if (capabilities.vision.is_none()
510 && capabilities.sensory.is_none()
511 && capabilities.custom.is_empty())
512 || capabilities.motor.is_none()
513 {
514 return Err(
515 "Bidirectional agent must have both input and output capabilities"
516 .to_string(),
517 );
518 }
519 }
520 AgentType::Visualization => {
521 if capabilities.visualization.is_none() {
522 return Err(
523 "Visualization agent must have visualization capability".to_string()
524 );
525 }
526 }
527 AgentType::Infrastructure => {
528 if capabilities.vision.is_none()
531 && capabilities.sensory.is_none()
532 && capabilities.motor.is_none()
533 && capabilities.visualization.is_none()
534 && capabilities.custom.is_empty()
535 {
536 return Err(
537 "Infrastructure agent must declare at least one capability".to_string()
538 );
539 }
540 }
541 }
542
543 Ok(())
544 }
545}