Skip to main content

feagi_services/types/
agent_registry.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Agent registry types - shared between feagi-services and feagi-io
5
6use serde::{Deserialize, Serialize};
7
8/// Type of agent based on I/O direction and purpose
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum AgentType {
12    /// Agent provides sensory input to FEAGI
13    Sensory,
14    /// Agent receives motor output from FEAGI
15    Motor,
16    /// Agent both sends and receives data
17    Both,
18    /// Agent consumes visualization stream only (e.g., Brain Visualizer clients)
19    Visualization,
20    /// Infrastructure agent (e.g., bridges, proxies) - needs viz + control streams
21    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/// Vision input capability
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct VisionCapability {
54    /// Type of vision sensor (camera, lidar, depth, etc.)
55    pub modality: String,
56    /// Frame dimensions [width, height]
57    pub dimensions: (usize, usize),
58    /// Number of channels (1=grayscale, 3=RGB, 4=RGBA)
59    pub channels: usize,
60    /// Target cortical area ID
61    pub target_cortical_area: String,
62    /// Semantic unit identifier (preferred).
63    ///
64    /// When set, the backend can derive cortical IDs without requiring agents to know
65    /// internal 3-letter unit designators (e.g., "svi"/"seg").
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub unit: Option<SensoryUnit>,
68    /// Cortical unit index (group) for the selected unit (preferred).
69    ///
70    /// FEAGI encodes the group in the cortical ID. This keeps the wire contract
71    /// language-agnostic and avoids leaking internal byte layouts to SDK users.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub group: Option<u8>,
74}
75
76/// Motor output capability
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct MotorCapability {
79    /// Type of motor (servo, stepper, dc, etc.)
80    pub modality: String,
81    /// Number of motor outputs
82    pub output_count: usize,
83    /// Source cortical area IDs
84    pub source_cortical_areas: Vec<String>,
85    /// Semantic unit identifier (preferred).
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub unit: Option<MotorUnit>,
88    /// Cortical unit index (group) for the selected unit (preferred).
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub group: Option<u8>,
91    /// Multiple semantic motor unit sources (preferred for multi-OPU agents).
92    ///
93    /// This supports agents that subscribe to multiple motor cortical unit types
94    /// (e.g., object_segmentation + simple_vision_output + text_english_output).
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub source_units: Option<Vec<MotorUnitSpec>>,
97}
98
99/// Motor unit + group pair for registration contracts.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
101pub struct MotorUnitSpec {
102    pub unit: MotorUnit,
103    pub group: u8,
104}
105
106/// Language-agnostic sensory unit identifiers for registration contracts.
107///
108/// These are **not** the same as the internal 3-letter unit designators. They are intended
109/// to be stable, human-readable, and suitable for auto-generated SDKs in other languages.
110#[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/// Language-agnostic motor unit identifiers for registration contracts.
130#[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/// Visualization capability for agents that consume neural activity stream
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct VisualizationCapability {
146    /// Type of visualization (3d_brain, 2d_plot, neural_graph, etc.)
147    pub visualization_type: String,
148    /// Display resolution if applicable
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub resolution: Option<(usize, usize)>,
151    /// Refresh rate in Hz if applicable
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub refresh_rate: Option<f64>,
154    /// Whether this is a bridge/proxy agent (vs direct consumer)
155    #[serde(default)]
156    pub bridge_proxy: bool,
157}
158
159/// Sensory capability for non-vision sensory modalities (text, audio, etc.)
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SensoryCapability {
162    pub rate_hz: f64,
163    pub shm_path: Option<String>,
164}
165
166/// Agent capabilities describing what data it can provide/consume
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct AgentCapabilities {
169    /// Vision input capability
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub vision: Option<VisionCapability>,
172
173    /// Motor output capability
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub motor: Option<MotorCapability>,
176
177    /// Visualization capability
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub visualization: Option<VisualizationCapability>,
180
181    /// Legacy sensory capability (for backward compatibility)
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub sensory: Option<SensoryCapability>,
184
185    /// Custom capabilities (extensible for audio, tactile, etc.)
186    #[serde(flatten)]
187    pub custom: serde_json::Map<String, serde_json::Value>,
188}
189
190/// Transport mechanism for agent communication
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "lowercase")]
193pub enum AgentTransport {
194    Zmq,
195    Shm,
196    Hybrid, // Uses both ZMQ and SHM
197}
198
199/// Complete agent information
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AgentInfo {
202    /// Unique agent identifier
203    pub agent_id: String,
204
205    /// Agent type (sensory, motor, or both)
206    pub agent_type: AgentType,
207
208    /// Agent capabilities
209    pub capabilities: AgentCapabilities,
210
211    /// Transport method the agent chose to use
212    pub chosen_transport: Option<String>, // "zmq", "websocket", "shm", or "hybrid"
213
214    /// Registration timestamp (Unix epoch milliseconds)
215    pub registered_at: u64,
216
217    /// Last activity timestamp (Unix epoch milliseconds)
218    pub last_seen: u64,
219
220    /// Transport mechanism
221    pub transport: AgentTransport,
222
223    /// Metadata (client version, hostname, etc.)
224    #[serde(default)]
225    pub metadata: serde_json::Map<String, serde_json::Value>,
226}
227
228impl AgentInfo {
229    /// Create a new agent info with current timestamp
230    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, // Set later when agent reports back
246            registered_at: now,
247            last_seen: now,
248            transport,
249            metadata: serde_json::Map::new(),
250        }
251    }
252
253    /// Update last_seen timestamp to current time
254    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    /// Check if agent has been inactive for more than timeout_ms
262    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
272/// Agent Registry - single source of truth for agent management
273pub struct AgentRegistry {
274    agents: std::collections::HashMap<String, AgentInfo>,
275    max_agents: usize,
276    timeout_ms: u64,
277}
278
279impl AgentRegistry {
280    /// Create a new agent registry
281    ///
282    /// # Arguments
283    /// * `max_agents` - Maximum number of concurrent agents (default: 100)
284    /// * `timeout_ms` - Inactivity timeout in milliseconds (default: 60000)
285    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    /// Create registry with default settings (100 agents, 60s timeout)
299    pub fn with_defaults() -> Self {
300        Self::new(100, 60000)
301    }
302
303    /// Register a new agent
304    pub fn register(&mut self, agent_info: AgentInfo) -> Result<(), String> {
305        let agent_id = agent_info.agent_id.clone();
306
307        // Validate configuration
308        self.validate_agent_config(&agent_id, &agent_info.agent_type, &agent_info.capabilities)?;
309
310        // Check if already registered (allow re-registration)
311        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            // Check capacity only for new registrations
319            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    /// Deregister an agent
339    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    /// Update heartbeat for an agent
353    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    /// Get agent info
376    pub fn get(&self, agent_id: &str) -> Option<&AgentInfo> {
377        self.agents.get(agent_id)
378    }
379
380    /// Get all agents
381    pub fn get_all(&self) -> Vec<&AgentInfo> {
382        self.agents.values().collect()
383    }
384
385    /// Get agents with stale heartbeats (older than configured timeout)
386    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    /// Prune inactive agents
395    ///
396    /// # Returns
397    /// Number of agents pruned
398    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    /// Get number of registered agents
424    pub fn count(&self) -> usize {
425        self.agents.len()
426    }
427
428    /// Check if any agent has sensory capability (for stream gating)
429    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    /// Check if any agent has motor capability (for stream gating)
436    pub fn has_motor_agents(&self) -> bool {
437        self.agents
438            .values()
439            .any(|agent| agent.capabilities.motor.is_some())
440    }
441
442    /// Check if any agent has visualization capability (for stream gating)
443    pub fn has_visualization_agents(&self) -> bool {
444        self.agents
445            .values()
446            .any(|agent| agent.capabilities.visualization.is_some())
447    }
448
449    /// Get count of agents with sensory capability
450    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    /// Get count of agents with motor capability
460    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    /// Get count of agents with visualization capability
468    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    /// Get the configured timeout threshold in milliseconds
476    pub fn get_timeout_ms(&self) -> u64 {
477        self.timeout_ms
478    }
479
480    /// Validate agent configuration
481    fn validate_agent_config(
482        &self,
483        agent_id: &str,
484        agent_type: &AgentType,
485        capabilities: &AgentCapabilities,
486    ) -> Result<(), String> {
487        // Agent ID must not be empty
488        if agent_id.is_empty() {
489            return Err("Agent ID cannot be empty".to_string());
490        }
491
492        // Validate capabilities match agent type
493        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                // Both requires at least one capability of each type
509                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                // Infrastructure agents are flexible - they can proxy any type
529                // Just require at least one capability to be declared
530                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}