1use crate::config::Config;
14use crate::reader::MicroscopeReader;
15use crate::{store_memory, LAYER_NAMES};
16use std::path::Path;
17use std::sync::Arc;
18#[cfg(unix)]
19use std::{io::Read, io::Write};
20
21#[repr(C)]
26#[derive(Debug, Copy, Clone)]
27pub struct AICommand {
28 pub block_id: u64, pub weight_delta: f32, pub op_code: u8, pub layer: u8, pub payload: [u8; 242], }
34
35impl AICommand {
36 pub fn read(layer: u8, block_id: u64) -> Self {
38 Self {
39 op_code: 0,
40 layer,
41 block_id,
42 weight_delta: 0.0,
43 payload: [0; 242],
44 }
45 }
46
47 pub fn write(layer: u8, block_id: u64, data: &[u8]) -> Self {
49 let mut payload = [0u8; 242];
50 let len = data.len().min(242);
51 payload[..len].copy_from_slice(&data[..len]);
52
53 Self {
54 op_code: 1,
55 layer,
56 block_id,
57 weight_delta: 0.0,
58 payload,
59 }
60 }
61
62 pub fn learn(block_id: u64, weight_delta: f32) -> Self {
64 Self {
65 op_code: 2,
66 layer: 0,
67 block_id,
68 weight_delta,
69 payload: [0; 242],
70 }
71 }
72}
73
74impl Default for AICommand {
75 fn default() -> Self {
76 Self {
77 op_code: 0,
78 layer: 0,
79 block_id: 0,
80 weight_delta: 0.0,
81 payload: [0; 242],
82 }
83 }
84}
85
86const _: () = assert!(std::mem::size_of::<AICommand>() == 256);
87
88pub struct AIAdapter {
92 config: Arc<Config>,
93 reader: Arc<MicroscopeReader>,
94 dirty_blocks: std::collections::HashSet<u64>, command_count: usize, }
97
98impl AIAdapter {
99 pub fn new(config: Config) -> Result<Self, String> {
101 let reader = MicroscopeReader::open(&config)?;
102 Ok(Self {
103 config: Arc::new(config),
104 reader: Arc::new(reader),
105 dirty_blocks: std::collections::HashSet::new(),
106 command_count: 0,
107 })
108 }
109
110 pub fn process_command(&mut self, cmd: AICommand) -> Result<AICommand, String> {
112 self.command_count += 1;
113
114 let result = match cmd.op_code {
115 0 => self.handle_read(cmd),
116 1 => self.handle_write(cmd),
117 2 => self.handle_learn(cmd),
118 _ => Err(format!("Unknown op_code: {}", cmd.op_code)),
119 };
120
121 if self.command_count.is_multiple_of(100) && !self.dirty_blocks.is_empty() {
123 self.update_merkle_tree()?;
124 }
125
126 result
127 }
128
129 fn handle_read(&self, cmd: AICommand) -> Result<AICommand, String> {
131 if cmd.block_id as usize >= self.reader.block_count {
132 return Err("Block ID out of range".to_string());
133 }
134
135 let text = self.reader.text(cmd.block_id as usize);
136 let data = text.as_bytes();
137 let mut response = AICommand {
138 op_code: 0, layer: cmd.layer,
140 block_id: cmd.block_id,
141 ..AICommand::default()
142 };
143 let len = data.len().min(242);
144 response.payload[..len].copy_from_slice(&data[..len]);
145
146 Ok(response)
147 }
148
149 fn handle_write(&mut self, cmd: AICommand) -> Result<AICommand, String> {
154 let len = cmd.payload[0] as usize;
155 if len > 241 {
156 return Err("Invalid payload length".to_string());
157 }
158 let text = String::from_utf8(cmd.payload[1..1 + len].to_vec())
159 .map_err(|e| format!("Invalid UTF-8 payload: {}", e))?;
160
161 let layer_name = LAYER_NAMES
162 .get(cmd.layer as usize)
163 .copied()
164 .unwrap_or("long_term");
165
166 store_memory(
167 &self.config,
168 &text,
169 layer_name,
170 5, )?;
172
173 self.dirty_blocks.insert(cmd.block_id);
175
176 Ok(AICommand {
177 op_code: 1, layer: cmd.layer,
179 block_id: cmd.block_id,
180 ..AICommand::default()
181 })
182 }
183
184 fn handle_learn(&mut self, cmd: AICommand) -> Result<AICommand, String> {
187 use crate::hebbian::HebbianState;
188
189 let mut state = HebbianState::load_or_init(
190 Path::new(&self.config.paths.output_dir),
191 self.reader.block_count,
192 );
193
194 let block_idx = cmd.block_id as u32;
195 let query_hash = cmd.block_id; state.record_activation(&[(block_idx, cmd.weight_delta)], query_hash);
197
198 state.save(Path::new(&self.config.paths.output_dir))?;
199
200 self.dirty_blocks.insert(cmd.block_id);
202
203 Ok(AICommand {
204 op_code: 2, layer: cmd.layer,
206 block_id: cmd.block_id,
207 ..AICommand::default()
208 })
209 }
210
211 pub fn update_merkle_tree(&mut self) -> Result<(), String> {
218 if self.dirty_blocks.is_empty() {
219 return Ok(());
220 }
221 Err(format!(
222 "incremental Merkle update not implemented ({} dirty blocks pending)",
223 self.dirty_blocks.len()
224 ))
225 }
226
227 pub fn current_merkle_root(&self) -> Result<[u8; 32], String> {
229 let output_dir = Path::new(&self.config.paths.output_dir);
230 let merkle_data = std::fs::read(output_dir.join("merkle.bin"))
231 .map_err(|e| format!("Failed to read merkle.bin: {}", e))?;
232 let tree = crate::merkle::MerkleTree::from_bytes(&merkle_data)
233 .ok_or("Invalid merkle.bin format")?;
234 Ok(tree.root)
235 }
236}
237
238#[cfg(windows)]
241mod windows_pipe {
242 use std::ffi::CString;
243 use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
244 use windows_sys::Win32::Storage::FileSystem::{ReadFile, WriteFile, PIPE_ACCESS_DUPLEX};
245 use windows_sys::Win32::System::Pipes::{
246 ConnectNamedPipe, CreateNamedPipeA, DisconnectNamedPipe, PIPE_READMODE_MESSAGE,
247 PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_MESSAGE, PIPE_WAIT,
248 };
249
250 pub struct NamedPipeListener {
251 handle: HANDLE,
252 }
253
254 impl NamedPipeListener {
255 pub fn new(name: &str) -> Result<Self, String> {
256 let pipe_name = if name.starts_with("\\\\.\\pipe\\") {
258 name.to_string()
259 } else {
260 format!("\\\\.\\pipe\\{}", name)
261 };
262 let c_name =
263 CString::new(pipe_name).map_err(|e| format!("Invalid pipe name: {}", e))?;
264
265 let handle = unsafe {
266 CreateNamedPipeA(
267 c_name.as_ptr() as *const u8,
268 PIPE_ACCESS_DUPLEX,
269 PIPE_TYPE_MESSAGE
270 | PIPE_READMODE_MESSAGE
271 | PIPE_WAIT
272 | PIPE_REJECT_REMOTE_CLIENTS,
273 1, 256, 256, 0, std::ptr::null(), )
279 };
280
281 if handle == INVALID_HANDLE_VALUE {
282 return Err("Failed to create named pipe".to_string());
283 }
284
285 Ok(Self { handle })
286 }
287
288 pub fn accept(&self) -> Result<NamedPipeConnection, String> {
289 let connected = unsafe { ConnectNamedPipe(self.handle, std::ptr::null_mut()) };
290 if connected == 0 {
291 }
293 Ok(NamedPipeConnection {
294 handle: self.handle,
295 })
296 }
297 }
298
299 impl Drop for NamedPipeListener {
300 fn drop(&mut self) {
301 unsafe {
302 CloseHandle(self.handle);
303 }
304 }
305 }
306
307 pub struct NamedPipeConnection {
308 handle: HANDLE,
309 }
310
311 impl NamedPipeConnection {
312 pub fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), String> {
313 let mut total = 0usize;
314 while total < buf.len() {
315 let mut read = 0u32;
316 let ok = unsafe {
317 ReadFile(
318 self.handle,
319 buf.as_mut_ptr().add(total),
320 (buf.len() - total) as u32,
321 &mut read,
322 std::ptr::null_mut(),
323 )
324 };
325 if ok == 0 {
326 return Err("ReadFile failed".to_string());
327 }
328 if read == 0 {
329 return Err("Pipe closed".to_string());
330 }
331 total += read as usize;
332 }
333 Ok(())
334 }
335
336 pub fn write_all(&mut self, buf: &[u8]) -> Result<(), String> {
337 let mut total = 0usize;
338 while total < buf.len() {
339 let mut written = 0u32;
340 let ok = unsafe {
341 WriteFile(
342 self.handle,
343 buf.as_ptr().add(total),
344 (buf.len() - total) as u32,
345 &mut written,
346 std::ptr::null_mut(),
347 )
348 };
349 if ok == 0 {
350 return Err("WriteFile failed".to_string());
351 }
352 total += written as usize;
353 }
354 Ok(())
355 }
356 }
357
358 impl Drop for NamedPipeConnection {
359 fn drop(&mut self) {
360 unsafe {
361 DisconnectNamedPipe(self.handle);
362 }
363 }
364 }
365} pub struct AISocketListener {
369 #[cfg(unix)]
370 listener: std::os::unix::net::UnixListener,
371 #[cfg(windows)]
372 listener: windows_pipe::NamedPipeListener,
373}
374
375impl AISocketListener {
376 #[cfg(unix)]
378 pub fn new(path: &str) -> Result<Self, String> {
379 let _ = std::fs::remove_file(path);
381
382 let listener = std::os::unix::net::UnixListener::bind(path)
383 .map_err(|e| format!("Failed to bind Unix socket: {}", e))?;
384
385 Ok(Self { listener })
386 }
387
388 #[cfg(windows)]
389 pub fn new(path: &str) -> Result<Self, String> {
390 let listener = windows_pipe::NamedPipeListener::new(path)
391 .map_err(|e| format!("Failed to create named pipe listener: {}", e))?;
392 Ok(Self { listener })
393 }
394
395 pub fn listen(&self, adapter: &mut AIAdapter) -> Result<(), String> {
397 #[cfg(windows)]
398 let _ = adapter;
399
400 #[cfg(unix)]
401 {
402 for stream in self.listener.incoming() {
403 let mut stream = match stream {
404 Ok(s) => s,
405 Err(e) => {
406 eprintln!("Connection error: {}", e);
407 continue;
408 }
409 };
410
411 let mut buffer = [0u8; 256];
413 if let Err(e) = stream.read_exact(&mut buffer) {
414 eprintln!("Read error: {}", e);
415 continue;
416 }
417
418 let cmd: AICommand = unsafe { std::mem::transmute(buffer) };
420
421 match adapter.process_command(cmd) {
423 Ok(response) => {
424 let response_bytes: [u8; 256] = unsafe { std::mem::transmute(response) };
426 if let Err(e) = stream.write_all(&response_bytes) {
427 eprintln!("Write error: {}", e);
428 }
429 }
430 Err(e) => {
431 eprintln!("Command processing error: {}", e);
432 let error_response = AICommand {
434 op_code: 255, ..AICommand::default()
436 };
437 let response_bytes: [u8; 256] =
438 unsafe { std::mem::transmute(error_response) };
439 let _ = stream.write_all(&response_bytes);
440 }
441 }
442 }
443 }
444
445 #[cfg(windows)]
446 {
447 loop {
448 let mut conn = match self.listener.accept() {
449 Ok(c) => c,
450 Err(e) => {
451 eprintln!("Connection error: {}", e);
452 continue;
453 }
454 };
455
456 let mut buffer = [0u8; 256];
457 if let Err(e) = conn.read_exact(&mut buffer) {
458 eprintln!("Read error: {}", e);
459 continue;
460 }
461
462 let cmd: AICommand = unsafe { std::mem::transmute(buffer) };
463
464 match adapter.process_command(cmd) {
465 Ok(response) => {
466 let response_bytes: [u8; 256] = unsafe { std::mem::transmute(response) };
467 if let Err(e) = conn.write_all(&response_bytes) {
468 eprintln!("Write error: {}", e);
469 }
470 }
471 Err(e) => {
472 eprintln!("Command processing error: {}", e);
473 let error_response = AICommand {
474 op_code: 255,
475 ..AICommand::default()
476 };
477 let response_bytes: [u8; 256] =
478 unsafe { std::mem::transmute(error_response) };
479 let _ = conn.write_all(&response_bytes);
480 }
481 }
482 }
483 }
484
485 #[cfg(not(windows))]
486 Ok(())
487 }
488}
489
490#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::config::Config;
496
497 fn test_config() -> Config {
498 let mut config = Config::default();
499 config.paths.output_dir = "test_output".to_string();
500 config.paths.layers_dir = "test_layers".to_string();
501 config
502 }
503
504 #[test]
505 fn test_ai_command_creation() {
506 let read_cmd = AICommand::read(1, 42);
507 assert_eq!(read_cmd.op_code, 0);
508 assert_eq!(read_cmd.layer, 1);
509 assert_eq!(read_cmd.block_id, 42);
510
511 let write_cmd = AICommand::write(2, 0, b"Hello AI");
512 assert_eq!(write_cmd.op_code, 1);
513 assert_eq!(write_cmd.layer, 2);
514 assert_eq!(write_cmd.payload[0], b'H');
515 }
516
517 #[test]
518 fn test_ai_adapter_creation() {
519 let _config = test_config();
522 }
526
527 #[test]
528 fn update_merkle_tree_is_honest_about_dirty_blocks() {
529 let tmp = tempfile::tempdir().unwrap();
530 let mut config = Config::default();
531 config.paths.layers_dir = tmp.path().join("layers").to_string_lossy().to_string();
532 config.paths.output_dir = tmp.path().join("data").to_string_lossy().to_string();
533 config.memory_layers.layers = vec!["long_term".to_string()];
534 config.embedding.provider = "none".to_string();
535 std::fs::create_dir_all(&config.paths.layers_dir).unwrap();
536 crate::build::build(&config, true, true).unwrap();
537
538 let mut adapter = AIAdapter::new(config).unwrap();
539
540 assert!(adapter.update_merkle_tree().is_ok());
542
543 adapter.dirty_blocks.insert(42);
545 let err = adapter.update_merkle_tree().unwrap_err();
546 assert!(err.contains("not implemented"), "err: {err}");
547 assert!(
548 adapter.dirty_blocks.contains(&42),
549 "dirty set must survive a failed update"
550 );
551 }
552}