diff --git a/Cargo.toml b/Cargo.toml index 902ac92..beaa8f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,9 @@ members = [ "crates/neuron-api", "crates/neuron-protocol", "crates/axon-events", + "crates/neuron-runtime", + "crates/neuron-migrate", + "crates/neuron-cli", ] [workspace.dependencies] diff --git a/crates/neuron-api/Cargo.toml b/crates/neuron-api/Cargo.toml index 09605ae..2733731 100644 --- a/crates/neuron-api/Cargo.toml +++ b/crates/neuron-api/Cargo.toml @@ -11,6 +11,8 @@ path = "src/main.rs" neuron-domain = { path = "../neuron-domain" } neuron-store = { path = "../neuron-store" } neuron-protocol = { path = "../neuron-protocol" } +neuron-runtime = { path = "../neuron-runtime" } +chrono = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } diff --git a/crates/neuron-api/src/routes/axon.rs b/crates/neuron-api/src/routes/axon.rs index dbb8837..4514479 100644 --- a/crates/neuron-api/src/routes/axon.rs +++ b/crates/neuron-api/src/routes/axon.rs @@ -4,31 +4,53 @@ use axum::{ Json, }; use futures_util::Stream; -use neuron_protocol::{AxonHandler, AxonMessage, AxonResponse}; +use neuron_protocol::{AxonMessage, AxonMethod, AxonResponse}; +use serde_json::Value; use std::sync::Arc; use std::time::Duration; use crate::state::AppState; -/// POST /axon/message — receive an Axon protocol message and return a response. +/// POST /axon/message — dispatch to NeuronRuntime. pub async fn post_message( - State(_state): State>, + State(state): State>, Json(msg): Json, ) -> Json { - let mut handler = AxonHandler::new(); + match msg.method { + AxonMethod::ToolCall => { + let tool_name = msg + .params + .get("tool") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let params = msg + .params + .get("params") + .cloned() + .unwrap_or(Value::Null); - // Register stub handlers for all known tool names. - for &tool in neuron_protocol::tools::ALL_TOOLS { - let name = tool.to_string(); - handler.register(name, |params| { - serde_json::json!({ - "status": "ok", - "params": params - }) - }); + if tool_name.is_empty() { + return Json(AxonResponse::err(msg.id, "missing 'tool' field in params")); + } + + let result = state.runtime.dispatch(&tool_name, params).await; + + // If the result contains an "error" field, surface it as an error response. + if let Some(err_msg) = result.get("error").and_then(|v| v.as_str()) { + Json(AxonResponse::err(msg.id, err_msg)) + } else { + Json(AxonResponse::ok(msg.id, result)) + } + } + + AxonMethod::Ping => Json(AxonResponse::ok( + msg.id, + serde_json::json!({ "ts": chrono::Utc::now().to_rfc3339() }), + )), + + _ => Json(AxonResponse::err(msg.id, "unsupported method")), } - - Json(handler.dispatch(msg)) } /// GET /axon/sse — Server-Sent Events stream for Axon events. diff --git a/crates/neuron-api/src/state.rs b/crates/neuron-api/src/state.rs index a517806..41527ab 100644 --- a/crates/neuron-api/src/state.rs +++ b/crates/neuron-api/src/state.rs @@ -1,4 +1,7 @@ use std::path::Path; +use std::sync::Arc; + +use neuron_runtime::NeuronRuntime; use neuron_store::{BacklogStore, ContextStore, IseStore, KnowledgeStore, MemoryStore, open_db}; /// Shared application state — one store per domain. @@ -12,17 +15,45 @@ pub struct AppState { pub context: ContextStore, pub ise: IseStore, pub api_key: String, + pub runtime: Arc, } impl AppState { pub fn new(db_path: &Path, api_key: String) -> Result { + // Module directory defaults to "~/.neuron/lang" (or wherever .el files live). + // Falls back to a non-existent directory — runtime will have zero modules + // and fall back to native bindings for all tools. + let module_dir_raw = + std::env::var("NEURON_LANG_DIR").unwrap_or_else(|_| "~/.neuron/lang".to_string()); + let module_dir = std::path::PathBuf::from( + module_dir_raw + .replace('~', &std::env::var("HOME").unwrap_or_else(|_| ".".to_string())), + ); + + // If the module directory doesn't exist, create an empty temp dir so + // load_directory doesn't error out. + let effective_dir = if module_dir.exists() { + module_dir + } else { + // Use a tmp dir — runtime will have zero modules (safe fallback). + std::env::temp_dir().join("neuron-lang-empty") + }; + let _ = std::fs::create_dir_all(&effective_dir); + + // Open the sled DB exactly once — Clone shares the same file lock. + let db = open_db(db_path)?; + + let runtime = NeuronRuntime::from_dir(&effective_dir, db.clone()) + .map_err(|e| neuron_domain::NeuronError::Storage(e.to_string()))?; + Ok(Self { - memory: MemoryStore::new(open_db(db_path)?), - knowledge: KnowledgeStore::new(open_db(db_path)?), - backlog: BacklogStore::new(open_db(db_path)?), - context: ContextStore::new(open_db(db_path)?), - ise: IseStore::new(open_db(db_path)?), + memory: MemoryStore::new(db.clone()), + knowledge: KnowledgeStore::new(db.clone()), + backlog: BacklogStore::new(db.clone()), + context: ContextStore::new(db.clone()), + ise: IseStore::new(db), api_key, + runtime: Arc::new(runtime), }) } }