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