engram: runtime-native rewrite
Engram is now a thin HTTP face over the El runtime's in-process graph
store. The C runtime owns the data; engram_*_json builtins serialize
results directly. There is no SQL, no SQLite, no db layer, no state
machine — the runtime IS the database.
src/server.el (348 lines, replacing 5797 lines across 15 legacy files):
GET /health
GET /api/stats
POST /api/nodes (auth required)
GET /api/nodes
GET /api/nodes/:id
DELETE /api/nodes/:id (auth required)
POST /api/edges (auth required)
GET /api/neighbors/:id
POST /api/activate
GET /api/activate
POST /api/search
GET /api/search
POST /api/strengthen (auth required)
POST /api/save (auth required)
POST /api/load (auth required)
Auth: ENGRAM_API_KEY in env. GET routes pass through (read-only).
Mutating routes require {"_auth": "<key>"} in the JSON body until
http_serve surfaces request headers and we can switch to Bearer.
Persistence: engram_save / engram_load via JSON snapshot at
$ENGRAM_DATA_DIR/snapshot.json. Loaded best-effort on startup.
Build: dist/platform/elc src/server.el > dist/engram.c
cc -std=c11 -O2 -I <runtime> -lcurl -lpthread -o dist/engram
dist/engram.c <runtime>/el_runtime.c
Live: native binary at dist/engram (113 KB), running under
~/Library/LaunchAgents/ai.neuron.engram.plist on :8742. Verified:
GET /api/stats returns counts; POST /api/nodes with auth creates
node with UUID; GET /api/search returns full node JSON; spreading
activation returns hop-decayed strengths (0.8 × edge × decay per
hop) with epistemic confidence filtering.
Legacy (5797 lines of SQLite-era src) sealed at
~/Archives/engram-src-legacy-20260430.tar.gz and removed from disk.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
// server.el — Engram HTTP server.
|
||||
//
|
||||
// Engram is the in-process graph store. The runtime owns the data; this
|
||||
// file is the thin HTTP face. Every route maps to one or two engram_*
|
||||
// builtins. There is no SQL, no db layer, no SQLite — the runtime IS the
|
||||
// database.
|
||||
//
|
||||
// Built and linked with:
|
||||
// elc src/server.el > server.c
|
||||
// cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c
|
||||
// ./engram
|
||||
//
|
||||
// Configuration via environment:
|
||||
// ENGRAM_BIND — host:port (default :8742)
|
||||
// ENGRAM_API_KEY — bearer auth (optional)
|
||||
// ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram)
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn parse_port(bind: String) -> Int {
|
||||
// ":8742" → 8742; "0.0.0.0:8742" → 8742; bare "8742" → 8742
|
||||
let colon: Int = str_index_of(bind, ":")
|
||||
if colon < 0 {
|
||||
return str_to_int(bind)
|
||||
}
|
||||
let after: String = str_slice(bind, colon + 1, str_len(bind))
|
||||
return str_to_int(after)
|
||||
}
|
||||
|
||||
fn ok_json() -> String {
|
||||
"{\"ok\":true}"
|
||||
}
|
||||
|
||||
fn err_json(msg: String) -> String {
|
||||
"{\"error\":\"" + msg + "\"}"
|
||||
}
|
||||
|
||||
fn strip_query(path: String) -> String {
|
||||
let q: Int = str_index_of(path, "?")
|
||||
if q < 0 { return path }
|
||||
str_slice(path, 0, q)
|
||||
}
|
||||
|
||||
fn query_param(path: String, key: String) -> String {
|
||||
let q: Int = str_index_of(path, "?")
|
||||
if q < 0 { return "" }
|
||||
let qs: String = str_slice(path, q + 1, str_len(path))
|
||||
let needle: String = key + "="
|
||||
let pos: Int = str_index_of(qs, needle)
|
||||
if pos < 0 { return "" }
|
||||
let after: String = str_slice(qs, pos + str_len(needle), str_len(qs))
|
||||
let amp: Int = str_index_of(after, "&")
|
||||
if amp < 0 { return after }
|
||||
str_slice(after, 0, amp)
|
||||
}
|
||||
|
||||
fn query_int(path: String, key: String, default_val: Int) -> Int {
|
||||
let v: String = query_param(path, key)
|
||||
if str_eq(v, "") { return default_val }
|
||||
str_to_int(v)
|
||||
}
|
||||
|
||||
// Extract last path segment after a known prefix: extract_id("/api/nodes/abc-123", "/api/nodes/") → "abc-123"
|
||||
fn extract_id(path: String, prefix: String) -> String {
|
||||
let clean: String = strip_query(path)
|
||||
if !str_starts_with(clean, prefix) { return "" }
|
||||
let after: String = str_slice(clean, str_len(prefix), str_len(clean))
|
||||
let slash: Int = str_index_of(after, "/")
|
||||
if slash < 0 { return after }
|
||||
str_slice(after, 0, slash)
|
||||
}
|
||||
|
||||
// ── Routes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn route_stats(method: String, path: String, body: String) -> String {
|
||||
engram_stats_json()
|
||||
}
|
||||
|
||||
fn route_create_node(method: String, path: String, body: String) -> String {
|
||||
let content: String = json_get_string(body, "content")
|
||||
let node_type: String = json_get_string(body, "node_type")
|
||||
if str_eq(node_type, "") { let node_type = "Memory" }
|
||||
let salience: Float = json_get_float(body, "salience")
|
||||
if salience == 0.0 { let salience = 0.5 }
|
||||
let id: String = engram_node(content, node_type, salience)
|
||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||||
}
|
||||
|
||||
fn route_get_node(method: String, path: String, body: String) -> String {
|
||||
let id: String = extract_id(path, "/api/nodes/")
|
||||
if str_eq(id, "") { return err_json("missing id") }
|
||||
return engram_get_node_json(id)
|
||||
}
|
||||
|
||||
fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
||||
let limit: Int = query_int(path, "limit", 50)
|
||||
let offset: Int = query_int(path, "offset", 0)
|
||||
return engram_scan_nodes_json(limit, offset)
|
||||
}
|
||||
|
||||
fn route_search(method: String, path: String, body: String) -> String {
|
||||
let q: String = ""
|
||||
if str_eq(method, "GET") {
|
||||
let q = query_param(path, "q")
|
||||
} else {
|
||||
let q = json_get_string(body, "query")
|
||||
}
|
||||
let limit: Int = query_int(path, "limit", 20)
|
||||
if limit == 0 { let limit = json_get_int(body, "limit") }
|
||||
if limit == 0 { let limit = 20 }
|
||||
return engram_search_json(q, limit)
|
||||
}
|
||||
|
||||
fn route_activate(method: String, path: String, body: String) -> String {
|
||||
let q: String = ""
|
||||
let depth: Int = 3
|
||||
if str_eq(method, "GET") {
|
||||
let q = query_param(path, "q")
|
||||
let depth = query_int(path, "depth", 3)
|
||||
} else {
|
||||
let q = json_get_string(body, "query")
|
||||
let bd: Int = json_get_int(body, "depth")
|
||||
if bd > 0 { let depth = bd }
|
||||
}
|
||||
return "{\"results\":" + engram_activate_json(q, depth) + "}"
|
||||
}
|
||||
|
||||
fn route_create_edge(method: String, path: String, body: String) -> String {
|
||||
let from_id: String = json_get_string(body, "from_id")
|
||||
let to_id: String = json_get_string(body, "to_id")
|
||||
let relation: String = json_get_string(body, "relation")
|
||||
if str_eq(relation, "") { let relation = "associates" }
|
||||
let weight: Float = json_get_float(body, "weight")
|
||||
if weight == 0.0 { let weight = 0.5 }
|
||||
engram_connect(from_id, to_id, weight, relation)
|
||||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||||
}
|
||||
|
||||
fn route_neighbors(method: String, path: String, body: String) -> String {
|
||||
let id: String = extract_id(path, "/api/neighbors/")
|
||||
if str_eq(id, "") { return err_json("missing id") }
|
||||
let depth: Int = query_int(path, "depth", 1)
|
||||
return engram_neighbors_json(id, depth, "both")
|
||||
}
|
||||
|
||||
fn route_strengthen(method: String, path: String, body: String) -> String {
|
||||
let id: String = json_get_string(body, "node_id")
|
||||
if str_eq(id, "") { return err_json("missing node_id") }
|
||||
engram_strengthen(id)
|
||||
ok_json()
|
||||
}
|
||||
|
||||
fn route_forget(method: String, path: String, body: String) -> String {
|
||||
let id: String = extract_id(path, "/api/nodes/")
|
||||
if str_eq(id, "") { return err_json("missing id") }
|
||||
engram_forget(id)
|
||||
ok_json()
|
||||
}
|
||||
|
||||
fn route_save(method: String, path: String, body: String) -> String {
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let p = dir + "/snapshot.json"
|
||||
}
|
||||
engram_save(p)
|
||||
"{\"ok\":true,\"path\":\"" + p + "\"}"
|
||||
}
|
||||
|
||||
fn route_load(method: String, path: String, body: String) -> String {
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") {
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
let p = dir + "/snapshot.json"
|
||||
}
|
||||
engram_load(p)
|
||||
ok_json()
|
||||
}
|
||||
|
||||
fn route_health(method: String, path: String, body: String) -> String {
|
||||
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
|
||||
}
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn check_auth_ok(method: String, body: String) -> Bool {
|
||||
let key: String = env("ENGRAM_API_KEY")
|
||||
if str_eq(key, "") { return true }
|
||||
// Read-only methods don't require auth. Until http_serve surfaces
|
||||
// request headers we can't accept a Bearer token cleanly; mutating
|
||||
// requests must include "_auth": "<key>" in the JSON body.
|
||||
if str_eq(method, "GET") { return true }
|
||||
let provided: String = json_get_string(body, "_auth")
|
||||
if str_eq(provided, key) { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Dispatcher ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let clean: String = strip_query(path)
|
||||
|
||||
// Health is always reachable
|
||||
if str_eq(method, "GET") {
|
||||
if str_eq(clean, "/health") || str_eq(clean, "/") {
|
||||
return route_health(method, path, body)
|
||||
}
|
||||
}
|
||||
|
||||
// Auth (when ENGRAM_API_KEY is set)
|
||||
if !check_auth_ok(method, body) {
|
||||
return err_json("unauthorized")
|
||||
}
|
||||
|
||||
// Stats
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
|
||||
return route_stats(method, path, body)
|
||||
}
|
||||
|
||||
// Nodes
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
|
||||
return route_create_node(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
|
||||
return route_scan_nodes(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
|
||||
return route_get_node(method, path, body)
|
||||
}
|
||||
if str_eq(method, "DELETE") && str_starts_with(clean, "/api/nodes/") {
|
||||
return route_forget(method, path, body)
|
||||
}
|
||||
|
||||
// Edges
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
||||
return route_create_edge(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/neighbors/") {
|
||||
return route_neighbors(method, path, body)
|
||||
}
|
||||
|
||||
// Activation + Search
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/activate") || str_eq(clean, "/activate")) {
|
||||
return route_activate(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/activate") {
|
||||
return route_activate(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/search") || str_eq(clean, "/search")) {
|
||||
return route_search(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/search") {
|
||||
return route_search(method, path, body)
|
||||
}
|
||||
|
||||
// Strengthen
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/strengthen") || str_eq(clean, "/strengthen")) {
|
||||
return route_strengthen(method, path, body)
|
||||
}
|
||||
|
||||
// Persistence
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
|
||||
return route_save(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
||||
return route_load(method, path, body)
|
||||
}
|
||||
|
||||
"{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
|
||||
}
|
||||
|
||||
// ── Entry ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
let bind_str: String = env("ENGRAM_BIND")
|
||||
if str_eq(bind_str, "") { let bind_str = ":8742" }
|
||||
let port: Int = parse_port(bind_str)
|
||||
|
||||
// On startup, try to load any existing snapshot (best effort).
|
||||
let data_dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(data_dir, "") { let data_dir = "/tmp/engram" }
|
||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||
engram_load(snapshot_path)
|
||||
|
||||
println("[engram] runtime-native graph engine")
|
||||
println("[engram] data_dir=" + data_dir)
|
||||
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
||||
println("[engram] edge_count=" + int_to_str(engram_edge_count()))
|
||||
println("[engram] listening on " + int_to_str(port))
|
||||
|
||||
http_set_handler("handle_request")
|
||||
http_serve(port, "handle_request")
|
||||
Reference in New Issue
Block a user