commit 2b45fc2f0fc7053e1cdac823440bf80a38ae988f Author: Will Anderson Date: Thu Apr 30 13:49:28 2026 -0500 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": ""} 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 -lcurl -lpthread -o dist/engram dist/engram.c /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. diff --git a/engram/.el/build-cache.json b/engram/.el/build-cache.json new file mode 100644 index 0000000..34002f9 --- /dev/null +++ b/engram/.el/build-cache.json @@ -0,0 +1,5 @@ +{ + "file_hashes": { + "src/server.el": "1bb43f78f2048cc3aeb2d1f68eda1c1af03425a356b6660dff22273ed50fdece" + } +} \ No newline at end of file diff --git a/engram/.gitignore b/engram/.gitignore new file mode 100644 index 0000000..954262e --- /dev/null +++ b/engram/.gitignore @@ -0,0 +1,3 @@ +dist/engram +dist/engram.c +*.log diff --git a/engram/dist/engram b/engram/dist/engram new file mode 100755 index 0000000..54c2889 Binary files /dev/null and b/engram/dist/engram differ diff --git a/engram/dist/engram.c b/engram/dist/engram.c new file mode 100644 index 0000000..374d663 --- /dev/null +++ b/engram/dist/engram.c @@ -0,0 +1,344 @@ +#include +#include +#include "el_runtime.h" + +el_val_t parse_port(el_val_t bind); +el_val_t ok_json(void); +el_val_t err_json(el_val_t msg); +el_val_t strip_query(el_val_t path); +el_val_t query_param(el_val_t path, el_val_t key); +el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val); +el_val_t extract_id(el_val_t path, el_val_t prefix); +el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_search(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_save(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_load(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_health(el_val_t method, el_val_t path, el_val_t body); +el_val_t check_auth_ok(el_val_t method, el_val_t body); +el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body); + +el_val_t parse_port(el_val_t bind) { + el_val_t colon = str_index_of(bind, EL_STR(":")); + if (colon < 0) { + return str_to_int(bind); + } + el_val_t after = str_slice(bind, (colon + 1), str_len(bind)); + return str_to_int(after); + return 0; +} + +el_val_t ok_json(void) { + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t err_json(el_val_t msg) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\""), msg), EL_STR("\"}")); + return 0; +} + +el_val_t strip_query(el_val_t path) { + el_val_t q = str_index_of(path, EL_STR("?")); + if (q < 0) { + return path; + } + return str_slice(path, 0, q); + return 0; +} + +el_val_t query_param(el_val_t path, el_val_t key) { + el_val_t q = str_index_of(path, EL_STR("?")); + if (q < 0) { + return EL_STR(""); + } + el_val_t qs = str_slice(path, (q + 1), str_len(path)); + el_val_t needle = el_str_concat(key, EL_STR("=")); + el_val_t pos = str_index_of(qs, needle); + if (pos < 0) { + return EL_STR(""); + } + el_val_t after = str_slice(qs, (pos + str_len(needle)), str_len(qs)); + el_val_t amp = str_index_of(after, EL_STR("&")); + if (amp < 0) { + return after; + } + return str_slice(after, 0, amp); + return 0; +} + +el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val) { + el_val_t v = query_param(path, key); + if (str_eq(v, EL_STR(""))) { + return default_val; + } + return str_to_int(v); + return 0; +} + +el_val_t extract_id(el_val_t path, el_val_t prefix) { + el_val_t clean = strip_query(path); + if (!str_starts_with(clean, prefix)) { + return EL_STR(""); + } + el_val_t after = str_slice(clean, str_len(prefix), str_len(clean)); + el_val_t slash = str_index_of(after, EL_STR("/")); + if (slash < 0) { + return after; + } + return str_slice(after, 0, slash); + return 0; +} + +el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) { + return engram_stats_json(); + return 0; +} + +el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) { + el_val_t content = json_get_string(body, EL_STR("content")); + el_val_t node_type = json_get_string(body, EL_STR("node_type")); + if (str_eq(node_type, EL_STR(""))) { + node_type = EL_STR("Memory"); + } + el_val_t salience = json_get_float(body, EL_STR("salience")); + if (str_eq(salience, el_from_float(0.0))) { + salience = el_from_float(0.5); + } + el_val_t id = engram_node(content, node_type, salience); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}")); + return 0; +} + +el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = extract_id(path, EL_STR("/api/nodes/")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing id")); + } + return engram_get_node_json(id); + return 0; +} + +el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) { + el_val_t limit = query_int(path, EL_STR("limit"), 50); + el_val_t offset = query_int(path, EL_STR("offset"), 0); + return engram_scan_nodes_json(limit, offset); + return 0; +} + +el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) { + el_val_t q = EL_STR(""); + if (str_eq(method, EL_STR("GET"))) { + q = query_param(path, EL_STR("q")); + } else { + q = json_get_string(body, EL_STR("query")); + } + el_val_t limit = query_int(path, EL_STR("limit"), 20); + if (limit == 0) { + limit = json_get_int(body, EL_STR("limit")); + } + if (limit == 0) { + limit = 20; + } + return engram_search_json(q, limit); + return 0; +} + +el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) { + el_val_t q = EL_STR(""); + el_val_t depth = 3; + if (str_eq(method, EL_STR("GET"))) { + q = query_param(path, EL_STR("q")); + depth = query_int(path, EL_STR("depth"), 3); + } else { + q = json_get_string(body, EL_STR("query")); + el_val_t bd = json_get_int(body, EL_STR("depth")); + if (bd > 0) { + depth = bd; + } + } + return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}")); + return 0; +} + +el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) { + el_val_t from_id = json_get_string(body, EL_STR("from_id")); + el_val_t to_id = json_get_string(body, EL_STR("to_id")); + el_val_t relation = json_get_string(body, EL_STR("relation")); + if (str_eq(relation, EL_STR(""))) { + relation = EL_STR("associates"); + } + el_val_t weight = json_get_float(body, EL_STR("weight")); + if (str_eq(weight, el_from_float(0.0))) { + weight = el_from_float(0.5); + } + engram_connect(from_id, to_id, weight, relation); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}")); + return 0; +} + +el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = extract_id(path, EL_STR("/api/neighbors/")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing id")); + } + el_val_t depth = query_int(path, EL_STR("depth"), 1); + return engram_neighbors_json(id, depth, EL_STR("both")); + return 0; +} + +el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = json_get_string(body, EL_STR("node_id")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing node_id")); + } + engram_strengthen(id); + return ok_json(); + return 0; +} + +el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = extract_id(path, EL_STR("/api/nodes/")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing id")); + } + engram_forget(id); + return ok_json(); + return 0; +} + +el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) { + el_val_t p = json_get_string(body, EL_STR("path")); + if (str_eq(p, EL_STR(""))) { + el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(dir, EL_STR(""))) { + dir = EL_STR("/tmp/engram"); + } + p = el_str_concat(dir, EL_STR("/snapshot.json")); + } + engram_save(p); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}")); + return 0; +} + +el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) { + el_val_t p = json_get_string(body, EL_STR("path")); + if (str_eq(p, EL_STR(""))) { + el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(dir, EL_STR(""))) { + dir = EL_STR("/tmp/engram"); + } + p = el_str_concat(dir, EL_STR("/snapshot.json")); + } + engram_load(p); + return ok_json(); + return 0; +} + +el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) { + return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"); + return 0; +} + +el_val_t check_auth_ok(el_val_t method, el_val_t body) { + el_val_t key = env(EL_STR("ENGRAM_API_KEY")); + if (str_eq(key, EL_STR(""))) { + return 1; + } + if (str_eq(method, EL_STR("GET"))) { + return 1; + } + el_val_t provided = json_get_string(body, EL_STR("_auth")); + if (str_eq(provided, key)) { + return 1; + } + return 0; + return 0; +} + +el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) { + el_val_t clean = strip_query(path); + if (str_eq(method, EL_STR("GET"))) { + if (str_eq(clean, EL_STR("/health")) || str_eq(clean, EL_STR("/"))) { + return route_health(method, path, body); + } + } + if (!check_auth_ok(method, body)) { + return err_json(EL_STR("unauthorized")); + } + if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) { + return route_stats(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) { + return route_create_node(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) { + return route_scan_nodes(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/nodes/"))) { + return route_get_node(method, path, body); + } + if (str_eq(method, EL_STR("DELETE")) && str_starts_with(clean, EL_STR("/api/nodes/"))) { + return route_forget(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) { + return route_create_edge(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/neighbors/"))) { + return route_neighbors(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/activate")) || str_eq(clean, EL_STR("/activate")))) { + return route_activate(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/activate"))) { + return route_activate(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/search")) || str_eq(clean, EL_STR("/search")))) { + return route_search(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/search"))) { + return route_search(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) { + return route_strengthen(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) { + return route_save(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) { + return route_load(method, path, body); + } + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}")); + return 0; +} + +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + el_val_t bind_str = env(EL_STR("ENGRAM_BIND")); + if (str_eq(bind_str, EL_STR(""))) { + bind_str = EL_STR(":8742"); + } + el_val_t port = parse_port(bind_str); + el_val_t data_dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(data_dir, EL_STR(""))) { + data_dir = EL_STR("/tmp/engram"); + } + el_val_t snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json")); + engram_load(snapshot_path); + println(EL_STR("[engram] runtime-native graph engine")); + println(el_str_concat(EL_STR("[engram] data_dir="), data_dir)); + println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count()))); + println(el_str_concat(EL_STR("[engram] edge_count="), int_to_str(engram_edge_count()))); + println(el_str_concat(EL_STR("[engram] listening on "), int_to_str(port))); + http_set_handler(EL_STR("handle_request")); + http_serve(port, EL_STR("handle_request")); + return 0; +} + diff --git a/engram/manifest.el b/engram/manifest.el new file mode 100644 index 0000000..b5e8ebc --- /dev/null +++ b/engram/manifest.el @@ -0,0 +1,11 @@ +package "engram-el" { + version "1.0.0" + description "Engram graph intelligence substrate — El implementation" + authors ["Will Anderson "] + edition "2026" +} + +build { + entry "src/server.el" + output "dist/" +} diff --git a/engram/spec/engram-el.md b/engram/spec/engram-el.md new file mode 100644 index 0000000..7dd88aa --- /dev/null +++ b/engram/spec/engram-el.md @@ -0,0 +1,284 @@ +# engram-el Specification + +Version 1.0.0 — April 29, 2026 + +--- + +## Overview + +engram-el is the El-native interface layer for the Engram graph engine. It is the integration point between El programs and the Engram knowledge substrate — providing a suite of El programs, test suites, and utilities that operate on a live Engram server via its HTTP API using El's native HTTP builtins. + +engram-el has three primary components: + +1. **Studio** — A full-featured terminal-based graph explorer written in El (`studio/studio.el`). Provides read access to all graph data: statistics, node browsing by type and tier, spreading activation visualization, edge exploration, and text search. + +2. **Test suite** — Language feature tests (`test/language_features_test.el`, `test/field_test.el`, `test/llm_test.el`) that exercise El builtins against a live Engram instance. + +3. **Integration point** — The pattern for how El programs use the Engram graph as their knowledge substrate, demonstrating the graph builtin API in practice. + +--- + +## 1. Architecture + +### 1.1 Relationship to El and Engram + +engram-el is not a library in the conventional sense. It is a collection of El programs that operate on Engram. The integration uses no additional runtime or SDK: + +- **El builtins** provide `http_get`, `http_post`, and JSON parsing natively. +- **Engram HTTP API** is the sole interface — all graph operations are HTTP requests. +- **No compilation step** beyond standard El compilation is required. + +This demonstrates the intended usage pattern for all El programs that incorporate graph knowledge: use the HTTP API via El's native builtins. + +### 1.2 Configuration + +All engram-el programs read configuration from environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `ENGRAM_URL` | `http://localhost:8340` | Engram server base URL | +| `ENGRAM_REPORT` | `/tmp/engram-studio-report.txt` | Studio report output path | + +--- + +## 2. Studio Application + +`studio/studio.el` is a complete data exploration application for the Engram graph, written entirely in El. It demonstrates El as a serious application language — not a scripting language but a capable system for building non-trivial tools. + +### 2.1 Features + +The studio renders a full-page terminal UI with box-drawing characters and ANSI color. Sections: + +| Section | Description | +|---------|-------------| +| Database Statistics | Node count, edge count, average salience, DB size | +| Recent Nodes | Most recently created nodes with type and salience | +| Top by Salience | Highest-salience nodes with graphical bar display | +| Nodes by Type | Browse Memory, Concept, Event, Entity, Process, InternalState | +| Nodes by Tier | Browse Working, Episodic, Semantic, Procedural tiers | +| Knowledge Browser | Concept nodes as domain knowledge anchors | +| Text Search | Full-text search results with relevance | +| Edge Explorer | Sample of edges with weights and relation types | +| Node Detail | Full node data plus BFS neighbors | +| Spreading Activation | Visual activation surface from a seed node | +| Interactive Mode Preview | Menu of available commands | +| Report Export | Write complete session report to file | + +### 2.2 API Access Pattern + +The studio uses a uniform API access pattern: + +```el +fn api_get(path: String) -> String { + let url: String = get_base_url() + path + let resp: String = http_get(url) + if str_starts_with(resp, "{\"error\"") { + return "" + } + return resp +} + +fn api_post(path: String, body: String) -> String { + let url: String = get_base_url() + path + let resp: String = http_post(url, body) + if str_starts_with(resp, "{\"error\"") { + return "" + } + return resp +} +``` + +Error responses (JSON objects beginning with `{"error"`) return empty string. All rendering logic checks for empty string and emits placeholder messages rather than crashing. + +### 2.3 Spreading Activation Visualization + +The activation section demonstrates reading live spreading activation results from Engram: + +```el +fn show_activation(seed_id: String, limit: Int, report: String) -> String { + let path: String = "/api/activate?seeds=" + seed_id + "&limit=" + int_to_str(limit) + "&depth=3" + let json_str: String = api_get(path) + // ... renders activation strength bars and hop distances +} +``` + +This provides visual confirmation that the spreading activation algorithm is operating — showing which nodes activate, at what strength, and at what hop distance from the seed. + +### 2.4 Report Export + +The studio accumulates a text report as it renders each section, then writes the complete report to a file: + +```el +export_report(report, report_path) +``` + +The report captures the full session output in machine-readable format, useful for automation and logging. + +--- + +## 3. Test Suite + +### 3.1 Language Features Test + +`test/language_features_test.el` exercises El language primitives including: + +- Modulo operator (`%`) +- Bitwise operators (`&`, `^`, `<<`, `>>`) +- Math builtins (`math_sin`, `math_cos`, `math_pi`) +- String padding (`str_pad_left`, `str_pad_right`) +- String formatting (`str_format` with `{key}` template interpolation) +- Float formatting (`format_float`) +- Time operations (`time_now_utc`, `time_format`, `time_add`, `time_diff`) +- List operations (`list_range`, `list_join`) +- Stack and queue builtins (`stack_new`, `stack_push`, `stack_pop`, `stack_peek`, `queue_enqueue`, `queue_dequeue`) +- Decimal rounding (`decimal_round`) +- Type conversion (`int_to_float`, `float_to_int`) +- Nil checks (`is_nil`, `unwrap_or`) +- Character operations (`str_char_at`, `str_char_code`, `str_from_char_code`) + +These tests serve as the canonical behavioral specification for El builtins — any correct El implementation must produce the documented output for these inputs. + +### 3.2 Field Test + +`test/field_test.el` exercises struct field access, map indexing, and nested data access patterns. + +### 3.3 LLM Test + +`test/llm_test.el` exercises the LLM inference builtins against a live Engram-connected inference endpoint. + +--- + +## 4. Integration Patterns + +### 4.1 Graph Read Pattern + +The standard pattern for reading from Engram in an El program: + +```el +fn get_nodes_of_type(node_type: String, limit: Int) -> List { + let path: String = "/api/nodes?node_type=" + node_type + "&limit=" + int_to_str(limit) + let json_str: String = http_get(env("ENGRAM_URL") + path) + if json_str == "" { + return list_new() + } + return json_parse(json_str) +} +``` + +### 4.2 Graph Write Pattern + +The standard pattern for writing to Engram from an El program: + +```el +fn create_node(label: String, content: String, node_type: String, tier: String) -> String { + let body: String = "{\"label\":\"" + label + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\",\"tier\":\"" + tier + "\",\"importance\":0.5}" + let resp: String = http_post(env("ENGRAM_URL") + "/api/nodes", body) + return json_get_string(resp, "id") +} +``` + +### 4.3 Search Pattern + +```el +fn search_graph(query: String, limit: Int) -> List { + let path: String = "/api/search?q=" + query + "&limit=" + int_to_str(limit) + let json_str: String = http_get(env("ENGRAM_URL") + path) + if json_str == "" { + return list_new() + } + return json_parse(json_str) +} +``` + +### 4.4 Activation Pattern + +```el +fn activate_from_node(node_id: String, depth: Int, limit: Int) -> List { + let path: String = "/api/activate?seeds=" + node_id + "&depth=" + int_to_str(depth) + "&limit=" + int_to_str(limit) + let resp_str: String = http_get(env("ENGRAM_URL") + path) + if resp_str == "" { + return list_new() + } + let results_raw: String = json_get_raw(resp_str, "results") + return json_parse(results_raw) +} +``` + +--- + +## 5. Builtin Extensions Demonstrated + +The engram-el programs demonstrate El builtins that are not in the core language but are implemented by the VM's builtin dispatch layer: + +### 5.1 JSON Builtins + +| Builtin | Used for | +|---------|---------| +| `json_parse(s)` | Parse Engram API responses | +| `json_stringify(v)` | Serialize values to JSON for API requests | +| `json_get_string(json, key)` | Extract string fields from node JSON | +| `json_get_int(json, key)` | Extract integer fields (counts, timestamps) | +| `json_get_float(json, key)` | Extract float fields (salience, weights) | +| `json_get_raw(json, key)` | Extract nested objects as raw JSON strings | + +### 5.2 Color/Terminal Builtins + +| Builtin | Used for | +|---------|---------| +| `color_bold(s)` | Section headers, labels | +| `color_dim(s)` | Timestamps, IDs, less important data | +| `color_green(s)` | Success states, high salience | +| `color_yellow(s)` | Warnings, medium salience | +| `color_cyan(s)` | URLs, relation names, special values | +| `color_red(s)` | Errors, low salience | + +### 5.3 String Formatting Builtins + +| Builtin | Signature | Description | +|---------|-----------|-------------| +| `str_pad_right(s, width, pad)` | Pad string to width on right | +| `str_pad_left(s, width, pad)` | Pad string to width on left | +| `format_float(f, decimals)` | Format float to N decimal places | +| `str_slice(s, start, end)` | Extract substring by character index | +| `str_len(s)` | String length in characters | + +--- + +## 6. Deployment + +### 6.1 Running the Studio + +```bash +# Connect to default local server +el run-file studio/studio.el + +# Connect to remote server +ENGRAM_URL=http://engram.example.com el run-file studio/studio.el + +# Save report to custom path +ENGRAM_REPORT=/var/log/engram-report.txt el run-file studio/studio.el +``` + +### 6.2 Running Tests + +```bash +el run-file test/language_features_test.el +el run-file test/field_test.el +ENGRAM_URL=http://localhost:8340 el run-file test/llm_test.el +``` + +--- + +## 7. Design Decisions + +### 7.1 Pure HTTP Integration + +engram-el uses HTTP exclusively. It does not use the lower-level `graph_compile` and `graph_traverse` VM builtins. This is by design: it demonstrates the HTTP API surface as the primary integration mechanism. The VM builtins are for tightly-integrated runtime code (the Neuron daemon); external tools use the HTTP API. + +### 7.2 Stateless Programs + +All engram-el programs are stateless — they read state from Engram on each run and write nothing back (the studio is read-only). This is the correct architecture for exploration tools: they observe the graph without mutating it. + +### 7.3 El as Application Language + +The studio's 788 lines of El demonstrate that El is a capable application language. It is not a configuration DSL or a scripting language for simple tasks. The studio handles: API communication, JSON parsing, recursive data rendering, ASCII art, ANSI color codes, file I/O, environment variable configuration, and complex string manipulation — all with El's native builtins, without imports. diff --git a/engram/src/server.el b/engram/src/server.el new file mode 100644 index 0000000..dc69f5d --- /dev/null +++ b/engram/src/server.el @@ -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": "" 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")