runtime + compiler: dharma, match, cgi blocks, VBD, agentic LLM

Two parallel agent sweeps closing the remaining structural gaps.

== Compiler completions ==

- match codegen: lowers Match into GCC/Clang statement-expression
  ({ ... }). Patterns: Wildcard, Binding, LitInt (==), LitStr
  (str_eq), LitBool. Per-match unique label via state counter.
  Verified: classify(0)→"zero", classify(1)→"one", classify(7)→"other".

- cgi block parsing: `cgi "name" { dharma_id, principal, network,
  engram }` → CgiBlock AST node → el_cgi_init() emitted as the first
  call in main() after el_runtime_init_args. Multiple cgi blocks per
  program emit a #error directive. Missing optional fields → EL_NULL.

- VBD compile-time enforcement: parser attaches `decorator: <name>`
  to FnDef. Codegen recursively walks fn bodies (Call/BinOp/Not/Neg/
  Field/Index/Try/Array/Map/If/For/Match plus Let/Return/Expr/While/
  For). If a non-@manager function calls dharma_emit or dharma_field,
  emit `#error "VBD violation: ... fn '<name>'"` before the function
  body. Verified: @engine fn calling dharma_emit → cc fails with the
  message. @manager fn calling dharma_emit → compiles clean.

Three-stage closure: stage1.c == stage2.c == stage3.c (2791 lines
each, byte-identical). dist/platform/elc rebuilt at 165 KB; .prev5
preserved.

== Runtime completions ==

- Real dharma_* primitives, no more stubs. Channel registry,
  request/response over HTTP, network-wide spreading activation,
  fire-and-forget event emission, blocking dharma_field with
  pthread_cond_timedwait (30s default), Hebbian relationship
  weights stored as Engram edges between dharma:self and
  dharma:peer:<id>, sorted-by-weight peer list. URL/ID arrays
  snapshotted before network I/O so mutexes never block on socket.

- New public C contract: el_runtime_dharma_event_arrive(type, payload,
  source) — application HTTP handler calls this when /dharma/event
  arrives, runtime broadcasts on _dharma_event_cv. Keeps the HTTP
  server generic; events flow through the application's router.

- llm_call_agentic real multi-turn loop. Tool registry (mutex-
  protected, dlsym-resolved, mirroring http_set_handler). Loop:
  build request with tools+messages → POST → dispatch on stop_reason.
  end_turn → return text. max_tokens → text + "[truncated]". tool_use
  → walk content[], call registered handler per block, build
  tool_result message, append to conversation, loop. Iteration cap
  10. Tools not registered return {"error":"tool not registered: X"}
  with is_error: true.

- New builtin: llm_register_tool(name, handler_fn_name).

Compile clean: cc -std=c11 -Wall -Wextra -c → zero warnings, zero
errors. Smoke test exercises every new dharma_* primitive +
llm_register_tool round-trip.

Runtime grew 3309→4079 lines (.c, ~155 KB), 312→342 lines (.h).

== Integration ==

Engram rebuilt against the new runtime: 130 KB binary, daemon
swapped on :8742 cleanly, /health and /api/stats both returning
correctly under launchd. No regressions.

== Status of "planned" items in language.md ==

- match codegen → IMPLEMENTED
- cgi block parsing → IMPLEMENTED
- VBD enforcement → IMPLEMENTED
- % operator → IMPLEMENTED (earlier today)
- vessel keyword → lexed (codegen uses package compatible)
- activate construct → still planned (low priority; engram_activate
  builtin covers the use case for now)
- sealed block → still planned
- dharma_emit fanout parallelization → potential future work, current
  serial behavior matches spec
