Skip to main content

feagi_api/endpoints/
agent.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Agent API endpoints - Exact port from Python `/v1/agent/*` routes
5//!
6//! These endpoints match the Python implementation at:
7//! feagi-py/feagi/api/v1/feagi_agent.py
8
9use std::collections::HashMap;
10
11use crate::common::ApiState;
12use crate::common::{ApiError, ApiResult, Json, Path, Query, State};
13use crate::v1::agent_dtos::*;
14use feagi_services::traits::agent_service::{
15    AgentRegistration, HeartbeatRequest as ServiceHeartbeatRequest,
16};
17use tracing::{error, info, warn};
18
19#[cfg(feature = "feagi-agent")]
20use feagi_agent::sdk::AgentDescriptor;
21#[cfg(feature = "feagi-agent")]
22use feagi_agent::sdk::ConnectorAgent;
23#[cfg(feature = "feagi-agent")]
24use feagi_services::types::CreateCorticalAreaParams;
25#[cfg(feature = "feagi-agent")]
26use feagi_structures::genomic::cortical_area::descriptors::{
27    CorticalSubUnitIndex, CorticalUnitIndex,
28};
29#[cfg(feature = "feagi-agent")]
30use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
31    FrameChangeHandling, PercentageNeuronPositioning,
32};
33#[cfg(feature = "feagi-agent")]
34use feagi_structures::genomic::MotorCorticalUnit;
35#[cfg(feature = "feagi-agent")]
36use feagi_structures::genomic::SensoryCorticalUnit;
37#[cfg(feature = "feagi-agent")]
38use std::sync::{Arc, Mutex};
39
40#[cfg(feature = "feagi-agent")]
41fn parse_agent_descriptor(agent_id: &str) -> ApiResult<AgentDescriptor> {
42    AgentDescriptor::try_from_base64(agent_id).map_err(|e| {
43        ApiError::invalid_input(format!(
44            "Invalid agent_id (expected AgentDescriptor base64): {e}"
45        ))
46    })
47}
48
49fn get_agent_name_from_id(agent_id: &str) -> ApiResult<String> {
50    #[cfg(feature = "feagi-agent")]
51    {
52        let descriptor = parse_agent_descriptor(agent_id)?;
53        let agent_name = descriptor.agent_name().to_string();
54        if agent_name.is_empty() {
55            return Err(ApiError::invalid_input(format!(
56                "Agent '{}' does not contain a readable name",
57                agent_id
58            )));
59        }
60        Ok(agent_name)
61    }
62    #[cfg(not(feature = "feagi-agent"))]
63    {
64        Err(ApiError::internal(
65            "Agent name requires feagi-agent feature".to_string(),
66        ))
67    }
68}
69
70#[cfg(feature = "feagi-agent")]
71async fn auto_create_cortical_areas_from_device_registrations(
72    state: &ApiState,
73    device_registrations: &serde_json::Value,
74) {
75    let connectome_service = state.connectome_service.as_ref();
76    let genome_service = state.genome_service.as_ref();
77
78    let Some(output_units) = device_registrations
79        .get("output_units_and_decoder_properties")
80        .and_then(|v| v.as_object())
81    else {
82        return;
83    };
84    let input_units = device_registrations
85        .get("input_units_and_encoder_properties")
86        .and_then(|v| v.as_object());
87
88    // Build creation params for missing OPU areas based on default topologies.
89    let mut to_create: Vec<CreateCorticalAreaParams> = Vec::new();
90
91    for (motor_unit_key, unit_defs) in output_units {
92        // MotorCorticalUnit is serde-deserializable from its string representation.
93        let motor_unit: MotorCorticalUnit = match serde_json::from_value::<MotorCorticalUnit>(
94            serde_json::Value::String(motor_unit_key.clone()),
95        ) {
96            Ok(v) => v,
97            Err(e) => {
98                warn!(
99                    "⚠️ [API] Unable to parse MotorCorticalUnit key '{}' from device_registrations: {}",
100                    motor_unit_key, e
101                );
102                continue;
103            }
104        };
105
106        let Some(unit_defs_arr) = unit_defs.as_array() else {
107            continue;
108        };
109
110        for entry in unit_defs_arr {
111            // Expected shape: [<unit_definition>, <decoder_properties>]
112            let Some(pair) = entry.as_array() else {
113                continue;
114            };
115            let Some(unit_def) = pair.first() else {
116                continue;
117            };
118            let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
119            else {
120                continue;
121            };
122            let group_u8: u8 = match group_u64.try_into() {
123                Ok(v) => v,
124                Err(_) => continue,
125            };
126            let group: CorticalUnitIndex = group_u8.into();
127
128            let device_count = unit_def
129                .get("device_grouping")
130                .and_then(|v| v.as_array())
131                .map(|a| a.len())
132                .unwrap_or(0);
133            if device_count == 0 {
134                warn!(
135                    "⚠️ [API] device_grouping is empty for motor unit '{}' group {}; skipping auto-create",
136                    motor_unit_key, group_u8
137                );
138                continue;
139            }
140
141            // Use defaults consistent with FEAGI registration handler.
142            let frame_change_handling = FrameChangeHandling::Absolute;
143            let percentage_neuron_positioning = PercentageNeuronPositioning::Linear;
144
145            let cortical_ids = match motor_unit {
146                MotorCorticalUnit::RotaryMotor => MotorCorticalUnit::get_cortical_ids_array_for_rotary_motor_with_parameters(
147                    frame_change_handling,
148                    percentage_neuron_positioning,
149                    group,
150                )
151                .to_vec(),
152                MotorCorticalUnit::PositionalServo => MotorCorticalUnit::get_cortical_ids_array_for_positional_servo_with_parameters(
153                    frame_change_handling,
154                    percentage_neuron_positioning,
155                    group,
156                )
157                .to_vec(),
158                MotorCorticalUnit::Gaze => MotorCorticalUnit::get_cortical_ids_array_for_gaze_with_parameters(
159                    frame_change_handling,
160                    percentage_neuron_positioning,
161                    group,
162                )
163                .to_vec(),
164                MotorCorticalUnit::MiscData => MotorCorticalUnit::get_cortical_ids_array_for_misc_data_with_parameters(
165                    frame_change_handling,
166                    group,
167                )
168                .to_vec(),
169                MotorCorticalUnit::TextEnglishOutput => MotorCorticalUnit::get_cortical_ids_array_for_text_english_output_with_parameters(
170                    frame_change_handling,
171                    group,
172                )
173                .to_vec(),
174                MotorCorticalUnit::CountOutput => MotorCorticalUnit::get_cortical_ids_array_for_count_output_with_parameters(
175                    frame_change_handling,
176                    percentage_neuron_positioning,
177                    group,
178                )
179                .to_vec(),
180                MotorCorticalUnit::ObjectSegmentation => MotorCorticalUnit::get_cortical_ids_array_for_object_segmentation_with_parameters(
181                    frame_change_handling,
182                    group,
183                )
184                .to_vec(),
185                MotorCorticalUnit::SimpleVisionOutput => MotorCorticalUnit::get_cortical_ids_array_for_simple_vision_output_with_parameters(
186                    frame_change_handling,
187                    group,
188                )
189                .to_vec(),
190                MotorCorticalUnit::DynamicImageProcessing => MotorCorticalUnit::get_cortical_ids_array_for_dynamic_image_processing_with_parameters(
191                    frame_change_handling,
192                    percentage_neuron_positioning,
193                    group,
194                )
195                .to_vec(),
196            };
197
198            let topology = motor_unit.get_unit_default_topology();
199
200            for (i, cortical_id) in cortical_ids.iter().enumerate() {
201                let cortical_id_b64 = cortical_id.as_base_64();
202                let exists = match connectome_service
203                    .cortical_area_exists(&cortical_id_b64)
204                    .await
205                {
206                    Ok(v) => v,
207                    Err(e) => {
208                        warn!(
209                            "⚠️ [API] Failed to check cortical area existence for '{}': {}",
210                            cortical_id_b64, e
211                        );
212                        continue;
213                    }
214                };
215
216                if exists {
217                    // If the area already exists but still has a placeholder name (often equal to the cortical_id),
218                    // update it to a deterministic friendly name so UIs (e.g., Brain Visualizer) show readable labels.
219                    //
220                    // IMPORTANT: We only auto-rename if the current name is clearly a placeholder (== cortical_id),
221                    // to avoid overwriting user-custom names.
222                    let desired_name = if matches!(motor_unit, MotorCorticalUnit::Gaze) {
223                        let subunit_name = match i {
224                            0 => "Eccentricity",
225                            1 => "Modulation",
226                            _ => "Subunit",
227                        };
228                        format!(
229                            "{} ({}) Unit {}",
230                            motor_unit.get_friendly_name(),
231                            subunit_name,
232                            group_u8
233                        )
234                    } else if cortical_ids.len() > 1 {
235                        format!(
236                            "{} Subunit {} Unit {}",
237                            motor_unit.get_friendly_name(),
238                            i,
239                            group_u8
240                        )
241                    } else {
242                        format!("{} Unit {}", motor_unit.get_friendly_name(), group_u8)
243                    };
244
245                    match connectome_service.get_cortical_area(&cortical_id_b64).await {
246                        Ok(existing_area) => {
247                            if existing_area.name.is_empty()
248                                || existing_area.name == existing_area.cortical_id
249                            {
250                                let mut changes = std::collections::HashMap::new();
251                                changes.insert(
252                                    "cortical_name".to_string(),
253                                    serde_json::json!(desired_name),
254                                );
255                                if let Err(e) = genome_service
256                                    .update_cortical_area(&cortical_id_b64, changes)
257                                    .await
258                                {
259                                    warn!(
260                                        "⚠️ [API] Failed to auto-rename existing motor cortical area '{}': {}",
261                                        cortical_id_b64, e
262                                    );
263                                }
264                            }
265                        }
266                        Err(e) => {
267                            warn!(
268                                "⚠️ [API] Failed to fetch existing cortical area '{}' for potential rename: {}",
269                                cortical_id_b64, e
270                            );
271                        }
272                    }
273                    continue;
274                }
275
276                let Some(unit_topology) = topology.get(&CorticalSubUnitIndex::from(i as u8)) else {
277                    warn!(
278                        "⚠️ [API] Missing unit topology for motor unit '{}' subunit {} (agent device_registrations); cannot auto-create '{}'",
279                        motor_unit.get_snake_case_name(),
280                        i,
281                        cortical_id_b64
282                    );
283                    continue;
284                };
285                let dims = unit_topology.channel_dimensions_default;
286                let pos = unit_topology.relative_position;
287                let total_x = (dims[0] as usize).saturating_mul(device_count);
288                let dimensions = (total_x, dims[1] as usize, dims[2] as usize);
289                let position = (pos[0], pos[1], pos[2]);
290
291                // IMPORTANT:
292                // Motor units like Gaze have multiple cortical subunits (eccentricity + modulation).
293                // These must get distinct names; otherwise a uniqueness constraint (or UI mapping)
294                // can cause only one of the subunits to appear.
295                let name = if matches!(motor_unit, MotorCorticalUnit::Gaze) {
296                    let subunit_name = match i {
297                        0 => "Eccentricity",
298                        1 => "Modulation",
299                        _ => "Subunit",
300                    };
301                    format!(
302                        "{} ({}) Unit {}",
303                        motor_unit.get_friendly_name(),
304                        subunit_name,
305                        group_u8
306                    )
307                } else if cortical_ids.len() > 1 {
308                    format!(
309                        "{} Subunit {} Unit {}",
310                        motor_unit.get_friendly_name(),
311                        i,
312                        group_u8
313                    )
314                } else {
315                    format!("{} Unit {}", motor_unit.get_friendly_name(), group_u8)
316                };
317
318                to_create.push(CreateCorticalAreaParams {
319                    cortical_id: cortical_id_b64.clone(),
320                    name,
321                    dimensions,
322                    position,
323                    area_type: "Motor".to_string(),
324                    visible: Some(true),
325                    sub_group: None,
326                    neurons_per_voxel: Some(1),
327                    postsynaptic_current: None,
328                    plasticity_constant: None,
329                    degeneration: None,
330                    psp_uniform_distribution: None,
331                    firing_threshold_increment: None,
332                    firing_threshold_limit: None,
333                    consecutive_fire_count: None,
334                    snooze_period: None,
335                    refractory_period: None,
336                    leak_coefficient: None,
337                    leak_variability: None,
338                    burst_engine_active: None,
339                    properties: None,
340                });
341            }
342        }
343    }
344
345    if let Some(input_units) = input_units {
346        // Auto-rename sensory cortical areas if they exist with placeholder names.
347        for (sensory_unit_key, unit_defs) in input_units {
348            let sensory_unit: SensoryCorticalUnit = match serde_json::from_value::<
349                SensoryCorticalUnit,
350            >(serde_json::Value::String(
351                sensory_unit_key.clone(),
352            )) {
353                Ok(v) => v,
354                Err(e) => {
355                    warn!(
356                        "⚠️ [API] Unable to parse SensoryCorticalUnit key '{}' from device_registrations: {}",
357                        sensory_unit_key, e
358                    );
359                    continue;
360                }
361            };
362
363            let Some(unit_defs_arr) = unit_defs.as_array() else {
364                continue;
365            };
366
367            for entry in unit_defs_arr {
368                let Some(pair) = entry.as_array() else {
369                    continue;
370                };
371                let Some(unit_def) = pair.first() else {
372                    continue;
373                };
374                let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
375                else {
376                    continue;
377                };
378                let group_u8: u8 = match group_u64.try_into() {
379                    Ok(v) => v,
380                    Err(_) => continue,
381                };
382                let group: CorticalUnitIndex = group_u8.into();
383
384                let device_count = unit_def
385                    .get("device_grouping")
386                    .and_then(|v| v.as_array())
387                    .map(|a| a.len())
388                    .unwrap_or(0);
389                if device_count == 0 {
390                    continue;
391                }
392
393                let frame_change_handling = FrameChangeHandling::Absolute;
394                let percentage_neuron_positioning = PercentageNeuronPositioning::Linear;
395                let cortical_ids = match sensory_unit {
396                    SensoryCorticalUnit::Infrared => {
397                        SensoryCorticalUnit::get_cortical_ids_array_for_infrared_with_parameters(
398                            frame_change_handling,
399                            percentage_neuron_positioning,
400                            group,
401                        )
402                        .to_vec()
403                    }
404                    SensoryCorticalUnit::Proximity => {
405                        SensoryCorticalUnit::get_cortical_ids_array_for_proximity_with_parameters(
406                            frame_change_handling,
407                            percentage_neuron_positioning,
408                            group,
409                        )
410                        .to_vec()
411                    }
412                    SensoryCorticalUnit::Shock => {
413                        SensoryCorticalUnit::get_cortical_ids_array_for_shock_with_parameters(
414                            frame_change_handling,
415                            percentage_neuron_positioning,
416                            group,
417                        )
418                        .to_vec()
419                    }
420                    SensoryCorticalUnit::Battery => {
421                        SensoryCorticalUnit::get_cortical_ids_array_for_battery_with_parameters(
422                            frame_change_handling,
423                            percentage_neuron_positioning,
424                            group,
425                        )
426                        .to_vec()
427                    }
428                    SensoryCorticalUnit::Servo => {
429                        SensoryCorticalUnit::get_cortical_ids_array_for_servo_with_parameters(
430                            frame_change_handling,
431                            percentage_neuron_positioning,
432                            group,
433                        )
434                        .to_vec()
435                    }
436                    SensoryCorticalUnit::AnalogGPIO => {
437                        SensoryCorticalUnit::get_cortical_ids_array_for_analog_g_p_i_o_with_parameters(
438                            frame_change_handling,
439                            percentage_neuron_positioning,
440                            group,
441                        )
442                        .to_vec()
443                    }
444                    SensoryCorticalUnit::DigitalGPIO => {
445                        SensoryCorticalUnit::get_cortical_ids_array_for_digital_g_p_i_o_with_parameters(
446                            group,
447                        )
448                        .to_vec()
449                    }
450                    SensoryCorticalUnit::MiscData => {
451                        SensoryCorticalUnit::get_cortical_ids_array_for_misc_data_with_parameters(
452                            frame_change_handling,
453                            group,
454                        )
455                        .to_vec()
456                    }
457                    SensoryCorticalUnit::TextEnglishInput => {
458                        SensoryCorticalUnit::get_cortical_ids_array_for_text_english_input_with_parameters(
459                            frame_change_handling,
460                            group,
461                        )
462                        .to_vec()
463                    }
464                    SensoryCorticalUnit::CountInput => {
465                        SensoryCorticalUnit::get_cortical_ids_array_for_count_input_with_parameters(
466                            frame_change_handling,
467                            percentage_neuron_positioning,
468                            group,
469                        )
470                        .to_vec()
471                    }
472                    SensoryCorticalUnit::Vision => {
473                        SensoryCorticalUnit::get_cortical_ids_array_for_vision_with_parameters(
474                            frame_change_handling,
475                            group,
476                        )
477                        .to_vec()
478                    }
479                    SensoryCorticalUnit::SegmentedVision => {
480                        SensoryCorticalUnit::get_cortical_ids_array_for_segmented_vision_with_parameters(
481                            frame_change_handling,
482                            group,
483                        )
484                        .to_vec()
485                    }
486                    SensoryCorticalUnit::Accelerometer => {
487                        SensoryCorticalUnit::get_cortical_ids_array_for_accelerometer_with_parameters(
488                            frame_change_handling,
489                            percentage_neuron_positioning,
490                            group,
491                        )
492                        .to_vec()
493                    }
494                    SensoryCorticalUnit::Gyroscope => {
495                        SensoryCorticalUnit::get_cortical_ids_array_for_gyroscope_with_parameters(
496                            frame_change_handling,
497                            percentage_neuron_positioning,
498                            group,
499                        )
500                        .to_vec()
501                    }
502                };
503
504                for (i, cortical_id) in cortical_ids.iter().enumerate() {
505                    let cortical_id_b64 = cortical_id.as_base_64();
506                    match connectome_service.get_cortical_area(&cortical_id_b64).await {
507                        Ok(existing_area) => {
508                            if existing_area.name.is_empty()
509                                || existing_area.name == existing_area.cortical_id
510                            {
511                                let desired_name = if cortical_ids.len() > 1 {
512                                    format!(
513                                        "{} Subunit {} Unit {}",
514                                        sensory_unit.get_friendly_name(),
515                                        i,
516                                        group_u8
517                                    )
518                                } else {
519                                    format!(
520                                        "{} Unit {}",
521                                        sensory_unit.get_friendly_name(),
522                                        group_u8
523                                    )
524                                };
525                                let mut changes = std::collections::HashMap::new();
526                                changes.insert(
527                                    "cortical_name".to_string(),
528                                    serde_json::json!(desired_name),
529                                );
530                                if let Err(e) = genome_service
531                                    .update_cortical_area(&cortical_id_b64, changes)
532                                    .await
533                                {
534                                    warn!(
535                                        "⚠️ [API] Failed to auto-rename existing sensory cortical area '{}': {}",
536                                        cortical_id_b64, e
537                                    );
538                                }
539                            }
540                        }
541                        Err(e) => {
542                            warn!(
543                                "⚠️ [API] Failed to fetch existing cortical area '{}' for potential rename: {}",
544                                cortical_id_b64, e
545                            );
546                        }
547                    }
548                }
549            }
550        }
551    }
552
553    if to_create.is_empty() {
554        return;
555    }
556
557    info!(
558        "🦀 [API] Auto-creating {} missing cortical areas from device registrations",
559        to_create.len()
560    );
561
562    if let Err(e) = genome_service.create_cortical_areas(to_create).await {
563        warn!(
564            "⚠️ [API] Failed to auto-create cortical areas from device registrations: {}",
565            e
566        );
567    }
568}
569
570/// Register a new agent with FEAGI and receive connection details including transport configuration and ports.
571#[utoipa::path(
572    post,
573    path = "/v1/agent/register",
574    request_body = AgentRegistrationRequest,
575    responses(
576        (status = 200, description = "Agent registered successfully", body = AgentRegistrationResponse),
577        (status = 500, description = "Registration failed", body = String)
578    ),
579    tag = "agent"
580)]
581pub async fn register_agent(
582    State(state): State<ApiState>,
583    Json(request): Json<AgentRegistrationRequest>,
584) -> ApiResult<Json<AgentRegistrationResponse>> {
585    info!(
586        "🦀 [API] Registration request received for agent '{}' (type: {})",
587        request.agent_id, request.agent_type
588    );
589
590    let agent_service = state
591        .agent_service
592        .as_ref()
593        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
594
595    // Extract device_registrations from capabilities before they're moved
596    #[cfg(feature = "feagi-agent")]
597    let device_registrations_opt = request
598        .capabilities
599        .get("device_registrations")
600        .and_then(|v| {
601            // Validate structure before cloning
602            if let Some(obj) = v.as_object() {
603                if obj.contains_key("input_units_and_encoder_properties")
604                    && obj.contains_key("output_units_and_decoder_properties")
605                    && obj.contains_key("feedbacks")
606                {
607                    Some(v.clone())
608                } else {
609                    None
610                }
611            } else {
612                None
613            }
614        });
615
616    let registration = AgentRegistration {
617        agent_id: request.agent_id.clone(),
618        agent_type: request.agent_type,
619        agent_data_port: request.agent_data_port,
620        agent_version: request.agent_version,
621        controller_version: request.controller_version,
622        agent_ip: request.agent_ip,
623        capabilities: request.capabilities,
624        metadata: request.metadata,
625        chosen_transport: request.chosen_transport,
626    };
627
628    match agent_service.register_agent(registration).await {
629        Ok(response) => {
630            info!(
631                "✅ [API] Agent '{}' registration succeeded (status: {})",
632                request.agent_id, response.status
633            );
634
635            // Initialize ConnectorAgent for this agent
636            // If capabilities contained device_registrations, import them
637            #[cfg(feature = "feagi-agent")]
638            if let Some(device_registrations_value) = device_registrations_opt {
639                let agent_descriptor = parse_agent_descriptor(&request.agent_id)?;
640                let device_registrations_for_autocreate = device_registrations_value.clone();
641                // IMPORTANT: do not hold a non-Send lock guard across an await.
642                let connector = {
643                    let mut connectors = state.agent_connectors.write();
644                    if let Some(existing) = connectors.get(&agent_descriptor) {
645                        existing.clone()
646                    } else {
647                        let connector = ConnectorAgent::new_from_device_registration_json(
648                            agent_descriptor,
649                            device_registrations_value.clone(),
650                        )
651                        .map_err(|e| {
652                            ApiError::invalid_input(format!(
653                                "Failed to initialize connector from device registrations: {e}"
654                            ))
655                        })?;
656                        let connector = Arc::new(Mutex::new(connector));
657                        connectors.insert(agent_descriptor, connector.clone());
658                        connector
659                    }
660                };
661
662                // IMPORTANT: do not hold a non-Send MutexGuard across an await.
663                let import_result = {
664                    let mut connector_guard = connector.lock().unwrap();
665                    connector_guard.set_device_registrations_from_json(device_registrations_value)
666                };
667
668                match import_result {
669                    Err(e) => {
670                        warn!(
671                            "⚠️ [API] Failed to import device registrations from capabilities for agent '{}': {}",
672                            request.agent_id, e
673                        );
674                    }
675                    Ok(()) => {
676                        info!(
677                            "✅ [API] Imported device registrations from capabilities for agent '{}'",
678                            request.agent_id
679                        );
680                        auto_create_cortical_areas_from_device_registrations(
681                            &state,
682                            &device_registrations_for_autocreate,
683                        )
684                        .await;
685                    }
686                }
687            } else {
688                let agent_descriptor = parse_agent_descriptor(&request.agent_id)?;
689                // Initialize empty ConnectorAgent even if no device_registrations
690                let mut connectors = state.agent_connectors.write();
691                connectors.entry(agent_descriptor).or_insert_with(|| {
692                    Arc::new(Mutex::new(ConnectorAgent::new_empty(agent_descriptor)))
693                });
694            }
695
696            // Convert service TransportConfig to API TransportConfig
697            let transports = response.transports.map(|ts| {
698                ts.into_iter()
699                    .map(|t| crate::v1::agent_dtos::TransportConfig {
700                        transport_type: t.transport_type,
701                        enabled: t.enabled,
702                        ports: t.ports,
703                        host: t.host,
704                    })
705                    .collect()
706            });
707
708            Ok(Json(AgentRegistrationResponse {
709                status: response.status,
710                message: response.message,
711                success: response.success,
712                transport: response.transport,
713                rates: response.rates,
714                transports,
715                recommended_transport: response.recommended_transport,
716                shm_paths: response.shm_paths,
717                cortical_areas: response.cortical_areas,
718            }))
719        }
720        Err(e) => {
721            // Check if error is about unsupported transport (validation error)
722            let error_msg = e.to_string();
723            warn!(
724                "❌ [API] Agent '{}' registration FAILED: {}",
725                request.agent_id, error_msg
726            );
727            if error_msg.contains("not supported") || error_msg.contains("disabled") {
728                Err(ApiError::invalid_input(error_msg))
729            } else {
730                Err(ApiError::internal(format!("Registration failed: {}", e)))
731            }
732        }
733    }
734}
735
736/// Send a heartbeat to keep the agent registered and prevent timeout disconnection.
737#[utoipa::path(
738    post,
739    path = "/v1/agent/heartbeat",
740    request_body = HeartbeatRequest,
741    responses(
742        (status = 200, description = "Heartbeat recorded", body = HeartbeatResponse),
743        (status = 404, description = "Agent not found"),
744        (status = 500, description = "Heartbeat failed")
745    ),
746    tag = "agent"
747)]
748pub async fn heartbeat(
749    State(state): State<ApiState>,
750    Json(request): Json<HeartbeatRequest>,
751) -> ApiResult<Json<HeartbeatResponse>> {
752    let agent_service = state
753        .agent_service
754        .as_ref()
755        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
756
757    let service_request = ServiceHeartbeatRequest {
758        agent_id: request.agent_id.clone(),
759    };
760
761    match agent_service.heartbeat(service_request).await {
762        Ok(_) => Ok(Json(HeartbeatResponse {
763            message: "heartbeat_ok".to_string(),
764            success: true,
765        })),
766        Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
767    }
768}
769
770/// Get a list of all currently registered agent IDs.
771#[utoipa::path(
772    get,
773    path = "/v1/agent/list",
774    responses(
775        (status = 200, description = "List of agent IDs", body = Vec<String>),
776        (status = 503, description = "Registration service unavailable")
777    ),
778    tag = "agent"
779)]
780pub async fn list_agents(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
781    let agent_service = state
782        .agent_service
783        .as_ref()
784        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
785
786    match agent_service.list_agents().await {
787        Ok(agent_ids) => Ok(Json(agent_ids)),
788        Err(e) => Err(ApiError::internal(format!("Failed to list agents: {}", e))),
789    }
790}
791
792/// Get agent properties including type, capabilities, version, and connection details. Uses query parameter ?agent_id=xxx.
793#[utoipa::path(
794    get,
795    path = "/v1/agent/properties",
796    params(
797        ("agent_id" = String, Query, description = "Agent ID to get properties for")
798    ),
799    responses(
800        (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
801        (status = 404, description = "Agent not found"),
802        (status = 500, description = "Failed to get agent properties")
803    ),
804    tag = "agent"
805)]
806pub async fn get_agent_properties(
807    State(state): State<ApiState>,
808    Query(params): Query<HashMap<String, String>>,
809) -> ApiResult<Json<AgentPropertiesResponse>> {
810    let agent_id = params
811        .get("agent_id")
812        .ok_or_else(|| ApiError::invalid_input("Missing agent_id query parameter"))?;
813
814    let agent_service = state
815        .agent_service
816        .as_ref()
817        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
818
819    let agent_name = get_agent_name_from_id(agent_id)?;
820    match agent_service.get_agent_properties(agent_id).await {
821        Ok(properties) => Ok(Json(AgentPropertiesResponse {
822            agent_name,
823            agent_type: properties.agent_type,
824            agent_ip: properties.agent_ip,
825            agent_data_port: properties.agent_data_port,
826            agent_router_address: properties.agent_router_address,
827            agent_version: properties.agent_version,
828            controller_version: properties.controller_version,
829            capabilities: properties.capabilities,
830            chosen_transport: properties.chosen_transport,
831        })),
832        Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
833    }
834}
835
836/// Get shared memory configuration and paths for all registered agents using shared memory transport.
837#[utoipa::path(
838    get,
839    path = "/v1/agent/shared_mem",
840    responses(
841        (status = 200, description = "Shared memory info", body = HashMap<String, HashMap<String, serde_json::Value>>),
842        (status = 500, description = "Failed to get shared memory info")
843    ),
844    tag = "agent"
845)]
846pub async fn get_shared_memory(
847    State(state): State<ApiState>,
848) -> ApiResult<Json<HashMap<String, HashMap<String, serde_json::Value>>>> {
849    let agent_service = state
850        .agent_service
851        .as_ref()
852        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
853
854    match agent_service.get_shared_memory_info().await {
855        Ok(shm_info) => Ok(Json(shm_info)),
856        Err(e) => Err(ApiError::internal(format!(
857            "Failed to get shared memory info: {}",
858            e
859        ))),
860    }
861}
862
863/// Deregister an agent from FEAGI and clean up its resources.
864#[utoipa::path(
865    delete,
866    path = "/v1/agent/deregister",
867    request_body = AgentDeregistrationRequest,
868    responses(
869        (status = 200, description = "Agent deregistered successfully", body = SuccessResponse),
870        (status = 500, description = "Deregistration failed")
871    ),
872    tag = "agent"
873)]
874pub async fn deregister_agent(
875    State(state): State<ApiState>,
876    Json(request): Json<AgentDeregistrationRequest>,
877) -> ApiResult<Json<SuccessResponse>> {
878    let agent_service = state
879        .agent_service
880        .as_ref()
881        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
882
883    match agent_service.deregister_agent(&request.agent_id).await {
884        Ok(_) => Ok(Json(SuccessResponse {
885            message: format!("Agent '{}' deregistered successfully", request.agent_id),
886            success: Some(true),
887        })),
888        Err(e) => Err(ApiError::internal(format!("Deregistration failed: {}", e))),
889    }
890}
891
892/// Manually stimulate neurons at specific coordinates across multiple cortical areas for testing and debugging.
893#[utoipa::path(
894    post,
895    path = "/v1/agent/manual_stimulation",
896    request_body = ManualStimulationRequest,
897    responses(
898        (status = 200, description = "Manual stimulation result", body = ManualStimulationResponse),
899        (status = 500, description = "Stimulation failed")
900    ),
901    tag = "agent"
902)]
903pub async fn manual_stimulation(
904    State(state): State<ApiState>,
905    Json(request): Json<ManualStimulationRequest>,
906) -> ApiResult<Json<ManualStimulationResponse>> {
907    let agent_service = state
908        .agent_service
909        .as_ref()
910        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
911
912    // Ensure runtime service is connected to agent service (if not already connected)
913    // This allows runtime_service to be set after AgentServiceImpl is wrapped in Arc
914    agent_service.try_set_runtime_service(state.runtime_service.clone());
915
916    match agent_service
917        .manual_stimulation(request.stimulation_payload)
918        .await
919    {
920        Ok(result) => {
921            let success = result
922                .get("success")
923                .and_then(|v| v.as_bool())
924                .unwrap_or(false);
925            let total_coordinates = result
926                .get("total_coordinates")
927                .and_then(|v| v.as_u64())
928                .unwrap_or(0) as usize;
929            let successful_areas = result
930                .get("successful_areas")
931                .and_then(|v| v.as_u64())
932                .unwrap_or(0) as usize;
933            let failed_areas = result
934                .get("failed_areas")
935                .and_then(|v| v.as_array())
936                .map(|arr| {
937                    arr.iter()
938                        .filter_map(|v| v.as_str().map(String::from))
939                        .collect()
940                })
941                .unwrap_or_default();
942            let error = result
943                .get("error")
944                .and_then(|v| v.as_str())
945                .map(String::from);
946
947            Ok(Json(ManualStimulationResponse {
948                success,
949                total_coordinates,
950                successful_areas,
951                failed_areas,
952                error,
953            }))
954        }
955        Err(e) => Err(ApiError::internal(format!("Stimulation failed: {}", e))),
956    }
957}
958
959/// Get Fire Queue (FQ) sampler coordination status including visualization and motor sampling configuration.
960#[utoipa::path(
961    get,
962    path = "/v1/agent/fq_sampler_status",
963    responses(
964        (status = 200, description = "FQ sampler status", body = HashMap<String, serde_json::Value>),
965        (status = 500, description = "Failed to get FQ sampler status")
966    ),
967    tag = "agent"
968)]
969pub async fn get_fq_sampler_status(
970    State(state): State<ApiState>,
971) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
972    let agent_service = state
973        .agent_service
974        .as_ref()
975        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
976
977    let runtime_service = state.runtime_service.as_ref();
978
979    // Get all agents
980    let agent_ids = agent_service
981        .list_agents()
982        .await
983        .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
984
985    // Get FCL sampler config from RuntimeService
986    let (fcl_frequency, fcl_consumer) = runtime_service
987        .get_fcl_sampler_config()
988        .await
989        .map_err(|e| ApiError::internal(format!("Failed to get sampler config: {}", e)))?;
990
991    // Build response matching Python structure
992    let mut visualization_agents = Vec::new();
993    let mut motor_agents = Vec::new();
994
995    for agent_id in &agent_ids {
996        if let Ok(props) = agent_service.get_agent_properties(agent_id).await {
997            if props.capabilities.contains_key("visualization") {
998                visualization_agents.push(agent_id.clone());
999            }
1000            if props.capabilities.contains_key("motor") {
1001                motor_agents.push(agent_id.clone());
1002            }
1003        }
1004    }
1005
1006    let mut fq_coordination = HashMap::new();
1007
1008    let mut viz_sampler = HashMap::new();
1009    viz_sampler.insert(
1010        "enabled".to_string(),
1011        serde_json::json!(!visualization_agents.is_empty()),
1012    );
1013    viz_sampler.insert(
1014        "reason".to_string(),
1015        serde_json::json!(if visualization_agents.is_empty() {
1016            "No visualization agents connected".to_string()
1017        } else {
1018            format!(
1019                "{} visualization agent(s) connected",
1020                visualization_agents.len()
1021            )
1022        }),
1023    );
1024    viz_sampler.insert(
1025        "agents_requiring".to_string(),
1026        serde_json::json!(visualization_agents),
1027    );
1028    viz_sampler.insert("frequency_hz".to_string(), serde_json::json!(fcl_frequency));
1029    fq_coordination.insert(
1030        "visualization_fq_sampler".to_string(),
1031        serde_json::json!(viz_sampler),
1032    );
1033
1034    let mut motor_sampler = HashMap::new();
1035    motor_sampler.insert(
1036        "enabled".to_string(),
1037        serde_json::json!(!motor_agents.is_empty()),
1038    );
1039    motor_sampler.insert(
1040        "reason".to_string(),
1041        serde_json::json!(if motor_agents.is_empty() {
1042            "No motor agents connected".to_string()
1043        } else {
1044            format!("{} motor agent(s) connected", motor_agents.len())
1045        }),
1046    );
1047    motor_sampler.insert(
1048        "agents_requiring".to_string(),
1049        serde_json::json!(motor_agents),
1050    );
1051    motor_sampler.insert("frequency_hz".to_string(), serde_json::json!(100.0));
1052    fq_coordination.insert(
1053        "motor_fq_sampler".to_string(),
1054        serde_json::json!(motor_sampler),
1055    );
1056
1057    let mut response = HashMap::new();
1058    response.insert(
1059        "fq_sampler_coordination".to_string(),
1060        serde_json::json!(fq_coordination),
1061    );
1062    response.insert(
1063        "agent_registry".to_string(),
1064        serde_json::json!({
1065            "total_agents": agent_ids.len(),
1066            "agent_ids": agent_ids
1067        }),
1068    );
1069    response.insert(
1070        "system_status".to_string(),
1071        serde_json::json!("coordinated_via_registration_manager"),
1072    );
1073    response.insert(
1074        "fcl_sampler_consumer".to_string(),
1075        serde_json::json!(fcl_consumer),
1076    );
1077
1078    Ok(Json(response))
1079}
1080
1081/// Get list of all supported agent types and capability types (sensory, motor, visualization, etc.).
1082#[utoipa::path(
1083    get,
1084    path = "/v1/agent/capabilities",
1085    responses(
1086        (status = 200, description = "List of capabilities", body = HashMap<String, Vec<String>>),
1087        (status = 500, description = "Failed to get capabilities")
1088    ),
1089    tag = "agent"
1090)]
1091pub async fn get_capabilities(
1092    State(_state): State<ApiState>,
1093) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1094    let mut response = HashMap::new();
1095    response.insert(
1096        "agent_types".to_string(),
1097        vec![
1098            "sensory".to_string(),
1099            "motor".to_string(),
1100            "both".to_string(),
1101            "visualization".to_string(),
1102            "infrastructure".to_string(),
1103        ],
1104    );
1105    response.insert(
1106        "capability_types".to_string(),
1107        vec![
1108            "vision".to_string(),
1109            "motor".to_string(),
1110            "visualization".to_string(),
1111            "sensory".to_string(),
1112        ],
1113    );
1114
1115    Ok(Json(response))
1116}
1117
1118/// Get capabilities for all agents with optional filtering and payload includes.
1119#[utoipa::path(
1120    get,
1121    path = "/v1/agent/capabilities/all",
1122    params(
1123        ("agent_type" = Option<String>, Query, description = "Filter by agent type (exact match)"),
1124        ("capability" = Option<String>, Query, description = "Filter by capability key(s), comma-separated"),
1125        ("include_device_registrations" = Option<bool>, Query, description = "Include device registration payloads per agent")
1126    ),
1127    responses(
1128        (status = 200, description = "Agent capabilities map", body = HashMap<String, AgentCapabilitiesSummary>),
1129        (status = 400, description = "Invalid query"),
1130        (status = 500, description = "Failed to get agent capabilities")
1131    ),
1132    tag = "agent"
1133)]
1134pub async fn get_all_agent_capabilities(
1135    State(state): State<ApiState>,
1136    Query(params): Query<AgentCapabilitiesAllQuery>,
1137) -> ApiResult<Json<HashMap<String, AgentCapabilitiesSummary>>> {
1138    let agent_service = state
1139        .agent_service
1140        .as_ref()
1141        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1142
1143    let include_device_registrations = params.include_device_registrations.unwrap_or(false);
1144    let capability_filters: Option<Vec<String>> = params.capability.as_ref().and_then(|value| {
1145        let filters: Vec<String> = value
1146            .split(',')
1147            .map(|item| item.trim())
1148            .filter(|item| !item.is_empty())
1149            .map(String::from)
1150            .collect();
1151        if filters.is_empty() {
1152            None
1153        } else {
1154            Some(filters)
1155        }
1156    });
1157
1158    let agent_ids = agent_service
1159        .list_agents()
1160        .await
1161        .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
1162
1163    let mut response: HashMap<String, AgentCapabilitiesSummary> = HashMap::new();
1164
1165    for agent_id in agent_ids {
1166        let agent_name = get_agent_name_from_id(&agent_id)?;
1167        let properties = match agent_service.get_agent_properties(&agent_id).await {
1168            Ok(props) => props,
1169            Err(_) => continue,
1170        };
1171
1172        if let Some(ref agent_type_filter) = params.agent_type {
1173            if properties.agent_type != *agent_type_filter {
1174                continue;
1175            }
1176        }
1177
1178        if let Some(ref filters) = capability_filters {
1179            let has_match = filters
1180                .iter()
1181                .any(|capability| properties.capabilities.contains_key(capability));
1182            if !has_match {
1183                continue;
1184            }
1185        }
1186
1187        let device_registrations = if include_device_registrations {
1188            #[cfg(feature = "feagi-agent")]
1189            {
1190                Some(export_device_registrations_from_connector(
1191                    &state, &agent_id,
1192                )?)
1193            }
1194            #[cfg(not(feature = "feagi-agent"))]
1195            {
1196                None
1197            }
1198        } else {
1199            None
1200        };
1201        response.insert(
1202            agent_id,
1203            AgentCapabilitiesSummary {
1204                agent_name,
1205                capabilities: properties.capabilities,
1206                device_registrations,
1207            },
1208        );
1209    }
1210
1211    Ok(Json(response))
1212}
1213
1214#[cfg(feature = "feagi-agent")]
1215fn export_device_registrations_from_connector(
1216    state: &ApiState,
1217    agent_id: &str,
1218) -> ApiResult<serde_json::Value> {
1219    let agent_descriptor = parse_agent_descriptor(agent_id)?;
1220    let connector = {
1221        let connectors = state.agent_connectors.read();
1222        connectors.get(&agent_descriptor).cloned()
1223    };
1224    let connector = connector.ok_or_else(|| {
1225        ApiError::not_found(
1226            "device_registrations",
1227            &format!("No ConnectorAgent found for agent '{}'", agent_id),
1228        )
1229    })?;
1230
1231    let connector_guard = connector
1232        .lock()
1233        .map_err(|_| ApiError::internal("ConnectorAgent lock poisoned".to_string()))?;
1234    connector_guard.get_device_registration_json().map_err(|e| {
1235        ApiError::internal(format!(
1236            "Failed to export device registrations for agent '{}': {}",
1237            agent_id, e
1238        ))
1239    })
1240}
1241
1242/// Get comprehensive agent information including status, capabilities, version, and connection details.
1243#[utoipa::path(
1244    get,
1245    path = "/v1/agent/info/{agent_id}",
1246    params(
1247        ("agent_id" = String, Path, description = "Agent ID")
1248    ),
1249    responses(
1250        (status = 200, description = "Agent detailed info", body = HashMap<String, serde_json::Value>),
1251        (status = 404, description = "Agent not found"),
1252        (status = 500, description = "Failed to get agent info")
1253    ),
1254    tag = "agent"
1255)]
1256pub async fn get_agent_info(
1257    State(state): State<ApiState>,
1258    Path(agent_id): Path<String>,
1259) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1260    let agent_service = state
1261        .agent_service
1262        .as_ref()
1263        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1264
1265    let properties = agent_service
1266        .get_agent_properties(&agent_id)
1267        .await
1268        .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1269
1270    let mut response = HashMap::new();
1271    response.insert("agent_id".to_string(), serde_json::json!(agent_id));
1272    response.insert(
1273        "agent_name".to_string(),
1274        serde_json::json!(get_agent_name_from_id(&agent_id)?),
1275    );
1276    response.insert(
1277        "agent_type".to_string(),
1278        serde_json::json!(properties.agent_type),
1279    );
1280    response.insert(
1281        "agent_ip".to_string(),
1282        serde_json::json!(properties.agent_ip),
1283    );
1284    response.insert(
1285        "agent_data_port".to_string(),
1286        serde_json::json!(properties.agent_data_port),
1287    );
1288    response.insert(
1289        "capabilities".to_string(),
1290        serde_json::json!(properties.capabilities),
1291    );
1292    response.insert(
1293        "agent_version".to_string(),
1294        serde_json::json!(properties.agent_version),
1295    );
1296    response.insert(
1297        "controller_version".to_string(),
1298        serde_json::json!(properties.controller_version),
1299    );
1300    response.insert("status".to_string(), serde_json::json!("active"));
1301    if let Some(ref transport) = properties.chosen_transport {
1302        response.insert("chosen_transport".to_string(), serde_json::json!(transport));
1303    }
1304
1305    Ok(Json(response))
1306}
1307
1308/// Get agent properties using path parameter. Same as /v1/agent/properties but with agent_id in the URL path.
1309#[utoipa::path(
1310    get,
1311    path = "/v1/agent/properties/{agent_id}",
1312    params(
1313        ("agent_id" = String, Path, description = "Agent ID")
1314    ),
1315    responses(
1316        (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
1317        (status = 404, description = "Agent not found"),
1318        (status = 500, description = "Failed to get agent properties")
1319    ),
1320    tag = "agent"
1321)]
1322pub async fn get_agent_properties_path(
1323    State(state): State<ApiState>,
1324    Path(agent_id): Path<String>,
1325) -> ApiResult<Json<AgentPropertiesResponse>> {
1326    let agent_service = state
1327        .agent_service
1328        .as_ref()
1329        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1330
1331    match agent_service.get_agent_properties(&agent_id).await {
1332        Ok(properties) => Ok(Json(AgentPropertiesResponse {
1333            agent_name: get_agent_name_from_id(&agent_id)?,
1334            agent_type: properties.agent_type,
1335            agent_ip: properties.agent_ip,
1336            agent_data_port: properties.agent_data_port,
1337            agent_router_address: properties.agent_router_address,
1338            agent_version: properties.agent_version,
1339            controller_version: properties.controller_version,
1340            capabilities: properties.capabilities,
1341            chosen_transport: properties.chosen_transport,
1342        })),
1343        Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
1344    }
1345}
1346
1347/// Configure agent parameters and settings. (Not yet implemented)
1348#[utoipa::path(
1349    post,
1350    path = "/v1/agent/configure",
1351    responses(
1352        (status = 200, description = "Agent configured", body = HashMap<String, String>),
1353        (status = 400, description = "Invalid input"),
1354        (status = 500, description = "Failed to configure agent")
1355    ),
1356    tag = "agent"
1357)]
1358pub async fn post_configure(
1359    State(_state): State<ApiState>,
1360    Json(config): Json<HashMap<String, serde_json::Value>>,
1361) -> ApiResult<Json<HashMap<String, String>>> {
1362    tracing::info!(target: "feagi-api", "Agent configuration requested: {} params", config.len());
1363
1364    Ok(Json(HashMap::from([
1365        (
1366            "message".to_string(),
1367            "Agent configuration updated".to_string(),
1368        ),
1369        ("status".to_string(), "not_yet_implemented".to_string()),
1370    ])))
1371}
1372
1373/// Export device registrations for an agent
1374///
1375/// Returns the complete device registration configuration including
1376/// sensor and motor device registrations, encoder/decoder properties,
1377/// and feedback configurations in the format compatible with
1378/// ConnectorAgent::get_device_registration_json.
1379#[utoipa::path(
1380    get,
1381    path = "/v1/agent/{agent_id}/device_registrations",
1382    params(
1383        ("agent_id" = String, Path, description = "Agent ID")
1384    ),
1385    responses(
1386        (status = 200, description = "Device registrations exported successfully", body = DeviceRegistrationExportResponse),
1387        (status = 404, description = "Agent not found"),
1388        (status = 500, description = "Failed to export device registrations")
1389    ),
1390    tag = "agent"
1391)]
1392pub async fn export_device_registrations(
1393    State(state): State<ApiState>,
1394    Path(agent_id): Path<String>,
1395) -> ApiResult<Json<DeviceRegistrationExportResponse>> {
1396    info!(
1397        "🦀 [API] Device registration export requested for agent '{}'",
1398        agent_id
1399    );
1400
1401    // Verify agent exists only if AgentService is available.
1402    //
1403    // Rationale: live contract tests and minimal deployments can run without an AgentService.
1404    // In that case, device registration import/export should still work as a pure per-agent
1405    // configuration store (ConnectorAgent-backed).
1406    if let Some(agent_service) = state.agent_service.as_ref() {
1407        let _properties = agent_service
1408            .get_agent_properties(&agent_id)
1409            .await
1410            .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1411    } else {
1412        info!(
1413            "ℹ️ [API] Agent service not available; skipping agent existence check for export (agent '{}')",
1414            agent_id
1415        );
1416    }
1417
1418    // Get existing ConnectorAgent for this agent (don't create new one)
1419    #[cfg(feature = "feagi-agent")]
1420    let device_registrations = {
1421        let agent_descriptor = parse_agent_descriptor(&agent_id)?;
1422        // Get existing ConnectorAgent - don't create a new one
1423        // If no ConnectorAgent exists, it means device registrations haven't been imported yet
1424        let connector = {
1425            let connectors = state.agent_connectors.read();
1426            connectors.get(&agent_descriptor).cloned()
1427        };
1428
1429        let connector = match connector {
1430            Some(c) => {
1431                info!(
1432                    "🔍 [API] Found existing ConnectorAgent for agent '{}'",
1433                    agent_id
1434                );
1435                c
1436            }
1437            None => {
1438                warn!(
1439                    "⚠️ [API] No ConnectorAgent found for agent '{}' - device registrations may not have been imported yet. Total agents in registry: {}",
1440                    agent_id,
1441                    {
1442                        let connectors = state.agent_connectors.read();
1443                        connectors.len()
1444                    }
1445                );
1446                // Return empty structure - don't create and store a new ConnectorAgent
1447                // This prevents interference with future imports
1448                return Ok(Json(DeviceRegistrationExportResponse {
1449                    device_registrations: serde_json::json!({
1450                        "input_units_and_encoder_properties": {},
1451                        "output_units_and_decoder_properties": {},
1452                        "feedbacks": []
1453                    }),
1454                    agent_id,
1455                }));
1456            }
1457        };
1458
1459        // Export device registrations using ConnectorAgent method
1460        let connector_guard = connector.lock().unwrap();
1461        match connector_guard.get_device_registration_json() {
1462            Ok(registrations) => {
1463                // Log what we're exporting for debugging
1464                let input_count = registrations
1465                    .get("input_units_and_encoder_properties")
1466                    .and_then(|v| v.as_object())
1467                    .map(|m| m.len())
1468                    .unwrap_or(0);
1469                let output_count = registrations
1470                    .get("output_units_and_decoder_properties")
1471                    .and_then(|v| v.as_object())
1472                    .map(|m| m.len())
1473                    .unwrap_or(0);
1474                let feedback_count = registrations
1475                    .get("feedbacks")
1476                    .and_then(|v| v.as_array())
1477                    .map(|a| a.len())
1478                    .unwrap_or(0);
1479
1480                info!(
1481                    "📤 [API] Exporting device registrations for agent '{}': {} input units, {} output units, {} feedbacks",
1482                    agent_id, input_count, output_count, feedback_count
1483                );
1484
1485                if input_count == 0 && output_count == 0 && feedback_count == 0 {
1486                    warn!(
1487                        "⚠️ [API] Exported device registrations for agent '{}' are empty - agent may not have synced device registrations yet",
1488                        agent_id
1489                    );
1490                }
1491
1492                registrations
1493            }
1494            Err(e) => {
1495                warn!(
1496                    "⚠️ [API] Failed to export device registrations for agent '{}': {}",
1497                    agent_id, e
1498                );
1499                // @architecture:acceptable - emergency fallback on export failure
1500                // Return empty structure on error to prevent API failure
1501                serde_json::json!({
1502                    "input_units_and_encoder_properties": {},
1503                    "output_units_and_decoder_properties": {},
1504                    "feedbacks": []
1505                })
1506            }
1507        }
1508    };
1509
1510    #[cfg(not(feature = "feagi-agent"))]
1511    // @architecture:acceptable - fallback when feature is disabled
1512    // Returns empty structure when feagi-agent feature is not compiled in
1513    let device_registrations = serde_json::json!({
1514        "input_units_and_encoder_properties": {},
1515        "output_units_and_decoder_properties": {},
1516        "feedbacks": []
1517    });
1518
1519    info!(
1520        "✅ [API] Device registration export succeeded for agent '{}'",
1521        agent_id
1522    );
1523
1524    Ok(Json(DeviceRegistrationExportResponse {
1525        device_registrations,
1526        agent_id,
1527    }))
1528}
1529
1530/// Import device registrations for an agent
1531///
1532/// Imports a device registration configuration, replacing all existing
1533/// device registrations for the agent. The configuration must be in
1534/// the format compatible with ConnectorAgent::set_device_registrations_from_json.
1535///
1536/// # Warning
1537/// This operation **wipes all existing registered devices** before importing
1538/// the new configuration.
1539#[utoipa::path(
1540    post,
1541    path = "/v1/agent/{agent_id}/device_registrations",
1542    params(
1543        ("agent_id" = String, Path, description = "Agent ID")
1544    ),
1545    request_body = DeviceRegistrationImportRequest,
1546    responses(
1547        (status = 200, description = "Device registrations imported successfully", body = DeviceRegistrationImportResponse),
1548        (status = 400, description = "Invalid device registration configuration"),
1549        (status = 404, description = "Agent not found"),
1550        (status = 500, description = "Failed to import device registrations")
1551    ),
1552    tag = "agent"
1553)]
1554pub async fn import_device_registrations(
1555    State(state): State<ApiState>,
1556    Path(agent_id): Path<String>,
1557    Json(request): Json<DeviceRegistrationImportRequest>,
1558) -> ApiResult<Json<DeviceRegistrationImportResponse>> {
1559    info!(
1560        "🦀 [API] Device registration import requested for agent '{}'",
1561        agent_id
1562    );
1563
1564    // Verify agent exists only if AgentService is available (see export_device_registrations).
1565    if let Some(agent_service) = state.agent_service.as_ref() {
1566        let _properties = agent_service
1567            .get_agent_properties(&agent_id)
1568            .await
1569            .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1570    } else {
1571        info!(
1572            "ℹ️ [API] Agent service not available; skipping agent existence check for import (agent '{}')",
1573            agent_id
1574        );
1575    }
1576
1577    // Validate the device registration JSON structure
1578    // Check that it has the expected fields
1579    if !request.device_registrations.is_object() {
1580        return Err(ApiError::invalid_input(
1581            "Device registrations must be a JSON object",
1582        ));
1583    }
1584
1585    // Validate required fields exist
1586    let obj = request.device_registrations.as_object().unwrap();
1587    if !obj.contains_key("input_units_and_encoder_properties")
1588        || !obj.contains_key("output_units_and_decoder_properties")
1589        || !obj.contains_key("feedbacks")
1590    {
1591        return Err(ApiError::invalid_input(
1592            "Device registrations must contain: input_units_and_encoder_properties, output_units_and_decoder_properties, and feedbacks",
1593        ));
1594    }
1595
1596    // Import device registrations using ConnectorAgent
1597    #[cfg(feature = "feagi-agent")]
1598    {
1599        let agent_descriptor = parse_agent_descriptor(&agent_id)?;
1600        // Get or create ConnectorAgent for this agent
1601        let connector = {
1602            let mut connectors = state.agent_connectors.write();
1603            let was_existing = connectors.contains_key(&agent_descriptor);
1604            let connector = connectors
1605                .entry(agent_descriptor)
1606                .or_insert_with(|| {
1607                    info!(
1608                        "🔧 [API] Creating new ConnectorAgent for agent '{}'",
1609                        agent_id
1610                    );
1611                    Arc::new(Mutex::new(ConnectorAgent::new_empty(agent_descriptor)))
1612                })
1613                .clone();
1614            if was_existing {
1615                info!(
1616                    "🔧 [API] Using existing ConnectorAgent for agent '{}'",
1617                    agent_id
1618                );
1619            }
1620            connector
1621        };
1622
1623        // Log what we're importing for debugging
1624        let input_count = request
1625            .device_registrations
1626            .get("input_units_and_encoder_properties")
1627            .and_then(|v| v.as_object())
1628            .map(|m| m.len())
1629            .unwrap_or(0);
1630        let output_count = request
1631            .device_registrations
1632            .get("output_units_and_decoder_properties")
1633            .and_then(|v| v.as_object())
1634            .map(|m| m.len())
1635            .unwrap_or(0);
1636        let feedback_count = request
1637            .device_registrations
1638            .get("feedbacks")
1639            .and_then(|v| v.as_array())
1640            .map(|a| a.len())
1641            .unwrap_or(0);
1642
1643        info!(
1644            "📥 [API] Importing device registrations for agent '{}': {} input units, {} output units, {} feedbacks",
1645            agent_id, input_count, output_count, feedback_count
1646        );
1647
1648        // IMPORTANT: do not hold a non-Send MutexGuard across an await.
1649        let import_result: Result<(), String> = (|| {
1650            let mut connector_guard = connector
1651                .lock()
1652                .map_err(|e| format!("ConnectorAgent lock poisoned: {e}"))?;
1653
1654            connector_guard
1655                .set_device_registrations_from_json(request.device_registrations.clone())
1656                .map_err(|e| format!("import failed: {e}"))?;
1657
1658            // Verify the import worked by exporting again (no await here).
1659            match connector_guard.get_device_registration_json() {
1660                Ok(exported) => {
1661                    let exported_input_count = exported
1662                        .get("input_units_and_encoder_properties")
1663                        .and_then(|v| v.as_object())
1664                        .map(|m| m.len())
1665                        .unwrap_or(0);
1666                    let exported_output_count = exported
1667                        .get("output_units_and_decoder_properties")
1668                        .and_then(|v| v.as_object())
1669                        .map(|m| m.len())
1670                        .unwrap_or(0);
1671
1672                    info!(
1673                        "✅ [API] Device registration import succeeded for agent '{}' (verified: {} input, {} output)",
1674                        agent_id, exported_input_count, exported_output_count
1675                    );
1676                }
1677                Err(e) => {
1678                    warn!(
1679                        "⚠️ [API] Import succeeded but verification export failed for agent '{}': {}",
1680                        agent_id, e
1681                    );
1682                }
1683            }
1684
1685            Ok(())
1686        })();
1687
1688        if let Err(msg) = import_result {
1689            error!(
1690                "❌ [API] Failed to import device registrations for agent '{}': {}",
1691                agent_id, msg
1692            );
1693            return Err(ApiError::invalid_input(format!(
1694                "Failed to import device registrations: {msg}"
1695            )));
1696        }
1697
1698        auto_create_cortical_areas_from_device_registrations(&state, &request.device_registrations)
1699            .await;
1700
1701        Ok(Json(DeviceRegistrationImportResponse {
1702            success: true,
1703            message: format!(
1704                "Device registrations imported successfully for agent '{}'",
1705                agent_id
1706            ),
1707            agent_id,
1708        }))
1709    }
1710
1711    #[cfg(not(feature = "feagi-agent"))]
1712    {
1713        info!(
1714            "✅ [API] Device registration import succeeded for agent '{}' (feagi-agent feature not enabled)",
1715            agent_id
1716        );
1717        Ok(Json(DeviceRegistrationImportResponse {
1718            success: true,
1719            message: format!(
1720                "Device registrations imported successfully for agent '{}' (feature not enabled)",
1721                agent_id
1722            ),
1723            agent_id,
1724        }))
1725    }
1726}