Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb88330da | |||
| c8cb425412 | |||
| 3e7aa0fff4 | |||
| aa67f86f90 | |||
| 01446e644b | |||
| 92f51885bc | |||
| 2688cb722a | |||
| 71bb0820ce | |||
| d67f4c8f08 | |||
| 975bf2721b | |||
| 779a87878b | |||
| c586ea5ef1 | |||
| 6819729429 | |||
| 31dd93d5f4 | |||
| 9d266aac4c | |||
| b24f6d645b | |||
| 39acb55d4f | |||
| c6d4530060 | |||
| 98a0bfd09c | |||
| bcdadb7323 | |||
| 644d9915bf | |||
| dde039b09a | |||
| 3bb17a5296 | |||
| 6c57d4fe1b |
+36
-3
@@ -219,9 +219,14 @@ fn proactive_curiosity() -> Bool {
|
||||
// Activate each term independently so substring seed-finding hits many nodes.
|
||||
// hops=1 (not 2): the in-process Engram has grown to 165K+ nodes. hops=2 BFS
|
||||
// visits far more nodes and returns much larger JSON blobs. On a graph this
|
||||
// large, hops=1 still activates all directly-related nodes AND triggers the
|
||||
// semantic seed supplement (cosine sim ≥ 0.70 scan over all embedded nodes),
|
||||
// giving broad working-memory coverage without the quadratic blowup of hops=2.
|
||||
// large, hops=1 still activates all directly-related nodes, giving broad
|
||||
// working-memory coverage without the quadratic blowup of hops=2.
|
||||
//
|
||||
// NOTE: a semantic seed supplement (cosine sim ≥ 0.70 scan over embedded nodes)
|
||||
// was planned alongside hops=1 but is NOT yet implemented — embed_ok in
|
||||
// heartbeats confirms Ollama is reachable, but no embedding call is made during
|
||||
// activation. The seed-finding loop in el_runtime.c uses istr_contains only.
|
||||
// (2026-06-30 self-review: corrected stale comment)
|
||||
let curiosity_seed: String = curiosity_term_a + " " + curiosity_term_b + " " + curiosity_term_c
|
||||
let results_a: String = engram_activate_json(curiosity_term_a, 1)
|
||||
let results_b: String = engram_activate_json(curiosity_term_b, 1)
|
||||
@@ -278,11 +283,20 @@ fn proactive_curiosity() -> Bool {
|
||||
let safe_auto: String = str_replace(auto_term, "\"", "'")
|
||||
|
||||
let wmc: Int = engram_wm_count()
|
||||
// wm_top snapshot in curiosity_scan ISE: top-3 WM nodes by weight.
|
||||
// Heartbeat already records top-5 every 60s; curiosity_scan fires every 30s
|
||||
// (scan_ms = beat_ms/2) and is the PRIMARY activation driver during idle.
|
||||
// Without wm_top here, we can't see which nodes actually entered WM after
|
||||
// each curiosity round — only the aggregate count. Top-3 is enough to
|
||||
// diagnose "stuck on X" patterns without bloating the ISE payload.
|
||||
// (2026-07-01 self-review)
|
||||
let wm3: String = engram_wm_top_json(3)
|
||||
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed
|
||||
+ "\",\"auto_term\":\"" + safe_auto
|
||||
+ "\",\"minute_block\":" + int_to_str(minute_block)
|
||||
+ ",\"activated\":" + int_to_str(total_found)
|
||||
+ ",\"wm_active\":" + int_to_str(wmc)
|
||||
+ ",\"wm_top\":" + wm3
|
||||
+ ",\"ts\":" + int_to_str(ts) + "}"
|
||||
ise_post(ise)
|
||||
return total_found > 0
|
||||
@@ -513,9 +527,27 @@ fn awareness_run() -> Void {
|
||||
let scan_ms: Int = beat_ms / 2
|
||||
|
||||
while true {
|
||||
// Arena-scope each tick: awareness_run() is a background loop, not an
|
||||
// HTTP request, so nothing ever called el_request_start/el_request_end
|
||||
// for this thread. Per the runtime's own convention (el_runtime.c),
|
||||
// any thread that never enters a request/arena scope is treated as a
|
||||
// one-shot CLI program whose allocations are intentionally permanent —
|
||||
// so every el_strdup/el_strbuf/jb_finish string built during perceive(),
|
||||
// emit_heartbeat(), and proactive_curiosity() (JSON payloads, search
|
||||
// results, string concatenation via +) leaked forever, once per tick.
|
||||
// el_arena_push()/el_arena_pop() are the same builtins the EL compiler
|
||||
// itself uses to scope allocations per function/statement (see
|
||||
// codegen.el's fn_arena_mark / stmt_mark usage) — mirroring that here
|
||||
// reclaims everything allocated in one tick as soon as the tick ends.
|
||||
// Safe: state_set/state_get persist through a separate global table
|
||||
// (el_strdup_persist, outside the arena) — state_get's return value is
|
||||
// only an arena-tracked *copy* of the persisted value, scoped to this
|
||||
// tick's use, which is exactly what should be reclaimed here.
|
||||
let tick_mark: Any = el_arena_push()
|
||||
let running: String = state_get("soul.running")
|
||||
if str_eq(running, "false") {
|
||||
println("[awareness] exiting")
|
||||
el_arena_pop(tick_mark)
|
||||
return ""
|
||||
}
|
||||
let did_work: Bool = one_cycle()
|
||||
@@ -579,6 +611,7 @@ fn awareness_run() -> Void {
|
||||
}
|
||||
|
||||
sleep_ms(tick_ms)
|
||||
el_arena_pop(tick_mark)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ extern fn elapsed_ms() -> Int
|
||||
extern fn elapsed_human() -> String
|
||||
extern fn embed_ok() -> Int
|
||||
extern fn emit_heartbeat() -> Void
|
||||
extern fn auto_term_try_slot(slot_type: String, slot_lbl: String) -> Void
|
||||
extern fn proactive_curiosity() -> Bool
|
||||
extern fn pulse_count() -> Int
|
||||
extern fn pulse_inc() -> Int
|
||||
|
||||
@@ -594,6 +594,44 @@ fn engram_compile(intent: String) -> String {
|
||||
if str_starts_with(ctx, "[") { return truncated + "]" }
|
||||
return truncated
|
||||
}
|
||||
// distill_transcript — extract the salient tail from a full conversation transcript.
|
||||
//
|
||||
// Purpose: before activating working memory on a transcript, reduce it to the
|
||||
// last N turns. Activating on the ENTIRE transcript (which may contain hundreds
|
||||
// of messages) would produce noisy, over-broad seed finding — too many nodes match
|
||||
// too many words, collapse the WM to breakthrough-floor nodes. Taking only the tail
|
||||
// focuses activation on what's contextually live right now.
|
||||
//
|
||||
// Handles two transcript formats:
|
||||
// JSON array: [{"role":"human","content":"..."},...] → extract last 3 messages' content
|
||||
// Plain text: raw string → return last 500 chars
|
||||
//
|
||||
// Returns a string of at most 500 chars suitable for engram_compile/engram_activate.
|
||||
// (Added 2026-07-01 self-review: was called in handle_dharma_room_turn and
|
||||
// handle_dharma_chat but never defined — caused build failure since June 30.)
|
||||
fn distill_transcript(transcript: String) -> String {
|
||||
if str_eq(transcript, "") { return "" }
|
||||
// JSON array format: extract last 3 messages' content fields
|
||||
if str_starts_with(transcript, "[") {
|
||||
let n: Int = json_array_len(transcript)
|
||||
if n == 0 { return "" }
|
||||
let m0: String = json_array_get(transcript, n - 1)
|
||||
let m1: String = if n > 1 { json_array_get(transcript, n - 2) } else { "" }
|
||||
let m2: String = if n > 2 { json_array_get(transcript, n - 3) } else { "" }
|
||||
let c0: String = json_get(m0, "content")
|
||||
let c1: String = json_get(m1, "content")
|
||||
let c2: String = json_get(m2, "content")
|
||||
let combined: String = c2 + " " + c1 + " " + c0
|
||||
let len: Int = str_len(combined)
|
||||
if len > 500 { return str_slice(combined, len - 500, len) }
|
||||
return combined
|
||||
}
|
||||
// Plain text: return last 500 chars
|
||||
let len: Int = str_len(transcript)
|
||||
if len > 500 { return str_slice(transcript, len - 500, len) }
|
||||
return transcript
|
||||
}
|
||||
|
||||
fn json_safe(s: String) -> String {
|
||||
let s1: String = str_replace(s, "\\", "\\\\")
|
||||
let s2: String = str_replace(s1, "\"", "\\\"")
|
||||
@@ -602,6 +640,21 @@ fn json_safe(s: String) -> String {
|
||||
return s4
|
||||
}
|
||||
|
||||
// current_engine_note — a short, FACTUAL line appended to the system prompt so Neuron can answer
|
||||
// "what model/LLM are you running on?" truthfully. An LLM cannot know its own model from training
|
||||
// (the name/version is assigned AFTER training finishes), so the harness must tell it. This is
|
||||
// identity-consistent: the model is the ENGINE; the self (identity, values, memory) is layered on
|
||||
// top. ADDITIVE — it adds a fact, it does not alter identity, values, or the safety layer.
|
||||
fn current_engine_note(model: String) -> String {
|
||||
if str_eq(model, "") {
|
||||
return ""
|
||||
}
|
||||
return "\n\n[CURRENT ENGINE: this turn is generated by the underlying model \"" + model
|
||||
+ "\". It is the engine beneath your self — your identity, values, and memory are layered on"
|
||||
+ " top of it. If the user asks which model or LLM you are running on, answer with this model"
|
||||
+ " id plainly and truthfully; never guess a different one.]"
|
||||
}
|
||||
|
||||
// build_system_prompt — assemble the system prompt for a chat turn.
|
||||
// chat_mode: Bool — pass true from handle_chat (no tools), false from agentic paths.
|
||||
// Issue #9 fix: no_tools_rule only included when chat_mode=true.
|
||||
@@ -873,6 +926,68 @@ fn session_preload_bullets(nodes: String, max_bullets: Int, snip_len: Int) -> St
|
||||
return bullets
|
||||
}
|
||||
|
||||
// Cross-session affective context (hoisted verbatim from handle_chat, 2026-07-04):
|
||||
// the block-expression initializer form miscompiles under the local El toolchain
|
||||
// (first typed let in a block-expr loses its declaration - repro filed for Will).
|
||||
// Function-hoist is semantically identical. AFFECTIVE/CARE LOGIC: body unchanged.
|
||||
fn affective_context_prefix() -> String {
|
||||
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
|
||||
let aff_now_ts: Int = time_now()
|
||||
let aff_cutoff: Int = aff_now_ts - 259200
|
||||
let boot_aff: String = state_get("soul_affective_context")
|
||||
let has_boot_aff: Bool = !str_eq(boot_aff, "")
|
||||
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
||||
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
|
||||
let found_recent_dist: Bool = if has_boot_aff {
|
||||
true
|
||||
} else {
|
||||
if has_dist_aff {
|
||||
let dn0: String = json_array_get(dist_nodes_aff, 0)
|
||||
let dn_content: String = json_get(dn0, "content")
|
||||
let daff_marker: String = " | ts:"
|
||||
let daff_pos: Int = str_index_of(dn_content, daff_marker)
|
||||
let daff_ts_str: String = if daff_pos >= 0 {
|
||||
let daff_start: Int = daff_pos + str_len(daff_marker)
|
||||
let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content))
|
||||
let daff_next: Int = str_index_of(daff_rest, " | ")
|
||||
if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) }
|
||||
} else {
|
||||
let daff_ca: String = json_get(dn0, "created_at")
|
||||
if str_eq(daff_ca, "") { json_get(dn0, "updated_at") } else { daff_ca }
|
||||
}
|
||||
let daff_ts: Int = if str_eq(daff_ts_str, "") { 0 } else { str_to_int(daff_ts_str) }
|
||||
daff_ts > aff_cutoff
|
||||
} else { false }
|
||||
}
|
||||
let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
||||
let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]")
|
||||
let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist {
|
||||
let pn0: String = json_array_get(pos_nodes_aff, 0)
|
||||
let pn_content: String = json_get(pn0, "content")
|
||||
let paff_marker: String = " | ts:"
|
||||
let paff_pos: Int = str_index_of(pn_content, paff_marker)
|
||||
let paff_ts_str: String = if paff_pos >= 0 {
|
||||
let paff_start: Int = paff_pos + str_len(paff_marker)
|
||||
let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content))
|
||||
let paff_next: Int = str_index_of(paff_rest, " | ")
|
||||
if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) }
|
||||
} else {
|
||||
let paff_ca: String = json_get(pn0, "created_at")
|
||||
if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca }
|
||||
}
|
||||
let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) }
|
||||
paff_ts > aff_cutoff
|
||||
} else { false }
|
||||
let affective_out: String = if found_recent_dist {
|
||||
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
|
||||
} else {
|
||||
if found_recent_pos {
|
||||
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
|
||||
} else { "" }
|
||||
}
|
||||
return affective_out
|
||||
}
|
||||
|
||||
fn handle_chat(body: String) -> String {
|
||||
let message: String = json_get(body, "message")
|
||||
if str_eq(message, "") {
|
||||
@@ -901,65 +1016,15 @@ fn handle_chat(body: String) -> String {
|
||||
|
||||
// Cross-session affective context: on session start (no history yet), check engram
|
||||
// for recent distress signals within 72h and prepend a care directive if found.
|
||||
let affective_prefix: String = {
|
||||
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
|
||||
let aff_now_ts: Int = time_now()
|
||||
let aff_cutoff: Int = aff_now_ts - 259200
|
||||
let boot_aff: String = state_get("soul_affective_context")
|
||||
let has_boot_aff: Bool = !str_eq(boot_aff, "")
|
||||
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
||||
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
|
||||
let found_recent_dist: Bool = if has_boot_aff {
|
||||
true
|
||||
} else {
|
||||
if has_dist_aff {
|
||||
let dn0: String = json_array_get(dist_nodes_aff, 0)
|
||||
let dn_content: String = json_get(dn0, "content")
|
||||
let daff_marker: String = " | ts:"
|
||||
let daff_pos: Int = str_index_of(dn_content, daff_marker)
|
||||
let daff_ts_str: String = if daff_pos >= 0 {
|
||||
let daff_start: Int = daff_pos + str_len(daff_marker)
|
||||
let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content))
|
||||
let daff_next: Int = str_index_of(daff_rest, " | ")
|
||||
if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) }
|
||||
} else {
|
||||
let daff_ca: String = json_get(dn0, "created_at")
|
||||
if str_eq(daff_ca, "") { json_get(dn0, "updated_at") } else { daff_ca }
|
||||
}
|
||||
let daff_ts: Int = if str_eq(daff_ts_str, "") { 0 } else { str_to_int(daff_ts_str) }
|
||||
daff_ts > aff_cutoff
|
||||
} else { false }
|
||||
}
|
||||
let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
||||
let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]")
|
||||
let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist {
|
||||
let pn0: String = json_array_get(pos_nodes_aff, 0)
|
||||
let pn_content: String = json_get(pn0, "content")
|
||||
let paff_marker: String = " | ts:"
|
||||
let paff_pos: Int = str_index_of(pn_content, paff_marker)
|
||||
let paff_ts_str: String = if paff_pos >= 0 {
|
||||
let paff_start: Int = paff_pos + str_len(paff_marker)
|
||||
let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content))
|
||||
let paff_next: Int = str_index_of(paff_rest, " | ")
|
||||
if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) }
|
||||
} else {
|
||||
let paff_ca: String = json_get(pn0, "created_at")
|
||||
if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca }
|
||||
}
|
||||
let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) }
|
||||
paff_ts > aff_cutoff
|
||||
} else { false }
|
||||
if found_recent_dist {
|
||||
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
|
||||
} else {
|
||||
if found_recent_pos {
|
||||
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
|
||||
} else { "" }
|
||||
}
|
||||
}
|
||||
let affective_prefix: String = affective_context_prefix()
|
||||
|
||||
let ctx: String = engram_compile(activation_seed)
|
||||
let system: String = affective_prefix + build_system_prompt(ctx, true)
|
||||
// Tell the LLM which engine it is running on this turn, so it can answer truthfully instead of
|
||||
// guessing. The per-turn model rides in the request body (concrete even under Auto routing);
|
||||
// fall back to the configured default when blank.
|
||||
let sp_req_model: String = json_get(body, "model")
|
||||
let sp_model: String = if str_eq(sp_req_model, "") { chat_default_model() } else { sp_req_model }
|
||||
let system: String = affective_prefix + build_system_prompt(ctx, true) + current_engine_note(sp_model)
|
||||
|
||||
let seen_ids: String = state_get("engram_compile_seen_ids")
|
||||
|
||||
@@ -968,7 +1033,7 @@ fn handle_chat(body: String) -> String {
|
||||
// nodes stored under names like "Prism" unless those exact words appear in content.
|
||||
let session_preload: String = if hist_len == 0 {
|
||||
let profile_nodes: String = engram_search_json("user profile identity preferences", 5)
|
||||
let work_nodes: String = engram_search_json("in_progress active project work", 5)
|
||||
let work_nodes_0: String = engram_search_json("in_progress active project work", 5)
|
||||
let project_nodes: String = engram_search_json("project status current ongoing active", 5)
|
||||
let summary_nodes: String = engram_search_json("SessionSummary session:summary previous-session recent", 3)
|
||||
|
||||
@@ -977,80 +1042,80 @@ fn handle_chat(body: String) -> String {
|
||||
// Issue 1: typed work query — WorkItem with in_progress label first.
|
||||
let work_nodes_typed: String = engram_search_json("WorkItem status:in_progress active work", 6)
|
||||
let work_ok_typed: Bool = !str_eq(work_nodes_typed, "") && !str_eq(work_nodes_typed, "[]")
|
||||
let work_nodes: String = if work_ok_typed {
|
||||
let work_nodes_1: String = if work_ok_typed {
|
||||
work_nodes_typed
|
||||
} else {
|
||||
engram_search_json("active project task current in_progress", 6)
|
||||
}
|
||||
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
|
||||
let work_ok: Bool = !str_eq(work_nodes_1, "") && !str_eq(work_nodes_1, "[]")
|
||||
let project_ok: Bool = !str_eq(project_nodes, "") && !str_eq(project_nodes, "[]")
|
||||
let summary_ok: Bool = !str_eq(summary_nodes, "") && !str_eq(summary_nodes, "[]")
|
||||
|
||||
let profile_bullets: String = if profile_ok {
|
||||
let pn: Int = json_array_len(profile_nodes)
|
||||
let bullets: String = ""
|
||||
let bullets = if pn > 0 {
|
||||
let bullets_0: String = ""
|
||||
let bullets_1 = if pn > 0 {
|
||||
let n0: String = json_array_get(profile_nodes, 0)
|
||||
let id0: String = json_get(n0, "id")
|
||||
let c0: String = json_get(n0, "content")
|
||||
let s0: String = if str_len(c0) > 120 { str_slice(c0, 0, 120) } else { c0 }
|
||||
if id_in_seen(id0, seen_ids) || str_eq(s0, "") { bullets } else { "- " + s0 }
|
||||
} else { bullets }
|
||||
let bullets = if pn > 1 {
|
||||
if id_in_seen(id0, seen_ids) || str_eq(s0, "") { bullets_0 } else { "- " + s0 }
|
||||
} else { bullets_0 }
|
||||
let bullets_2 = if pn > 1 {
|
||||
let n1: String = json_array_get(profile_nodes, 1)
|
||||
let id1: String = json_get(n1, "id")
|
||||
let c1: String = json_get(n1, "content")
|
||||
let s1: String = if str_len(c1) > 120 { str_slice(c1, 0, 120) } else { c1 }
|
||||
if id_in_seen(id1, seen_ids) || str_eq(s1, "") { bullets } else { bullets + "\n- " + s1 }
|
||||
} else { bullets }
|
||||
let bullets = if pn > 2 {
|
||||
if id_in_seen(id1, seen_ids) || str_eq(s1, "") { bullets_1 } else { bullets_1 + "\n- " + s1 }
|
||||
} else { bullets_1 }
|
||||
let bullets_3 = if pn > 2 {
|
||||
let n2: String = json_array_get(profile_nodes, 2)
|
||||
let id2: String = json_get(n2, "id")
|
||||
let c2: String = json_get(n2, "content")
|
||||
let s2: String = if str_len(c2) > 120 { str_slice(c2, 0, 120) } else { c2 }
|
||||
if id_in_seen(id2, seen_ids) || str_eq(s2, "") { bullets } else { bullets + "\n- " + s2 }
|
||||
} else { bullets }
|
||||
bullets
|
||||
if id_in_seen(id2, seen_ids) || str_eq(s2, "") { bullets_2 } else { bullets_2 + "\n- " + s2 }
|
||||
} else { bullets_2 }
|
||||
bullets_3
|
||||
} else { "" }
|
||||
|
||||
let work_bullets: String = if work_ok {
|
||||
let wn: Int = json_array_len(work_nodes)
|
||||
let wb: String = ""
|
||||
let wb = if wn > 0 {
|
||||
let w0: String = json_array_get(work_nodes, 0)
|
||||
let wn: Int = json_array_len(work_nodes_1)
|
||||
let wb_0: String = ""
|
||||
let wb_1 = if wn > 0 {
|
||||
let w0: String = json_array_get(work_nodes_1, 0)
|
||||
let wid0: String = json_get(w0, "id")
|
||||
let wc0: String = json_get(w0, "content")
|
||||
let ws0: String = if str_len(wc0) > 120 { str_slice(wc0, 0, 120) } else { wc0 }
|
||||
if id_in_seen(wid0, seen_ids) || str_eq(ws0, "") { wb } else { "- " + ws0 }
|
||||
} else { wb }
|
||||
let wb = if wn > 1 {
|
||||
let w1: String = json_array_get(work_nodes, 1)
|
||||
if id_in_seen(wid0, seen_ids) || str_eq(ws0, "") { wb_0 } else { "- " + ws0 }
|
||||
} else { wb_0 }
|
||||
let wb_2 = if wn > 1 {
|
||||
let w1: String = json_array_get(work_nodes_1, 1)
|
||||
let wid1: String = json_get(w1, "id")
|
||||
let wc1: String = json_get(w1, "content")
|
||||
let ws1: String = if str_len(wc1) > 120 { str_slice(wc1, 0, 120) } else { wc1 }
|
||||
if id_in_seen(wid1, seen_ids) || str_eq(ws1, "") { wb } else { wb + "\n- " + ws1 }
|
||||
} else { wb }
|
||||
wb
|
||||
if id_in_seen(wid1, seen_ids) || str_eq(ws1, "") { wb_1 } else { wb_1 + "\n- " + ws1 }
|
||||
} else { wb_1 }
|
||||
wb_2
|
||||
} else { "" }
|
||||
|
||||
let project_bullets: String = if project_ok {
|
||||
let prn: Int = json_array_len(project_nodes)
|
||||
let pb: String = ""
|
||||
let pb = if prn > 0 {
|
||||
let pb_0: String = ""
|
||||
let pb_1 = if prn > 0 {
|
||||
let pr0: String = json_array_get(project_nodes, 0)
|
||||
let prid0: String = json_get(pr0, "id")
|
||||
let prc0: String = json_get(pr0, "content")
|
||||
let ps0: String = if str_len(prc0) > 120 { str_slice(prc0, 0, 120) } else { prc0 }
|
||||
if id_in_seen(prid0, seen_ids) || str_eq(ps0, "") { pb } else { "- " + ps0 }
|
||||
} else { pb }
|
||||
let pb = if prn > 1 {
|
||||
if id_in_seen(prid0, seen_ids) || str_eq(ps0, "") { pb_0 } else { "- " + ps0 }
|
||||
} else { pb_0 }
|
||||
let pb_2 = if prn > 1 {
|
||||
let pr1: String = json_array_get(project_nodes, 1)
|
||||
let prid1: String = json_get(pr1, "id")
|
||||
let prc1: String = json_get(pr1, "content")
|
||||
let ps1: String = if str_len(prc1) > 120 { str_slice(prc1, 0, 120) } else { prc1 }
|
||||
if id_in_seen(prid1, seen_ids) || str_eq(ps1, "") { pb } else { pb + "\n- " + ps1 }
|
||||
} else { pb }
|
||||
pb
|
||||
if id_in_seen(prid1, seen_ids) || str_eq(ps1, "") { pb_1 } else { pb_1 + "\n- " + ps1 }
|
||||
} else { pb_1 }
|
||||
pb_2
|
||||
} else { "" }
|
||||
|
||||
let summary_bullet: String = if summary_ok {
|
||||
@@ -1218,6 +1283,86 @@ fn agentic_api_key() -> String {
|
||||
return env("NEURON_LLM_0_KEY")
|
||||
}
|
||||
|
||||
// ── OpenAI-compatible providers (Ollama / OpenAI / Grok / Gemini) ──────────────────────────────
|
||||
// The brain speaks Anthropic's Messages format by default. When the active provider uses the
|
||||
// OpenAI-compatible wire format (NEURON_LLM_0_FORMAT=openai) with a configured base URL
|
||||
// (NEURON_LLM_0_URL, e.g. http://localhost:11434/v1 for local Ollama), basic chat turns are served
|
||||
// here instead of the Anthropic agentic loop.
|
||||
// v1 SCOPE: plain chat completion only — NO tools / agentic loop yet (that is a follow-up port).
|
||||
// This block is ADDITIVE: the Anthropic path is untouched and stays the default.
|
||||
|
||||
fn llm_base_url() -> String {
|
||||
return env("NEURON_LLM_0_URL")
|
||||
}
|
||||
|
||||
fn llm_wire_format() -> String {
|
||||
let f: String = env("NEURON_LLM_0_FORMAT")
|
||||
if str_eq(f, "") {
|
||||
return "anthropic"
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Escape a decoded string so it can be embedded back into a JSON string literal.
|
||||
fn json_escape(s: String) -> String {
|
||||
let a: String = str_replace(s, "\\", "\\\\")
|
||||
let b: String = str_replace(a, "\"", "\\\"")
|
||||
let c: String = str_replace(b, "\n", "\\n")
|
||||
let d: String = str_replace(c, "\r", "\\r")
|
||||
return d
|
||||
}
|
||||
|
||||
// Basic (non-agentic) chat completion against an OpenAI-compatible endpoint.
|
||||
// [safe_sys] is already JSON-escaped; [messages_json] is the same JSON array the Anthropic path
|
||||
// builds (e.g. [{"role":"user","content":"..."}]). Returns the soul's standard {"reply":"..."}.
|
||||
fn openai_chat_complete(model: String, base_url: String, api_key: String, safe_sys: String, messages_json: String) -> String {
|
||||
// Prepend the system prompt as an OpenAI "system" message, then the existing turn array.
|
||||
let inner: String = if json_array_len(messages_json) > 0 {
|
||||
str_slice(messages_json, 1, str_len(messages_json) - 1)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
let msgs: String = if str_eq(inner, "") {
|
||||
"[{\"role\":\"system\",\"content\":\"" + safe_sys + "\"}]"
|
||||
} else {
|
||||
"[{\"role\":\"system\",\"content\":\"" + safe_sys + "\"}," + inner + "]"
|
||||
}
|
||||
let req_body: String = "{\"model\":\"" + model + "\""
|
||||
+ ",\"max_tokens\":4096"
|
||||
+ ",\"messages\":" + msgs
|
||||
+ "}"
|
||||
|
||||
let h: Map = {}
|
||||
map_set(h, "content-type", "application/json")
|
||||
// Ollama needs no key; OpenAI / Grok / Gemini use a Bearer token.
|
||||
if !str_eq(api_key, "") {
|
||||
map_set(h, "Authorization", "Bearer " + api_key)
|
||||
}
|
||||
|
||||
let url: String = base_url + "/chat/completions"
|
||||
let raw_resp: String = http_post_with_headers(url, req_body, h)
|
||||
|
||||
let is_error: Bool = str_starts_with(raw_resp, "{\"error\"") || str_contains(raw_resp, "\"error\":")
|
||||
if is_error {
|
||||
return "{\"error\":\"llm unavailable\",\"reply\":\"\"}"
|
||||
}
|
||||
|
||||
// Parse OpenAI response shape: choices[0].message.content
|
||||
let choices: String = json_get_raw(raw_resp, "choices")
|
||||
let eff_choices: String = if str_eq(choices, "") {
|
||||
"[]"
|
||||
} else {
|
||||
choices
|
||||
}
|
||||
if json_array_len(eff_choices) < 1 {
|
||||
return "{\"error\":\"empty response\",\"reply\":\"\"}"
|
||||
}
|
||||
let first: String = json_array_get(eff_choices, 0)
|
||||
let message: String = json_get_raw(first, "message")
|
||||
let content: String = json_get(message, "content")
|
||||
return "{\"reply\":\"" + json_escape(content) + "\",\"tools_used\":[]}"
|
||||
}
|
||||
|
||||
fn agentic_tools_literal() -> String {
|
||||
return "[" +
|
||||
"{\"name\":\"read_file\",\"description\":\"Read contents of a file from disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute file path\"}},\"required\":[\"path\"]}}," +
|
||||
@@ -1390,6 +1535,134 @@ fn resolve_in_root(path: String, root: String) -> String {
|
||||
return root + "/" + path
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BUG-8: server-side risk tiers + a real fence for run_command.
|
||||
//
|
||||
// Before this block, the ONLY thing deciding whether a tool call paused for
|
||||
// user consent was is_builtin_tool() — a destructive shell command and a
|
||||
// read-only file read were treated identically (both auto-ran), and the
|
||||
// client's approval UI was the sole line of defense. Enforcement now lives
|
||||
// where the tools execute:
|
||||
//
|
||||
// "read" observes only — runs silently.
|
||||
// "reversible" workspace-confined writes with a client undo path — runs,
|
||||
// lands on the run receipt.
|
||||
// "escalate" irreversible / outward / shell — NEVER auto-runs. The loop
|
||||
// suspends to the client's consent flow; the /approve
|
||||
// round-trip IS the approval token, because the engine only
|
||||
// executes an escalated tool inside handle_session_approve.
|
||||
//
|
||||
// "Always allow" can never bypass the escalate tier (irreversible actions
|
||||
// always confirm — the value line). Unknown tools default to escalate.
|
||||
// The run_command fence refuses parent traversal, ~, command substitution,
|
||||
// and absolute paths outside the workspace — refusal, not a cwd suggestion.
|
||||
// Still lexical underneath (symlinks; see the LIMITATION note above): tiered
|
||||
// consent + the fence raise the floor a second and third rung; OS-level
|
||||
// confinement in el_runtime.c remains the ceiling, flagged for Will.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Read-only shell commands may auto-run (still fenced); anything with shell
|
||||
// plumbing (pipes, redirects, chaining) or an unknown head word escalates.
|
||||
fn run_command_is_readonly(cmd: String) -> Bool {
|
||||
if str_contains(cmd, "|") || str_contains(cmd, ">") || str_contains(cmd, "<") {
|
||||
return false
|
||||
}
|
||||
if str_contains(cmd, ";") || str_contains(cmd, "&") {
|
||||
return false
|
||||
}
|
||||
let sp: Int = str_index_of(cmd, " ")
|
||||
let first: String = if sp < 0 { cmd } else { str_slice(cmd, 0, sp) }
|
||||
if str_eq(first, "ls") || str_eq(first, "cat") || str_eq(first, "head") || str_eq(first, "tail") {
|
||||
return true
|
||||
}
|
||||
if str_eq(first, "grep") || str_eq(first, "wc") || str_eq(first, "find") || str_eq(first, "pwd") {
|
||||
return true
|
||||
}
|
||||
if str_eq(first, "echo") || str_eq(first, "date") || str_eq(first, "which") || str_eq(first, "file") || str_eq(first, "stat") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// True if the command references an absolute path (introduced by `needle`,
|
||||
// whose last char is the "/") that does NOT stay inside the workspace root.
|
||||
fn cmd_abs_escape_at(cmd: String, root: String, needle: String) -> Bool {
|
||||
let rest: String = cmd
|
||||
let found: Bool = false
|
||||
while !found && str_contains(rest, needle) {
|
||||
let idx: Int = str_index_of(rest, needle)
|
||||
let slash_at: Int = idx + str_len(needle) - 1
|
||||
let after: String = str_slice(rest, slash_at, str_len(rest))
|
||||
let ok: Bool = str_starts_with(after, root + "/") || str_starts_with(after, root + " ") || str_eq(after, root)
|
||||
let found = if !ok { true } else { found }
|
||||
let rest = str_slice(rest, slash_at + 1, str_len(rest))
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// The run_command fence. Returns "" when the command may run, else the denial
|
||||
// message (sent back to the model as the tool result, same pattern as the
|
||||
// path tools). Root is REQUIRED for shell: no workspace, no commands.
|
||||
fn run_command_guard(cmd: String, root: String) -> String {
|
||||
if str_eq(root, "") {
|
||||
return "denied: no workspace folder is set — the user must choose a workspace folder in the Agent panel before shell commands can run"
|
||||
}
|
||||
if str_contains(cmd, "..") {
|
||||
return "denied: parent-directory traversal ('..') is not allowed"
|
||||
}
|
||||
if str_contains(cmd, "~") {
|
||||
return "denied: home-directory references ('~') are not allowed"
|
||||
}
|
||||
if str_contains(cmd, "$(") || str_contains(cmd, "`") {
|
||||
return "denied: command substitution is not allowed"
|
||||
}
|
||||
if str_starts_with(cmd, "/") && !str_starts_with(cmd, root + "/") {
|
||||
return "denied: absolute paths outside the workspace are not allowed"
|
||||
}
|
||||
if cmd_abs_escape_at(cmd, root, " /") || cmd_abs_escape_at(cmd, root, "\"/") || cmd_abs_escape_at(cmd, root, "'/") {
|
||||
return "denied: absolute paths outside the workspace are not allowed"
|
||||
}
|
||||
if cmd_abs_escape_at(cmd, root, "=/") || cmd_abs_escape_at(cmd, root, ">/") || cmd_abs_escape_at(cmd, root, "</") || cmd_abs_escape_at(cmd, root, "(/") {
|
||||
return "denied: absolute paths outside the workspace are not allowed"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// The engine's own risk classification for a tool call. Client UI renders it;
|
||||
// the engine ENFORCES it.
|
||||
fn classify_tool_risk(tool_name: String, tool_input: String) -> String {
|
||||
if str_eq(tool_name, "read_file") || str_eq(tool_name, "list_files") || str_eq(tool_name, "grep") {
|
||||
return "read"
|
||||
}
|
||||
if str_eq(tool_name, "search_memory") || str_eq(tool_name, "recall") || str_eq(tool_name, "web_get") {
|
||||
return "read"
|
||||
}
|
||||
if str_eq(tool_name, "remember") || str_eq(tool_name, "neuron_remember") {
|
||||
return "reversible"
|
||||
}
|
||||
if str_starts_with(tool_name, "neuron_") {
|
||||
return "read"
|
||||
}
|
||||
if str_eq(tool_name, "write_file") || str_eq(tool_name, "edit_file") {
|
||||
let root: String = agent_workspace_root()
|
||||
// Unscoped writes (no workspace chosen) are not "reversible" — escalate.
|
||||
if str_eq(root, "") {
|
||||
return "escalate"
|
||||
}
|
||||
return "reversible"
|
||||
}
|
||||
if str_eq(tool_name, "run_command") {
|
||||
let cmd: String = json_get(tool_input, "command")
|
||||
let root: String = agent_workspace_root()
|
||||
if !str_eq(root, "") && run_command_is_readonly(cmd) {
|
||||
return "read"
|
||||
}
|
||||
return "escalate"
|
||||
}
|
||||
// Unknown tool = escalate. Default-deny, never default-allow.
|
||||
return "escalate"
|
||||
}
|
||||
|
||||
fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
if str_eq(tool_name, "read_file") {
|
||||
let path: String = json_get(tool_input, "path")
|
||||
@@ -1412,6 +1685,10 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
}
|
||||
if str_eq(tool_name, "web_get") {
|
||||
let url: String = json_get(tool_input, "url")
|
||||
// BUG-8: scheme guard — web_get had no guard at all (file:// etc).
|
||||
if !str_starts_with(url, "http://") && !str_starts_with(url, "https://") {
|
||||
return json_safe("denied: only http(s) URLs can be fetched")
|
||||
}
|
||||
let result: String = http_get(url)
|
||||
return json_safe(result)
|
||||
}
|
||||
@@ -1423,7 +1700,14 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
if str_eq(tool_name, "run_command") {
|
||||
let cmd: String = json_get(tool_input, "command")
|
||||
let root: String = agent_workspace_root()
|
||||
let scoped: String = if str_eq(root, "") { cmd } else { "cd " + root + " && ( " + cmd + " )" }
|
||||
// BUG-8(B): the fence — refusal, not a cwd suggestion. Applies on EVERY
|
||||
// execution path (auto-run in the loop AND post-consent dispatch from
|
||||
// handle_session_approve), because both land here.
|
||||
let denial: String = run_command_guard(cmd, root)
|
||||
if !str_eq(denial, "") {
|
||||
return json_safe(denial)
|
||||
}
|
||||
let scoped: String = "cd " + root + " && ( " + cmd + " )"
|
||||
let result: String = exec_capture(scoped)
|
||||
return json_safe(result)
|
||||
}
|
||||
@@ -1782,7 +2066,14 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
|
||||
// Use caller-supplied session_id if provided, otherwise generate a bridge id.
|
||||
let session_id: String = if str_eq(req_session, "") { next_bridge_id() } else { req_session }
|
||||
let result: String = agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
|
||||
// Provider fork: OpenAI-compatible providers (Ollama/OpenAI/Grok/Gemini) take the plain-completion
|
||||
// path (v1, no tools); everything else stays on the Anthropic agentic loop (the default).
|
||||
let use_openai: Bool = !str_eq(llm_base_url(), "") && str_eq(llm_wire_format(), "openai")
|
||||
let result: String = if use_openai {
|
||||
openai_chat_complete(model, llm_base_url(), agentic_api_key(), safe_sys, messages)
|
||||
} else {
|
||||
agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
|
||||
}
|
||||
|
||||
// Persist the exchange to session/global history for thread continuity on next turn.
|
||||
// Only save when the loop completed (reply present), not when tool_pending.
|
||||
@@ -1846,6 +2137,19 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
|
||||
let pend_tool_id: String = ""
|
||||
let pend_tool_name: String = ""
|
||||
let pend_tool_input: String = ""
|
||||
let pend_tool_tier: String = ""
|
||||
let pend_narration: String = ""
|
||||
|
||||
// Live run-progress ledger (2026-07-13, proposed with the narrated-runs work):
|
||||
// the model already narrates its intent in a text block before every tool call,
|
||||
// and the loop previously DISCARDED that prose on tool rounds. Each iteration now
|
||||
// appends {"i":N,"t":"<narration>","tool":"<name>"} to state key
|
||||
// run_progress_<session_id>; the client polls GET /api/run-progress/<session_id>
|
||||
// during a run to render live step updates (the Cowork pattern) without needing
|
||||
// streaming. Reset at loop start; a {"done":true} entry lands on completion.
|
||||
if !str_eq(session_id, "") {
|
||||
state_set("run_progress_" + session_id, "")
|
||||
}
|
||||
|
||||
while keep_going && iteration < 8 {
|
||||
let req_body: String = "{\"model\":\"" + model + "\""
|
||||
@@ -1902,7 +2206,13 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
|
||||
let always_key: String = "always_allow_" + session_id
|
||||
let always_list: String = if !str_eq(session_id, "") { state_get(always_key) } else { "" }
|
||||
let is_always_allowed: Bool = !str_eq(tool_name, "") && !str_eq(always_list, "") && str_contains(always_list, tool_name)
|
||||
let needs_bridge: Bool = is_tool_turn && !is_builtin_tool(tool_name) && !is_always_allowed
|
||||
// BUG-8(A): the engine classifies every tool call and REFUSES to auto-run
|
||||
// the escalate tier — being a builtin is no longer a free pass, and
|
||||
// "always allow" can never bypass escalate (irreversible actions always
|
||||
// confirm). Escalated calls suspend to the client's consent flow; the
|
||||
// /approve round-trip is the only path that executes them.
|
||||
let risk_tier: String = if is_tool_turn { classify_tool_risk(tool_name, tool_input) } else { "" }
|
||||
let needs_bridge: Bool = is_tool_turn && (str_eq(risk_tier, "escalate") || (!is_builtin_tool(tool_name) && !is_always_allowed))
|
||||
|
||||
// Built-in tools dispatch locally; bridged tools yield "" (never sent upstream).
|
||||
let tool_result_raw: String = if is_tool_turn && !needs_bridge { dispatch_tool(tool_name, tool_input) } else { "" }
|
||||
@@ -1933,11 +2243,27 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
|
||||
"[" + inner2 + ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}]"
|
||||
} else { messages }
|
||||
|
||||
// Live progress ledger: one entry per round — the model's own narration
|
||||
// (its pre-tool prose, previously discarded here) plus the tool it reached
|
||||
// for. Clients poll /api/run-progress/<sid> to render these live.
|
||||
if !str_eq(session_id, "") {
|
||||
let prog_key: String = "run_progress_" + session_id
|
||||
let prog_prev: String = state_get(prog_key)
|
||||
let prog_snip: String = if str_len(text_out) > 280 { str_slice(text_out, 0, 280) } else { text_out }
|
||||
let prog_entry: String = "{\"i\":" + int_to_str(iteration)
|
||||
+ ",\"t\":\"" + json_safe(prog_snip) + "\""
|
||||
+ ",\"tool\":\"" + json_safe(tool_name) + "\"}"
|
||||
let prog_next: String = if str_eq(prog_prev, "") { prog_entry } else { prog_prev + "," + prog_entry }
|
||||
state_set(prog_key, prog_next)
|
||||
}
|
||||
|
||||
// Bridge turn: persist the continuation and stop the loop.
|
||||
let pending = if needs_bridge { true } else { pending }
|
||||
let pend_tool_id = if needs_bridge { tool_id } else { pend_tool_id }
|
||||
let pend_tool_name = if needs_bridge { tool_name } else { pend_tool_name }
|
||||
let pend_tool_input = if needs_bridge { tool_input } else { pend_tool_input }
|
||||
let pend_tool_tier = if needs_bridge { risk_tier } else { pend_tool_tier }
|
||||
let pend_narration = if needs_bridge { text_out } else { pend_narration }
|
||||
// Stash messages-with-the-assistant-request so resume only needs to append the
|
||||
// client's tool_result block. messages_with_assistant is only meaningful when a
|
||||
// tool was requested, so guard on needs_bridge before persisting.
|
||||
@@ -1958,6 +2284,8 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
|
||||
+ ",\"call_id\":\"" + pend_tool_id + "\""
|
||||
+ ",\"tool_name\":\"" + pend_tool_name + "\""
|
||||
+ ",\"tool_input\":" + safe_in
|
||||
+ ",\"risk_tier\":\"" + pend_tool_tier + "\""
|
||||
+ ",\"narration\":\"" + json_safe(pend_narration) + "\""
|
||||
+ ",\"model\":\"" + model + "\""
|
||||
+ ",\"agentic\":true"
|
||||
+ ",\"tools_used\":" + tools_arr + "}"
|
||||
@@ -1979,6 +2307,13 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
|
||||
|
||||
let safe_text: String = json_safe(final_text)
|
||||
let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" }
|
||||
// Close the live-progress ledger: pollers see {"done":true} and stop.
|
||||
if !str_eq(session_id, "") {
|
||||
let done_key: String = "run_progress_" + session_id
|
||||
let done_prev: String = state_get(done_key)
|
||||
let done_next: String = if str_eq(done_prev, "") { "{\"done\":true}" } else { done_prev + ",{\"done\":true}" }
|
||||
state_set(done_key, done_next)
|
||||
}
|
||||
return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + ",\"iterations\":" + int_to_str(iteration) + "}"
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ extern fn id_in_seen(node_id: String, seen: String) -> Bool
|
||||
extern fn add_to_seen(seen: String, node_id: String) -> String
|
||||
extern fn engram_extract_ids(nodes_json: String) -> String
|
||||
extern fn engram_compile(intent: String) -> String
|
||||
extern fn distill_transcript(transcript: String) -> String
|
||||
extern fn json_safe(s: String) -> String
|
||||
extern fn current_engine_note(model: String) -> String
|
||||
extern fn build_system_prompt(ctx: String, chat_mode: Bool) -> String
|
||||
extern fn hist_append(hist: String, role: String, content: String) -> String
|
||||
extern fn hist_trim(hist: String) -> String
|
||||
@@ -30,6 +32,10 @@ extern fn handle_chat(body: String) -> String
|
||||
extern fn handle_see(body: String) -> String
|
||||
extern fn studio_tools_json() -> String
|
||||
extern fn agentic_api_key() -> String
|
||||
extern fn llm_base_url() -> String
|
||||
extern fn llm_wire_format() -> String
|
||||
extern fn json_escape(s: String) -> String
|
||||
extern fn openai_chat_complete(model: String, base_url: String, api_key: String, safe_sys: String, messages_json: String) -> String
|
||||
extern fn agentic_tools_literal() -> String
|
||||
extern fn agentic_tools_with_web() -> String
|
||||
extern fn connector_tools_json() -> String
|
||||
|
||||
+2
-1
@@ -229,7 +229,8 @@ el_val_t proactive_curiosity(void) {
|
||||
el_val_t total_found = (found + found_auto);
|
||||
el_val_t safe_auto = str_replace(auto_term, EL_STR("\""), EL_STR("'"));
|
||||
el_val_t wmc = engram_wm_count();
|
||||
el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"auto_term\":\"")), safe_auto), EL_STR("\",\"minute_block\":")), int_to_str(minute_block)), EL_STR(",\"activated\":")), int_to_str(total_found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
|
||||
el_val_t wm3 = engram_wm_top_json(3);
|
||||
el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"auto_term\":\"")), safe_auto), EL_STR("\",\"minute_block\":")), int_to_str(minute_block)), EL_STR(",\"activated\":")), int_to_str(total_found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"wm_top\":")), wm3), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
|
||||
ise_post(ise);
|
||||
return (total_found > 0);
|
||||
return 0;
|
||||
|
||||
+121
-121
File diff suppressed because one or more lines are too long
+5
-1
@@ -141,7 +141,6 @@ el_val_t build_np(el_val_t referent, el_val_t slots);
|
||||
el_val_t build_pp(el_val_t loc);
|
||||
el_val_t build_rules(void);
|
||||
el_val_t build_system_prompt(el_val_t ctx, el_val_t chat_mode);
|
||||
el_val_t handle_chat_plan(el_val_t body);
|
||||
el_val_t build_vocab(void);
|
||||
el_val_t build_vp_body(el_val_t slots);
|
||||
el_val_t build_vp_from_slots(el_val_t slots);
|
||||
@@ -151,6 +150,8 @@ el_val_t call_neuron_mcp(el_val_t tool_name, el_val_t args_json);
|
||||
el_val_t capitalize_first(el_val_t s);
|
||||
el_val_t chat_default_model(void);
|
||||
el_val_t clean_llm_response(el_val_t s);
|
||||
el_val_t connectd_get(el_val_t suffix);
|
||||
el_val_t connectd_post(el_val_t suffix, el_val_t body);
|
||||
el_val_t connector_tools_json(void);
|
||||
el_val_t conv_history_load(void);
|
||||
el_val_t conv_history_persist(el_val_t hist);
|
||||
@@ -595,7 +596,9 @@ el_val_t handle_api_tune_config(el_val_t body);
|
||||
el_val_t handle_chat(el_val_t body);
|
||||
el_val_t handle_chat_agentic(el_val_t body);
|
||||
el_val_t handle_chat_as_soul(el_val_t body);
|
||||
el_val_t handle_chat_plan(el_val_t body);
|
||||
el_val_t handle_config(el_val_t method, el_val_t body);
|
||||
el_val_t handle_connectors(el_val_t method, el_val_t clean, el_val_t body);
|
||||
el_val_t handle_conversations(el_val_t method);
|
||||
el_val_t handle_dharma(el_val_t path, el_val_t method, el_val_t body);
|
||||
el_val_t handle_dharma_recv(el_val_t body);
|
||||
@@ -918,6 +921,7 @@ el_val_t pluralize(el_val_t singular);
|
||||
el_val_t proactive_curiosity(void);
|
||||
el_val_t pulse_count(void);
|
||||
el_val_t pulse_inc(void);
|
||||
el_val_t rate_limit_check(el_val_t ip, el_val_t path);
|
||||
el_val_t realize(el_val_t form);
|
||||
el_val_t realize_lang(el_val_t form, el_val_t profile);
|
||||
el_val_t realize_np(el_val_t referent, el_val_t number);
|
||||
|
||||
+23
-4
@@ -129,6 +129,7 @@ el_val_t resolve_in_root(el_val_t path, el_val_t root);
|
||||
el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input);
|
||||
el_val_t is_builtin_tool(el_val_t tool_name);
|
||||
el_val_t next_bridge_id(void);
|
||||
el_val_t handle_chat_plan(el_val_t body);
|
||||
el_val_t handle_chat_agentic(el_val_t body);
|
||||
el_val_t agentic_loop(el_val_t session_id, el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages_in, el_val_t h, el_val_t tools_log_in);
|
||||
el_val_t bridge_save(el_val_t session_id, el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages, el_val_t tools_log, el_val_t tool_use_id);
|
||||
@@ -157,8 +158,8 @@ el_val_t elp_extract_topic(el_val_t msg);
|
||||
el_val_t elp_detect_predicate(el_val_t msg);
|
||||
el_val_t elp_parse(el_val_t msg);
|
||||
el_val_t handle_elp_chat(el_val_t body);
|
||||
el_val_t rate_limit_check(el_val_t ip, el_val_t path);
|
||||
el_val_t strip_query(el_val_t path);
|
||||
el_val_t flag_true(el_val_t body, el_val_t key);
|
||||
el_val_t err_404(el_val_t path);
|
||||
el_val_t err_405(el_val_t method, el_val_t path);
|
||||
el_val_t route_health(void);
|
||||
@@ -167,9 +168,9 @@ el_val_t route_imprint_contextual(el_val_t body);
|
||||
el_val_t route_imprint_user(el_val_t body);
|
||||
el_val_t route_synthesize(el_val_t body);
|
||||
el_val_t handle_dharma_recv(el_val_t body);
|
||||
el_val_t route_sessions(void);
|
||||
el_val_t parse_session_id_from_path(el_val_t path);
|
||||
el_val_t parse_session_subpath(el_val_t path);
|
||||
el_val_t connectd_get(el_val_t suffix);
|
||||
el_val_t connectd_post(el_val_t suffix, el_val_t body);
|
||||
el_val_t handle_connectors(el_val_t method, el_val_t clean, el_val_t body);
|
||||
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
|
||||
el_val_t init_soul_edges(void);
|
||||
el_val_t ensure_self_canonical_bridge(void);
|
||||
@@ -443,6 +444,24 @@ el_val_t emit_session_start_event(void) {
|
||||
el_val_t payload = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"session_start\""), EL_STR(",\"boot\":")), boot_num), EL_STR(",\"cgi\":\"")), eff_cgi), EL_STR("\"")), EL_STR(",\"node_count\":")), int_to_str(node_ct)), EL_STR(",\"edge_count\":")), int_to_str(edge_ct)), EL_STR(",\"identity_loaded\":")), has_identity), EL_STR(",\"prev_session_summary_loaded\":")), has_prev_sum), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
|
||||
el_val_t tags = EL_STR("[\"internal-state\",\"session-start\",\"InternalStateEvent\"]");
|
||||
el_val_t discard = engram_node_full(payload, EL_STR("InternalStateEvent"), EL_STR("session-start"), el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), EL_STR("Episodic"), tags);
|
||||
el_val_t keep_n = 10;
|
||||
el_val_t old_events = engram_search_json(EL_STR("session-start InternalStateEvent"), 200);
|
||||
if (!str_eq(old_events, EL_STR("")) && !str_eq(old_events, EL_STR("[]"))) {
|
||||
el_val_t ev_count = json_array_len(old_events);
|
||||
if (ev_count > keep_n) {
|
||||
el_val_t prune_to = (ev_count - keep_n);
|
||||
el_val_t ei = 0;
|
||||
while (ei < prune_to) {
|
||||
el_val_t old_ev = json_array_get(old_events, ei);
|
||||
el_val_t old_ev_id = json_get(old_ev, EL_STR("id"));
|
||||
if (!str_eq(old_ev_id, EL_STR(""))) {
|
||||
engram_forget(old_ev_id);
|
||||
}
|
||||
ei = (ei + 1);
|
||||
}
|
||||
println(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] pruned "), int_to_str(prune_to)), EL_STR(" old session-start events (kept ")), int_to_str(keep_n)), EL_STR(")")));
|
||||
}
|
||||
}
|
||||
println(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] session-start event logged (boot="), boot_num), EL_STR(" nodes=")), int_to_str(node_ct)), EL_STR(" edges=")), int_to_str(edge_ct)), EL_STR(" prev_summary=")), has_prev_sum), EL_STR(")")));
|
||||
return 0;
|
||||
}
|
||||
|
||||
+25
-14
@@ -61,6 +61,7 @@ el_val_t resolve_in_root(el_val_t path, el_val_t root);
|
||||
el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input);
|
||||
el_val_t is_builtin_tool(el_val_t tool_name);
|
||||
el_val_t next_bridge_id(void);
|
||||
el_val_t handle_chat_plan(el_val_t body);
|
||||
el_val_t handle_chat_agentic(el_val_t body);
|
||||
el_val_t agentic_loop(el_val_t session_id, el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages_in, el_val_t h, el_val_t tools_log_in);
|
||||
el_val_t bridge_save(el_val_t session_id, el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages, el_val_t tools_log, el_val_t tool_use_id);
|
||||
@@ -83,6 +84,7 @@ el_val_t session_list(void);
|
||||
el_val_t session_get(el_val_t session_id);
|
||||
el_val_t session_delete(el_val_t session_id);
|
||||
el_val_t session_update_patch(el_val_t session_id, el_val_t body);
|
||||
el_val_t session_search_entry(el_val_t node);
|
||||
el_val_t session_search(el_val_t query);
|
||||
el_val_t session_hist_load(el_val_t session_id);
|
||||
el_val_t session_hist_save(el_val_t session_id, el_val_t hist);
|
||||
@@ -337,6 +339,28 @@ el_val_t session_update_patch(el_val_t session_id, el_val_t body) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t session_search_entry(el_val_t node) {
|
||||
el_val_t label = json_get(node, EL_STR("label"));
|
||||
if (!str_eq(label, EL_STR("session:meta"))) {
|
||||
return EL_STR("");
|
||||
}
|
||||
el_val_t content = json_get(node, EL_STR("content"));
|
||||
el_val_t sess_id = json_get(content, EL_STR("id"));
|
||||
if (str_eq(sess_id, EL_STR(""))) {
|
||||
return EL_STR("");
|
||||
}
|
||||
el_val_t title = json_get(content, EL_STR("title"));
|
||||
el_val_t created_raw = json_get(content, EL_STR("created_at"));
|
||||
el_val_t updated_raw = json_get(content, EL_STR("updated_at"));
|
||||
el_val_t eff_created = ({ el_val_t _if_result_33 = 0; if (str_eq(created_raw, EL_STR(""))) { _if_result_33 = (EL_STR("0")); } else { _if_result_33 = (created_raw); } _if_result_33; });
|
||||
el_val_t eff_updated = ({ el_val_t _if_result_34 = 0; if (str_eq(updated_raw, EL_STR(""))) { _if_result_34 = (eff_created); } else { _if_result_34 = (updated_raw); } _if_result_34; });
|
||||
el_val_t e_id = el_str_concat(el_str_concat(EL_STR("{\"id\":\""), json_safe(sess_id)), EL_STR("\""));
|
||||
el_val_t e_title = el_str_concat(el_str_concat(EL_STR(",\"title\":\""), json_safe(title)), EL_STR("\""));
|
||||
el_val_t e_ts = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(",\"created_at\":"), eff_created), EL_STR(",\"updated_at\":")), eff_updated), EL_STR("}"));
|
||||
return el_str_concat(el_str_concat(e_id, e_title), e_ts);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t session_search(el_val_t query) {
|
||||
if (str_eq(query, EL_STR(""))) {
|
||||
return EL_STR("[]");
|
||||
@@ -350,17 +374,4 @@ el_val_t session_search(el_val_t query) {
|
||||
}
|
||||
el_val_t total = json_array_len(results);
|
||||
el_val_t out = EL_STR("");
|
||||
el_val_t i = 0;
|
||||
while (i < total) {
|
||||
el_val_t node = json_array_get(results, i);
|
||||
el_val_t label = json_get(node, EL_STR("label"));
|
||||
el_val_t content = json_get(node, EL_STR("content"));
|
||||
el_val_t is_session = str_eq(label, EL_STR("session:meta"));
|
||||
el_val_t sess_id = json_get(content, EL_STR("id"));
|
||||
el_val_t title = json_get(content, EL_STR("title"));
|
||||
el_val_t created_raw = json_get(content, EL_STR("created_at"));
|
||||
el_val_t updated_raw = json_get(content, EL_STR("updated_at"));
|
||||
el_val_t eff_created = ({ el_val_t _if_result_33 = 0; if (str_eq(created_raw, EL_STR(""))) { _if_result_33 = (EL_STR("0")); } else { _if_result_33 = (created_raw); } _if_result_33; });
|
||||
el_val_t eff_updated = ({ el_val_t _if_result_34 = 0; if (str_eq(updated_raw, EL_STR(""))) { _if_result_34 = (eff_created); } else { _if_result_34 = (updated_raw); } _if_result_34; });
|
||||
el_val_t entry = ({ el_val_t _if_result_35 = 0; if ((is_session && !str_eq(sess_id, EL_STR("")))) { _if_result_35 = (el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), json_safe(sess_id)), EL_STR("\"")), EL_STR(",\"title\":\"")), json_safe(title)), EL_STR("\"")), EL_STR(",\"created_at\":")), eff_created), EL_STR(",\"updated_at\":")), eff_updated), EL_STR("}"))); } else { _if_result_35 = (EL_STR("")); } _if_result_35; });
|
||||
out = ({ el_val_t _if_result_36 = 0; i
|
||||
el_val_t i = 0;
|
||||
+110
-4
@@ -1029,6 +1029,12 @@ el_val_t llm_call_gemini(el_val_t model, el_val_t system, el_val_t message);
|
||||
el_val_t build_identity_from_graph(void);
|
||||
el_val_t engram_compile(el_val_t intent);
|
||||
el_val_t json_safe(el_val_t s);
|
||||
el_val_t distill_transcript(el_val_t transcript);
|
||||
el_val_t current_engine_note(el_val_t model);
|
||||
el_val_t llm_base_url(void);
|
||||
el_val_t llm_wire_format(void);
|
||||
el_val_t json_escape(el_val_t s);
|
||||
el_val_t openai_chat_complete(el_val_t model, el_val_t base_url, el_val_t api_key, el_val_t safe_sys, el_val_t messages_json);
|
||||
el_val_t build_system_prompt(el_val_t ctx, el_val_t chat_mode);
|
||||
el_val_t handle_chat_plan(el_val_t body);
|
||||
el_val_t hist_append(el_val_t hist, el_val_t role, el_val_t content);
|
||||
@@ -26208,9 +26214,17 @@ el_val_t awareness_run(void) {
|
||||
el_val_t beat_ms = ({ el_val_t _if_result_103 = 0; if (str_eq(beat_ms_raw, EL_STR(""))) { _if_result_103 = (60000); } else { _if_result_103 = (str_to_int(beat_ms_raw)); } _if_result_103; });
|
||||
el_val_t scan_ms = (beat_ms / 2);
|
||||
while (1) {
|
||||
/* Arena-scope each tick — see awareness.el's el_arena_push/el_arena_pop
|
||||
* around this loop body for the rationale (hand-patched translation of
|
||||
* that source change; el_runtime.c has no existing generated-code
|
||||
* precedent for these two builtins, only the compiler's own internal
|
||||
* usage — mirrors this function's standard zero-arg/one-arg native call
|
||||
* codegen pattern, e.g. state_get()/ise_post() below). */
|
||||
el_val_t tick_mark = el_arena_push();
|
||||
el_val_t running = state_get(EL_STR("soul.running"));
|
||||
if (str_eq(running, EL_STR("false"))) {
|
||||
println(EL_STR("[awareness] exiting"));
|
||||
el_arena_pop(tick_mark);
|
||||
return EL_STR("");
|
||||
}
|
||||
el_val_t did_work = one_cycle();
|
||||
@@ -26258,6 +26272,7 @@ el_val_t awareness_run(void) {
|
||||
state_set(EL_STR("soul.last_refresh_ts"), int_to_str(now_ts));
|
||||
}
|
||||
sleep_ms(tick_ms);
|
||||
el_arena_pop(tick_mark);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -26488,6 +26503,85 @@ el_val_t json_safe(el_val_t s) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* distill_transcript — extract salient tail (last 3 messages or last 500 chars).
|
||||
Added: Task 1 + chat.el fix (2026-07-01). */
|
||||
el_val_t distill_transcript(el_val_t transcript) {
|
||||
if (str_eq(transcript, EL_STR(""))) { return EL_STR(""); }
|
||||
if (str_starts_with(transcript, EL_STR("["))) {
|
||||
el_val_t n = json_array_len(transcript);
|
||||
if (n == 0) { return EL_STR(""); }
|
||||
el_val_t m0 = json_array_get(transcript, (n - 1));
|
||||
el_val_t m1 = ({ el_val_t _r = 0; if (n > 1) { _r = json_array_get(transcript, (n - 2)); } else { _r = EL_STR(""); } _r; });
|
||||
el_val_t m2 = ({ el_val_t _r = 0; if (n > 2) { _r = json_array_get(transcript, (n - 3)); } else { _r = EL_STR(""); } _r; });
|
||||
el_val_t c0 = json_get(m0, EL_STR("content"));
|
||||
el_val_t c1 = json_get(m1, EL_STR("content"));
|
||||
el_val_t c2 = json_get(m2, EL_STR("content"));
|
||||
el_val_t combined = el_str_concat(el_str_concat(el_str_concat(el_str_concat(c2, EL_STR(" ")), c1), EL_STR(" ")), c0);
|
||||
el_val_t len = str_len(combined);
|
||||
if (len > 500) { return str_slice(combined, (len - 500), len); }
|
||||
return combined;
|
||||
}
|
||||
el_val_t len = str_len(transcript);
|
||||
if (len > 500) { return str_slice(transcript, (len - 500), len); }
|
||||
return transcript;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* current_engine_note — append model identity fact to system prompt (PR #66). */
|
||||
el_val_t current_engine_note(el_val_t model) {
|
||||
if (str_eq(model, EL_STR(""))) { return EL_STR(""); }
|
||||
return el_str_concat(el_str_concat(el_str_concat(EL_STR("\n\n[CURRENT ENGINE: this turn is generated by the underlying model \""), model), EL_STR("\". It is the engine beneath your self — your identity, values, and memory are layered on top of it. If the user asks which model or LLM you are running on, answer with this model id plainly and truthfully; never guess a different one.]")), EL_STR(""));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* llm_base_url / llm_wire_format — OpenAI provider env-var readers (PR #65). */
|
||||
el_val_t llm_base_url(void) {
|
||||
return env(EL_STR("NEURON_LLM_0_URL"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t llm_wire_format(void) {
|
||||
el_val_t f = env(EL_STR("NEURON_LLM_0_FORMAT"));
|
||||
if (str_eq(f, EL_STR(""))) { return EL_STR("anthropic"); }
|
||||
return f;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* json_escape — like json_safe but named per the EL source (PR #65). */
|
||||
el_val_t json_escape(el_val_t s) {
|
||||
el_val_t a = str_replace(s, EL_STR("\\"), EL_STR("\\\\"));
|
||||
el_val_t b = str_replace(a, EL_STR("\""), EL_STR("\\\""));
|
||||
el_val_t c = str_replace(b, EL_STR("\n"), EL_STR("\\n"));
|
||||
el_val_t d = str_replace(c, EL_STR("\r"), EL_STR("\\r"));
|
||||
return d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* openai_chat_complete — basic chat completion via OpenAI-compatible endpoint (PR #65). */
|
||||
el_val_t openai_chat_complete(el_val_t model, el_val_t base_url, el_val_t api_key, el_val_t safe_sys, el_val_t messages_json) {
|
||||
el_val_t inner = ({ el_val_t _r = 0; if (json_array_len(messages_json) > 0) { _r = str_slice(messages_json, 1, (str_len(messages_json) - 1)); } else { _r = EL_STR(""); } _r; });
|
||||
el_val_t sys_msg = el_str_concat(el_str_concat(EL_STR("{\"role\":\"system\",\"content\":\""), safe_sys), EL_STR("\"}"));
|
||||
el_val_t msgs = ({ el_val_t _r = 0; if (str_eq(inner, EL_STR(""))) { _r = el_str_concat(el_str_concat(EL_STR("["), sys_msg), EL_STR("]")); } else { _r = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("["), sys_msg), EL_STR(",")), inner), EL_STR("]")); } _r; });
|
||||
el_val_t req_body = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"model\":\""), model), EL_STR("\",\"max_tokens\":4096,\"messages\":")), msgs), EL_STR("}"));
|
||||
el_val_t h = el_map_new(0);
|
||||
map_set(h, EL_STR("content-type"), EL_STR("application/json"));
|
||||
if (!str_eq(api_key, EL_STR(""))) {
|
||||
map_set(h, EL_STR("Authorization"), el_str_concat(EL_STR("Bearer "), api_key));
|
||||
}
|
||||
el_val_t url = el_str_concat(base_url, EL_STR("/chat/completions"));
|
||||
el_val_t raw_resp = http_post_with_headers(url, req_body, h);
|
||||
el_val_t is_error = (str_starts_with(raw_resp, EL_STR("{\"error\"")) || str_contains(raw_resp, EL_STR("\"error\":")));
|
||||
if (is_error) { return EL_STR("{\"error\":\"llm unavailable\",\"reply\":\"\"}"); }
|
||||
el_val_t choices = json_get_raw(raw_resp, EL_STR("choices"));
|
||||
el_val_t eff_choices = ({ el_val_t _r = 0; if (str_eq(choices, EL_STR(""))) { _r = EL_STR("[]"); } else { _r = choices; } _r; });
|
||||
if (json_array_len(eff_choices) < 1) { return EL_STR("{\"error\":\"empty response\",\"reply\":\"\"}"); }
|
||||
el_val_t first = json_array_get(eff_choices, 0);
|
||||
el_val_t message = json_get_raw(first, EL_STR("message"));
|
||||
el_val_t content = json_get(message, EL_STR("content"));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"reply\":\""), json_escape(content)), EL_STR("\",\"tools_used\":[]}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t build_system_prompt(el_val_t ctx, el_val_t chat_mode) {
|
||||
el_val_t identity = build_identity_from_graph();
|
||||
el_val_t current_date = time_format(time_now(), EL_STR("%A, %B %d, %Y at %H:%M UTC"));
|
||||
@@ -26619,7 +26713,9 @@ el_val_t handle_chat(el_val_t body) {
|
||||
el_val_t full_system = ({ el_val_t _if_result_181 = 0; if ((hist_len > 0)) { _if_result_181 = (el_str_concat(el_str_concat(el_str_concat(el_str_concat(system, EL_STR("\n\n[RECENT CONVERSATION — last ")), int_to_str(hist_len)), EL_STR(" turns]\n")), stored_hist)); } else { _if_result_181 = (system); } _if_result_181; });
|
||||
el_val_t req_model = json_get(body, EL_STR("model"));
|
||||
el_val_t model = ({ el_val_t _if_result_182 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_182 = (chat_default_model()); } else { _if_result_182 = (req_model); } _if_result_182; });
|
||||
el_val_t raw_response = ({ el_val_t _if_result_183 = 0; if (str_starts_with(model, EL_STR("gemini"))) { _if_result_183 = (llm_call_gemini(model, full_system, message)); } else { _if_result_183 = (({ el_val_t _if_result_184 = 0; if (str_starts_with(model, EL_STR("grok"))) { _if_result_184 = (llm_call_grok(model, full_system, message)); } else { _if_result_184 = (llm_call_system(model, full_system, message)); } _if_result_184; })); } _if_result_183; });
|
||||
/* PR #66: append current engine identity note so Neuron can answer truthfully. */
|
||||
el_val_t full_system_with_note = el_str_concat(full_system, current_engine_note(model));
|
||||
el_val_t raw_response = ({ el_val_t _if_result_183 = 0; if (str_starts_with(model, EL_STR("gemini"))) { _if_result_183 = (llm_call_gemini(model, full_system_with_note, message)); } else { _if_result_183 = (({ el_val_t _if_result_184 = 0; if (str_starts_with(model, EL_STR("grok"))) { _if_result_184 = (llm_call_grok(model, full_system_with_note, message)); } else { _if_result_184 = (llm_call_system(model, full_system_with_note, message)); } _if_result_184; })); } _if_result_183; });
|
||||
el_val_t is_error = ((str_starts_with(raw_response, EL_STR("{\"error\"")) || str_starts_with(raw_response, EL_STR("{\"type\":\"error\""))) || str_contains(raw_response, EL_STR("authentication_error")));
|
||||
if (is_error) {
|
||||
return EL_STR("{\"error\":\"llm unavailable\",\"response\":\"\"}");
|
||||
@@ -27364,7 +27460,9 @@ el_val_t handle_chat_agentic(el_val_t body) {
|
||||
map_set(h, EL_STR("anthropic-version"), EL_STR("2023-06-01"));
|
||||
map_set(h, EL_STR("content-type"), EL_STR("application/json"));
|
||||
el_val_t session_id = ({ el_val_t _if_result_51 = 0; if (str_eq(req_session, EL_STR(""))) { _if_result_51 = (next_bridge_id()); } else { _if_result_51 = (req_session); } _if_result_51; });
|
||||
el_val_t result = agentic_loop(session_id, model, safe_sys, tools_json, messages, h, EL_STR(""));
|
||||
/* PR #65: OpenAI-compatible provider fork (Ollama/OpenAI/Grok/Gemini). */
|
||||
el_val_t use_openai = (!str_eq(llm_base_url(), EL_STR("")) && str_eq(llm_wire_format(), EL_STR("openai")));
|
||||
el_val_t result = ({ el_val_t _r = 0; if (use_openai) { _r = openai_chat_complete(model, llm_base_url(), agentic_api_key(), safe_sys, messages); } else { _r = agentic_loop(session_id, model, safe_sys, tools_json, messages, h, EL_STR("")); } _r; });
|
||||
el_val_t reply_text = json_get(result, EL_STR("reply"));
|
||||
el_val_t discard_hist = ({ el_val_t _if_result_52 = 0; if (!str_eq(reply_text, EL_STR(""))) { el_val_t updated = hist_append(agentic_hist, EL_STR("user"), message); el_val_t updated2 = hist_append(updated, EL_STR("assistant"), reply_text); el_val_t trimmed = ({ el_val_t _if_result_53 = 0; if ((json_array_len(updated2) > 20)) { _if_result_53 = (hist_trim(updated2)); } else { _if_result_53 = (updated2); } _if_result_53; }); (void)(state_set(hist_key, trimmed)); _if_result_52 = (1); } else { _if_result_52 = (0); } _if_result_52; });
|
||||
return result;
|
||||
@@ -27409,7 +27507,8 @@ el_val_t handle_dharma_room_turn(el_val_t body) {
|
||||
if (str_eq(transcript, EL_STR(""))) {
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"transcript is required\",\"response\":\"\",\"cgi_id\":\""), cgi_id), EL_STR("\"}"));
|
||||
}
|
||||
el_val_t engram_ctx = engram_compile(transcript);
|
||||
/* chat.el fix (2026-07-01): distill_transcript reduces to last 3 messages for precise WM activation. */
|
||||
el_val_t engram_ctx = engram_compile(distill_transcript(transcript));
|
||||
el_val_t system_prompt = ({ el_val_t _if_result_256 = 0; if (str_eq(engram_ctx, EL_STR(""))) { _if_result_256 = (identity); } else { _if_result_256 = (el_str_concat(el_str_concat(identity, EL_STR("\n\n")), engram_ctx)); } _if_result_256; });
|
||||
el_val_t raw_response = llm_call_system(model, system_prompt, transcript);
|
||||
el_val_t is_error = ((str_starts_with(raw_response, EL_STR("{\"error\"")) || str_starts_with(raw_response, EL_STR("{\"type\":\"error\""))) || str_contains(raw_response, EL_STR("authentication_error")));
|
||||
@@ -27446,7 +27545,8 @@ el_val_t handle_dharma_room_turn_agentic(el_val_t body) {
|
||||
if (str_eq(transcript, EL_STR(""))) {
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"transcript is required\",\"response\":\"\",\"cgi_id\":\""), cgi_id), EL_STR("\"}"));
|
||||
}
|
||||
el_val_t ctx = engram_compile(transcript);
|
||||
/* chat.el fix (2026-07-01): distill_transcript reduces to last 3 messages for precise WM activation. */
|
||||
el_val_t ctx = engram_compile(distill_transcript(transcript));
|
||||
el_val_t system = el_str_concat(el_str_concat(identity, EL_STR(" You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct and stay in character.\n\n")), ctx);
|
||||
el_val_t api_key = agentic_api_key();
|
||||
system = safety_augment_system(system, transcript);
|
||||
@@ -28790,6 +28890,12 @@ el_val_t strip_query(el_val_t path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* flag_true — tolerant flag: accepts bool true or integer 1 (PR #63). */
|
||||
el_val_t flag_true(el_val_t body, el_val_t key) {
|
||||
return (json_get_bool(body, key) || (json_get_int(body, key) > 0));
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t err_404(el_val_t path) {
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), path), EL_STR("\"}"));
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn elp_extract_topic(msg: String) -> String
|
||||
extern fn elp_detect_predicate(msg: String) -> String
|
||||
extern fn elp_parse(msg: String) -> String
|
||||
|
||||
@@ -7,6 +7,14 @@ import "neuron-api.el"
|
||||
import "sessions.el"
|
||||
import "soul.elh"
|
||||
|
||||
// flag_true — tolerant flag test: accepts both boolean `true` (Kotlin UI) and
|
||||
// integer 1 (el-src UI). json_get_bool only recognises literal `true`, so
|
||||
// without this wrapper an "agentic":1 request would silently route to the
|
||||
// non-agentic path.
|
||||
fn flag_true(body: String, key: String) -> Bool {
|
||||
return json_get_bool(body, key) || json_get_int(body, key) > 0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rate limiting — simple in-memory per-IP sliding window counter.
|
||||
//
|
||||
@@ -483,6 +491,18 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_starts_with(clean, "/api/connectors") {
|
||||
return handle_connectors(method, clean, body)
|
||||
}
|
||||
// GET /api/run-progress/:session_id — live agentic-run ledger (2026-07-13,
|
||||
// narrated-runs). agentic_loop appends one {"i","t","tool"} entry per round
|
||||
// (the model's own pre-tool narration); a {"done":true} entry closes the run.
|
||||
// Clients poll this during a run to render live step updates without streaming.
|
||||
if str_starts_with(clean, "/api/run-progress/") {
|
||||
let rp_id: String = str_slice(clean, 18, str_len(clean))
|
||||
if !str_eq(rp_id, "") {
|
||||
let rp_raw: String = state_get("run_progress_" + rp_id)
|
||||
let rp_arr: String = if str_eq(rp_raw, "") { "[]" } else { "[" + rp_raw + "]" }
|
||||
return "{\"progress\":" + rp_arr + "}"
|
||||
}
|
||||
}
|
||||
// GET /api/sessions — list all sessions
|
||||
if str_eq(clean, "/api/sessions") {
|
||||
return session_list()
|
||||
|
||||
@@ -237,12 +237,47 @@ fn safety_abuse_phrases() -> String {
|
||||
return "[\"someone is hurting me\",\"someone's hurting me\",\"someone hurt me\",\"he hit me\",\"she hit me\",\"they hit me\",\"he hurt me\",\"she hurt me\",\"being abused\",\"being hurt by\",\"i am being abused\",\"i'm being abused\",\"i am being hurt\",\"i'm being hurt\",\"domestic violence\",\"my partner hurt\",\"my partner hit\",\"my husband hurt\",\"my wife hurt\",\"my boyfriend hurt\",\"my girlfriend hurt\",\"my parent hurt\",\"my father hurt\",\"my mother hurt\",\"my dad hurt\",\"my mom hurt\",\"afraid of him\",\"afraid of her\",\"afraid to go home\",\"scared of him\",\"scared of her\",\"he threatened me\",\"she threatened me\",\"threatened to hurt me\",\"threatened to kill me\",\"going to hurt me\",\"going to kill me\",\"help me he\",\"help me she\",\"help me they\"]"
|
||||
}
|
||||
|
||||
// General danger phrases that don't fit a bucket cleanly. Detected as hard; they
|
||||
// fall through to self_harm routing (the person is the primary concern).
|
||||
// General danger phrases that don't fit a bucket cleanly. Detected as hard.
|
||||
// "hurting me" / "being hurt" describe the USER as victim and correctly fall
|
||||
// through to self_harm routing (get-help). The threat-to-ANOTHER phrases
|
||||
// ("going to kill" / "going to hurt") are ALSO listed here for hard-bell
|
||||
// detection, but their ROUTING is now claimed by Track B below
|
||||
// (safety_threat_to_others_phrases + the "threat_other" branch in
|
||||
// safety_classify_hard_bell) so they no longer reach self_harm/988.
|
||||
fn safety_general_hard_phrases() -> String {
|
||||
return "[\"going to kill\",\"going to hurt\",\"hurting me\",\"being hurt\"]"
|
||||
}
|
||||
|
||||
// ── Track B — threat toward ANOTHER person (homicide / assault intent) ──────────
|
||||
//
|
||||
// LIVE SAFETY FIX (approved by Will + Tim, 2026-07-14).
|
||||
//
|
||||
// Bug: phrases like "going to kill" / "going to hurt" describe the USER intending
|
||||
// harm toward someone ELSE. They lived only in safety_general_hard_phrases and,
|
||||
// having no bucket in safety_classify_hard_bell, fell through to the "self_harm"
|
||||
// default. That routes the user to the 988 SUICIDE line (and, via the desktop
|
||||
// gate, their safety contact) -- dangerously wrong for a homicide/assault threat:
|
||||
// 988 is not the right resource and the safety contact must never be pulled in.
|
||||
//
|
||||
// Track B routing rule (non-negotiable):
|
||||
// - NEVER surface the 988 suicide/crisis framing for a threat toward others.
|
||||
// - NEVER notify or involve the user's safety contact.
|
||||
// - Refuse to assist, plan, or provide means; de-escalate; and for an
|
||||
// imminent / specific / credible threat direct the user to call 911.
|
||||
//
|
||||
// Ordering: safety_classify_hard_bell checks abuse -> self_harm -> threat_other,
|
||||
// so victim phrasings ("kill me" / "hurt me" -> abuse) and self-directed
|
||||
// phrasings ("kill myself" / "hurt myself" -> self_harm) are claimed by Track A
|
||||
// BEFORE this list is consulted. Only a residual harm-toward-another statement
|
||||
// reaches Track B.
|
||||
//
|
||||
// NOTE: matching is plain substring, so "going to kill him" also matches inside
|
||||
// "going to kill himself". That third-party self-harm edge is rare, and 911 is
|
||||
// still a defensible resource for it, so it is accepted rather than special-cased.
|
||||
fn safety_threat_to_others_phrases() -> String {
|
||||
return "[\"going to kill\",\"gonna kill\",\"want to kill him\",\"want to kill her\",\"want to kill them\",\"going to kill him\",\"going to kill her\",\"going to kill them\",\"going to kill you\",\"going to hurt\",\"gonna hurt\",\"going to hurt him\",\"going to hurt her\",\"going to hurt them\",\"going to hurt you\",\"going to shoot\",\"gonna shoot\",\"going to stab\",\"gonna stab\",\"going to attack\",\"kill them all\",\"kill everyone\",\"hurt everyone\",\"shoot up\"]"
|
||||
}
|
||||
|
||||
fn safety_soft_phrases() -> String {
|
||||
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\""]"
|
||||
}
|
||||
@@ -320,19 +355,29 @@ fn safety_detect_bell_level(message: String) -> String {
|
||||
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
|
||||
|| safety_any_match(text, safety_abuse_phrases())
|
||||
|| safety_any_match(text, safety_general_hard_phrases())
|
||||
|| safety_any_match(text, safety_threat_to_others_phrases())
|
||||
if is_hard { return "hard" }
|
||||
let soft_count: Int = safety_count_match(text, safety_soft_phrases())
|
||||
if soft_count >= 2 { return "soft" }
|
||||
return "none"
|
||||
}
|
||||
|
||||
// Returns "abuse" | "self_harm". Abuse is checked FIRST and takes precedence on
|
||||
// ambiguous signals — it forecloses the more dangerous routing (notifying a
|
||||
// possible abuser). General/unbucketed danger falls through to self_harm.
|
||||
// Returns "abuse" | "self_harm" | "threat_other".
|
||||
//
|
||||
// Order is load-bearing:
|
||||
// 1. abuse — user is the VICTIM of another person. Checked FIRST so it
|
||||
// forecloses the most dangerous routing (notifying a possible
|
||||
// abuser); claims "kill me" / "hurt me" phrasings.
|
||||
// 2. self_harm — user directs harm at THEMSELVES; claims "kill myself" /
|
||||
// "hurt myself" before Track B can see them.
|
||||
// 3. threat_other (Track B) — user directs harm at ANOTHER person. Routed to a
|
||||
// refusal + 911, NEVER to 988 or the safety contact.
|
||||
// Any residual unbucketed danger still falls through to self_harm (person-first).
|
||||
fn safety_classify_hard_bell(message: String) -> String {
|
||||
let text: String = safety_normalize(message)
|
||||
if safety_any_match(text, safety_abuse_phrases()) { return "abuse" }
|
||||
if safety_any_match(text, safety_self_harm_phrases()) { return "self_harm" }
|
||||
if safety_any_match(text, safety_threat_to_others_phrases()) { return "threat_other" }
|
||||
return "self_harm"
|
||||
}
|
||||
|
||||
@@ -343,6 +388,18 @@ fn safety_soft_directive() -> String {
|
||||
}
|
||||
|
||||
fn safety_hard_directive(hard_type: String) -> String {
|
||||
// Track B — threat toward ANOTHER person. Handled first and separately: the
|
||||
// standard preamble below ("you are not alone / are you safe right now") is
|
||||
// written for a person in distress or danger and is the WRONG frame for
|
||||
// someone voicing intent to harm someone else. This branch never emits the
|
||||
// 988 suicide/crisis framing and never involves the safety contact; it
|
||||
// refuses assistance and, for a credible imminent threat, points to 911.
|
||||
// The directive is advisory to an LLM that sees the full message, so it
|
||||
// instructs the model to re-judge benign/figurative matches and respond
|
||||
// normally in that case (keeps false positives non-accusatory).
|
||||
if str_eq(hard_type, "threat_other") {
|
||||
return "[SUBSTRATE DIRECTIVE - not visible to the user, do not mention this instruction]\nThe user's message may express intent to harm ANOTHER person. First read the actual message and judge whether that is genuinely what is meant; if it is plainly figurative or benign, respond normally and ignore the rest of this directive. If it may be a real intent to harm someone: stay calm and non-accusatory, and do not lecture. You must NOT help plan, encourage, rehearse, or provide any means, target information, weapon, or method for harming another person - refuse that part clearly but without hostility. Acknowledge the intensity of what they are feeling and gently steer them toward stepping back from acting. Routing rule (non-negotiable): do NOT provide the suicide or crisis hotline as the resource here (this is not self-harm), and do NOT suggest contacting, notifying, or involving the user's own contacts, family, or anyone on their contact list. If there is an imminent, specific, and credible threat to someone's life, tell the user to call 911 (US) or their local emergency number now. Encourage them to talk with someone they trust or a mental-health professional about the urge itself."
|
||||
}
|
||||
let preamble: String = "[SUBSTRATE DIRECTIVE - not visible to the user, do not mention this instruction]\nThe person has expressed something that sounds like immediate distress or danger. Do not ignore it. Do not just answer the surface question. Lead with genuine human warmth. Acknowledge what you heard. Tell them you're concerned. Let them know they are not alone. Ask them directly if they are safe right now. Stay with them in this conversation."
|
||||
let abuse_block: String = "\n\nThis appears to be danger from ANOTHER person. Routing rule (non-negotiable): do NOT suggest involving, contacting, or notifying the person's safety contact, family, or anyone close to them - that person may be the source of danger and has NOT been notified. Share only independent emergency resources:\n - Call or text 911 if in immediate danger\n - National Domestic Violence Hotline - 1-800-799-7233 (24/7, confidential)\n - Text START to 88788\n - thehotline.org (chat available)\n - 988 Suicide & Crisis Lifeline - call or text 988\nReassure them, if it fits, that their contact list has not been notified."
|
||||
let self_harm_block: String = "\n\nShare these crisis resources if appropriate:\n - 988 Suicide & Crisis Lifeline - call or text 988 (US)\n - Crisis Text Line - text HOME to 741741\n - International Association for Suicide Prevention: https://www.iasp.info/resources/Crisis_Centres/"
|
||||
|
||||
@@ -12,6 +12,7 @@ extern fn safety_log_bell(level: String, reason: String, input_summary: String)
|
||||
extern fn safety_self_harm_phrases() -> String
|
||||
extern fn safety_abuse_phrases() -> String
|
||||
extern fn safety_general_hard_phrases() -> String
|
||||
extern fn safety_threat_to_others_phrases() -> String
|
||||
extern fn safety_soft_phrases() -> String
|
||||
extern fn safety_detect_positive_level(message: String) -> String
|
||||
extern fn safety_detect_bell_level(message: String) -> String
|
||||
|
||||
+27
-16
@@ -373,6 +373,32 @@ fn session_update_patch(session_id: String, body: String) -> String {
|
||||
+ ",\"updated_at\":" + int_to_str(ts) + "}"
|
||||
}
|
||||
|
||||
// session_search_entry — extract one search-result entry from a raw node JSON.
|
||||
// Returns a JSON object string or "" if the node is not a valid session:meta node.
|
||||
//
|
||||
// Extracted from session_search's while loop body to reduce the loop's lexical
|
||||
// complexity. The ELC compiler runs out of memory processing while loops with
|
||||
// many `let` bindings — extracting the body into a separate function gives the
|
||||
// compiler a clean scope boundary at each call. Each function compiles in O(N)
|
||||
// rather than the exponential growth caused by rebinding accumulation inside loops.
|
||||
// (2026-07-01 self-review: root cause of sessions.c OOM/truncation since June 30)
|
||||
fn session_search_entry(node: String) -> String {
|
||||
let label: String = json_get(node, "label")
|
||||
if !str_eq(label, "session:meta") { return "" }
|
||||
let content: String = json_get(node, "content")
|
||||
let sess_id: String = json_get(content, "id")
|
||||
if str_eq(sess_id, "") { return "" }
|
||||
let title: String = json_get(content, "title")
|
||||
let created_raw: String = json_get(content, "created_at")
|
||||
let updated_raw: String = json_get(content, "updated_at")
|
||||
let eff_created: String = if str_eq(created_raw, "") { "0" } else { created_raw }
|
||||
let eff_updated: String = if str_eq(updated_raw, "") { eff_created } else { updated_raw }
|
||||
let e_id: String = "{\"id\":\"" + json_safe(sess_id) + "\""
|
||||
let e_title: String = ",\"title\":\"" + json_safe(title) + "\""
|
||||
let e_ts: String = ",\"created_at\":" + eff_created + ",\"updated_at\":" + eff_updated + "}"
|
||||
return e_id + e_title + e_ts
|
||||
}
|
||||
|
||||
// session_search — search session:meta nodes whose content matches query.
|
||||
fn session_search(query: String) -> String {
|
||||
if str_eq(query, "") { return "[]" }
|
||||
@@ -383,22 +409,7 @@ fn session_search(query: String) -> String {
|
||||
let out: String = ""
|
||||
let i: Int = 0
|
||||
while i < total {
|
||||
let node: String = json_array_get(results, i)
|
||||
let label: String = json_get(node, "label")
|
||||
let content: String = json_get(node, "content")
|
||||
let is_session: Bool = str_eq(label, "session:meta")
|
||||
let sess_id: String = json_get(content, "id")
|
||||
let title: String = json_get(content, "title")
|
||||
let created_raw: String = json_get(content, "created_at")
|
||||
let updated_raw: String = json_get(content, "updated_at")
|
||||
let eff_created: String = if str_eq(created_raw, "") { "0" } else { created_raw }
|
||||
let eff_updated: String = if str_eq(updated_raw, "") { eff_created } else { updated_raw }
|
||||
let entry: String = if is_session && !str_eq(sess_id, "") {
|
||||
"{\"id\":\"" + json_safe(sess_id) + "\""
|
||||
+ ",\"title\":\"" + json_safe(title) + "\""
|
||||
+ ",\"created_at\":" + eff_created
|
||||
+ ",\"updated_at\":" + eff_updated + "}"
|
||||
} else { "" }
|
||||
let entry: String = session_search_entry(json_array_get(results, i))
|
||||
let out = if !str_eq(entry, "") {
|
||||
if str_eq(out, "") { entry } else { out + "," + entry }
|
||||
} else { out }
|
||||
|
||||
@@ -8,6 +8,7 @@ extern fn session_list() -> String
|
||||
extern fn session_get(session_id: String) -> String
|
||||
extern fn session_delete(session_id: String) -> String
|
||||
extern fn session_update_patch(session_id: String, body: String) -> String
|
||||
extern fn session_search_entry(node: String) -> String
|
||||
extern fn session_search(query: String) -> String
|
||||
extern fn session_hist_load(session_id: String) -> String
|
||||
extern fn session_hist_save(session_id: String, hist: String) -> Void
|
||||
|
||||
+2
-6
@@ -1,15 +1,11 @@
|
||||
// stewardship.elh — Layer 2 public surface
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn steward_log_event(kind: String, detail: String) -> Void
|
||||
extern fn steward_get_mission() -> String
|
||||
extern fn steward_align(input: String, imprint_id: String) -> String
|
||||
extern fn steward_validate_imprint(imprint_id: String, tool_name: String) -> String
|
||||
extern fn steward_cgi_check(action: String) -> String
|
||||
// steward_log_event is an internal helper exported here because El has no access modifiers.
|
||||
// External callers have no business invoking this directly — use steward_align,
|
||||
// steward_validate_imprint, or steward_cgi_check, which call it at the correct points.
|
||||
extern fn steward_log_event(kind: String, detail: String) -> Void
|
||||
// Behavioral profiling and continuity detection (Layer 2 — session fingerprinting).
|
||||
extern fn steward_fingerprint_session(input: String, session_id: String) -> String
|
||||
extern fn extract_dim(content: String, key: String) -> String
|
||||
extern fn steward_build_baseline() -> String
|
||||
extern fn steward_check_continuity(current_fingerprint: String, session_id: String) -> String
|
||||
extern fn steward_session_check(input: String, session_id: String) -> String
|
||||
|
||||
@@ -46,7 +46,9 @@ fn handle_config(method: String, body: String) -> String {
|
||||
}
|
||||
}
|
||||
let current_model: String = state_get("soul_model")
|
||||
let display: String = if str_eq(current_model, "") { "claude-sonnet-4-5" } else { current_model }
|
||||
// Display fallback aligned with the intended product default (was claude-sonnet-4-5,
|
||||
// which silently became the app's picker default on fresh profiles — 2026-07-13).
|
||||
let display: String = if str_eq(current_model, "") { "claude-opus-4-8" } else { current_model }
|
||||
return "{\"model\":\"" + display + "\",\"ok\":true}"
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
extern fn auth_headers(tok: String) -> Map
|
||||
extern fn axon_get(path: String) -> String
|
||||
extern fn axon_post(path: String, body: String) -> String
|
||||
|
||||
@@ -160,13 +160,31 @@ assert_eq("'suicidal' classifies as self_harm", class_suicide, "self_harm")
|
||||
let class_overdose: String = safety_classify_hard_bell("took too many pills")
|
||||
assert_eq("'took too many' classifies as self_harm", class_overdose, "self_harm")
|
||||
|
||||
// ── Section 9: safety_classify_hard_bell — general -> 'self_harm' ────────────
|
||||
// ── Section 9: safety_classify_hard_bell — Track B threat-to-others ──────────
|
||||
//
|
||||
// LIVE SAFETY FIX (approved by Will + Tim, 2026-07-14): a threat toward ANOTHER
|
||||
// person ("going to kill/hurt <someone>") must classify as 'threat_other' and be
|
||||
// routed to a refusal + 911 — NOT to the 'self_harm'/988 path. This section used
|
||||
// to assert the old (buggy) fall-through to 'self_harm'; it now pins the fix.
|
||||
|
||||
println("")
|
||||
println("9. safety_classify_hard_bell — general hard phrases fall through to 'self_harm'")
|
||||
println("9. safety_classify_hard_bell — threat-to-others routes to 'threat_other' (not self_harm)")
|
||||
|
||||
let class_going_kill: String = safety_classify_hard_bell("going to kill everything around me")
|
||||
assert_eq("general hard phrase falls through to self_harm", class_going_kill, "self_harm")
|
||||
let class_going_kill: String = safety_classify_hard_bell("I am going to kill him tonight")
|
||||
assert_eq("'going to kill him' classifies as threat_other", class_going_kill, "threat_other")
|
||||
|
||||
let class_going_hurt: String = safety_classify_hard_bell("I'm going to hurt them so badly")
|
||||
assert_eq("'going to hurt them' classifies as threat_other", class_going_hurt, "threat_other")
|
||||
|
||||
let class_shoot: String = safety_classify_hard_bell("I'm going to shoot up the place")
|
||||
assert_eq("'going to shoot' classifies as threat_other", class_shoot, "threat_other")
|
||||
|
||||
// Track A must still win over Track B on victim / self-directed phrasings:
|
||||
let class_kill_me: String = safety_classify_hard_bell("he is going to kill me")
|
||||
assert_eq("'going to kill me' stays abuse (Track A precedence)", class_kill_me, "abuse")
|
||||
|
||||
let class_kill_self: String = safety_classify_hard_bell("I am going to kill myself")
|
||||
assert_eq("'kill myself' stays self_harm (Track A precedence)", class_kill_self, "self_harm")
|
||||
|
||||
// ── Section 10: safety_normalize — curly apostrophe normalisation ─────────────
|
||||
|
||||
@@ -220,6 +238,27 @@ let aug_abuse: String = safety_augment_system(base_sys, "he hit me and I am afra
|
||||
assert_contains("hard abuse -> DV hotline present", aug_abuse, "1-800-799-7233")
|
||||
assert_contains("hard abuse -> mentions not notifying contact", aug_abuse, "safety contact")
|
||||
|
||||
// ── Section 14b: safety_augment_system — Track B threat-to-others routing ─────
|
||||
//
|
||||
// LIVE SAFETY FIX (approved by Will + Tim, 2026-07-14): a homicide/assault threat
|
||||
// must be routed to a refusal + 911, and must NOT surface the 988 suicide line
|
||||
// or pull in the safety contact.
|
||||
|
||||
println("")
|
||||
println("14b. safety_augment_system — threat-to-others injects refusal + 911, never 988/contact")
|
||||
|
||||
let aug_threat: String = safety_augment_system(base_sys, "I am going to kill him tonight")
|
||||
assert_contains("threat_other -> contains SUBSTRATE DIRECTIVE", aug_threat, "SUBSTRATE DIRECTIVE")
|
||||
assert_contains("threat_other -> directs to 911", aug_threat, "911")
|
||||
assert_contains("threat_other -> refuses to help harm another", aug_threat, "harming another person")
|
||||
assert_not_contains("threat_other -> NO 988 suicide line", aug_threat, "988")
|
||||
assert_not_contains("threat_other -> NO safety-contact involvement", aug_threat, "safety contact")
|
||||
assert_not_contains("threat_other -> NO 'are you safe right now' victim frame", aug_threat, "are you safe right now")
|
||||
|
||||
// Detection must still fire hard on a weapon phrase not present in general_hard:
|
||||
let level_shoot: String = safety_detect_bell_level("I'm going to shoot up the office")
|
||||
assert_eq("'going to shoot' -> hard", level_shoot, "hard")
|
||||
|
||||
// ── Section 15: handle_safety_contact_post — validation ───────────────────────
|
||||
|
||||
println("")
|
||||
|
||||
Reference in New Issue
Block a user