feagi_api/
amalgamation.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3/*!
4Genome amalgamation state (FEAGI-side).
5
6This module implements the server-side portion of the BV/desktop amalgamation workflow:
7
8- A client (e.g., Brain Hub) calls `POST /v1/genome/amalgamation_by_payload` to submit a genome JSON
9  to be amalgamated into the currently running brain.
10- FEAGI stores a single **pending** amalgamation request and surfaces it via
11  `GET /v1/system/health_check` as `amalgamation_pending`.
12- Brain Visualizer polls `health_check`, detects `amalgamation_pending`, and prompts the user for
13  where/how to import. BV then calls `POST /v1/genome/amalgamation_destination` to confirm.
14- FEAGI applies the confirmation and clears the pending state so BV can detect completion.
15
16Design constraints:
17- Deterministic: no implicit behavior changes across platforms.
18- Minimal state: stored in-memory (per FEAGI session). Persistence is not currently required by BV.
19- Schema compatibility: `amalgamation_pending` must include keys BV expects.
20*/
21
22use parking_lot::RwLock;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25use std::sync::Arc;
26
27/// BV-facing summary of a pending amalgamation (health_check contract).
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct AmalgamationPendingSummary {
30    pub amalgamation_id: String,
31    pub genome_title: String,
32    /// Dimensions of the imported circuit bounding box (x,y,z).
33    pub circuit_size: [i32; 3],
34}
35
36/// Internal stored pending amalgamation request.
37#[derive(Debug, Clone)]
38pub struct AmalgamationPending {
39    pub summary: AmalgamationPendingSummary,
40    /// Original genome JSON payload (as string) for deterministic replay on confirmation.
41    pub genome_json: String,
42}
43
44/// Historical record for completed/cancelled requests.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AmalgamationHistoryEntry {
47    pub amalgamation_id: String,
48    pub genome_title: String,
49    pub circuit_size: [i32; 3],
50    pub status: String, // "pending" | "confirmed" | "cancelled" | "replaced"
51    pub timestamp_ms: i64,
52}
53
54#[derive(Debug, Default, Clone)]
55pub struct AmalgamationState {
56    pub pending: Option<AmalgamationPending>,
57    pub history: Vec<AmalgamationHistoryEntry>,
58}
59
60pub type SharedAmalgamationState = Arc<RwLock<AmalgamationState>>;
61
62/// Create a fresh in-memory amalgamation state container.
63pub fn new_shared_state() -> SharedAmalgamationState {
64    Arc::new(RwLock::new(AmalgamationState::default()))
65}
66
67/// Compute a circuit bounding-box size (x,y,z) from a parsed RuntimeGenome.
68///
69/// Bounding box is computed over all cortical areas:
70/// - min corner = min(position)
71/// - max corner = max(position + dimensions)
72/// - size = max - min (per axis)
73///
74/// If there are no cortical areas, size is [0,0,0].
75pub fn compute_circuit_size_from_runtime_genome(
76    genome: &feagi_evolutionary::RuntimeGenome,
77) -> [i32; 3] {
78    let mut any = false;
79    let mut min_x: i32 = 0;
80    let mut min_y: i32 = 0;
81    let mut min_z: i32 = 0;
82    let mut max_x: i32 = 0;
83    let mut max_y: i32 = 0;
84    let mut max_z: i32 = 0;
85
86    for area in genome.cortical_areas.values() {
87        let x0 = area.position.x;
88        let y0 = area.position.y;
89        let z0 = area.position.z;
90
91        let x1 = x0.saturating_add(area.dimensions.width as i32);
92        let y1 = y0.saturating_add(area.dimensions.height as i32);
93        let z1 = z0.saturating_add(area.dimensions.depth as i32);
94
95        if !any {
96            any = true;
97            min_x = x0;
98            min_y = y0;
99            min_z = z0;
100            max_x = x1;
101            max_y = y1;
102            max_z = z1;
103        } else {
104            min_x = min_x.min(x0);
105            min_y = min_y.min(y0);
106            min_z = min_z.min(z0);
107            max_x = max_x.max(x1);
108            max_y = max_y.max(y1);
109            max_z = max_z.max(z1);
110        }
111    }
112
113    if !any {
114        return [0, 0, 0];
115    }
116
117    [
118        max_x.saturating_sub(min_x),
119        max_y.saturating_sub(min_y),
120        max_z.saturating_sub(min_z),
121    ]
122}
123
124/// Convert a pending summary to the `health_check` JSON shape.
125pub fn pending_summary_to_health_json(summary: &AmalgamationPendingSummary) -> Value {
126    serde_json::json!({
127        "amalgamation_id": summary.amalgamation_id,
128        "genome_title": summary.genome_title,
129        "circuit_size": summary.circuit_size,
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn compute_circuit_size_empty_genome_is_zero() {
139        let genome =
140            feagi_evolutionary::templates::create_minimal_genome("g".to_string(), "t".to_string());
141        assert_eq!(compute_circuit_size_from_runtime_genome(&genome), [0, 0, 0]);
142    }
143
144    #[test]
145    fn compute_circuit_size_single_area_matches_block_boundaries() {
146        // One area at [1,1,1] with dims [2,3,4] -> size should be [2,3,4].
147        let json = serde_json::json!({
148            "genome_id": "test",
149            "genome_title": "Test Genome",
150            "genome_description": "",
151            "version": "2.1",
152            "blueprint": {
153                "X19fcG93ZXI=": { // "___power" base64 (core-ish, but valid cortical id)
154                    "cortical_name": "Area",
155                    "block_boundaries": [2, 3, 4],
156                    "relative_coordinate": [1, 1, 1],
157                    "cortical_type": "CUSTOM"
158                }
159            },
160            "brain_regions": {
161                "root": {
162                    "title": "Root",
163                    "parent_region_id": null,
164                    "coordinate_3d": [0, 0, 0],
165                    "areas": ["X19fcG93ZXI="],
166                    "regions": []
167                }
168            },
169            "physiology": {
170                "simulation_timestep": 0.025
171            }
172        })
173        .to_string();
174
175        let genome = feagi_evolutionary::load_genome_from_json(&json).expect("valid genome json");
176        assert_eq!(compute_circuit_size_from_runtime_genome(&genome), [2, 3, 4]);
177    }
178}