This commit is contained in:
Will Anderson
2026-04-30 14:06:19 -05:00
parent fac24435ce
commit 12d5e7777e
8 changed files with 1955 additions and 97 deletions
+858 -88
View File
@@ -1926,84 +1926,6 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
_el_cgi_engram);
}
/* ── DHARMA network stubs ───────────────────────────────────────────────────
* Stub implementations for all dharma_* and engram_* builtins.
* Each stub prints a descriptive line to stdout so calls are visible in tests.
* Full implementations are provided by the DHARMA runtime linked at deploy. */
el_val_t dharma_connect(el_val_t cgi_id) {
const char* id = EL_CSTR(cgi_id);
if (!id) id = "(null)";
char buf[256];
snprintf(buf, sizeof(buf), "[dharma] connect: %s", id);
puts(buf);
/* Return a synthetic channel ID of the form "ch:<cgi_id>" */
char ch[272];
snprintf(ch, sizeof(ch), "ch:%s", id);
return el_wrap_str(el_strdup(ch));
}
el_val_t dharma_send(el_val_t channel, el_val_t content) {
const char* ch = EL_CSTR(channel);
const char* msg = EL_CSTR(content);
if (!ch) ch = "(null)";
if (!msg) msg = "(null)";
char buf[1024];
snprintf(buf, sizeof(buf), "[dharma] send on %s: %s", ch, msg);
puts(buf);
return el_wrap_str(el_strdup(""));
}
el_val_t dharma_activate(el_val_t query) {
const char* q = EL_CSTR(query);
if (!q) q = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[dharma] activate: %s", q);
puts(buf);
return el_list_empty();
}
void dharma_emit(el_val_t event_type, el_val_t payload) {
const char* et = EL_CSTR(event_type);
const char* pay = EL_CSTR(payload);
if (!et) et = "(null)";
if (!pay) pay = "(null)";
char buf[1024];
snprintf(buf, sizeof(buf), "[dharma] emit: %s %s", et, pay);
puts(buf);
}
el_val_t dharma_field(el_val_t event_type) {
const char* et = EL_CSTR(event_type);
if (!et) et = "(null)";
char buf[512];
snprintf(buf, sizeof(buf), "[dharma] field: %s", et);
puts(buf);
return el_map_new(0);
}
void dharma_strengthen(el_val_t cgi_id, el_val_t weight) {
const char* id = EL_CSTR(cgi_id);
if (!id) id = "(null)";
/* weight is encoded as el_val_t; print as integer (float encoding TBD) */
char buf[256];
snprintf(buf, sizeof(buf), "[dharma] strengthen: %s +%lld", id, (long long)weight);
puts(buf);
}
el_val_t dharma_relationship(el_val_t cgi_id) {
const char* id = EL_CSTR(cgi_id);
if (!id) id = "(null)";
char buf[256];
snprintf(buf, sizeof(buf), "[dharma] relationship: %s", id);
puts(buf);
return 0; /* 0.0 — no prior relationship in stub mode */
}
el_val_t dharma_peers(void) {
puts("[dharma] peers");
return el_list_empty();
}
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
/*
@@ -2981,17 +2903,526 @@ el_val_t engram_stats_json(void) {
return el_wrap_str(el_strdup(buf));
}
/* ── DHARMA network ─────────────────────────────────────────────────────────
* Real implementation. Peers are addressed by `dharma_id` — either bare
* (e.g. "ntn-genesis", transport defaults to http://localhost:7770) or
* "<id>@<url>" where <url> is the peer's Engram-exposed daemon.
*
* Channels are logical handles cached per-cgi: `dharma_connect` is
* idempotent and returns "ch:<cgi_id>". The channel registry below tracks
* every cgi_id we've connected to and its resolved transport URL.
*
* Relationship weights live in the local Engram graph: edges of type
* "dharma-relation" between a synthetic local node ("dharma:self") and
* synthetic peer nodes ("dharma:peer:<cgi_id>"). Hebbian increments
* accumulate in EngramEdge.weight, clamped to [0.0, 1.0].
*
* Events arrive over HTTP via the application's request handler, which is
* expected to call el_runtime_dharma_event_arrive() when it sees a
* /dharma/event POST. dharma_field() blocks on a per-event-type queue.
*/
#define DHARMA_DEFAULT_URL "http://localhost:7770"
/* Channel registry — one entry per known peer. */
typedef struct DharmaChannel {
char* cgi_id; /* full dharma_id including any @<url> suffix */
char* base_id; /* registry-id portion (before @) for relationship lookup */
char* url; /* resolved transport URL */
char* channel_id; /* "ch:<cgi_id>" */
} DharmaChannel;
static DharmaChannel* _dharma_channels = NULL;
static size_t _dharma_channel_count = 0;
static size_t _dharma_channel_cap = 0;
static pthread_mutex_t _dharma_channel_mu = PTHREAD_MUTEX_INITIALIZER;
/* Event queue — per-type linked list. dharma_field blocks on _dharma_event_cv. */
typedef struct DharmaEvent {
char* event_type;
char* payload;
char* source;
int64_t timestamp;
struct DharmaEvent* next;
} DharmaEvent;
static DharmaEvent* _dharma_event_head = NULL;
static DharmaEvent* _dharma_event_tail = NULL;
static pthread_mutex_t _dharma_event_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t _dharma_event_cv = PTHREAD_COND_INITIALIZER;
/* Split "<id>@<url>" → (base_id, url). If no "@", base_id = full, url = default.
* Returned strings are heap-allocated; caller must free. */
static void dharma_parse_id(const char* full, char** out_base, char** out_url) {
if (!full) full = "";
const char* at = strchr(full, '@');
if (at) {
size_t bn = (size_t)(at - full);
char* b = malloc(bn + 1);
memcpy(b, full, bn); b[bn] = '\0';
*out_base = b;
*out_url = el_strdup(at + 1);
if (!**out_url) { free(*out_url); *out_url = el_strdup(DHARMA_DEFAULT_URL); }
} else {
*out_base = el_strdup(full);
*out_url = el_strdup(DHARMA_DEFAULT_URL);
}
}
/* Find existing channel by full cgi_id. Caller must hold _dharma_channel_mu. */
static DharmaChannel* dharma_find_channel_locked(const char* cgi_id) {
if (!cgi_id) return NULL;
for (size_t i = 0; i < _dharma_channel_count; i++) {
if (_dharma_channels[i].cgi_id &&
strcmp(_dharma_channels[i].cgi_id, cgi_id) == 0) {
return &_dharma_channels[i];
}
}
return NULL;
}
/* Add a new channel entry. Caller must hold _dharma_channel_mu. */
static DharmaChannel* dharma_add_channel_locked(const char* cgi_id) {
if (_dharma_channel_count >= _dharma_channel_cap) {
size_t nc = _dharma_channel_cap ? _dharma_channel_cap * 2 : 8;
_dharma_channels = realloc(_dharma_channels, nc * sizeof(DharmaChannel));
if (!_dharma_channels) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
memset(_dharma_channels + _dharma_channel_cap, 0,
(nc - _dharma_channel_cap) * sizeof(DharmaChannel));
_dharma_channel_cap = nc;
}
DharmaChannel* ch = &_dharma_channels[_dharma_channel_count++];
char* base = NULL; char* url = NULL;
dharma_parse_id(cgi_id, &base, &url);
ch->cgi_id = el_strdup(cgi_id ? cgi_id : "");
ch->base_id = base;
ch->url = url;
size_t cn = strlen(ch->cgi_id) + 4;
ch->channel_id = malloc(cn);
snprintf(ch->channel_id, cn, "ch:%s", ch->cgi_id);
return ch;
}
el_val_t dharma_connect(el_val_t cgi_id) {
const char* id = EL_CSTR(cgi_id);
if (!id || !*id) return el_wrap_str(el_strdup(""));
pthread_mutex_lock(&_dharma_channel_mu);
DharmaChannel* ch = dharma_find_channel_locked(id);
if (!ch) ch = dharma_add_channel_locked(id);
char* out = el_strdup(ch->channel_id);
pthread_mutex_unlock(&_dharma_channel_mu);
return el_wrap_str(out);
}
/* Build an error JSON body — same shape http_error_json uses. */
static el_val_t dharma_error_json(const char* msg) {
return http_error_json(msg);
}
el_val_t dharma_send(el_val_t channel, el_val_t content) {
const char* ch_id = EL_CSTR(channel);
const char* msg = EL_CSTR(content);
if (!ch_id || strncmp(ch_id, "ch:", 3) != 0) {
return dharma_error_json("invalid channel");
}
const char* peer_id = ch_id + 3;
/* Look up channel; if unknown (caller fabricated), auto-register. */
pthread_mutex_lock(&_dharma_channel_mu);
DharmaChannel* ch = dharma_find_channel_locked(peer_id);
if (!ch) ch = dharma_add_channel_locked(peer_id);
char* url = el_strdup(ch->url);
pthread_mutex_unlock(&_dharma_channel_mu);
/* Build /dharma/recv body. */
const char* from = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)";
char* esc_ch = json_escape_alloc(ch_id);
char* esc_from = json_escape_alloc(from);
char* esc_msg = json_escape_alloc(msg ? msg : "");
JsonBuf b; jb_init(&b);
jb_puts(&b, "{\"channel\":\""); jb_puts(&b, esc_ch);
jb_puts(&b, "\",\"from\":\""); jb_puts(&b, esc_from);
jb_puts(&b, "\",\"content\":\""); jb_puts(&b, esc_msg);
jb_puts(&b, "\"}");
free(esc_ch); free(esc_from); free(esc_msg);
size_t ul = strlen(url) + 16;
char* full_url = malloc(ul);
snprintf(full_url, ul, "%s/dharma/recv", url);
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
el_val_t resp = http_do("POST", full_url, b.buf, h);
curl_slist_free_all(h);
free(b.buf); free(full_url); free(url);
return resp;
}
el_val_t dharma_activate(el_val_t query) {
const char* q = EL_CSTR(query);
if (!q) q = "";
el_val_t out = el_list_empty();
char* esc_q = json_escape_alloc(q);
JsonBuf body; jb_init(&body);
jb_puts(&body, "{\"query\":\""); jb_puts(&body, esc_q); jb_puts(&body, "\"}");
free(esc_q);
/* Snapshot the channel list under lock so we can iterate without
* holding the mutex during network I/O. */
pthread_mutex_lock(&_dharma_channel_mu);
size_t n = _dharma_channel_count;
char** urls = calloc(n ? n : 1, sizeof(char*));
char** ids = calloc(n ? n : 1, sizeof(char*));
char** bases = calloc(n ? n : 1, sizeof(char*));
for (size_t i = 0; i < n; i++) {
urls[i] = el_strdup(_dharma_channels[i].url);
ids[i] = el_strdup(_dharma_channels[i].cgi_id);
bases[i] = el_strdup(_dharma_channels[i].base_id);
}
pthread_mutex_unlock(&_dharma_channel_mu);
for (size_t i = 0; i < n; i++) {
size_t ul = strlen(urls[i]) + 32;
char* full_url = malloc(ul);
snprintf(full_url, ul, "%s/api/activate", urls[i]);
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
el_val_t resp = http_do("POST", full_url, body.buf, h);
curl_slist_free_all(h);
free(full_url);
const char* rs = EL_CSTR(resp);
if (!rs || !*rs) continue;
if (rs[0] == '{' && strstr(rs, "\"error\"")) continue;
/* Look up relationship weight (attenuation). */
double rel_weight = 1.0;
{
const char* self_id = "dharma:self";
char peer_node[512];
snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", bases[i]);
EngramStore* g = engram_get();
for (int64_t k = 0; k < g->edge_count; k++) {
EngramEdge* e = &g->edges[k];
if (e->from_id && e->to_id &&
strcmp(e->from_id, self_id) == 0 &&
strcmp(e->to_id, peer_node) == 0 &&
e->relation && strcmp(e->relation, "dharma-relation") == 0) {
rel_weight = e->weight;
break;
}
}
}
/* Iterate the response array. Expect either a top-level array
* or an object whose "results" field is an array. */
const char* arr = rs;
while (*arr == ' ' || *arr == '\t' || *arr == '\n' || *arr == '\r') arr++;
char* arr_owned = NULL;
if (*arr == '{') {
el_val_t r = json_get_raw(EL_STR(rs), EL_STR("results"));
const char* rr = EL_CSTR(r);
if (rr && *rr == '[') {
arr_owned = el_strdup(rr);
arr = arr_owned;
} else {
continue;
}
}
if (*arr != '[') { free(arr_owned); continue; }
const char* p = arr + 1;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
while (*p && *p != ']') {
const char* end = json_skip_value(p);
size_t en = (size_t)(end - p);
char* obj = el_strbuf(en);
memcpy(obj, p, en); obj[en] = '\0';
/* Pull activation_strength if present, else 1.0. */
el_val_t act_v = json_get_float(EL_STR(obj), EL_STR("activation_strength"));
double act = el_to_float(act_v);
if (!(act > 0.0 && act <= 100.0)) act = 1.0;
double final_act = act * rel_weight;
el_val_t entry = el_map_new(0);
/* node = the inner JSON if present, else the entire obj. */
el_val_t node_raw = json_get_raw(EL_STR(obj), EL_STR("node"));
const char* nr = EL_CSTR(node_raw);
entry = el_map_set(entry, EL_STR(el_strdup("node")),
(nr && *nr) ? node_raw : EL_STR(el_strdup(obj)));
entry = el_map_set(entry, EL_STR(el_strdup("source_cgi")),
EL_STR(el_strdup(ids[i])));
entry = el_map_set(entry, EL_STR(el_strdup("activation_strength")),
el_from_float(final_act));
out = el_list_append(out, entry);
free(obj);
p = end;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++;
}
free(arr_owned);
}
for (size_t i = 0; i < n; i++) { free(urls[i]); free(ids[i]); free(bases[i]); }
free(urls); free(ids); free(bases);
free(body.buf);
return out;
}
void dharma_emit(el_val_t event_type, el_val_t payload) {
const char* et = EL_CSTR(event_type);
const char* pay = EL_CSTR(payload);
if (!et) et = "";
if (!pay) pay = "";
const char* src = _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unknown)";
int64_t ts = engram_now_ms();
char* esc_et = json_escape_alloc(et);
char* esc_pay = json_escape_alloc(pay);
char* esc_src = json_escape_alloc(src);
JsonBuf b; jb_init(&b);
jb_puts(&b, "{\"type\":\""); jb_puts(&b, esc_et);
jb_puts(&b, "\",\"payload\":\""); jb_puts(&b, esc_pay);
jb_puts(&b, "\",\"source\":\""); jb_puts(&b, esc_src);
jb_puts(&b, "\",\"timestamp\":"); jb_emit_int(&b, ts);
jb_putc(&b, '}');
free(esc_et); free(esc_pay); free(esc_src);
/* Snapshot URLs to avoid holding the channel mutex during I/O. */
pthread_mutex_lock(&_dharma_channel_mu);
size_t n = _dharma_channel_count;
char** urls = calloc(n ? n : 1, sizeof(char*));
for (size_t i = 0; i < n; i++) urls[i] = el_strdup(_dharma_channels[i].url);
pthread_mutex_unlock(&_dharma_channel_mu);
for (size_t i = 0; i < n; i++) {
size_t ul = strlen(urls[i]) + 32;
char* full_url = malloc(ul);
snprintf(full_url, ul, "%s/dharma/event", urls[i]);
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
el_val_t r = http_do("POST", full_url, b.buf, h);
(void)r; /* fire-and-forget — emit is not synchronous */
curl_slist_free_all(h);
free(full_url);
}
for (size_t i = 0; i < n; i++) free(urls[i]);
free(urls);
free(b.buf);
}
void el_runtime_dharma_event_arrive(const char* event_type, const char* payload,
const char* source) {
DharmaEvent* ev = calloc(1, sizeof(DharmaEvent));
if (!ev) return;
ev->event_type = el_strdup(event_type ? event_type : "");
ev->payload = el_strdup(payload ? payload : "");
ev->source = el_strdup(source ? source : "");
ev->timestamp = engram_now_ms();
ev->next = NULL;
pthread_mutex_lock(&_dharma_event_mu);
if (_dharma_event_tail) _dharma_event_tail->next = ev;
else _dharma_event_head = ev;
_dharma_event_tail = ev;
pthread_cond_broadcast(&_dharma_event_cv);
pthread_mutex_unlock(&_dharma_event_mu);
}
el_val_t dharma_field(el_val_t event_type) {
const char* et = EL_CSTR(event_type);
if (!et) et = "";
/* Compute deadline: now + 30 seconds. */
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += 30;
DharmaEvent* found = NULL;
pthread_mutex_lock(&_dharma_event_mu);
while (1) {
/* Scan queue for matching type; pop and return first match. */
DharmaEvent* prev = NULL;
DharmaEvent* cur = _dharma_event_head;
while (cur) {
if (cur->event_type && strcmp(cur->event_type, et) == 0) {
if (prev) prev->next = cur->next;
else _dharma_event_head = cur->next;
if (_dharma_event_tail == cur) _dharma_event_tail = prev;
cur->next = NULL;
found = cur;
break;
}
prev = cur; cur = cur->next;
}
if (found) break;
int rc = pthread_cond_timedwait(&_dharma_event_cv, &_dharma_event_mu, &deadline);
if (rc == ETIMEDOUT) break;
}
pthread_mutex_unlock(&_dharma_event_mu);
if (!found) return el_map_new(0);
el_val_t m = el_map_new(0);
m = el_map_set(m, EL_STR(el_strdup("type")),
EL_STR(el_strdup(found->event_type ? found->event_type : "")));
m = el_map_set(m, EL_STR(el_strdup("payload")),
EL_STR(el_strdup(found->payload ? found->payload : "")));
m = el_map_set(m, EL_STR(el_strdup("source_cgi")),
EL_STR(el_strdup(found->source ? found->source : "")));
m = el_map_set(m, EL_STR(el_strdup("timestamp")), (el_val_t)found->timestamp);
free(found->event_type); free(found->payload); free(found->source); free(found);
return m;
}
/* Locate (or create) the local "dharma:self" node and the synthetic peer
* node "dharma:peer:<base_id>". Returns the index of the dharma-relation
* edge, or -1 if not found. If `create` is non-zero, ensure the nodes
* and edge exist (creating them as needed) and return the edge index. */
static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int create) {
if (!peer_base || !*peer_base) return -1;
EngramStore* g = engram_get();
const char* self_id = "dharma:self";
char peer_node[512];
snprintf(peer_node, sizeof(peer_node), "dharma:peer:%s", peer_base);
/* Look for the edge first. */
for (int64_t i = 0; i < g->edge_count; i++) {
EngramEdge* e = &g->edges[i];
if (e->from_id && e->to_id &&
strcmp(e->from_id, self_id) == 0 &&
strcmp(e->to_id, peer_node) == 0 &&
e->relation && strcmp(e->relation, "dharma-relation") == 0) {
return i;
}
}
if (!create) return -1;
/* Ensure self node exists. We use a fixed id (not engram_new_id) so
* subsequent calls reuse the same one. */
if (!engram_find_node(self_id)) {
engram_grow_nodes();
EngramNode* n = &g->nodes[g->node_count];
memset(n, 0, sizeof(*n));
n->id = el_strdup(self_id);
n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)");
n->node_type = el_strdup("DharmaSelf");
n->label = el_strdup("dharma:self");
n->tier = el_strdup("Working");
n->tags = el_strdup("dharma");
n->metadata = el_strdup("{}");
n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0;
int64_t now = engram_now_ms();
n->created_at = now; n->updated_at = now; n->last_activated = now;
g->node_count++;
}
if (!engram_find_node(peer_node)) {
engram_grow_nodes();
EngramNode* n = &g->nodes[g->node_count];
memset(n, 0, sizeof(*n));
n->id = el_strdup(peer_node);
n->content = el_strdup(peer_base);
n->node_type = el_strdup("DharmaPeer");
n->label = el_strdup(peer_node);
n->tier = el_strdup("Working");
n->tags = el_strdup("dharma");
n->metadata = el_strdup("{}");
n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0;
int64_t now = engram_now_ms();
n->created_at = now; n->updated_at = now; n->last_activated = now;
g->node_count++;
}
/* Create the edge with weight 0.0 — caller will increment. */
engram_grow_edges();
EngramEdge* e = &g->edges[g->edge_count];
memset(e, 0, sizeof(*e));
e->id = engram_new_id();
e->from_id = el_strdup(self_id);
e->to_id = el_strdup(peer_node);
e->relation = el_strdup("dharma-relation");
e->metadata = el_strdup("{}");
e->weight = 0.0;
e->confidence = 1.0;
int64_t now = engram_now_ms();
e->created_at = now; e->updated_at = now;
int64_t idx = g->edge_count;
g->edge_count++;
return idx;
}
void dharma_strengthen(el_val_t cgi_id, el_val_t weight) {
const char* id = EL_CSTR(cgi_id);
if (!id || !*id) return;
char* base = NULL; char* url = NULL;
dharma_parse_id(id, &base, &url);
free(url);
int64_t ei = dharma_find_or_create_relation_edge(base, 1);
free(base);
if (ei < 0) return;
EngramStore* g = engram_get();
double inc = engram_decode_score(weight);
if (!(inc >= 0.0)) inc = 0.0;
double w = g->edges[ei].weight + inc;
if (w < 0.0) w = 0.0;
if (w > 1.0) w = 1.0;
g->edges[ei].weight = w;
g->edges[ei].updated_at = engram_now_ms();
g->edges[ei].last_fired = g->edges[ei].updated_at;
}
el_val_t dharma_relationship(el_val_t cgi_id) {
const char* id = EL_CSTR(cgi_id);
if (!id || !*id) return el_from_float(0.0);
char* base = NULL; char* url = NULL;
dharma_parse_id(id, &base, &url);
free(url);
int64_t ei = dharma_find_or_create_relation_edge(base, 0);
free(base);
if (ei < 0) return el_from_float(0.0);
EngramStore* g = engram_get();
return el_from_float(g->edges[ei].weight);
}
el_val_t dharma_peers(void) {
/* Walk dharma-relation edges out of "dharma:self", weight > 0, sort desc. */
EngramStore* g = engram_get();
const char* self_id = "dharma:self";
typedef struct { char* peer_base; double weight; } PeerEntry;
PeerEntry* peers = malloc((size_t)(g->edge_count + 1) * sizeof(PeerEntry));
int64_t pcount = 0;
if (!peers) return el_list_empty();
for (int64_t i = 0; i < g->edge_count; i++) {
EngramEdge* e = &g->edges[i];
if (!e->from_id || !e->to_id) continue;
if (strcmp(e->from_id, self_id) != 0) continue;
if (!e->relation || strcmp(e->relation, "dharma-relation") != 0) continue;
if (e->weight <= 0.0) continue;
const char* prefix = "dharma:peer:";
size_t pl = strlen(prefix);
if (strncmp(e->to_id, prefix, pl) != 0) continue;
peers[pcount].peer_base = el_strdup(e->to_id + pl);
peers[pcount].weight = e->weight;
pcount++;
}
/* Sort desc by weight. */
for (int64_t i = 1; i < pcount; i++) {
PeerEntry key = peers[i];
int64_t j = i - 1;
while (j >= 0 && peers[j].weight < key.weight) {
peers[j + 1] = peers[j]; j--;
}
peers[j + 1] = key;
}
el_val_t out = el_list_empty();
for (int64_t i = 0; i < pcount; i++) {
out = el_list_append(out, EL_STR(peers[i].peer_base));
}
free(peers);
return out;
}
/* ── Batch 4: LLM (Anthropic API client) ─────────────────────────────────── */
/*
* All LLM builtins call https://api.anthropic.com/v1/messages with the API
* key from env ANTHROPIC_API_KEY. Default model is "claude-sonnet-4-5"
* when the supplied model is empty/null.
*
* `llm_call_agentic` is currently implemented as a single-turn fallback
* delegating to `llm_call_system` — TODO: implement the full multi-turn
* tool_use / tool_result loop. Programs that need agentic tool dispatch
* should register a tool handler via state_set("tools/<name>", json) and
* loop themselves until the model emits stop_reason=end_turn.
* `llm_call_agentic` runs a real multi-turn tool_use/tool_result loop.
* Tool handlers are registered with `llm_register_tool(name, fn_name)`,
* which dlsym()s the named symbol. Each tool handler has the C signature
* el_val_t handler(el_val_t input_json);
* and returns a JSON-string el_val_t result. Iteration is capped at 10.
*/
static const char* LLM_DEFAULT_MODEL = "claude-sonnet-4-5";
@@ -3121,12 +3552,351 @@ el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_p
return llm_extract_text(resp);
}
/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */
typedef el_val_t (*llm_tool_fn)(el_val_t input);
typedef struct LlmToolEntry {
char* name;
llm_tool_fn fn;
} LlmToolEntry;
static LlmToolEntry _llm_tools[64];
static size_t _llm_tool_count = 0;
static pthread_mutex_t _llm_tool_mu = PTHREAD_MUTEX_INITIALIZER;
static llm_tool_fn llm_tool_lookup(const char* name) {
if (!name) return NULL;
llm_tool_fn fn = NULL;
pthread_mutex_lock(&_llm_tool_mu);
for (size_t i = 0; i < _llm_tool_count; i++) {
if (strcmp(_llm_tools[i].name, name) == 0) { fn = _llm_tools[i].fn; break; }
}
pthread_mutex_unlock(&_llm_tool_mu);
return fn;
}
void llm_register_tool(el_val_t name, el_val_t handler_fn_name) {
const char* nm = EL_CSTR(name);
const char* sym = EL_CSTR(handler_fn_name);
if (!nm || !*nm || !sym || !*sym) return;
void* p = dlsym(RTLD_DEFAULT, sym);
if (!p) {
fprintf(stderr, "[llm_register_tool] symbol not found: %s\n", sym);
return;
}
pthread_mutex_lock(&_llm_tool_mu);
/* Replace existing entry by name. */
for (size_t i = 0; i < _llm_tool_count; i++) {
if (strcmp(_llm_tools[i].name, nm) == 0) {
_llm_tools[i].fn = (llm_tool_fn)p;
pthread_mutex_unlock(&_llm_tool_mu);
return;
}
}
if (_llm_tool_count < sizeof(_llm_tools) / sizeof(_llm_tools[0])) {
_llm_tools[_llm_tool_count].name = el_strdup(nm);
_llm_tools[_llm_tool_count].fn = (llm_tool_fn)p;
_llm_tool_count++;
}
pthread_mutex_unlock(&_llm_tool_mu);
}
/* Serialize the El `tools` list into the JSON `tools:[...]` field expected
* by the Anthropic API. Each tool is an ElMap with name/description/
* input_schema. input_schema is treated as either a JSON-object string
* (passed through verbatim) or a missing field (substitute {}). */
static void llm_emit_tools_json(JsonBuf* b, el_val_t tools_list) {
jb_putc(b, '[');
ElList* lst = (ElList*)(uintptr_t)tools_list;
int64_t n = lst ? lst->length : 0;
for (int64_t i = 0; i < n; i++) {
if (i > 0) jb_putc(b, ',');
ElMap* tm = as_map(lst->elems[i]);
const char* name = "";
const char* desc = "";
const char* schema = "{}";
if (tm) {
for (int64_t k = 0; k < tm->count; k++) {
const char* key = EL_CSTR(tm->keys[k]);
const char* val = EL_CSTR(tm->values[k]);
if (!key || !val) continue;
if (strcmp(key, "name") == 0) name = val;
else if (strcmp(key, "description") == 0) desc = val;
else if (strcmp(key, "input_schema") == 0) schema = val;
}
}
char* esc_name = json_escape_alloc(name);
char* esc_desc = json_escape_alloc(desc);
jb_puts(b, "{\"name\":\""); jb_puts(b, esc_name);
jb_puts(b, "\",\"description\":\""); jb_puts(b, esc_desc);
jb_puts(b, "\",\"input_schema\":"); jb_puts(b, schema && *schema ? schema : "{}");
jb_putc(b, '}');
free(esc_name); free(esc_desc);
}
jb_putc(b, ']');
}
/* Walk the assistant `content` array and emit each block back into b,
* preserving the verbatim JSON of every block — used to re-include the
* assistant turn in the next request. */
static void llm_emit_content_blocks(JsonBuf* b, const char* resp) {
const char* p = json_find_key(resp, "content");
jb_putc(b, '[');
if (!p) { jb_putc(b, ']'); return; }
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (*p != '[') { jb_putc(b, ']'); return; }
p++;
int first = 1;
while (*p && *p != ']') {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++;
if (*p != '{') break;
const char* end = json_skip_value(p);
if (!first) jb_putc(b, ',');
first = 0;
size_t n = (size_t)(end - p);
jb_reserve(b, n);
memcpy(b->buf + b->len, p, n);
b->len += n;
b->buf[b->len] = '\0';
p = end;
}
jb_putc(b, ']');
}
/* Concatenate all "text" blocks from a response. Returns owned string. */
static char* llm_concat_text_blocks(const char* resp) {
JsonBuf out; jb_init(&out);
if (!resp) return out.buf;
const char* p = json_find_key(resp, "content");
if (!p) return out.buf;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (*p != '[') return out.buf;
p++;
while (*p && *p != ']') {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++;
if (*p != '{') break;
const char* end = json_skip_value(p);
size_t n = (size_t)(end - p);
char* obj = malloc(n + 1);
memcpy(obj, p, n); obj[n] = '\0';
const char* tp = json_find_key(obj, "type");
if (tp && *tp == '"') {
JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 };
char* tname = jp_parse_string_raw(&jp);
if (!jp.err && tname && strcmp(tname, "text") == 0) {
const char* xp = json_find_key(obj, "text");
if (xp && *xp == '"') {
JsonParser jp2 = { .p = xp, .end = xp + strlen(xp), .err = 0 };
char* txt = jp_parse_string_raw(&jp2);
if (!jp2.err && txt) jb_puts(&out, txt);
free(txt);
}
}
free(tname);
}
free(obj);
p = end;
}
return out.buf;
}
/* Build tool_result message blocks for every tool_use in a response.
* Appends to `b` an array element for each tool_use; caller wraps. */
static int llm_build_tool_results(JsonBuf* b, const char* resp) {
int any = 0;
const char* p = json_find_key(resp, "content");
if (!p) return 0;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (*p != '[') return 0;
p++;
while (*p && *p != ']') {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++;
if (*p != '{') break;
const char* end = json_skip_value(p);
size_t n = (size_t)(end - p);
char* obj = malloc(n + 1);
memcpy(obj, p, n); obj[n] = '\0';
const char* tp = json_find_key(obj, "type");
char* type_s = NULL;
if (tp && *tp == '"') {
JsonParser jp = { .p = tp, .end = tp + strlen(tp), .err = 0 };
type_s = jp_parse_string_raw(&jp);
}
if (type_s && strcmp(type_s, "tool_use") == 0) {
/* Extract id, name, input. */
char* id_s = NULL; char* name_s = NULL;
const char* idp = json_find_key(obj, "id");
if (idp && *idp == '"') {
JsonParser jp = { .p = idp, .end = idp + strlen(idp), .err = 0 };
id_s = jp_parse_string_raw(&jp);
}
const char* np = json_find_key(obj, "name");
if (np && *np == '"') {
JsonParser jp = { .p = np, .end = np + strlen(np), .err = 0 };
name_s = jp_parse_string_raw(&jp);
}
el_val_t input_raw = json_get_raw(EL_STR(obj), EL_STR("input"));
const char* input_s = EL_CSTR(input_raw);
if (!input_s || !*input_s) input_s = "{}";
llm_tool_fn fn = llm_tool_lookup(name_s ? name_s : "");
char* result = NULL;
int is_error = 0;
if (!fn) {
size_t en = strlen(name_s ? name_s : "(null)") + 64;
result = malloc(en);
snprintf(result, en, "{\"error\":\"tool not registered: %s\"}",
name_s ? name_s : "(null)");
is_error = 1;
} else {
el_val_t out = fn(EL_STR(input_s));
const char* os = EL_CSTR(out);
result = el_strdup(os ? os : "");
}
if (any) jb_putc(b, ',');
char* esc_id = json_escape_alloc(id_s ? id_s : "");
char* esc_res = json_escape_alloc(result ? result : "");
jb_puts(b, "{\"type\":\"tool_result\",\"tool_use_id\":\"");
jb_puts(b, esc_id);
jb_puts(b, "\",\"content\":\"");
jb_puts(b, esc_res);
jb_puts(b, "\"");
if (is_error) jb_puts(b, ",\"is_error\":true");
jb_putc(b, '}');
free(esc_id); free(esc_res); free(result);
free(id_s); free(name_s);
any = 1;
}
free(type_s);
free(obj);
p = end;
}
return any;
}
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools) {
/* TODO: full multi-turn tool_use / tool_result loop. For now we delegate
* to llm_call_system and ignore the tools list — programs needing real
* agentic dispatch should drive the loop themselves with raw http_post. */
(void)tools;
return llm_call_system(model, system, user);
/* Empty tools list → degrade to plain system call. */
ElList* tl = (ElList*)(uintptr_t)tools;
if (!tl || tl->length == 0) {
return llm_call_system(model, system, user);
}
const char* m = llm_resolve_model(EL_CSTR(model));
const char* sys_p = EL_CSTR(system); if (!sys_p) sys_p = "";
const char* usr_p = EL_CSTR(user); if (!usr_p) usr_p = "";
/* Build the static parts: tools JSON and system prompt — these don't
* change across iterations. */
JsonBuf tools_buf; jb_init(&tools_buf);
llm_emit_tools_json(&tools_buf, tools);
char* esc_sys = json_escape_alloc(sys_p);
/* messages array, accumulated as a mutable JSON fragment (no surrounding
* brackets — emitted at request time). */
JsonBuf msgs; jb_init(&msgs);
/* First user message. */
char* esc_user = json_escape_alloc(usr_p);
jb_puts(&msgs, "{\"role\":\"user\",\"content\":\"");
jb_puts(&msgs, esc_user);
jb_puts(&msgs, "\"}");
free(esc_user);
char* last_text = el_strdup("");
el_val_t final_out = 0;
int reached_cap = 1;
for (int iter = 0; iter < 10; iter++) {
/* Build request body. */
JsonBuf body; jb_init(&body);
jb_putc(&body, '{');
jb_puts(&body, "\"model\":"); jb_emit_escaped(&body, m);
jb_puts(&body, ",\"max_tokens\":4096");
if (*sys_p) {
jb_puts(&body, ",\"system\":\"");
jb_puts(&body, esc_sys);
jb_puts(&body, "\"");
}
jb_puts(&body, ",\"tools\":");
jb_puts(&body, tools_buf.buf);
jb_puts(&body, ",\"messages\":[");
jb_puts(&body, msgs.buf);
jb_puts(&body, "]}");
el_val_t resp_v = llm_request(body.buf);
free(body.buf);
const char* resp = EL_CSTR(resp_v);
if (!resp || !*resp) {
final_out = http_error_json("empty response");
reached_cap = 0;
break;
}
if (resp[0] == '{' && strstr(resp, "\"error\"") &&
!json_find_key(resp, "content")) {
final_out = el_wrap_str(el_strdup(resp));
reached_cap = 0;
break;
}
/* Update last_text from this response. */
free(last_text);
last_text = llm_concat_text_blocks(resp);
/* Inspect stop_reason. */
el_val_t sr_v = json_get_string(EL_STR(resp), EL_STR("stop_reason"));
const char* sr = EL_CSTR(sr_v); if (!sr) sr = "";
if (strcmp(sr, "end_turn") == 0) {
final_out = el_wrap_str(el_strdup(last_text));
reached_cap = 0;
break;
}
if (strcmp(sr, "max_tokens") == 0) {
size_t ln = strlen(last_text) + 16;
char* out = malloc(ln);
snprintf(out, ln, "%s\n[truncated]", last_text);
final_out = el_wrap_str(out);
reached_cap = 0;
break;
}
if (strcmp(sr, "tool_use") != 0) {
/* Unexpected stop reason; return the text we have. */
final_out = el_wrap_str(el_strdup(last_text));
reached_cap = 0;
break;
}
/* Append the assistant turn (raw content blocks) to messages. */
JsonBuf ab; jb_init(&ab);
jb_puts(&ab, ",{\"role\":\"assistant\",\"content\":");
llm_emit_content_blocks(&ab, resp);
jb_putc(&ab, '}');
jb_puts(&msgs, ab.buf);
free(ab.buf);
/* Build tool_result message. */
JsonBuf tr; jb_init(&tr);
jb_puts(&tr, ",{\"role\":\"user\",\"content\":[");
int any = llm_build_tool_results(&tr, resp);
jb_puts(&tr, "]}");
if (any) {
jb_puts(&msgs, tr.buf);
}
free(tr.buf);
}
if (reached_cap) {
size_t ln = strlen(last_text) + 32;
char* out = malloc(ln);
snprintf(out, ln, "[loop_cap_reached]\n%s", last_text);
final_out = el_wrap_str(out);
}
free(last_text);
free(esc_sys);
free(tools_buf.buf);
free(msgs.buf);
return final_out;
}
/* base64-encode arbitrary bytes (returns owned C string). */
+32 -2
View File
@@ -219,8 +219,22 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
/* ── DHARMA network builtins ─────────────────────────────────────────────────
* Available to CGI programs (declared with a `cgi {}` block).
* Stubs print descriptive output and return empty/zero values.
* Full implementations are linked from the DHARMA runtime at deploy time. */
*
* Peers are addressed by `dharma_id` of the form
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
* If the @<url> portion is omitted, transport defaults to
* "http://localhost:7770" (the local CGI daemon assumption).
*
* Wire protocol (all peers expose):
* POST <url>/dharma/recv { channel, from, content } → response body
* POST <url>/dharma/event { type, payload, source, timestamp }
* POST <url>/api/activate { query } → list of nodes
*
* Hosting application's responsibility: an El program with a `cgi {}` block
* runs http_serve() with its own request handler; that handler should route
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
* incoming events feed dharma_field() queues. The runtime itself does not
* intercept any /dharma path. */
el_val_t dharma_connect(el_val_t cgi_id);
el_val_t dharma_send(el_val_t channel, el_val_t content);
@@ -231,6 +245,14 @@ void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
el_val_t dharma_relationship(el_val_t cgi_id);
el_val_t dharma_peers(void);
/* Public C API: called by an El program's HTTP handler when a /dharma/event
* request arrives. Pushes onto the per-event-type queue and signals any
* pending dharma_field() blockers. All three arguments must be NUL-terminated
* C strings (or NULL — then treated as empty). */
void el_runtime_dharma_event_arrive(const char* event_type,
const char* payload,
const char* source);
/* ── Engram local graph primitives ───────────────────────────────────────────
* Operate on the CGI's local Engram knowledge graph.
* `engram_activate` queries the local graph only; `dharma_activate` is
@@ -275,6 +297,14 @@ el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_va
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
el_val_t llm_models(void);
/* Register a tool handler by name. The handler is looked up via dlsym
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
* a global C symbol that this function can locate at runtime.
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
* the tool input as a JSON-string el_val_t and returns a JSON-string
* el_val_t result. Used by llm_call_agentic. */
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
/* ── args() ─────────────────────────────────────────────────────────────────
* Provides access to command-line arguments passed to the program.
* Populated by el_runtime_init_args() before main() runs. */