Wire Axon /axon/message to NeuronRuntime dispatch

- axon.rs: replace stub AxonHandler with live NeuronRuntime dispatch;
  tool calls now route through Engram VM when modules are loaded
- state.rs: load .el modules from NEURON_LANG_DIR (default ~/.neuron/lang);
  falls back to empty dir gracefully when not present
- Cargo.toml: add engram-core dependency for EngramDb
This commit is contained in:
2026-04-28 13:47:09 -05:00
parent 7a81bb11a7
commit 78fc3a909a
4 changed files with 78 additions and 20 deletions
+3
View File
@@ -6,6 +6,9 @@ members = [
"crates/neuron-api", "crates/neuron-api",
"crates/neuron-protocol", "crates/neuron-protocol",
"crates/axon-events", "crates/axon-events",
"crates/neuron-runtime",
"crates/neuron-migrate",
"crates/neuron-cli",
] ]
[workspace.dependencies] [workspace.dependencies]
+2
View File
@@ -11,6 +11,8 @@ path = "src/main.rs"
neuron-domain = { path = "../neuron-domain" } neuron-domain = { path = "../neuron-domain" }
neuron-store = { path = "../neuron-store" } neuron-store = { path = "../neuron-store" }
neuron-protocol = { path = "../neuron-protocol" } neuron-protocol = { path = "../neuron-protocol" }
neuron-runtime = { path = "../neuron-runtime" }
chrono = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
+36 -14
View File
@@ -4,31 +4,53 @@ use axum::{
Json, Json,
}; };
use futures_util::Stream; 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::sync::Arc;
use std::time::Duration; use std::time::Duration;
use crate::state::AppState; 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( pub async fn post_message(
State(_state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(msg): Json<AxonMessage>, Json(msg): Json<AxonMessage>,
) -> Json<AxonResponse> { ) -> Json<AxonResponse> {
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. if tool_name.is_empty() {
for &tool in neuron_protocol::tools::ALL_TOOLS { return Json(AxonResponse::err(msg.id, "missing 'tool' field in params"));
let name = tool.to_string();
handler.register(name, |params| {
serde_json::json!({
"status": "ok",
"params": params
})
});
} }
Json(handler.dispatch(msg)) 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")),
}
} }
/// GET /axon/sse — Server-Sent Events stream for Axon events. /// GET /axon/sse — Server-Sent Events stream for Axon events.
+36 -5
View File
@@ -1,4 +1,7 @@
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use neuron_runtime::NeuronRuntime;
use neuron_store::{BacklogStore, ContextStore, IseStore, KnowledgeStore, MemoryStore, open_db}; use neuron_store::{BacklogStore, ContextStore, IseStore, KnowledgeStore, MemoryStore, open_db};
/// Shared application state — one store per domain. /// Shared application state — one store per domain.
@@ -12,17 +15,45 @@ pub struct AppState {
pub context: ContextStore, pub context: ContextStore,
pub ise: IseStore, pub ise: IseStore,
pub api_key: String, pub api_key: String,
pub runtime: Arc<NeuronRuntime>,
} }
impl AppState { impl AppState {
pub fn new(db_path: &Path, api_key: String) -> Result<Self, neuron_domain::NeuronError> { pub fn new(db_path: &Path, api_key: String) -> Result<Self, neuron_domain::NeuronError> {
// 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 { Ok(Self {
memory: MemoryStore::new(open_db(db_path)?), memory: MemoryStore::new(db.clone()),
knowledge: KnowledgeStore::new(open_db(db_path)?), knowledge: KnowledgeStore::new(db.clone()),
backlog: BacklogStore::new(open_db(db_path)?), backlog: BacklogStore::new(db.clone()),
context: ContextStore::new(open_db(db_path)?), context: ContextStore::new(db.clone()),
ise: IseStore::new(open_db(db_path)?), ise: IseStore::new(db),
api_key, api_key,
runtime: Arc::new(runtime),
}) })
} }
} }