Skip to main content

feagi_agent/core/
transport.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transport selection and configuration
5//!
6//! Provides types and utilities for parsing FEAGI's transport
7//! information from registration responses.
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Transport configuration from FEAGI
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TransportConfig {
15    pub transport_type: String,
16    pub enabled: bool,
17    pub ports: HashMap<String, u16>,
18    pub host: String,
19}
20
21impl TransportConfig {
22    /// Get port for a specific stream
23    pub fn get_port(&self, stream: &str) -> Option<u16> {
24        self.ports.get(stream).copied()
25    }
26
27    /// Check if transport supports a stream
28    pub fn supports_stream(&self, stream: &str) -> bool {
29        self.ports.contains_key(stream)
30    }
31}
32
33/// Registration response from FEAGI
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RegistrationResponse {
36    pub status: String,
37    pub message: Option<String>,
38    pub shm_paths: Option<HashMap<String, String>>,
39    pub transports: Option<Vec<TransportConfig>>,
40    pub recommended_transport: Option<String>,
41    /// Cortical area availability status for agent operations
42    pub cortical_areas: serde_json::Value,
43}
44
45impl RegistrationResponse {
46    /// Parse from JSON value
47    pub fn from_json(value: &serde_json::Value) -> Result<Self, feagi_structures::FeagiDataError> {
48        serde_json::from_value(value.clone()).map_err(|e| {
49            feagi_structures::FeagiDataError::DeserializationError(format!(
50                "Failed to parse registration response: {}",
51                e
52            ))
53        })
54    }
55
56    /// Get all available (enabled) transports
57    pub fn available_transports(&self) -> Vec<&TransportConfig> {
58        self.transports
59            .as_ref()
60            .map(|transports| transports.iter().filter(|t| t.enabled).collect())
61            .unwrap_or_default()
62    }
63
64    /// Get transport by type
65    pub fn get_transport(&self, transport_type: &str) -> Option<&TransportConfig> {
66        self.transports.as_ref().and_then(|transports| {
67            transports
68                .iter()
69                .find(|t| t.transport_type == transport_type && t.enabled)
70        })
71    }
72
73    /// Choose best transport based on preference
74    pub fn choose_transport(&self, preference: Option<&str>) -> Option<&TransportConfig> {
75        let available = self.available_transports();
76
77        if available.is_empty() {
78            return None;
79        }
80
81        // Try preference first
82        if let Some(pref) = preference {
83            if let Some(transport) = self.get_transport(pref) {
84                return Some(transport);
85            }
86        }
87
88        // Fall back to recommended
89        if let Some(recommended) = &self.recommended_transport {
90            if let Some(transport) = self.get_transport(recommended) {
91                return Some(transport);
92            }
93        }
94
95        // Last resort: first available
96        available.first().copied()
97    }
98
99    /// Check if transport is available
100    pub fn has_transport(&self, transport_type: &str) -> bool {
101        self.get_transport(transport_type).is_some()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_parse_registration_response() {
111        let json = serde_json::json!({
112            "status": "success",
113            "message": "Agent registered successfully",
114            "transports": [
115                {
116                    "transport_type": "zmq",
117                    "enabled": true,
118                    "ports": {
119                        "sensory": 5558,
120                        "motor": 5564,
121                        "visualization": 5562
122                    },
123                    "host": "0.0.0.0"
124                },
125                {
126                    "transport_type": "websocket",
127                    "enabled": true,
128                    "ports": {
129                        "sensory": 9051,
130                        "motor": 9052,
131                        "visualization": 9050
132                    },
133                    "host": "0.0.0.0"
134                }
135            ],
136            "recommended_transport": "zmq",
137            "cortical_areas": {}
138        });
139
140        let response = RegistrationResponse::from_json(&json).unwrap();
141
142        assert_eq!(response.status, "success");
143        assert_eq!(response.available_transports().len(), 2);
144        assert!(response.has_transport("zmq"));
145        assert!(response.has_transport("websocket"));
146
147        let zmq = response.get_transport("zmq").unwrap();
148        assert_eq!(zmq.get_port("sensory"), Some(5558));
149
150        let ws = response.get_transport("websocket").unwrap();
151        assert_eq!(ws.get_port("sensory"), Some(9051));
152    }
153
154    #[test]
155    fn test_choose_transport() {
156        let json = serde_json::json!({
157            "status": "success",
158            "transports": [
159                {
160                    "transport_type": "zmq",
161                    "enabled": true,
162                    "ports": {"sensory": 5558},
163                    "host": "0.0.0.0"
164                },
165                {
166                    "transport_type": "websocket",
167                    "enabled": true,
168                    "ports": {"sensory": 9051},
169                    "host": "0.0.0.0"
170                }
171            ],
172            "recommended_transport": "zmq",
173            "cortical_areas": {}
174        });
175
176        let response = RegistrationResponse::from_json(&json).unwrap();
177
178        // Auto-select (uses recommended)
179        let auto = response.choose_transport(None).unwrap();
180        assert_eq!(auto.transport_type, "zmq");
181
182        // Prefer WebSocket
183        let ws = response.choose_transport(Some("websocket")).unwrap();
184        assert_eq!(ws.transport_type, "websocket");
185
186        // Prefer ZMQ
187        let zmq = response.choose_transport(Some("zmq")).unwrap();
188        assert_eq!(zmq.transport_type, "zmq");
189    }
190}