1use crate::ewc::{EwcConfig, EwcPlusPlus};
4use crate::loops::background::{BackgroundLoop, BackgroundLoopConfig, BackgroundResult};
5use crate::loops::instant::InstantLoop;
6use crate::lora::{BaseLoRA, MicroLoRA};
7use crate::reasoning_bank::{PatternConfig, ReasoningBank};
8use crate::types::{QueryTrajectory, SonaConfig};
9use parking_lot::RwLock;
10use std::sync::Arc;
11
12pub struct LoopCoordinator {
14 _config: SonaConfig,
16 instant: InstantLoop,
18 background: BackgroundLoop,
20 reasoning_bank: Arc<RwLock<ReasoningBank>>,
22 ewc: Arc<RwLock<EwcPlusPlus>>,
23 base_lora: Arc<RwLock<BaseLoRA>>,
24 instant_enabled: bool,
26 background_enabled: bool,
27}
28
29impl LoopCoordinator {
30 pub fn new(hidden_dim: usize) -> Self {
32 Self::with_config(SonaConfig {
33 hidden_dim,
34 embedding_dim: hidden_dim,
35 ..Default::default()
36 })
37 }
38
39 pub fn with_config(config: SonaConfig) -> Self {
41 let reasoning_bank = Arc::new(RwLock::new(ReasoningBank::new(PatternConfig {
42 embedding_dim: config.embedding_dim,
43 k_clusters: config.pattern_clusters,
44 ..Default::default()
45 })));
46
47 let ewc = Arc::new(RwLock::new(EwcPlusPlus::new(EwcConfig {
48 param_count: config.hidden_dim * config.base_lora_rank * 2,
49 initial_lambda: config.ewc_lambda,
50 ..Default::default()
51 })));
52
53 let base_lora = Arc::new(RwLock::new(BaseLoRA::new(
54 config.hidden_dim,
55 config.base_lora_rank,
56 12, )));
58
59 let instant = InstantLoop::from_sona_config(&config);
60 let background = BackgroundLoop::new(
61 BackgroundLoopConfig::from(&config),
62 reasoning_bank.clone(),
63 ewc.clone(),
64 base_lora.clone(),
65 );
66
67 Self {
68 _config: config,
69 instant,
70 background,
71 reasoning_bank,
72 ewc,
73 base_lora,
74 instant_enabled: true,
75 background_enabled: true,
76 }
77 }
78
79 pub fn on_inference(&self, trajectory: QueryTrajectory) {
81 if self.instant_enabled {
82 self.instant.on_trajectory(trajectory);
83 }
84 }
85
86 pub fn next_trajectory_id(&self) -> u64 {
88 self.instant.next_id()
89 }
90
91 pub fn maybe_run_background(&self) -> Option<BackgroundResult> {
93 if !self.background_enabled {
94 return None;
95 }
96
97 if self.background.should_run() {
98 let trajectories = self.instant.drain_trajectories();
99 if !trajectories.is_empty() {
100 return Some(self.background.run_cycle(trajectories, false));
101 }
102 }
103
104 None
105 }
106
107 pub fn force_background(&self) -> BackgroundResult {
109 let trajectories = self.instant.drain_trajectories();
110 self.background.run_cycle(trajectories, true)
111 }
112
113 pub fn flush_instant(&self) {
115 self.instant.flush();
116 }
117
118 pub fn micro_lora(&self) -> &Arc<RwLock<MicroLoRA>> {
120 self.instant.micro_lora()
121 }
122
123 pub fn get_micro_lora_weights(&self) -> (Vec<f32>, Vec<f32>) {
127 let guard = self.instant.micro_lora().read();
128 let (down, up) = guard.get_weights();
129 (down.clone(), up.clone())
130 }
131
132 pub fn restore_micro_lora_weights(&self, down: Vec<f32>, up: Vec<f32>) -> Result<(), String> {
134 self.instant.micro_lora().write().set_weights(down, up)
135 }
136
137 pub fn base_lora(&self) -> &Arc<RwLock<BaseLoRA>> {
139 &self.base_lora
140 }
141
142 pub fn reasoning_bank(&self) -> &Arc<RwLock<ReasoningBank>> {
144 &self.reasoning_bank
145 }
146
147 pub fn ewc(&self) -> &Arc<RwLock<EwcPlusPlus>> {
149 &self.ewc
150 }
151
152 pub fn set_instant_enabled(&mut self, enabled: bool) {
154 self.instant_enabled = enabled;
155 }
156
157 pub fn set_background_enabled(&mut self, enabled: bool) {
159 self.background_enabled = enabled;
160 }
161
162 pub fn stats(&self) -> CoordinatorStats {
164 let (buffer_len, dropped, success_rate) = self.instant.buffer_stats();
165
166 CoordinatorStats {
167 trajectories_recorded: buffer_len as u64 + dropped,
168 trajectories_buffered: buffer_len,
169 trajectories_dropped: dropped,
170 buffer_success_rate: success_rate,
171 patterns_stored: self.reasoning_bank.read().pattern_count(),
172 patterns_learned: self.reasoning_bank.read().pattern_count(),
173 ewc_tasks: self.ewc.read().task_count(),
174 instant_enabled: self.instant_enabled,
175 background_enabled: self.background_enabled,
176 }
177 }
178
179 pub fn serialize_state(&self) -> String {
181 let rb = self.reasoning_bank.read();
182 let patterns = rb.get_all_patterns();
183 let ewc = self.ewc.read();
184 serde_json::json!({
185 "version": 1,
186 "patterns": patterns,
187 "ewc_task_count": ewc.task_count(),
188 "instant_enabled": self.instant_enabled,
189 "background_enabled": self.background_enabled,
190 })
191 .to_string()
192 }
193
194 pub fn load_state(&self, json: &str) -> Result<usize, String> {
197 let state: serde_json::Value =
198 serde_json::from_str(json).map_err(|e| format!("Invalid state JSON: {}", e))?;
199
200 let mut loaded = 0;
201
202 if let Some(patterns) = state.get("patterns").and_then(|p| p.as_array()) {
204 let mut rb = self.reasoning_bank.write();
205 for p in patterns {
206 if let Ok(pattern) = serde_json::from_value::<crate::LearnedPattern>(p.clone()) {
207 rb.insert_pattern(pattern);
208 loaded += 1;
209 }
210 }
211 }
212
213 Ok(loaded)
214 }
215}
216
217#[derive(Debug, Clone)]
219#[cfg_attr(
220 feature = "serde-support",
221 derive(serde::Serialize, serde::Deserialize)
222)]
223pub struct CoordinatorStats {
224 #[cfg_attr(feature = "serde-support", serde(alias = "trajectoriesRecorded"))]
226 pub trajectories_recorded: u64,
227 pub trajectories_buffered: usize,
228 pub trajectories_dropped: u64,
229 pub buffer_success_rate: f64,
230 pub patterns_stored: usize,
231 #[cfg_attr(feature = "serde-support", serde(alias = "patternsLearned"))]
233 pub patterns_learned: usize,
234 pub ewc_tasks: usize,
235 pub instant_enabled: bool,
236 pub background_enabled: bool,
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::types::TrajectoryStep;
243
244 fn make_trajectory(id: u64) -> QueryTrajectory {
245 let mut t = QueryTrajectory::new(id, vec![0.1; 256]);
246 t.add_step(TrajectoryStep::new(vec![0.5; 256], vec![], 0.8, 0));
247 t.finalize(0.8, 1000);
248 t
249 }
250
251 #[test]
252 fn test_coordinator_creation() {
253 let coord = LoopCoordinator::new(256);
254 let stats = coord.stats();
255 assert_eq!(stats.trajectories_buffered, 0);
256 }
257
258 #[test]
259 fn test_inference_processing() {
260 let coord = LoopCoordinator::new(256);
261
262 for i in 0..10 {
263 let t = make_trajectory(coord.next_trajectory_id());
264 coord.on_inference(t);
265 }
266
267 let stats = coord.stats();
268 assert_eq!(stats.trajectories_buffered, 10);
269 }
270
271 #[test]
272 fn test_force_background() {
273 let coord = LoopCoordinator::new(256);
274
275 for i in 0..150 {
276 let t = make_trajectory(coord.next_trajectory_id());
277 coord.on_inference(t);
278 }
279
280 let result = coord.force_background();
281 assert_eq!(result.trajectories_processed, 150);
282 assert!(result.patterns_extracted > 0);
283 }
284
285 #[test]
286 fn test_get_micro_lora_weights() {
287 let coord = LoopCoordinator::new(256);
288 let (down, up) = coord.get_micro_lora_weights();
289
290 assert_eq!(down.len(), 256 * 2);
291 assert_eq!(up.len(), 2 * 256);
292 }
293
294 #[test]
295 fn test_restore_micro_lora_weights() {
296 let coord = LoopCoordinator::new(256);
297
298 let custom_down = vec![0.5f32; 256 * 2];
299 let custom_up = vec![0.3f32; 2 * 256];
300
301 let result = coord.restore_micro_lora_weights(custom_down.clone(), custom_up.clone());
302 assert!(result.is_ok());
303
304 let (got_down, got_up) = coord.get_micro_lora_weights();
305 assert_eq!(got_down, custom_down);
306 assert_eq!(got_up, custom_up);
307 }
308
309 #[test]
310 fn test_restore_micro_lora_weights_wrong_dim() {
311 let coord = LoopCoordinator::new(256);
312
313 let wrong_down = vec![0.5f32; 256 * 3];
314 let custom_up = vec![0.3f32; 2 * 256];
315
316 let result = coord.restore_micro_lora_weights(wrong_down, custom_up);
317 assert!(result.is_err());
318 assert!(result.unwrap_err().contains("down_proj dimension mismatch"));
319 }
320}