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