Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfa540c066 | |||
| 13241aae25 | |||
| 2c346ee2b8 | |||
| fa5de69358 | |||
| a54770d606 | |||
| aa67f86f90 | |||
| 01446e644b | |||
| 92f51885bc | |||
| 2688cb722a | |||
| 71bb0820ce | |||
| d67f4c8f08 | |||
| b24f6d645b | |||
| 39acb55d4f |
+2
-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 idle_count() -> Int
|
||||
extern fn idle_inc() -> Int
|
||||
extern fn idle_reset() -> Void
|
||||
@@ -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
|
||||
|
||||
@@ -640,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.
|
||||
@@ -911,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, "") {
|
||||
@@ -939,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")
|
||||
|
||||
@@ -1006,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)
|
||||
|
||||
@@ -1015,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 {
|
||||
@@ -1256,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\"]}}," +
|
||||
@@ -1428,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")
|
||||
@@ -1445,11 +1680,23 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
if !path_within_root(path, root) {
|
||||
return json_safe("denied: path is outside the agent workspace root")
|
||||
}
|
||||
fs_write(resolve_in_root(path, root), content)
|
||||
return json_safe("{\"ok\":true}")
|
||||
// BUG-6 fix (2026-07-17): never claim ok without disk truth. fs_write's result was
|
||||
// never checked, so a failed write reported ok — the exact false-receipt failure
|
||||
// the run guards exist to kill. Verify the file landed and return the RESOLVED
|
||||
// path so callers and the model can only narrate what is really on disk.
|
||||
let dest: String = resolve_in_root(path, root)
|
||||
fs_write(dest, content)
|
||||
if !fs_exists(dest) {
|
||||
return json_safe("{\"error\":\"write failed - nothing landed at " + dest + "\"}")
|
||||
}
|
||||
return json_safe("{\"ok\":true,\"path\":\"" + dest + "\"}")
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -1461,7 +1708,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)
|
||||
}
|
||||
@@ -1689,8 +1943,24 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
// no root (or cleared the field), and we must not overwrite a server-configured root
|
||||
// from NEURON_AGENT_ROOT with an empty string, which would silently un-scope the agent.
|
||||
let ws_root: String = json_get(body, "agent_workspace_root")
|
||||
// BUG-LEAK fix (2026-07-16): the root used to live ONLY in the shared key, so any
|
||||
// request that omitted it INHERITED the previous session's folder (proven: a rootless
|
||||
// curl session wrote into another session's run folder). Now each session keeps its
|
||||
// own copy, and every request RE-ASSERTS its own root (possibly empty) into the shared
|
||||
// key the tool guards read — no session can ever act under another session's root.
|
||||
// Empty state still falls through to env NEURON_AGENT_ROOT inside
|
||||
// agent_workspace_root(), so a server-configured root survives unchanged.
|
||||
// LIMITATION (for review): assumes serialized request handling; true per-call scoping
|
||||
// means threading session_id through dispatch_tool/classify — deeper change, Will's call.
|
||||
let sess_for_root: String = json_get(body, "session_id")
|
||||
if !str_eq(ws_root, "") {
|
||||
if !str_eq(sess_for_root, "") {
|
||||
state_set("agent_workspace_root_" + sess_for_root, ws_root)
|
||||
}
|
||||
state_set("agent_workspace_root", ws_root)
|
||||
} else {
|
||||
let own_root: String = if str_eq(sess_for_root, "") { "" } else { state_get("agent_workspace_root_" + sess_for_root) }
|
||||
state_set("agent_workspace_root", own_root)
|
||||
}
|
||||
|
||||
// L1 safety screen — agentic path must pass the same gate as layered_cycle.
|
||||
@@ -1820,7 +2090,22 @@ 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, "")
|
||||
// PAUSE-CONTRACT fix (2026-07-16): honor the client's require_approval field — the
|
||||
// Phase 1c contract ("the soul pauses on EVERY tool; the client's tier gate decides
|
||||
// what actually prompts") was never implemented engine-side, which made the client's
|
||||
// Ask autonomy silently inert for builtin sub-escalate tools. Persisted per session
|
||||
// (set/reset on every request) so the /approve resume path keeps the same behavior
|
||||
// for the rest of the run. Absent/false = behavior identical to before this fix.
|
||||
let req_ask_all: String = json_get(body, "require_approval")
|
||||
state_set("require_approval_" + session_id, if str_eq(req_ask_all, "true") { "true" } else { "" })
|
||||
// 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.
|
||||
@@ -1873,6 +2158,12 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: String, messages_in: String, h: Map, tools_log_in: String) -> String {
|
||||
let api_url: String = "https://api.anthropic.com/v1/messages"
|
||||
|
||||
// PAUSE-CONTRACT fix (2026-07-16): when the client asked to approve every action
|
||||
// (require_approval on the request, persisted per session), EVERY tool turn bridges —
|
||||
// the client's tier gate decides what actually prompts vs auto-continues. Read from
|
||||
// session state so the /approve resume re-entry keeps the same behavior mid-run.
|
||||
let ask_all: Bool = !str_eq(session_id, "") && str_eq(state_get("require_approval_" + session_id), "true")
|
||||
|
||||
let messages: String = messages_in
|
||||
let final_text: String = ""
|
||||
let tools_log: String = tools_log_in
|
||||
@@ -1884,6 +2175,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 + "\""
|
||||
@@ -1940,7 +2244,16 @@ 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 { "" }
|
||||
// PAUSE-CONTRACT fix (2026-07-16): ask_all bridges EVERYTHING — stricter only.
|
||||
// Escalate keeps its unconditional bridge; "always allow" shortcuts never apply
|
||||
// under ask_all (the client owns its own standing grants at its tier gate).
|
||||
let needs_bridge: Bool = is_tool_turn && (ask_all || 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 { "" }
|
||||
@@ -1971,11 +2284,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.
|
||||
@@ -1996,6 +2325,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 + "}"
|
||||
@@ -2017,6 +2348,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) + "}"
|
||||
}
|
||||
|
||||
@@ -2055,6 +2393,10 @@ fn agentic_resume(session_id: String, tool_use_id: String, content: String) -> S
|
||||
if str_eq(blob, "") {
|
||||
return "{\"error\":\"unknown session_id\",\"reply\":\"\"}"
|
||||
}
|
||||
// BUG-LEAK fix (2026-07-16): re-assert THIS session's own workspace root before the
|
||||
// loop continues — a resume must never run under whatever root the last unrelated
|
||||
// request happened to leave in the shared key.
|
||||
state_set("agent_workspace_root", state_get("agent_workspace_root_" + session_id))
|
||||
|
||||
let model: String = json_get(blob, "model")
|
||||
let safe_sys: String = json_get(blob, "safe_sys")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn chat_default_model() -> String
|
||||
extern fn engram_numeric_valid(s: String) -> Bool
|
||||
extern fn parse_float_x100(s: String) -> Int
|
||||
@@ -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
|
||||
@@ -26,10 +28,15 @@ extern fn clean_llm_response(s: String) -> String
|
||||
extern fn conv_history_persist(hist: String) -> Void
|
||||
extern fn conv_history_load() -> String
|
||||
extern fn session_preload_bullets(nodes: String, max_bullets: Int, snip_len: Int) -> String
|
||||
extern fn affective_context_prefix() -> String
|
||||
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
|
||||
@@ -40,6 +47,10 @@ extern fn call_neuron_mcp(tool_name: String, args: String) -> String
|
||||
extern fn agent_workspace_root() -> String
|
||||
extern fn path_within_root(path: String, root: String) -> Bool
|
||||
extern fn resolve_in_root(path: String, root: String) -> String
|
||||
extern fn run_command_is_readonly(cmd: String) -> Bool
|
||||
extern fn cmd_abs_escape_at(cmd: String, root: String, needle: String) -> Bool
|
||||
extern fn run_command_guard(cmd: String, root: String) -> String
|
||||
extern fn classify_tool_risk(tool_name: String, tool_input: String) -> String
|
||||
extern fn dispatch_tool(tool_name: String, tool_input: String) -> String
|
||||
extern fn is_builtin_tool(tool_name: String) -> Bool
|
||||
extern fn next_bridge_id() -> String
|
||||
|
||||
+101
-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);
|
||||
@@ -26488,6 +26494,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 +26704,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 +27451,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 +27498,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 +27536,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 +28881,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;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Narrated runs — engine notes for Will (2026-07-13)
|
||||
|
||||
Source half: commit aa67f86 on feat/agent-phase1-soul (run-progress ledger,
|
||||
`/api/run-progress/<sid>` route, narration on the pause envelope, config display
|
||||
default). E2E-verified via the compiled test bed on Tim's clean profile.
|
||||
|
||||
Compiled-form-only fixes (in `neuron-container-build/soul-narrated-runs-20260713.patch`,
|
||||
applies ON TOP of `soul-webfix-20260711.patch` — these need porting to chat.el when the
|
||||
webfix itself is ported):
|
||||
|
||||
1. **pause_turn + tool_use interleave**: a pause_turn response can ALSO carry a client
|
||||
tool_use; resuming verbatim leaves it unpaired → Anthropic 400 "tool_use ids were
|
||||
found without tool_result". Fix: tool-bearing pause rounds are tool turns
|
||||
(dispatch + pair); verbatim resume only when the round has no client tool.
|
||||
2. **Agentic toolset scope**: agentic_tools_all() fed EVERY connector/MCP tool (Notion,
|
||||
code-execution…) into the loop. Code-execution flips the API into programmatic
|
||||
tool calling, whose pairing protocol the single-tool manual loop does not speak —
|
||||
source of the dangling-pair 400s AND the bash_code_execution workspace-dodge.
|
||||
Fix: handle_chat_agentic declares builtins + ONE server web_search only.
|
||||
Connector tools return when the loop gains real multi-tool/programmatic support.
|
||||
3. **disable_parallel_tool_use: true** on agentic requests — the loop captures only the
|
||||
first tool_use per round; Opus-class models parallel-call. Enforce the invariant.
|
||||
4. **web_search server-tool default variant → web_search_20250305 (GA)**. The 20260209
|
||||
variant couples to code-execution ⇒ programmatic mode (see #2, and the June note:
|
||||
"inert unless code-execution attached").
|
||||
5. **Homegrown web_search removed** from the tool catalog (server-side is the one tool).
|
||||
|
||||
Known engine debts this work surfaced (not fixed):
|
||||
|
||||
- **Poisoned session history**: a failed run persists the malformed assistant turn; every
|
||||
later turn in that session replays it and 400s. Needs history sanitation on load.
|
||||
- **Huge-history invalid-escape 400** (~346KB request) — likely the same poisoned blob.
|
||||
- **macOS note**: replacing a binary in place invalidates its ad-hoc signature (instant
|
||||
silent SIGKILL, looks like exit 0). `rm + cp + codesign -f -s -` is the swap ritual.
|
||||
+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 imprint_current() -> String
|
||||
extern fn imprint_load(imprint_id: String) -> String
|
||||
extern fn imprint_respond(input: String, imprint_id: String) -> String
|
||||
|
||||
+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 is_protected_node(id: String) -> Bool
|
||||
extern fn api_err_protected(id: String) -> String
|
||||
extern fn api_json_escape(s: String) -> String
|
||||
|
||||
@@ -491,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()
|
||||
|
||||
+1
-10
@@ -1,4 +1,4 @@
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// auto-generated by elc --emit-header - do not edit
|
||||
extern fn soft_bell_threshold() -> Int
|
||||
extern fn hard_bell_threshold() -> Int
|
||||
extern fn safety_score_crisis(input: String) -> Int
|
||||
@@ -13,12 +13,3 @@ extern fn safety_self_harm_phrases() -> String
|
||||
extern fn safety_abuse_phrases() -> String
|
||||
extern fn safety_general_hard_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
|
||||
extern fn safety_classify_hard_bell(message: String) -> String
|
||||
extern fn safety_soft_directive() -> String
|
||||
extern fn safety_hard_directive(hard_type: String) -> String
|
||||
extern fn safety_augment_system(system: String, user_msg: String) -> String
|
||||
extern fn safety_contact_path() -> String
|
||||
extern fn handle_safety_contact_get() -> String
|
||||
extern fn handle_safety_contact_post(body: String) -> String
|
||||
|
||||
+12
-1
@@ -677,6 +677,11 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
// path for all sessions created through handle_chat_agentic / agentic_loop.
|
||||
let bridge_blob: String = state_get("mcp_bridge:" + session_id)
|
||||
if !str_eq(bridge_blob, "") {
|
||||
// BUG-LEAK fix (2026-07-16): the approved tool executes below via dispatch_tool,
|
||||
// whose path/command guards read the shared workspace-root key. Re-assert THIS
|
||||
// session's own root first — an approval must never execute under whatever root
|
||||
// the last unrelated request left behind.
|
||||
state_set("agent_workspace_root", state_get("agent_workspace_root_" + session_id))
|
||||
// For "always": record tool_name in the always-allow list before resuming.
|
||||
// The tool_name is not stored in the bridge blob (only tool_use_id is).
|
||||
// Accept it from the body so the client can pass it along.
|
||||
@@ -708,7 +713,13 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
// For builtin tools with no client-provided content: fall back to
|
||||
// dispatch_tool so those tools still execute correctly.
|
||||
let client_content: String = json_get(body, "content")
|
||||
let use_client_content: Bool = !str_eq(client_content, "")
|
||||
// BUG-6 fix (2026-07-17): the naive json_get scanner matches "content" ANYWHERE
|
||||
// in the body — including INSIDE tool_input — so every approved write_file (whose
|
||||
// input always carries a content field) was mistaken for client-executed, never
|
||||
// dispatched, and narrated as done: a false receipt with no file on disk. Builtin
|
||||
// tools now ALWAYS dispatch server-side; client content is only honored for
|
||||
// non-builtin (MCP/client-executed) tools. Stricter only.
|
||||
let use_client_content: Bool = !str_eq(client_content, "") && !is_builtin_tool(approve_tool_name)
|
||||
let use_dispatch: Bool = is_builtin_tool(approve_tool_name) && !use_client_content
|
||||
let raw_input: String = json_get_raw(body, "tool_input")
|
||||
let eff_input: String = if str_eq(raw_input, "") { "{}" } else { raw_input }
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-7
@@ -1,15 +1,11 @@
|
||||
// stewardship.elh — Layer 2 public surface
|
||||
// auto-generated by elc --emit-header — do not edit
|
||||
// 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}"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user