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:
Will Anderson
2026-04-30 13:49:28 -05:00
commit 2b45fc2f0f
7 changed files with 940 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"file_hashes": {
"src/server.el": "1bb43f78f2048cc3aeb2d1f68eda1c1af03425a356b6660dff22273ed50fdece"
}
}
+3
View File
@@ -0,0 +1,3 @@
dist/engram
dist/engram.c
*.log
Vendored Executable
BIN
View File
Binary file not shown.
+344
View File
@@ -0,0 +1,344 @@
#include <stdint.h>
#include <stdlib.h>
#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;
}
+11
View File
@@ -0,0 +1,11 @@
package "engram-el" {
version "1.0.0"
description "Engram graph intelligence substrate — El implementation"
authors ["Will Anderson <will@neurontechnologies.ai>"]
edition "2026"
}
build {
entry "src/server.el"
output "dist/"
}
+284
View File
@@ -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.
+293
View File
@@ -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")