feat(soul): MCP connectors — /api/connectors proxy + per-connector auto-approve

Adds the soul side of the connectors feature (spec: docs/research/
mcp-connectors-adoption-spec.md). The soul thin-proxies the neuron-connectd
bridge on 127.0.0.1:7771 so the UI talks to one origin and never reaches the
bridge directly.

routes.el:
- handle_connectors + connectd_get/connectd_post helpers (POST bodies go via
  a temp file + curl -d @file, so model/UI input can't reach the shell).
- GET /api/connectors and POST /api/connectors/{add,toggle,auto-approve,
  remove,secret,oauth/start} registered in both GET and POST routers.

chat.el:
- tool_auto_approved(): an mcp__* tool skips the approval card only when its
  server is explicitly opted in (off by default; built-in tools unaffected;
  bridge down -> false). Wired into the agentic approval gate so an
  auto-approved connector tool flows straight to execution.

Regenerated dist/chat.c and dist/routes.c. Verified live on :7770: real chat,
recall, and /api/connectors all work after promotion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tim Lingo
2026-06-13 18:43:14 -05:00
parent 8eea1d94ff
commit 6c57d4fe1b
4 changed files with 1079 additions and 314 deletions
+522 -182
View File
@@ -52,12 +52,13 @@ fn engram_compile(intent: String) -> String {
return ctx
}
// Escape a string for embedding inside a JSON string literal. Delegates to the
// native json_escape_string, which handles " \ \n \r AND \t. The old hand-rolled
// version missed tabs (and other control chars), so engram context pulled from
// the graph — which routinely contains tabs — produced malformed request bodies
// that the API rejected, surfacing as "llm unavailable" on the agentic path.
fn json_safe(s: String) -> String {
let s1: String = str_replace(s, "\\", "\\\\")
let s2: String = str_replace(s1, "\"", "\\\"")
let s3: String = str_replace(s2, "\n", "\\n")
let s4: String = str_replace(s3, "\r", "\\r")
return s4
return json_escape_string(s)
}
fn build_system_prompt(ctx: String) -> String {
@@ -259,17 +260,92 @@ fn agentic_tools_literal() -> String {
"]"
}
// agentic_tools_with_web the standard tool set, always plus Anthropic's NATIVE
// server-side web_search tool. Web search is BUILT IN: the model invokes it only when a
// query needs fresh info (max_uses caps it), so there is no user-facing toggle. The native
// tool is executed by Anthropic (not by the soul), so it returns real results with citations
// and needs no local runtime it sidesteps the soul's lack of executable tools entirely.
// agentic_tools_with_web — local tools PLUS Anthropic's native server-side web_search.
// IMPORTANT: the tool TYPE must be "web_search_20250305" (GA, executed by Anthropic).
// The newer "web_search_20260209" is INERT unless the code-execution tool is also
// attached — a tool that silently never returns is exactly what made the model
// narrate web_search(...) as text for 8 days instead of invoking it. 20250305 needs
// no extra tool and returns cited results directly (verified against the live API).
fn agentic_tools_with_web() -> String {
let base: String = agentic_tools_literal()
let inner: String = str_slice(base, 1, str_len(base) - 1)
return "[" + inner + ",{\"type\":\"web_search_20250305\",\"name\":\"web_search\",\"max_uses\":5}]"
}
// ---------------------------------------------------------------------------
// MCP connectors. The soul consumes external MCP tools through neuron-connectd,
// the loopback bridge (Accessor) on 127.0.0.1:7771. The bridge isolates all MCP
// wire complexity (stdio framing, SSE, OAuth, server lifecycle); the soul only
// speaks flat HTTP. Spec: docs/research/mcp-connectors-adoption-spec.md.
// ---------------------------------------------------------------------------
// True when s begins with the literal prefix pre. Built from str_slice/str_eq —
// the soul runtime has no str_starts_with.
fn str_begins(s: String, pre: String) -> Bool {
let n: Int = str_len(pre)
if str_len(s) < n {
return false
}
return str_eq(str_slice(s, 0, n), pre)
}
// Fetch the merged, namespaced tool schemas (mcp__<srv>__<tool>) from the bridge.
// Short timeout + empty-array fallback: if the bridge is down, the soul runs
// exactly as before with only its built-in tools (graceful degradation).
fn connector_tools_json() -> String {
let raw: String = exec_capture("curl -s --max-time 2 http://127.0.0.1:7771/mcp/tools")
if str_eq(raw, "") {
return "[]"
}
let arr: String = json_get_raw(raw, "tools")
if str_eq(arr, "") {
return "[]"
}
return arr
}
// Built-in tools + native web_search + every connector tool, as one tools array.
// Splices connector tools in before the closing bracket of the base array.
fn agentic_tools_all() -> String {
let base: String = agentic_tools_with_web()
let conn: String = connector_tools_json()
let conn_inner: String = str_slice(conn, 1, str_len(conn) - 1)
if str_eq(conn_inner, "") {
return base
}
let base_open: String = str_slice(base, 0, str_len(base) - 1)
return base_open + "," + conn_inner + "]"
}
// Proxy one tool call to the bridge. The model-supplied input is written to a
// temp file and handed to curl via -d @file, so arbitrary JSON can never reach
// the shell as an argument (no injection through tool_input).
fn call_mcp_bridge(tool_name: String, tool_input: String) -> String {
let eff_input: String = if str_eq(tool_input, "") { "{}" } else { tool_input }
let body: String = "{\"name\":\"" + tool_name + "\",\"input\":" + eff_input + "}"
let tmp: String = "/tmp/neuron-mcp-call.json"
fs_write(tmp, body)
return exec_capture("curl -s --max-time 30 -X POST http://127.0.0.1:7771/mcp/call -H 'Content-Type: application/json' -d @" + tmp)
}
// Per-connector auto-approve: true only for an mcp__* tool whose server the user has
// explicitly opted into skipping the approval card (off by default). Built-in tools are
// never auto-approved here — they keep their existing gating. Bridge down → false (safe).
fn tool_auto_approved(tool_name: String) -> Bool {
if !str_begins(tool_name, "mcp__") {
return false
}
let raw: String = exec_capture("curl -s --max-time 2 http://127.0.0.1:7771/mcp/auto-approved")
if str_eq(raw, "") {
return false
}
let list: String = json_get_raw(raw, "tools")
if str_eq(list, "") {
return false
}
return str_contains(list, "\"" + tool_name + "\"")
}
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")
@@ -287,6 +363,12 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
let result: String = http_get(url)
return json_safe(result)
}
if str_eq(tool_name, "web_search") {
let query: String = json_get(tool_input, "query")
let encoded: String = str_replace(str_replace(str_replace(str_replace(query, " ", "+"), "\"", ""), "'", ""), "&", "and")
let rss: String = http_get("https://news.google.com/rss/search?q=" + encoded + "&hl=en-US&gl=US&ceid=US:en")
return json_safe(rss)
}
if str_eq(tool_name, "search_memory") {
let query: String = json_get(tool_input, "query")
let result: String = engram_search_json(query, 10)
@@ -297,9 +379,342 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
let result: String = exec_capture(cmd)
return json_safe(result)
}
if str_begins(tool_name, "mcp__") {
let out: String = call_mcp_bridge(tool_name, tool_input)
if str_eq(out, "") {
return json_safe("MCP bridge unreachable (neuron-connectd on :7771)")
}
let content: String = json_get(out, "content")
if str_eq(content, "") {
let err: String = json_get(out, "error")
let msg: String = if str_eq(err, "") { "MCP call failed" } else { "MCP error: " + err }
return json_safe(msg)
}
return json_safe(content)
}
return "unknown tool: " + tool_name
}
// ---------------------------------------------------------------------------
// Agentic engine — resumable tool loop with server-side approval.
//
// Design doc: docs/research/agentic-tool-approval-design.md. One state machine
// replaces the loops previously duplicated in handle_chat_agentic and
// handle_dharma_room_turn_agentic. All loop state lives in a JSON blob so a
// pending tool call can be parked in the soul's KV store between the
// tool_pending response and the UI's POST /api/sessions/{id}/approve.
// ---------------------------------------------------------------------------
fn json_array_append(arr: String, item: String) -> String {
let eff: String = if str_eq(arr, "") { "[]" } else { arr }
let inner: String = str_slice(eff, 1, str_len(eff) - 1)
if str_eq(inner, "") {
return "[" + item + "]"
}
return "[" + inner + "," + item + "]"
}
fn append_tool_log(log: String, name: String) -> String {
let quoted: String = "\"" + name + "\""
if str_eq(log, "") {
return quoted
}
return log + "," + quoted
}
// Execute one tool_use block and wrap the (truncated) output as a tool_result
// message. dispatch_tool already json_safe()s its output.
fn exec_tool_block(block: String) -> String {
let t_id: String = json_get(block, "id")
let t_name: String = json_get(block, "name")
let t_input: String = json_get_raw(block, "input")
let raw: String = dispatch_tool(t_name, t_input)
let trunc: String = if str_len(raw) > 6000 {
str_slice(raw, 0, 6000) + "...[truncated]"
} else { raw }
return "{\"type\":\"tool_result\",\"tool_use_id\":\"" + t_id + "\",\"content\":\"" + trunc + "\"}"
}
// Serialize engine state. String fields are escaped on store and read back
// with json_get; arrays nest raw and read back with json_get_raw.
fn agentic_blob(model: String, system: String, tools_json: String, messages: String, origin: String, approval: Bool, iteration: Int, tools_log: String, content: String, queue: String, results: String, next: Int) -> String {
let appr: String = if approval { "true" } else { "false" }
return "{\"model\":\"" + model + "\""
+ ",\"system\":\"" + json_safe(system) + "\""
+ ",\"origin\":\"" + json_safe(origin) + "\""
+ ",\"approval\":" + appr
+ ",\"iteration\":" + int_to_str(iteration)
+ ",\"next\":" + int_to_str(next)
+ ",\"tools_log\":\"" + json_safe(tools_log) + "\""
+ ",\"tools\":" + tools_json
+ ",\"messages\":" + messages
+ ",\"content\":" + content
+ ",\"queue\":" + queue
+ ",\"results\":" + results
+ "}"
}
// Remove every ,"citations":[ ... ] span from a content-array JSON string.
// Native web_search splits the answer across many text blocks; the ones carrying
// the actual facts have a nested "citations" array, and the runtime's naive
// json_get returns "" for the "text" field of such blocks — which dropped the
// real content and left empty bullets. Stripping the citations span first makes
// each text block a plain {"type":"text","text":"..."} the parser reads cleanly.
// Bracket-depth matched (cited_text from news/sports effectively never contains
// raw brackets); guard-capped so a malformed payload can never spin.
// Concatenate the decoded value of every "text":"..." field in a content-array
// string. Single-pass — does NOT use json_array_get, which the runtime implements
// naively and which mis-splits a content array containing a large nested
// web_search_tool_result block (dropping the interleaved answer text). Only real
// text blocks carry a "text": key here (server_tool_use/web_search_tool_result use
// other keys), and citations are stripped beforehand, so this captures exactly the
// model's prose. Decodes the JSON escapes json_get would have decoded.
fn extract_all_text(s: String) -> String {
let key: String = "\"text\":\""
let klen: Int = str_len(key)
let n: Int = str_len(s)
let out: String = ""
let i: Int = 0
let inval: Bool = false
let esc: Bool = false
while i < n {
let at_key: Bool = !inval && i + klen <= n && str_eq(str_slice(s, i, i + klen), key)
let ch: String = str_slice(s, i, i + 1)
let was_esc: Bool = esc
let inval = if at_key { true } else { inval }
let proc: Bool = inval && !at_key
let esc = if proc && !was_esc && str_eq(ch, "\\") { true } else { false }
let decoded: String = if proc && was_esc {
if str_eq(ch, "n") { "\n" } else {
if str_eq(ch, "t") { "\t" } else {
if str_eq(ch, "r") { "" } else {
if str_eq(ch, "\"") { "\"" } else {
if str_eq(ch, "\\") { "\\" } else {
if str_eq(ch, "/") { "/" } else { ch }}}}}}
} else { ch }
let end_val: Bool = proc && !was_esc && str_eq(ch, "\"")
let do_app: Bool = proc && !end_val && !esc
let out = if do_app { out + decoded } else { out }
let inval = if end_val { false } else { inval }
let i = if at_key { i + klen } else { i + 1 }
}
return out
}
fn strip_citations(s: String) -> String {
// Single-pass char scanner (el only lets mutations escape a while body via
// top-level if-expressions, so a NESTED loop would not advance — one loop only).
// `skip` is the open-bracket depth inside a citations array; while skip>0 we drop
// characters. On the marker we jump past it and enter skip=1.
let marker: String = ",\"citations\":["
let mlen: Int = str_len(marker)
let n: Int = str_len(s)
let out: String = ""
let i: Int = 0
let skip: Int = 0
while i < n {
let ch: String = str_slice(s, i, i + 1)
let at_marker: Bool = skip == 0 && i + mlen <= n && str_eq(str_slice(s, i, i + mlen), marker)
let was_skip: Bool = skip > 0
let skip = if at_marker { 1 } else {
if was_skip {
if str_eq(ch, "[") { skip + 1 } else { if str_eq(ch, "]") { skip - 1 } else { skip } }
} else { skip }
}
let out = if at_marker { out } else { if was_skip { out } else { out + ch } }
let i = if at_marker { i + mlen } else { i + 1 }
}
return out
}
// One API round. Returns a verdict the engine can branch on without needing
// mutations to escape an if block:
// {"kind":"error","payload":{...}} | {"kind":"refusal"}
// {"kind":"final","text":"..."} | {"kind":"tools","content":[...],"queue":[...]}
fn agentic_api_turn(model: String, safe_sys: String, tools_json: String, messages: String) -> String {
let api_key: String = agentic_api_key()
let api_url: String = "https://api.anthropic.com/v1/messages"
let h: Map = {}
map_set(h, "x-api-key", api_key)
map_set(h, "anthropic-version", "2023-06-01")
map_set(h, "content-type", "application/json")
let req_body: String = "{\"model\":\"" + model + "\""
+ ",\"max_tokens\":4096"
+ ",\"system\":\"" + safe_sys + "\""
+ ",\"tools\":" + tools_json
+ ",\"messages\":" + messages
+ "}"
let raw_resp: String = http_post_with_headers(api_url, req_body, h)
let is_error: Bool = str_starts_with(raw_resp, "{\"error\"")
|| str_starts_with(raw_resp, "{\"type\":\"error\"")
|| str_contains(raw_resp, "authentication_error")
if is_error {
// Keep error="llm unavailable" (the UI maps it to a friendly message) but
// attach a truncated detail so failures are diagnosable instead of opaque.
let detail_raw: String = if str_len(raw_resp) > 300 { str_slice(raw_resp, 0, 300) } else { raw_resp }
let detail: String = json_safe(detail_raw)
return "{\"kind\":\"error\",\"payload\":{\"error\":\"llm unavailable\",\"reply\":\"\",\"detail\":\"" + detail + "\"}}"
}
let stop_reason: String = json_get(raw_resp, "stop_reason")
// Refusal (e.g. Fable 5): the API returns 200 with stop_reason "refusal" and empty
// content. Surface it so the caller can return a graceful reply.
if str_eq(stop_reason, "refusal") {
return "{\"kind\":\"refusal\"}"
}
// json_get_raw needed — content is an array, json_get returns "" for non-strings
let content_arr: String = json_get_raw(raw_resp, "content")
let eff_content: String = if str_eq(content_arr, "") { "[]" } else { content_arr }
// Walk content blocks: accumulate text, queue EVERY tool_use block (the API
// requires one tool_result per tool_use — capturing only the first broke
// multi-tool turns). Native server_tool_use blocks run Anthropic-side and
// are never dispatched locally. Walk a citation-stripped COPY so cited text
// blocks (native web_search) yield their "text"; eff_content stays raw for
// the verdict returns, which must echo back to Anthropic unchanged.
let walk_content: String = strip_citations(eff_content)
// Answer text via direct scan (robust to the runtime's naive array splitter).
let text_out: String = extract_all_text(walk_content)
// Queue tool_use blocks via the block walk — local tool turns have no large
// nested blocks, so json_array_get is reliable there.
let queue: String = "[]"
let ci: Int = 0
let c_total: Int = json_array_len(eff_content)
while ci < c_total {
let block: String = json_array_get(eff_content, ci)
let btype: String = json_get(block, "type")
let queue = if str_eq(btype, "tool_use") { json_array_append(queue, block) } else { queue }
let ci = ci + 1
}
let q_len: Int = json_array_len(queue)
let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && q_len > 0
if is_tool_turn {
return "{\"kind\":\"tools\",\"content\":" + eff_content + ",\"queue\":" + queue + "}"
}
// pause_turn: server-side tool (e.g. web_search) ran; search result is already in
// content as web_search_tool_result blocks. The client must send the whole content
// back as an assistant message to get the model's final synthesized answer.
if str_eq(stop_reason, "pause_turn") {
return "{\"kind\":\"pause\",\"content\":" + eff_content + "}"
}
return "{\"kind\":\"final\",\"text\":\"" + json_safe(text_out) + "\"}"
}
// The engine. Each step runs exactly one phase, chosen by guards computed from
// entry state (el rule: mutations escape a while body only as top-level
// if-expressions; returns inside if blocks are fine):
// 1. queue active → approval: park blob + return tool_pending; else execute
// 2. queue drained → fold assistant turn + tool results into history
// 3. no queue → API round: final / refusal / error / new tool queue
fn agentic_engine(session_id: String, blob: String) -> String {
let model: String = json_get(blob, "model")
let system: String = json_get(blob, "system")
let safe_sys: String = json_safe(system)
let origin: String = json_get(blob, "origin")
let approval: Bool = json_get_bool(blob, "approval")
let tools_json: String = json_get_raw(blob, "tools")
let pend_key: String = "agentic_pending_" + session_id
let messages: String = json_get_raw(blob, "messages")
let content_in: String = json_get_raw(blob, "content")
let queue_in: String = json_get_raw(blob, "queue")
let results_in: String = json_get_raw(blob, "results")
let content: String = if str_eq(content_in, "") { "[]" } else { content_in }
let queue: String = if str_eq(queue_in, "") { "[]" } else { queue_in }
let results: String = if str_eq(results_in, "") { "[]" } else { results_in }
let next: Int = json_get_int(blob, "next")
let iteration: Int = json_get_int(blob, "iteration")
let tools_log: String = json_get(blob, "tools_log")
let steps: Int = 0
while steps < 100 {
let q_len: Int = json_array_len(queue)
let has_pending: Bool = next < q_len
// Phase 1a: a tool call awaits a user decision — park state and pause, UNLESS the
// pending tool is an auto-approved connector tool (per-connector opt-in), which flows
// straight through to execution below.
if has_pending && approval {
let blk_a: String = json_array_get(queue, next)
let pend_name: String = json_get(blk_a, "name")
if !tool_auto_approved(pend_name) {
let park: String = agentic_blob(model, system, tools_json, messages, origin, approval, iteration, tools_log, content, queue, results, next)
state_set(pend_key, park)
let t_input: String = json_get_raw(blk_a, "input")
let eff_input: String = if str_eq(t_input, "") { "{}" } else { t_input }
return "{\"status\":\"tool_pending\",\"call_id\":\"" + json_get(blk_a, "id") + "\",\"tool_name\":\"" + json_get(blk_a, "name") + "\",\"tool_input\":" + eff_input + ",\"model\":\"" + model + "\",\"reply\":\"\"}"
}
}
let do_exec: Bool = has_pending
let do_fold: Bool = !has_pending && q_len > 0
let do_api: Bool = !has_pending && q_len < 1
// Phase 1b: auto-execute the next queued tool call.
let blk: String = if do_exec { json_array_get(queue, next) } else { "" }
let t_msg: String = if do_exec { exec_tool_block(blk) } else { "" }
let tools_log = if do_exec { append_tool_log(tools_log, json_get(blk, "name")) } else { tools_log }
let results = if do_exec { json_array_append(results, t_msg) } else { results }
let next = if do_exec { next + 1 } else { next }
// Phase 2: all results collected — fold the turn into history.
let messages = if do_fold {
json_array_append(
json_array_append(messages, "{\"role\":\"assistant\",\"content\":" + content + "}"),
"{\"role\":\"user\",\"content\":" + results + "}")
} else { messages }
let content = if do_fold { "[]" } else { content }
let queue = if do_fold { "[]" } else { queue }
let results = if do_fold { "[]" } else { results }
let next = if do_fold { 0 } else { next }
// Phase 3: call the model.
if do_api && iteration >= 8 {
state_set(pend_key, "")
return "{\"error\":\"no response\",\"reply\":\"\"}"
}
let verdict: String = if do_api { agentic_api_turn(model, safe_sys, tools_json, messages) } else { "" }
let kind: String = if do_api { json_get(verdict, "kind") } else { "" }
if str_eq(kind, "error") {
state_set(pend_key, "")
return json_get_raw(verdict, "payload")
}
if str_eq(kind, "refusal") {
state_set(pend_key, "")
return "{\"status\":\"ok\",\"reply\":\"I'm not able to help with that request.\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":[]}"
}
if str_eq(kind, "final") {
state_set(pend_key, "")
let safe_text: String = json_safe(json_get(verdict, "text"))
let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" }
return "{\"status\":\"ok\",\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + "}"
}
// pause_turn: server-side tool (web_search) completed inside Anthropic.
// The full content block (server_tool_use + web_search_tool_result) must be
// appended as an assistant message so the model can synthesize the final answer.
// Queue stays empty → next iteration immediately makes another API call.
let is_pause: Bool = str_eq(kind, "pause")
let messages = if is_pause {
json_array_append(messages, "{\"role\":\"assistant\",\"content\":" + json_get_raw(verdict, "content") + "}")
} else { messages }
let is_tools: Bool = str_eq(kind, "tools")
let content = if is_tools { json_get_raw(verdict, "content") } else { content }
let queue = if is_tools { json_get_raw(verdict, "queue") } else { queue }
let results = if is_tools { "[]" } else { results }
let next = if is_tools { 0 } else { next }
let iteration = if do_api { iteration + 1 } else { iteration }
let steps = steps + 1
}
state_set(pend_key, "")
return "{\"error\":\"agentic engine step limit exceeded\",\"reply\":\"\"}"
}
fn handle_chat_agentic(body: String) -> String {
let message: String = json_get(body, "message")
if str_eq(message, "") {
@@ -308,109 +723,103 @@ fn handle_chat_agentic(body: String) -> String {
let req_model: String = json_get(body, "model")
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
let session_id: String = json_get(body, "session_id")
// require_approval: the Kotlin UI sends true/false, the el-src UI sends 1/0
// — accept both. Approval needs a session id to park state under; without
// one it degrades to auto-run (the desktop UI always sends one).
let ra_bool: Bool = json_get_bool(body, "require_approval")
let ra_raw: String = json_get_raw(body, "require_approval")
let want_approval: Bool = ra_bool || str_eq(ra_raw, "1") || str_eq(ra_raw, "true")
let approval: Bool = want_approval && !str_eq(session_id, "")
let ctx: String = engram_compile(message)
let identity: String = state_get("soul_identity")
let system: String = identity + " 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.\n\n" + ctx
let current_date: String = time_format(time_now(), "%A, %B %d, %Y")
// The web_search directive lives HERE in the soul, not in the UI payload — so it
// applies no matter how the message arrives. Without a forceful instruction the
// model answers time-sensitive questions from training data or stalls asking the
// user to clarify; with web_search available it must search FIRST and answer from
// results. This is the fix for "model narrates web_search(...) instead of calling it."
let web_directive: String = " Today's date is " + current_date + ". You have a web_search tool that returns live results from the internet. For ANY question about current events, live scores, standings, schedules, recent news, people, prices, or anything that may have changed since your training, you MUST call web_search immediately and answer from the results. Do NOT ask the user to clarify first, do NOT say you lack live access, and do NOT answer time-sensitive questions from memory alone search, then answer. When a question is ambiguous about timeframe (e.g. 'the tournament', 'the game', 'the playoffs'), assume the user means whatever is happening or most recent RIGHT NOW as of today's date, search for that, and lead with it."
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands, plus any connected MCP tools (named mcp__<server>__<tool>). Use them when they add genuine value. Be direct." + web_directive + "\n\n" + ctx
let api_key: String = agentic_api_key()
let tools_json: String = agentic_tools_with_web()
let tools_json: String = agentic_tools_all()
let safe_msg: String = json_safe(message)
let safe_sys: String = json_safe(system)
let messages: String = "[{\"role\":\"user\",\"content\":\"" + safe_msg + "\"}]"
let api_url: String = "https://api.anthropic.com/v1/messages"
let h: Map = {}
map_set(h, "x-api-key", api_key)
map_set(h, "anthropic-version", "2023-06-01")
map_set(h, "content-type", "application/json")
let final_text: String = ""
let tools_log: String = ""
let iteration: Int = 0
let keep_going: Bool = true
while keep_going && iteration < 8 {
let req_body: String = "{\"model\":\"" + model + "\""
+ ",\"max_tokens\":4096"
+ ",\"system\":\"" + safe_sys + "\""
+ ",\"tools\":" + tools_json
+ ",\"messages\":" + messages
+ "}"
let raw_resp: String = http_post_with_headers(api_url, req_body, h)
let is_error: Bool = str_starts_with(raw_resp, "{\"error\"")
|| str_starts_with(raw_resp, "{\"type\":\"error\"")
|| str_contains(raw_resp, "authentication_error")
if is_error {
return "{\"error\":\"llm unavailable\",\"reply\":\"\"}"
}
let stop_reason: String = json_get(raw_resp, "stop_reason")
// json_get_raw needed content is an array, json_get returns "" for non-strings
let content_arr: String = json_get_raw(raw_resp, "content")
let eff_content: String = if str_eq(content_arr, "") { "[]" } else { content_arr }
// Walk content blocks. El rule: mutations must be at top level of while body
// using if-expressions mutations inside if *blocks* don't escape scope.
let text_out: String = ""
let has_tool: Bool = false
let tool_id: String = ""
let tool_name: String = ""
let tool_input: String = ""
let ci: Int = 0
let c_total: Int = json_array_len(eff_content)
while ci < c_total {
let block: String = json_array_get(eff_content, ci)
let btype: String = json_get(block, "type")
// Accumulate text at top level using if-expression
let text_out = if str_eq(btype, "text") { text_out + json_get(block, "text") } else { text_out }
// Capture first tool_use block only
let is_new_tool: Bool = str_eq(btype, "tool_use") && !has_tool
let has_tool = if is_new_tool { true } else { has_tool }
let tool_id = if is_new_tool { json_get(block, "id") } else { tool_id }
let tool_name = if is_new_tool { json_get(block, "name") } else { tool_name }
// input is a JSON object must use json_get_raw, not json_get
let tool_input = if is_new_tool { json_get_raw(block, "input") } else { tool_input }
let ci = ci + 1
}
// Dispatch tool and build result message
let tool_result_raw: String = if has_tool { dispatch_tool(tool_name, tool_input) } else { "" }
// Truncate large tool results (web pages etc) to avoid oversized requests
let tool_result: String = if str_len(tool_result_raw) > 6000 {
str_slice(tool_result_raw, 0, 6000) + "...[truncated]"
} else { tool_result_raw }
let tool_msg: String = "{\"type\":\"tool_result\",\"tool_use_id\":\"" + tool_id + "\",\"content\":\"" + tool_result + "\"}"
// Accumulate tool names for the tools_used log surfaced in the response.
let tool_quoted: String = "\"" + tool_name + "\""
let tools_log = if has_tool {
if str_eq(tools_log, "") { tool_quoted } else { tools_log + "," + tool_quoted }
} else { tools_log }
// Update messages and loop state all at top level using if-expressions
let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && has_tool
let inner: String = str_slice(messages, 1, str_len(messages) - 1)
let messages = if is_tool_turn {
"[" + inner
+ ",{\"role\":\"assistant\",\"content\":" + eff_content + "}"
+ ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}"
+ "]"
} else { messages }
let final_text = if !is_tool_turn { text_out } else { final_text }
let keep_going = if !is_tool_turn { false } else { keep_going }
let iteration = iteration + 1
// A new message abandons any stale pending call for this session.
if !str_eq(session_id, "") {
state_set("agentic_pending_" + session_id, "")
}
if str_eq(final_text, "") {
return "{\"error\":\"no response\",\"reply\":\"\"}"
let blob: String = agentic_blob(model, system, tools_json, messages, message, approval, 0, "", "[]", "[]", "[]", 0)
return agentic_engine(session_id, blob)
}
// POST /api/sessions/{session_id}/approve — resume a parked agentic loop with
// the user's decision on the pending tool call. Body: {"call_id","action"}
// where action is "allow" or "deny" (the UI's "always" never reaches the wire;
// it auto-sends allow client-side).
fn handle_session_approve(session_id: String, body: String) -> String {
if str_eq(session_id, "") {
return "{\"error\":\"session_id required\",\"reply\":\"\"}"
}
let pend_key: String = "agentic_pending_" + session_id
let blob: String = state_get(pend_key)
if str_eq(blob, "") {
return "{\"error\":\"no pending tool call for this session\",\"reply\":\"\"}"
}
let safe_text: String = json_safe(final_text)
let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" }
return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + "}"
let call_id: String = json_get(body, "call_id")
let action: String = json_get(body, "action")
let queue: String = json_get_raw(blob, "queue")
let next: Int = json_get_int(blob, "next")
let q_len: Int = json_array_len(queue)
if next >= q_len {
state_set(pend_key, "")
return "{\"error\":\"pending state corrupt\",\"reply\":\"\"}"
}
let block: String = json_array_get(queue, next)
let block_id: String = json_get(block, "id")
if !str_eq(call_id, block_id) {
return "{\"error\":\"stale call_id\",\"reply\":\"\"}"
}
// Denied calls still need a tool_result (one per tool_use) so the model
// can adapt instead of the request being rejected.
let deny: Bool = str_eq(action, "deny")
let t_msg: String = if deny {
"{\"type\":\"tool_result\",\"tool_use_id\":\"" + block_id + "\",\"content\":\"User denied this tool call.\"}"
} else {
exec_tool_block(block)
}
let tools_log_in: String = json_get(blob, "tools_log")
let tools_log: String = if deny { tools_log_in } else { append_tool_log(tools_log_in, json_get(block, "name")) }
let results: String = json_array_append(json_get_raw(blob, "results"), t_msg)
let model: String = json_get(blob, "model")
let system: String = json_get(blob, "system")
let origin: String = json_get(blob, "origin")
let tools_json: String = json_get_raw(blob, "tools")
let messages: String = json_get_raw(blob, "messages")
let content: String = json_get_raw(blob, "content")
let iteration: Int = json_get_int(blob, "iteration")
// Re-seed with the decision applied; the engine re-parks if another call
// in the same turn still needs approval.
state_set(pend_key, "")
let updated: String = agentic_blob(model, system, tools_json, messages, origin, true, iteration, tools_log, content, queue, results, next + 1)
let reply: String = agentic_engine(session_id, updated)
// The /api/chat route skips persistence when it pauses — persist the
// completed exchange here instead, exactly once.
let is_final: Bool = str_contains(reply, "\"status\":\"ok\"")
if is_final {
auto_persist("{\"message\":\"" + json_safe(origin) + "\"}", reply)
}
return reply
}
// handle_chat_as_soul — multi-soul room dispatch handler.
@@ -542,93 +951,24 @@ fn handle_dharma_room_turn_agentic(body: String) -> String {
let ctx: String = engram_compile(transcript)
let system: String = identity + " 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
let api_key: String = agentic_api_key()
let tools_json: String = agentic_tools_literal()
let safe_transcript: String = json_safe(transcript)
let safe_sys: String = json_safe(system)
let messages: String = "[{\"role\":\"user\",\"content\":\"" + safe_transcript + "\"}]"
let api_url: String = "https://api.anthropic.com/v1/messages"
let h: Map = {}
map_set(h, "x-api-key", api_key)
map_set(h, "anthropic-version", "2023-06-01")
map_set(h, "content-type", "application/json")
let final_text: String = ""
let tools_log: String = ""
let iteration: Int = 0
let keep_going: Bool = true
// Room turns have no UI to approve from — always auto-run, then re-wrap
// the engine's envelope into the dharma {response, cgi_id} shape.
let blob: String = agentic_blob(model, system, tools_json, messages, transcript, false, 0, "", "[]", "[]", "[]", 0)
let reply: String = agentic_engine("", blob)
while keep_going && iteration < 8 {
let req_body: String = "{\"model\":\"" + model + "\""
+ ",\"max_tokens\":4096"
+ ",\"system\":\"" + safe_sys + "\""
+ ",\"tools\":" + tools_json
+ ",\"messages\":" + messages
+ "}"
let raw_resp: String = http_post_with_headers(api_url, req_body, h)
let is_error: Bool = str_starts_with(raw_resp, "{\"error\"")
|| str_starts_with(raw_resp, "{\"type\":\"error\"")
|| str_contains(raw_resp, "authentication_error")
if is_error {
return "{\"error\":\"llm unavailable\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}"
}
let stop_reason: String = json_get(raw_resp, "stop_reason")
let content_arr: String = json_get_raw(raw_resp, "content")
let eff_content: String = if str_eq(content_arr, "") { "[]" } else { content_arr }
let text_out: String = ""
let has_tool: Bool = false
let tool_id: String = ""
let tool_name: String = ""
let tool_input: String = ""
let ci: Int = 0
let c_total: Int = json_array_len(eff_content)
while ci < c_total {
let block: String = json_array_get(eff_content, ci)
let btype: String = json_get(block, "type")
let text_out = if str_eq(btype, "text") { text_out + json_get(block, "text") } else { text_out }
let is_new_tool: Bool = str_eq(btype, "tool_use") && !has_tool
let has_tool = if is_new_tool { true } else { has_tool }
let tool_id = if is_new_tool { json_get(block, "id") } else { tool_id }
let tool_name = if is_new_tool { json_get(block, "name") } else { tool_name }
let tool_input = if is_new_tool { json_get_raw(block, "input") } else { tool_input }
let ci = ci + 1
}
let tool_result_raw: String = if has_tool { dispatch_tool(tool_name, tool_input) } else { "" }
let tool_result: String = if str_len(tool_result_raw) > 6000 {
str_slice(tool_result_raw, 0, 6000) + "...[truncated]"
} else { tool_result_raw }
let tool_msg: String = "{\"type\":\"tool_result\",\"tool_use_id\":\"" + tool_id + "\",\"content\":\"" + tool_result + "\"}"
let tool_quoted: String = "\"" + tool_name + "\""
let tools_log = if has_tool {
if str_eq(tools_log, "") { tool_quoted } else { tools_log + "," + tool_quoted }
} else { tools_log }
let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && has_tool
let inner: String = str_slice(messages, 1, str_len(messages) - 1)
let messages = if is_tool_turn {
"[" + inner
+ ",{\"role\":\"assistant\",\"content\":" + eff_content + "}"
+ ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}"
+ "]"
} else { messages }
let final_text = if !is_tool_turn { text_out } else { final_text }
let keep_going = if !is_tool_turn { false } else { keep_going }
let iteration = iteration + 1
let err: String = json_get(reply, "error")
if !str_eq(err, "") {
return "{\"error\":\"" + json_safe(err) + "\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}"
}
if str_eq(final_text, "") {
return "{\"error\":\"no response\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}"
}
let safe_text: String = json_safe(final_text)
let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" }
let text: String = json_get(reply, "reply")
let safe_text: String = json_safe(text)
let tools_used: String = json_get_raw(reply, "tools_used")
let tools_arr: String = if str_eq(tools_used, "") { "[]" } else { tools_used }
return "{\"response\":\"" + safe_text + "\",\"cgi_id\":\"" + cgi_id + "\",\"tools_used\":" + tools_arr + "}"
}
Vendored
+378 -122
View File
@@ -31,8 +31,23 @@ el_val_t handle_see(el_val_t body);
el_val_t studio_tools_json(void);
el_val_t agentic_api_key(void);
el_val_t agentic_tools_literal(void);
el_val_t agentic_tools_with_web(void);
el_val_t str_begins(el_val_t s, el_val_t pre);
el_val_t connector_tools_json(void);
el_val_t agentic_tools_all(void);
el_val_t call_mcp_bridge(el_val_t tool_name, el_val_t tool_input);
el_val_t tool_auto_approved(el_val_t tool_name);
el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input);
el_val_t json_array_append(el_val_t arr, el_val_t item);
el_val_t append_tool_log(el_val_t log, el_val_t name);
el_val_t exec_tool_block(el_val_t block);
el_val_t agentic_blob(el_val_t model, el_val_t system, el_val_t tools_json, el_val_t messages, el_val_t origin, el_val_t approval, el_val_t iteration, el_val_t tools_log, el_val_t content, el_val_t queue, el_val_t results, el_val_t next);
el_val_t extract_all_text(el_val_t s);
el_val_t strip_citations(el_val_t s);
el_val_t agentic_api_turn(el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages);
el_val_t agentic_engine(el_val_t session_id, el_val_t blob);
el_val_t handle_chat_agentic(el_val_t body);
el_val_t handle_session_approve(el_val_t session_id, el_val_t body);
el_val_t handle_chat_as_soul(el_val_t body);
el_val_t handle_dharma_room_turn(el_val_t body);
el_val_t handle_dharma_room_turn_agentic(el_val_t body);
@@ -74,11 +89,7 @@ el_val_t engram_compile(el_val_t intent) {
}
el_val_t json_safe(el_val_t s) {
el_val_t s1 = str_replace(s, EL_STR("\\"), EL_STR("\\\\"));
el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\""));
el_val_t s3 = str_replace(s2, EL_STR("\n"), EL_STR("\\n"));
el_val_t s4 = str_replace(s3, EL_STR("\r"), EL_STR("\\r"));
return s4;
return json_escape_string(s);
return 0;
}
@@ -231,6 +242,72 @@ el_val_t agentic_tools_literal(void) {
return 0;
}
el_val_t agentic_tools_with_web(void) {
el_val_t base = agentic_tools_literal();
el_val_t inner = str_slice(base, 1, (str_len(base) - 1));
return el_str_concat(el_str_concat(EL_STR("["), inner), EL_STR(",{\"type\":\"web_search_20250305\",\"name\":\"web_search\",\"max_uses\":5}]"));
return 0;
}
el_val_t str_begins(el_val_t s, el_val_t pre) {
el_val_t n = str_len(pre);
if (str_len(s) < n) {
return 0;
}
return str_eq(str_slice(s, 0, n), pre);
return 0;
}
el_val_t connector_tools_json(void) {
el_val_t raw = exec_capture(EL_STR("curl -s --max-time 2 http://127.0.0.1:7771/mcp/tools"));
if (str_eq(raw, EL_STR(""))) {
return EL_STR("[]");
}
el_val_t arr = json_get_raw(raw, EL_STR("tools"));
if (str_eq(arr, EL_STR(""))) {
return EL_STR("[]");
}
return arr;
return 0;
}
el_val_t agentic_tools_all(void) {
el_val_t base = agentic_tools_with_web();
el_val_t conn = connector_tools_json();
el_val_t conn_inner = str_slice(conn, 1, (str_len(conn) - 1));
if (str_eq(conn_inner, EL_STR(""))) {
return base;
}
el_val_t base_open = str_slice(base, 0, (str_len(base) - 1));
return el_str_concat(el_str_concat(el_str_concat(base_open, EL_STR(",")), conn_inner), EL_STR("]"));
return 0;
}
el_val_t call_mcp_bridge(el_val_t tool_name, el_val_t tool_input) {
el_val_t eff_input = ({ el_val_t _if_result_20 = 0; if (str_eq(tool_input, EL_STR(""))) { _if_result_20 = (EL_STR("{}")); } else { _if_result_20 = (tool_input); } _if_result_20; });
el_val_t body = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"name\":\""), tool_name), EL_STR("\",\"input\":")), eff_input), EL_STR("}"));
el_val_t tmp = EL_STR("/tmp/neuron-mcp-call.json");
fs_write(tmp, body);
return exec_capture(el_str_concat(EL_STR("curl -s --max-time 30 -X POST http://127.0.0.1:7771/mcp/call -H 'Content-Type: application/json' -d @"), tmp));
return 0;
}
el_val_t tool_auto_approved(el_val_t tool_name) {
if (!str_begins(tool_name, EL_STR("mcp__"))) {
return 0;
}
el_val_t raw = exec_capture(EL_STR("curl -s --max-time 2 http://127.0.0.1:7771/mcp/auto-approved"));
if (str_eq(raw, EL_STR(""))) {
return 0;
}
el_val_t list = json_get_raw(raw, EL_STR("tools"));
if (str_eq(list, EL_STR(""))) {
return 0;
}
return str_contains(list, el_str_concat(el_str_concat(EL_STR("\""), tool_name), EL_STR("\"")));
return 0;
}
el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input) {
if (str_eq(tool_name, EL_STR("read_file"))) {
el_val_t path = json_get(tool_input, EL_STR("path"));
@@ -248,6 +325,12 @@ el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input) {
el_val_t result = http_get(url);
return json_safe(result);
}
if (str_eq(tool_name, EL_STR("web_search"))) {
el_val_t query = json_get(tool_input, EL_STR("query"));
el_val_t encoded = str_replace(str_replace(str_replace(str_replace(query, EL_STR(" "), EL_STR("+")), EL_STR("\""), EL_STR("")), EL_STR("'"), EL_STR("")), EL_STR("&"), EL_STR("and"));
el_val_t rss = http_get(el_str_concat(el_str_concat(EL_STR("https://news.google.com/rss/search?q="), encoded), EL_STR("&hl=en-US&gl=US&ceid=US:en")));
return json_safe(rss);
}
if (str_eq(tool_name, EL_STR("search_memory"))) {
el_val_t query = json_get(tool_input, EL_STR("query"));
el_val_t result = engram_search_json(query, 10);
@@ -258,80 +341,299 @@ el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input) {
el_val_t result = exec_capture(cmd);
return json_safe(result);
}
if (str_begins(tool_name, EL_STR("mcp__"))) {
el_val_t out = call_mcp_bridge(tool_name, tool_input);
if (str_eq(out, EL_STR(""))) {
return json_safe(EL_STR("MCP bridge unreachable (neuron-connectd on :7771)"));
}
el_val_t content = json_get(out, EL_STR("content"));
if (str_eq(content, EL_STR(""))) {
el_val_t err = json_get(out, EL_STR("error"));
el_val_t msg = ({ el_val_t _if_result_21 = 0; if (str_eq(err, EL_STR(""))) { _if_result_21 = (EL_STR("MCP call failed")); } else { _if_result_21 = (el_str_concat(EL_STR("MCP error: "), err)); } _if_result_21; });
return json_safe(msg);
}
return json_safe(content);
}
return el_str_concat(EL_STR("unknown tool: "), tool_name);
return 0;
}
el_val_t json_array_append(el_val_t arr, el_val_t item) {
el_val_t eff = ({ el_val_t _if_result_22 = 0; if (str_eq(arr, EL_STR(""))) { _if_result_22 = (EL_STR("[]")); } else { _if_result_22 = (arr); } _if_result_22; });
el_val_t inner = str_slice(eff, 1, (str_len(eff) - 1));
if (str_eq(inner, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("["), item), EL_STR("]"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("["), inner), EL_STR(",")), item), EL_STR("]"));
return 0;
}
el_val_t append_tool_log(el_val_t log, el_val_t name) {
el_val_t quoted = el_str_concat(el_str_concat(EL_STR("\""), name), EL_STR("\""));
if (str_eq(log, EL_STR(""))) {
return quoted;
}
return el_str_concat(el_str_concat(log, EL_STR(",")), quoted);
return 0;
}
el_val_t exec_tool_block(el_val_t block) {
el_val_t t_id = json_get(block, EL_STR("id"));
el_val_t t_name = json_get(block, EL_STR("name"));
el_val_t t_input = json_get_raw(block, EL_STR("input"));
el_val_t raw = dispatch_tool(t_name, t_input);
el_val_t trunc = ({ el_val_t _if_result_23 = 0; if ((str_len(raw) > 6000)) { _if_result_23 = (el_str_concat(str_slice(raw, 0, 6000), EL_STR("...[truncated]"))); } else { _if_result_23 = (raw); } _if_result_23; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"tool_result\",\"tool_use_id\":\""), t_id), EL_STR("\",\"content\":\"")), trunc), EL_STR("\"}"));
return 0;
}
el_val_t agentic_blob(el_val_t model, el_val_t system, el_val_t tools_json, el_val_t messages, el_val_t origin, el_val_t approval, el_val_t iteration, el_val_t tools_log, el_val_t content, el_val_t queue, el_val_t results, el_val_t next) {
el_val_t appr = ({ el_val_t _if_result_24 = 0; if (approval) { _if_result_24 = (EL_STR("true")); } else { _if_result_24 = (EL_STR("false")); } _if_result_24; });
return 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_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("{\"model\":\""), model), EL_STR("\"")), EL_STR(",\"system\":\"")), json_safe(system)), EL_STR("\"")), EL_STR(",\"origin\":\"")), json_safe(origin)), EL_STR("\"")), EL_STR(",\"approval\":")), appr), EL_STR(",\"iteration\":")), int_to_str(iteration)), EL_STR(",\"next\":")), int_to_str(next)), EL_STR(",\"tools_log\":\"")), json_safe(tools_log)), EL_STR("\"")), EL_STR(",\"tools\":")), tools_json), EL_STR(",\"messages\":")), messages), EL_STR(",\"content\":")), content), EL_STR(",\"queue\":")), queue), EL_STR(",\"results\":")), results), EL_STR("}"));
return 0;
}
el_val_t extract_all_text(el_val_t s) {
el_val_t key = EL_STR("\"text\":\"");
el_val_t klen = str_len(key);
el_val_t n = str_len(s);
el_val_t out = EL_STR("");
el_val_t i = 0;
el_val_t inval = 0;
el_val_t esc = 0;
while (i < n) {
el_val_t at_key = ((!inval && ((i + klen) <= n)) && str_eq(str_slice(s, i, (i + klen)), key));
el_val_t ch = str_slice(s, i, (i + 1));
el_val_t was_esc = esc;
inval = ({ el_val_t _if_result_25 = 0; if (at_key) { _if_result_25 = (1); } else { _if_result_25 = (inval); } _if_result_25; });
el_val_t proc = (inval && !at_key);
esc = ({ el_val_t _if_result_26 = 0; if (((proc && !was_esc) && str_eq(ch, EL_STR("\\")))) { _if_result_26 = (1); } else { _if_result_26 = (0); } _if_result_26; });
el_val_t decoded = ({ el_val_t _if_result_27 = 0; if ((proc && was_esc)) { _if_result_27 = (({ el_val_t _if_result_28 = 0; if (str_eq(ch, EL_STR("n"))) { _if_result_28 = (EL_STR("\n")); } else { _if_result_28 = (({ el_val_t _if_result_29 = 0; if (str_eq(ch, EL_STR("t"))) { _if_result_29 = (EL_STR("\t")); } else { _if_result_29 = (({ el_val_t _if_result_30 = 0; if (str_eq(ch, EL_STR("r"))) { _if_result_30 = (EL_STR("")); } else { _if_result_30 = (({ el_val_t _if_result_31 = 0; if (str_eq(ch, EL_STR("\""))) { _if_result_31 = (EL_STR("\"")); } else { _if_result_31 = (({ el_val_t _if_result_32 = 0; if (str_eq(ch, EL_STR("\\"))) { _if_result_32 = (EL_STR("\\")); } else { _if_result_32 = (({ el_val_t _if_result_33 = 0; if (str_eq(ch, EL_STR("/"))) { _if_result_33 = (EL_STR("/")); } else { _if_result_33 = (ch); } _if_result_33; })); } _if_result_32; })); } _if_result_31; })); } _if_result_30; })); } _if_result_29; })); } _if_result_28; })); } else { _if_result_27 = (ch); } _if_result_27; });
el_val_t end_val = ((proc && !was_esc) && str_eq(ch, EL_STR("\"")));
el_val_t do_app = ((proc && !end_val) && !esc);
out = ({ el_val_t _if_result_34 = 0; if (do_app) { _if_result_34 = (el_str_concat(out, decoded)); } else { _if_result_34 = (out); } _if_result_34; });
inval = ({ el_val_t _if_result_35 = 0; if (end_val) { _if_result_35 = (0); } else { _if_result_35 = (inval); } _if_result_35; });
i = ({ el_val_t _if_result_36 = 0; if (at_key) { _if_result_36 = ((i + klen)); } else { _if_result_36 = ((i + 1)); } _if_result_36; });
}
return out;
return 0;
}
el_val_t strip_citations(el_val_t s) {
el_val_t marker = EL_STR(",\"citations\":[");
el_val_t mlen = str_len(marker);
el_val_t n = str_len(s);
el_val_t out = EL_STR("");
el_val_t i = 0;
el_val_t skip = 0;
while (i < n) {
el_val_t ch = str_slice(s, i, (i + 1));
el_val_t at_marker = (((skip == 0) && ((i + mlen) <= n)) && str_eq(str_slice(s, i, (i + mlen)), marker));
el_val_t was_skip = (skip > 0);
skip = ({ el_val_t _if_result_37 = 0; if (at_marker) { _if_result_37 = (1); } else { _if_result_37 = (({ el_val_t _if_result_38 = 0; if (was_skip) { _if_result_38 = (({ el_val_t _if_result_39 = 0; if (str_eq(ch, EL_STR("["))) { _if_result_39 = ((skip + 1)); } else { _if_result_39 = (({ el_val_t _if_result_40 = 0; if (str_eq(ch, EL_STR("]"))) { _if_result_40 = ((skip - 1)); } else { _if_result_40 = (skip); } _if_result_40; })); } _if_result_39; })); } else { _if_result_38 = (skip); } _if_result_38; })); } _if_result_37; });
out = ({ el_val_t _if_result_41 = 0; if (at_marker) { _if_result_41 = (out); } else { _if_result_41 = (({ el_val_t _if_result_42 = 0; if (was_skip) { _if_result_42 = (out); } else { _if_result_42 = (el_str_concat(out, ch)); } _if_result_42; })); } _if_result_41; });
i = ({ el_val_t _if_result_43 = 0; if (at_marker) { _if_result_43 = ((i + mlen)); } else { _if_result_43 = ((i + 1)); } _if_result_43; });
}
return out;
return 0;
}
el_val_t agentic_api_turn(el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages) {
el_val_t api_key = agentic_api_key();
el_val_t api_url = EL_STR("https://api.anthropic.com/v1/messages");
el_val_t h = el_map_new(0);
map_set(h, EL_STR("x-api-key"), api_key);
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 req_body = 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("{\"model\":\""), model), EL_STR("\"")), EL_STR(",\"max_tokens\":4096")), EL_STR(",\"system\":\"")), safe_sys), EL_STR("\"")), EL_STR(",\"tools\":")), tools_json), EL_STR(",\"messages\":")), messages), EL_STR("}"));
el_val_t raw_resp = http_post_with_headers(api_url, req_body, h);
el_val_t is_error = ((str_starts_with(raw_resp, EL_STR("{\"error\"")) || str_starts_with(raw_resp, EL_STR("{\"type\":\"error\""))) || str_contains(raw_resp, EL_STR("authentication_error")));
if (is_error) {
el_val_t detail_raw = ({ el_val_t _if_result_44 = 0; if ((str_len(raw_resp) > 300)) { _if_result_44 = (str_slice(raw_resp, 0, 300)); } else { _if_result_44 = (raw_resp); } _if_result_44; });
el_val_t detail = json_safe(detail_raw);
return el_str_concat(el_str_concat(EL_STR("{\"kind\":\"error\",\"payload\":{\"error\":\"llm unavailable\",\"reply\":\"\",\"detail\":\""), detail), EL_STR("\"}}"));
}
el_val_t stop_reason = json_get(raw_resp, EL_STR("stop_reason"));
if (str_eq(stop_reason, EL_STR("refusal"))) {
return EL_STR("{\"kind\":\"refusal\"}");
}
el_val_t content_arr = json_get_raw(raw_resp, EL_STR("content"));
el_val_t eff_content = ({ el_val_t _if_result_45 = 0; if (str_eq(content_arr, EL_STR(""))) { _if_result_45 = (EL_STR("[]")); } else { _if_result_45 = (content_arr); } _if_result_45; });
el_val_t walk_content = strip_citations(eff_content);
el_val_t text_out = extract_all_text(walk_content);
el_val_t queue = EL_STR("[]");
el_val_t ci = 0;
el_val_t c_total = json_array_len(eff_content);
while (ci < c_total) {
el_val_t block = json_array_get(eff_content, ci);
el_val_t btype = json_get(block, EL_STR("type"));
queue = ({ el_val_t _if_result_46 = 0; if (str_eq(btype, EL_STR("tool_use"))) { _if_result_46 = (json_array_append(queue, block)); } else { _if_result_46 = (queue); } _if_result_46; });
ci = (ci + 1);
}
el_val_t q_len = json_array_len(queue);
el_val_t is_tool_turn = (str_eq(stop_reason, EL_STR("tool_use")) && (q_len > 0));
if (is_tool_turn) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"kind\":\"tools\",\"content\":"), eff_content), EL_STR(",\"queue\":")), queue), EL_STR("}"));
}
if (str_eq(stop_reason, EL_STR("pause_turn"))) {
return el_str_concat(el_str_concat(EL_STR("{\"kind\":\"pause\",\"content\":"), eff_content), EL_STR("}"));
}
return el_str_concat(el_str_concat(EL_STR("{\"kind\":\"final\",\"text\":\""), json_safe(text_out)), EL_STR("\"}"));
return 0;
}
el_val_t agentic_engine(el_val_t session_id, el_val_t blob) {
el_val_t model = json_get(blob, EL_STR("model"));
el_val_t system = json_get(blob, EL_STR("system"));
el_val_t safe_sys = json_safe(system);
el_val_t origin = json_get(blob, EL_STR("origin"));
el_val_t approval = json_get_bool(blob, EL_STR("approval"));
el_val_t tools_json = json_get_raw(blob, EL_STR("tools"));
el_val_t pend_key = el_str_concat(EL_STR("agentic_pending_"), session_id);
el_val_t messages = json_get_raw(blob, EL_STR("messages"));
el_val_t content_in = json_get_raw(blob, EL_STR("content"));
el_val_t queue_in = json_get_raw(blob, EL_STR("queue"));
el_val_t results_in = json_get_raw(blob, EL_STR("results"));
el_val_t content = ({ el_val_t _if_result_47 = 0; if (str_eq(content_in, EL_STR(""))) { _if_result_47 = (EL_STR("[]")); } else { _if_result_47 = (content_in); } _if_result_47; });
el_val_t queue = ({ el_val_t _if_result_48 = 0; if (str_eq(queue_in, EL_STR(""))) { _if_result_48 = (EL_STR("[]")); } else { _if_result_48 = (queue_in); } _if_result_48; });
el_val_t results = ({ el_val_t _if_result_49 = 0; if (str_eq(results_in, EL_STR(""))) { _if_result_49 = (EL_STR("[]")); } else { _if_result_49 = (results_in); } _if_result_49; });
el_val_t next = json_get_int(blob, EL_STR("next"));
el_val_t iteration = json_get_int(blob, EL_STR("iteration"));
el_val_t tools_log = json_get(blob, EL_STR("tools_log"));
el_val_t steps = 0;
while (steps < 100) {
el_val_t q_len = json_array_len(queue);
el_val_t has_pending = (next < q_len);
if (has_pending && approval) {
el_val_t blk_a = json_array_get(queue, next);
el_val_t pend_name = json_get(blk_a, EL_STR("name"));
if (!tool_auto_approved(pend_name)) {
el_val_t park = agentic_blob(model, system, tools_json, messages, origin, approval, iteration, tools_log, content, queue, results, next);
state_set(pend_key, park);
el_val_t t_input = json_get_raw(blk_a, EL_STR("input"));
el_val_t eff_input = ({ el_val_t _if_result_50 = 0; if (str_eq(t_input, EL_STR(""))) { _if_result_50 = (EL_STR("{}")); } else { _if_result_50 = (t_input); } _if_result_50; });
return 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("{\"status\":\"tool_pending\",\"call_id\":\""), json_get(blk_a, EL_STR("id"))), EL_STR("\",\"tool_name\":\"")), json_get(blk_a, EL_STR("name"))), EL_STR("\",\"tool_input\":")), eff_input), EL_STR(",\"model\":\"")), model), EL_STR("\",\"reply\":\"\"}"));
}
}
el_val_t do_exec = has_pending;
el_val_t do_fold = (!has_pending && (q_len > 0));
el_val_t do_api = (!has_pending && (q_len < 1));
el_val_t blk = ({ el_val_t _if_result_51 = 0; if (do_exec) { _if_result_51 = (json_array_get(queue, next)); } else { _if_result_51 = (EL_STR("")); } _if_result_51; });
el_val_t t_msg = ({ el_val_t _if_result_52 = 0; if (do_exec) { _if_result_52 = (exec_tool_block(blk)); } else { _if_result_52 = (EL_STR("")); } _if_result_52; });
tools_log = ({ el_val_t _if_result_53 = 0; if (do_exec) { _if_result_53 = (append_tool_log(tools_log, json_get(blk, EL_STR("name")))); } else { _if_result_53 = (tools_log); } _if_result_53; });
results = ({ el_val_t _if_result_54 = 0; if (do_exec) { _if_result_54 = (json_array_append(results, t_msg)); } else { _if_result_54 = (results); } _if_result_54; });
next = ({ el_val_t _if_result_55 = 0; if (do_exec) { _if_result_55 = ((next + 1)); } else { _if_result_55 = (next); } _if_result_55; });
messages = ({ el_val_t _if_result_56 = 0; if (do_fold) { _if_result_56 = (json_array_append(json_array_append(messages, el_str_concat(el_str_concat(EL_STR("{\"role\":\"assistant\",\"content\":"), content), EL_STR("}"))), el_str_concat(el_str_concat(EL_STR("{\"role\":\"user\",\"content\":"), results), EL_STR("}")))); } else { _if_result_56 = (messages); } _if_result_56; });
content = ({ el_val_t _if_result_57 = 0; if (do_fold) { _if_result_57 = (EL_STR("[]")); } else { _if_result_57 = (content); } _if_result_57; });
queue = ({ el_val_t _if_result_58 = 0; if (do_fold) { _if_result_58 = (EL_STR("[]")); } else { _if_result_58 = (queue); } _if_result_58; });
results = ({ el_val_t _if_result_59 = 0; if (do_fold) { _if_result_59 = (EL_STR("[]")); } else { _if_result_59 = (results); } _if_result_59; });
next = ({ el_val_t _if_result_60 = 0; if (do_fold) { _if_result_60 = (0); } else { _if_result_60 = (next); } _if_result_60; });
if (do_api && (iteration >= 8)) {
state_set(pend_key, EL_STR(""));
return EL_STR("{\"error\":\"no response\",\"reply\":\"\"}");
}
el_val_t verdict = ({ el_val_t _if_result_61 = 0; if (do_api) { _if_result_61 = (agentic_api_turn(model, safe_sys, tools_json, messages)); } else { _if_result_61 = (EL_STR("")); } _if_result_61; });
el_val_t kind = ({ el_val_t _if_result_62 = 0; if (do_api) { _if_result_62 = (json_get(verdict, EL_STR("kind"))); } else { _if_result_62 = (EL_STR("")); } _if_result_62; });
if (str_eq(kind, EL_STR("error"))) {
state_set(pend_key, EL_STR(""));
return json_get_raw(verdict, EL_STR("payload"));
}
if (str_eq(kind, EL_STR("refusal"))) {
state_set(pend_key, EL_STR(""));
return el_str_concat(el_str_concat(EL_STR("{\"status\":\"ok\",\"reply\":\"I'm not able to help with that request.\",\"model\":\""), model), EL_STR("\",\"agentic\":true,\"tools_used\":[]}"));
}
if (str_eq(kind, EL_STR("final"))) {
state_set(pend_key, EL_STR(""));
el_val_t safe_text = json_safe(json_get(verdict, EL_STR("text")));
el_val_t tools_arr = ({ el_val_t _if_result_63 = 0; if (str_eq(tools_log, EL_STR(""))) { _if_result_63 = (EL_STR("[]")); } else { _if_result_63 = (el_str_concat(el_str_concat(EL_STR("["), tools_log), EL_STR("]"))); } _if_result_63; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"status\":\"ok\",\"reply\":\""), safe_text), EL_STR("\",\"model\":\"")), model), EL_STR("\",\"agentic\":true,\"tools_used\":")), tools_arr), EL_STR("}"));
}
el_val_t is_pause = str_eq(kind, EL_STR("pause"));
messages = ({ el_val_t _if_result_64 = 0; if (is_pause) { _if_result_64 = (json_array_append(messages, el_str_concat(el_str_concat(EL_STR("{\"role\":\"assistant\",\"content\":"), json_get_raw(verdict, EL_STR("content"))), EL_STR("}")))); } else { _if_result_64 = (messages); } _if_result_64; });
el_val_t is_tools = str_eq(kind, EL_STR("tools"));
content = ({ el_val_t _if_result_65 = 0; if (is_tools) { _if_result_65 = (json_get_raw(verdict, EL_STR("content"))); } else { _if_result_65 = (content); } _if_result_65; });
queue = ({ el_val_t _if_result_66 = 0; if (is_tools) { _if_result_66 = (json_get_raw(verdict, EL_STR("queue"))); } else { _if_result_66 = (queue); } _if_result_66; });
results = ({ el_val_t _if_result_67 = 0; if (is_tools) { _if_result_67 = (EL_STR("[]")); } else { _if_result_67 = (results); } _if_result_67; });
next = ({ el_val_t _if_result_68 = 0; if (is_tools) { _if_result_68 = (0); } else { _if_result_68 = (next); } _if_result_68; });
iteration = ({ el_val_t _if_result_69 = 0; if (do_api) { _if_result_69 = ((iteration + 1)); } else { _if_result_69 = (iteration); } _if_result_69; });
steps = (steps + 1);
}
state_set(pend_key, EL_STR(""));
return EL_STR("{\"error\":\"agentic engine step limit exceeded\",\"reply\":\"\"}");
return 0;
}
el_val_t handle_chat_agentic(el_val_t body) {
el_val_t message = json_get(body, EL_STR("message"));
if (str_eq(message, EL_STR(""))) {
return EL_STR("{\"error\":\"message required\",\"reply\":\"\"}");
}
el_val_t req_model = json_get(body, EL_STR("model"));
el_val_t model = ({ el_val_t _if_result_20 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_20 = (chat_default_model()); } else { _if_result_20 = (req_model); } _if_result_20; });
el_val_t model = ({ el_val_t _if_result_70 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_70 = (chat_default_model()); } else { _if_result_70 = (req_model); } _if_result_70; });
el_val_t session_id = json_get(body, EL_STR("session_id"));
el_val_t ra_bool = json_get_bool(body, EL_STR("require_approval"));
el_val_t ra_raw = json_get_raw(body, EL_STR("require_approval"));
el_val_t want_approval = ((ra_bool || str_eq(ra_raw, EL_STR("1"))) || str_eq(ra_raw, EL_STR("true")));
el_val_t approval = (want_approval && !str_eq(session_id, EL_STR("")));
el_val_t ctx = engram_compile(message);
el_val_t identity = state_get(EL_STR("soul_identity"));
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.\n\n")), ctx);
el_val_t api_key = agentic_api_key();
el_val_t tools_json = agentic_tools_literal();
el_val_t current_date = time_format(time_now(), EL_STR("%A, %B %d, %Y"));
el_val_t web_directive = el_str_concat(el_str_concat(EL_STR(" Today's date is "), current_date), EL_STR(". You have a web_search tool that returns live results from the internet. For ANY question about current events, live scores, standings, schedules, recent news, people, prices, or anything that may have changed since your training, you MUST call web_search immediately and answer from the results. Do NOT ask the user to clarify first, do NOT say you lack live access, and do NOT answer time-sensitive questions from memory alone \xe2\x80\x94 search, then answer. When a question is ambiguous about timeframe (e.g. 'the tournament', 'the game', 'the playoffs'), assume the user means whatever is happening or most recent RIGHT NOW as of today's date, search for that, and lead with it."));
el_val_t system = el_str_concat(el_str_concat(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, plus any connected MCP tools (named mcp__<server>__<tool>). Use them when they add genuine value. Be direct.")), web_directive), EL_STR("\n\n")), ctx);
el_val_t tools_json = agentic_tools_all();
el_val_t safe_msg = json_safe(message);
el_val_t safe_sys = json_safe(system);
el_val_t messages = el_str_concat(el_str_concat(EL_STR("[{\"role\":\"user\",\"content\":\""), safe_msg), EL_STR("\"}]"));
el_val_t api_url = EL_STR("https://api.anthropic.com/v1/messages");
el_val_t h = el_map_new(0);
map_set(h, EL_STR("x-api-key"), api_key);
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 final_text = EL_STR("");
el_val_t tools_log = EL_STR("");
el_val_t iteration = 0;
el_val_t keep_going = 1;
while (keep_going && (iteration < 8)) {
el_val_t req_body = 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("{\"model\":\""), model), EL_STR("\"")), EL_STR(",\"max_tokens\":4096")), EL_STR(",\"system\":\"")), safe_sys), EL_STR("\"")), EL_STR(",\"tools\":")), tools_json), EL_STR(",\"messages\":")), messages), EL_STR("}"));
el_val_t raw_resp = http_post_with_headers(api_url, req_body, h);
el_val_t is_error = ((str_starts_with(raw_resp, EL_STR("{\"error\"")) || str_starts_with(raw_resp, EL_STR("{\"type\":\"error\""))) || str_contains(raw_resp, EL_STR("authentication_error")));
if (is_error) {
return EL_STR("{\"error\":\"llm unavailable\",\"reply\":\"\"}");
}
el_val_t stop_reason = json_get(raw_resp, EL_STR("stop_reason"));
el_val_t content_arr = json_get_raw(raw_resp, EL_STR("content"));
el_val_t eff_content = ({ el_val_t _if_result_21 = 0; if (str_eq(content_arr, EL_STR(""))) { _if_result_21 = (EL_STR("[]")); } else { _if_result_21 = (content_arr); } _if_result_21; });
el_val_t text_out = EL_STR("");
el_val_t has_tool = 0;
el_val_t tool_id = EL_STR("");
el_val_t tool_name = EL_STR("");
el_val_t tool_input = EL_STR("");
el_val_t ci = 0;
el_val_t c_total = json_array_len(eff_content);
while (ci < c_total) {
el_val_t block = json_array_get(eff_content, ci);
el_val_t btype = json_get(block, EL_STR("type"));
text_out = ({ el_val_t _if_result_22 = 0; if (str_eq(btype, EL_STR("text"))) { _if_result_22 = (el_str_concat(text_out, json_get(block, EL_STR("text")))); } else { _if_result_22 = (text_out); } _if_result_22; });
el_val_t is_new_tool = (str_eq(btype, EL_STR("tool_use")) && !has_tool);
has_tool = ({ el_val_t _if_result_23 = 0; if (is_new_tool) { _if_result_23 = (1); } else { _if_result_23 = (has_tool); } _if_result_23; });
tool_id = ({ el_val_t _if_result_24 = 0; if (is_new_tool) { _if_result_24 = (json_get(block, EL_STR("id"))); } else { _if_result_24 = (tool_id); } _if_result_24; });
tool_name = ({ el_val_t _if_result_25 = 0; if (is_new_tool) { _if_result_25 = (json_get(block, EL_STR("name"))); } else { _if_result_25 = (tool_name); } _if_result_25; });
tool_input = ({ el_val_t _if_result_26 = 0; if (is_new_tool) { _if_result_26 = (json_get_raw(block, EL_STR("input"))); } else { _if_result_26 = (tool_input); } _if_result_26; });
ci = (ci + 1);
}
el_val_t tool_result_raw = ({ el_val_t _if_result_27 = 0; if (has_tool) { _if_result_27 = (dispatch_tool(tool_name, tool_input)); } else { _if_result_27 = (EL_STR("")); } _if_result_27; });
el_val_t tool_result = ({ el_val_t _if_result_28 = 0; if ((str_len(tool_result_raw) > 6000)) { _if_result_28 = (el_str_concat(str_slice(tool_result_raw, 0, 6000), EL_STR("...[truncated]"))); } else { _if_result_28 = (tool_result_raw); } _if_result_28; });
el_val_t tool_msg = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"tool_result\",\"tool_use_id\":\""), tool_id), EL_STR("\",\"content\":\"")), tool_result), EL_STR("\"}"));
el_val_t tool_quoted = el_str_concat(el_str_concat(EL_STR("\""), tool_name), EL_STR("\""));
tools_log = ({ el_val_t _if_result_29 = 0; if (has_tool) { _if_result_29 = (({ el_val_t _if_result_30 = 0; if (str_eq(tools_log, EL_STR(""))) { _if_result_30 = (tool_quoted); } else { _if_result_30 = (el_str_concat(el_str_concat(tools_log, EL_STR(",")), tool_quoted)); } _if_result_30; })); } else { _if_result_29 = (tools_log); } _if_result_29; });
el_val_t is_tool_turn = (str_eq(stop_reason, EL_STR("tool_use")) && has_tool);
el_val_t inner = str_slice(messages, 1, (str_len(messages) - 1));
messages = ({ el_val_t _if_result_31 = 0; if (is_tool_turn) { _if_result_31 = (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("["), inner), EL_STR(",{\"role\":\"assistant\",\"content\":")), eff_content), EL_STR("}")), EL_STR(",{\"role\":\"user\",\"content\":[")), tool_msg), EL_STR("]}")), EL_STR("]"))); } else { _if_result_31 = (messages); } _if_result_31; });
final_text = ({ el_val_t _if_result_32 = 0; if (!is_tool_turn) { _if_result_32 = (text_out); } else { _if_result_32 = (final_text); } _if_result_32; });
keep_going = ({ el_val_t _if_result_33 = 0; if (!is_tool_turn) { _if_result_33 = (0); } else { _if_result_33 = (keep_going); } _if_result_33; });
iteration = (iteration + 1);
if (!str_eq(session_id, EL_STR(""))) {
state_set(el_str_concat(EL_STR("agentic_pending_"), session_id), EL_STR(""));
}
if (str_eq(final_text, EL_STR(""))) {
return EL_STR("{\"error\":\"no response\",\"reply\":\"\"}");
el_val_t blob = agentic_blob(model, system, tools_json, messages, message, approval, 0, EL_STR(""), EL_STR("[]"), EL_STR("[]"), EL_STR("[]"), 0);
return agentic_engine(session_id, blob);
return 0;
}
el_val_t handle_session_approve(el_val_t session_id, el_val_t body) {
if (str_eq(session_id, EL_STR(""))) {
return EL_STR("{\"error\":\"session_id required\",\"reply\":\"\"}");
}
el_val_t safe_text = json_safe(final_text);
el_val_t tools_arr = ({ el_val_t _if_result_34 = 0; if (str_eq(tools_log, EL_STR(""))) { _if_result_34 = (EL_STR("[]")); } else { _if_result_34 = (el_str_concat(el_str_concat(EL_STR("["), tools_log), EL_STR("]"))); } _if_result_34; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"reply\":\""), safe_text), EL_STR("\",\"model\":\"")), model), EL_STR("\",\"agentic\":true,\"tools_used\":")), tools_arr), EL_STR("}"));
el_val_t pend_key = el_str_concat(EL_STR("agentic_pending_"), session_id);
el_val_t blob = state_get(pend_key);
if (str_eq(blob, EL_STR(""))) {
return EL_STR("{\"error\":\"no pending tool call for this session\",\"reply\":\"\"}");
}
el_val_t call_id = json_get(body, EL_STR("call_id"));
el_val_t action = json_get(body, EL_STR("action"));
el_val_t queue = json_get_raw(blob, EL_STR("queue"));
el_val_t next = json_get_int(blob, EL_STR("next"));
el_val_t q_len = json_array_len(queue);
if (next >= q_len) {
state_set(pend_key, EL_STR(""));
return EL_STR("{\"error\":\"pending state corrupt\",\"reply\":\"\"}");
}
el_val_t block = json_array_get(queue, next);
el_val_t block_id = json_get(block, EL_STR("id"));
if (!str_eq(call_id, block_id)) {
return EL_STR("{\"error\":\"stale call_id\",\"reply\":\"\"}");
}
el_val_t deny = str_eq(action, EL_STR("deny"));
el_val_t t_msg = ({ el_val_t _if_result_71 = 0; if (deny) { _if_result_71 = (el_str_concat(el_str_concat(EL_STR("{\"type\":\"tool_result\",\"tool_use_id\":\""), block_id), EL_STR("\",\"content\":\"User denied this tool call.\"}"))); } else { _if_result_71 = (exec_tool_block(block)); } _if_result_71; });
el_val_t tools_log_in = json_get(blob, EL_STR("tools_log"));
el_val_t tools_log = ({ el_val_t _if_result_72 = 0; if (deny) { _if_result_72 = (tools_log_in); } else { _if_result_72 = (append_tool_log(tools_log_in, json_get(block, EL_STR("name")))); } _if_result_72; });
el_val_t results = json_array_append(json_get_raw(blob, EL_STR("results")), t_msg);
el_val_t model = json_get(blob, EL_STR("model"));
el_val_t system = json_get(blob, EL_STR("system"));
el_val_t origin = json_get(blob, EL_STR("origin"));
el_val_t tools_json = json_get_raw(blob, EL_STR("tools"));
el_val_t messages = json_get_raw(blob, EL_STR("messages"));
el_val_t content = json_get_raw(blob, EL_STR("content"));
el_val_t iteration = json_get_int(blob, EL_STR("iteration"));
state_set(pend_key, EL_STR(""));
el_val_t updated = agentic_blob(model, system, tools_json, messages, origin, 1, iteration, tools_log, content, queue, results, (next + 1));
el_val_t reply = agentic_engine(session_id, updated);
el_val_t is_final = str_contains(reply, EL_STR("\"status\":\"ok\""));
if (is_final) {
auto_persist(el_str_concat(el_str_concat(EL_STR("{\"message\":\""), json_safe(origin)), EL_STR("\"}")), reply);
}
return reply;
return 0;
}
@@ -346,12 +648,12 @@ el_val_t handle_chat_as_soul(el_val_t body) {
}
el_val_t message = json_get(body, EL_STR("message"));
el_val_t transcript = json_get(body, EL_STR("transcript"));
el_val_t eff_message = ({ el_val_t _if_result_35 = 0; if (str_eq(message, EL_STR(""))) { _if_result_35 = (transcript); } else { _if_result_35 = (message); } _if_result_35; });
el_val_t eff_message = ({ el_val_t _if_result_73 = 0; if (str_eq(message, EL_STR(""))) { _if_result_73 = (transcript); } else { _if_result_73 = (message); } _if_result_73; });
if (str_eq(eff_message, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"message or transcript is required\",\"response\":\"\",\"speaker_slug\":\""), speaker), EL_STR("\"}"));
}
el_val_t req_model = json_get(body, EL_STR("model"));
el_val_t model = ({ el_val_t _if_result_36 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_36 = (chat_default_model()); } else { _if_result_36 = (req_model); } _if_result_36; });
el_val_t model = ({ el_val_t _if_result_74 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_74 = (chat_default_model()); } else { _if_result_74 = (req_model); } _if_result_74; });
el_val_t raw_response = llm_call_system(model, system_prompt, eff_message);
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) {
@@ -373,7 +675,7 @@ el_val_t handle_dharma_room_turn(el_val_t body) {
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);
el_val_t system_prompt = ({ el_val_t _if_result_37 = 0; if (str_eq(engram_ctx, EL_STR(""))) { _if_result_37 = (identity); } else { _if_result_37 = (el_str_concat(el_str_concat(identity, EL_STR("\n\n")), engram_ctx)); } _if_result_37; });
el_val_t system_prompt = ({ el_val_t _if_result_75 = 0; if (str_eq(engram_ctx, EL_STR(""))) { _if_result_75 = (identity); } else { _if_result_75 = (el_str_concat(el_str_concat(identity, EL_STR("\n\n")), engram_ctx)); } _if_result_75; });
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")));
if (is_error) {
@@ -400,65 +702,19 @@ el_val_t handle_dharma_room_turn_agentic(el_val_t body) {
}
el_val_t ctx = engram_compile(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();
el_val_t tools_json = agentic_tools_literal();
el_val_t safe_transcript = json_safe(transcript);
el_val_t safe_sys = json_safe(system);
el_val_t messages = el_str_concat(el_str_concat(EL_STR("[{\"role\":\"user\",\"content\":\""), safe_transcript), EL_STR("\"}]"));
el_val_t api_url = EL_STR("https://api.anthropic.com/v1/messages");
el_val_t h = el_map_new(0);
map_set(h, EL_STR("x-api-key"), api_key);
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 final_text = EL_STR("");
el_val_t tools_log = EL_STR("");
el_val_t iteration = 0;
el_val_t keep_going = 1;
while (keep_going && (iteration < 8)) {
el_val_t req_body = 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("{\"model\":\""), model), EL_STR("\"")), EL_STR(",\"max_tokens\":4096")), EL_STR(",\"system\":\"")), safe_sys), EL_STR("\"")), EL_STR(",\"tools\":")), tools_json), EL_STR(",\"messages\":")), messages), EL_STR("}"));
el_val_t raw_resp = http_post_with_headers(api_url, req_body, h);
el_val_t is_error = ((str_starts_with(raw_resp, EL_STR("{\"error\"")) || str_starts_with(raw_resp, EL_STR("{\"type\":\"error\""))) || str_contains(raw_resp, EL_STR("authentication_error")));
if (is_error) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"llm unavailable\",\"response\":\"\",\"cgi_id\":\""), cgi_id), EL_STR("\"}"));
}
el_val_t stop_reason = json_get(raw_resp, EL_STR("stop_reason"));
el_val_t content_arr = json_get_raw(raw_resp, EL_STR("content"));
el_val_t eff_content = ({ el_val_t _if_result_38 = 0; if (str_eq(content_arr, EL_STR(""))) { _if_result_38 = (EL_STR("[]")); } else { _if_result_38 = (content_arr); } _if_result_38; });
el_val_t text_out = EL_STR("");
el_val_t has_tool = 0;
el_val_t tool_id = EL_STR("");
el_val_t tool_name = EL_STR("");
el_val_t tool_input = EL_STR("");
el_val_t ci = 0;
el_val_t c_total = json_array_len(eff_content);
while (ci < c_total) {
el_val_t block = json_array_get(eff_content, ci);
el_val_t btype = json_get(block, EL_STR("type"));
text_out = ({ el_val_t _if_result_39 = 0; if (str_eq(btype, EL_STR("text"))) { _if_result_39 = (el_str_concat(text_out, json_get(block, EL_STR("text")))); } else { _if_result_39 = (text_out); } _if_result_39; });
el_val_t is_new_tool = (str_eq(btype, EL_STR("tool_use")) && !has_tool);
has_tool = ({ el_val_t _if_result_40 = 0; if (is_new_tool) { _if_result_40 = (1); } else { _if_result_40 = (has_tool); } _if_result_40; });
tool_id = ({ el_val_t _if_result_41 = 0; if (is_new_tool) { _if_result_41 = (json_get(block, EL_STR("id"))); } else { _if_result_41 = (tool_id); } _if_result_41; });
tool_name = ({ el_val_t _if_result_42 = 0; if (is_new_tool) { _if_result_42 = (json_get(block, EL_STR("name"))); } else { _if_result_42 = (tool_name); } _if_result_42; });
tool_input = ({ el_val_t _if_result_43 = 0; if (is_new_tool) { _if_result_43 = (json_get_raw(block, EL_STR("input"))); } else { _if_result_43 = (tool_input); } _if_result_43; });
ci = (ci + 1);
}
el_val_t tool_result_raw = ({ el_val_t _if_result_44 = 0; if (has_tool) { _if_result_44 = (dispatch_tool(tool_name, tool_input)); } else { _if_result_44 = (EL_STR("")); } _if_result_44; });
el_val_t tool_result = ({ el_val_t _if_result_45 = 0; if ((str_len(tool_result_raw) > 6000)) { _if_result_45 = (el_str_concat(str_slice(tool_result_raw, 0, 6000), EL_STR("...[truncated]"))); } else { _if_result_45 = (tool_result_raw); } _if_result_45; });
el_val_t tool_msg = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"tool_result\",\"tool_use_id\":\""), tool_id), EL_STR("\",\"content\":\"")), tool_result), EL_STR("\"}"));
el_val_t tool_quoted = el_str_concat(el_str_concat(EL_STR("\""), tool_name), EL_STR("\""));
tools_log = ({ el_val_t _if_result_46 = 0; if (has_tool) { _if_result_46 = (({ el_val_t _if_result_47 = 0; if (str_eq(tools_log, EL_STR(""))) { _if_result_47 = (tool_quoted); } else { _if_result_47 = (el_str_concat(el_str_concat(tools_log, EL_STR(",")), tool_quoted)); } _if_result_47; })); } else { _if_result_46 = (tools_log); } _if_result_46; });
el_val_t is_tool_turn = (str_eq(stop_reason, EL_STR("tool_use")) && has_tool);
el_val_t inner = str_slice(messages, 1, (str_len(messages) - 1));
messages = ({ el_val_t _if_result_48 = 0; if (is_tool_turn) { _if_result_48 = (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("["), inner), EL_STR(",{\"role\":\"assistant\",\"content\":")), eff_content), EL_STR("}")), EL_STR(",{\"role\":\"user\",\"content\":[")), tool_msg), EL_STR("]}")), EL_STR("]"))); } else { _if_result_48 = (messages); } _if_result_48; });
final_text = ({ el_val_t _if_result_49 = 0; if (!is_tool_turn) { _if_result_49 = (text_out); } else { _if_result_49 = (final_text); } _if_result_49; });
keep_going = ({ el_val_t _if_result_50 = 0; if (!is_tool_turn) { _if_result_50 = (0); } else { _if_result_50 = (keep_going); } _if_result_50; });
iteration = (iteration + 1);
el_val_t blob = agentic_blob(model, system, tools_json, messages, transcript, 0, 0, EL_STR(""), EL_STR("[]"), EL_STR("[]"), EL_STR("[]"), 0);
el_val_t reply = agentic_engine(EL_STR(""), blob);
el_val_t err = json_get(reply, EL_STR("error"));
if (!str_eq(err, EL_STR(""))) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"error\":\""), json_safe(err)), EL_STR("\",\"response\":\"\",\"cgi_id\":\"")), cgi_id), EL_STR("\"}"));
}
if (str_eq(final_text, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"no response\",\"response\":\"\",\"cgi_id\":\""), cgi_id), EL_STR("\"}"));
}
el_val_t safe_text = json_safe(final_text);
el_val_t tools_arr = ({ el_val_t _if_result_51 = 0; if (str_eq(tools_log, EL_STR(""))) { _if_result_51 = (EL_STR("[]")); } else { _if_result_51 = (el_str_concat(el_str_concat(EL_STR("["), tools_log), EL_STR("]"))); } _if_result_51; });
el_val_t text = json_get(reply, EL_STR("reply"));
el_val_t safe_text = json_safe(text);
el_val_t tools_used = json_get_raw(reply, EL_STR("tools_used"));
el_val_t tools_arr = ({ el_val_t _if_result_76 = 0; if (str_eq(tools_used, EL_STR(""))) { _if_result_76 = (EL_STR("[]")); } else { _if_result_76 = (tools_used); } _if_result_76; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"response\":\""), safe_text), EL_STR("\",\"cgi_id\":\"")), cgi_id), EL_STR("\",\"tools_used\":")), tools_arr), EL_STR("}"));
return 0;
}
@@ -466,7 +722,7 @@ el_val_t handle_dharma_room_turn_agentic(el_val_t body) {
el_val_t auto_persist(el_val_t req, el_val_t resp) {
el_val_t message = json_get(req, EL_STR("message"));
el_val_t reply = json_get(resp, EL_STR("response"));
el_val_t reply2 = ({ el_val_t _if_result_52 = 0; if (str_eq(reply, EL_STR(""))) { _if_result_52 = (json_get(resp, EL_STR("reply"))); } else { _if_result_52 = (reply); } _if_result_52; });
el_val_t reply2 = ({ el_val_t _if_result_77 = 0; if (str_eq(reply, EL_STR(""))) { _if_result_77 = (json_get(resp, EL_STR("reply"))); } else { _if_result_77 = (reply); } _if_result_77; });
if (str_eq(message, EL_STR(""))) {
return EL_STR("");
}
+90 -6
View File
@@ -40,8 +40,18 @@ el_val_t handle_see(el_val_t body);
el_val_t studio_tools_json(void);
el_val_t agentic_api_key(void);
el_val_t agentic_tools_literal(void);
el_val_t agentic_tools_with_web(void);
el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input);
el_val_t json_array_append(el_val_t arr, el_val_t item);
el_val_t append_tool_log(el_val_t log, el_val_t name);
el_val_t exec_tool_block(el_val_t block);
el_val_t agentic_blob(el_val_t model, el_val_t system, el_val_t tools_json, el_val_t messages, el_val_t origin, el_val_t approval, el_val_t iteration, el_val_t tools_log, el_val_t content, el_val_t queue, el_val_t results, el_val_t next);
el_val_t extract_all_text(el_val_t s);
el_val_t strip_citations(el_val_t s);
el_val_t agentic_api_turn(el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages);
el_val_t agentic_engine(el_val_t session_id, el_val_t blob);
el_val_t handle_chat_agentic(el_val_t body);
el_val_t handle_session_approve(el_val_t session_id, el_val_t body);
el_val_t handle_chat_as_soul(el_val_t body);
el_val_t handle_dharma_room_turn(el_val_t body);
el_val_t handle_dharma_room_turn_agentic(el_val_t body);
@@ -89,6 +99,7 @@ el_val_t handle_api_link_entities(el_val_t body);
el_val_t handle_api_list_typed(el_val_t node_type, el_val_t path, el_val_t body);
el_val_t handle_api_consolidate(el_val_t body);
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);
@@ -98,6 +109,9 @@ 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 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 strip_query(el_val_t path) {
@@ -109,6 +123,11 @@ el_val_t strip_query(el_val_t path) {
return 0;
}
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;
@@ -201,9 +220,12 @@ el_val_t handle_dharma_recv(el_val_t body) {
if (str_eq(eff_event, EL_STR("chat"))) {
el_val_t msg = json_get(eff_payload, EL_STR("message"));
el_val_t chat_body = ({ el_val_t _if_result_5 = 0; if (str_eq(msg, EL_STR(""))) { _if_result_5 = (el_str_concat(el_str_concat(EL_STR("{\"message\":\""), str_replace(str_replace(eff_payload, EL_STR("\\"), EL_STR("\\\\")), EL_STR("\""), EL_STR("\\\""))), EL_STR("\"}"))); } else { _if_result_5 = (eff_payload); } _if_result_5; });
el_val_t agentic_flag = json_get_bool(eff_payload, EL_STR("agentic"));
el_val_t agentic_flag = flag_true(eff_payload, EL_STR("agentic"));
el_val_t reply = ({ el_val_t _if_result_6 = 0; if (agentic_flag) { _if_result_6 = (handle_chat_agentic(chat_body)); } else { _if_result_6 = (handle_chat(chat_body)); } _if_result_6; });
auto_persist(chat_body, reply);
el_val_t is_pending = str_contains(reply, EL_STR("\"status\":\"tool_pending\""));
if (!is_pending) {
auto_persist(chat_body, reply);
}
return reply;
}
if (str_eq(eff_event, EL_STR("memory"))) {
@@ -254,6 +276,53 @@ el_val_t route_sessions(void) {
return 0;
}
el_val_t connectd_get(el_val_t suffix) {
el_val_t out = exec_capture(el_str_concat(EL_STR("curl -s --max-time 5 http://127.0.0.1:7771"), suffix));
if (str_eq(out, EL_STR(""))) {
return EL_STR("{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}");
}
return out;
return 0;
}
el_val_t connectd_post(el_val_t suffix, el_val_t body) {
el_val_t eff = ({ el_val_t _if_result_10 = 0; if (str_eq(body, EL_STR(""))) { _if_result_10 = (EL_STR("{}")); } else { _if_result_10 = (body); } _if_result_10; });
el_val_t tmp = EL_STR("/tmp/neuron-connectors-req.json");
fs_write(tmp, eff);
el_val_t out = exec_capture(el_str_concat(el_str_concat(el_str_concat(EL_STR("curl -s --max-time 20 -X POST http://127.0.0.1:7771"), suffix), EL_STR(" -H 'Content-Type: application/json' -d @")), tmp));
if (str_eq(out, EL_STR(""))) {
return EL_STR("{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}");
}
return out;
return 0;
}
el_val_t handle_connectors(el_val_t method, el_val_t clean, el_val_t body) {
if (str_eq(method, EL_STR("GET"))) {
return connectd_get(EL_STR("/mcp/servers"));
}
if (str_eq(clean, EL_STR("/api/connectors/add"))) {
return connectd_post(EL_STR("/mcp/servers/add"), body);
}
if (str_eq(clean, EL_STR("/api/connectors/toggle"))) {
return connectd_post(EL_STR("/mcp/servers/toggle"), body);
}
if (str_eq(clean, EL_STR("/api/connectors/auto-approve"))) {
return connectd_post(EL_STR("/mcp/servers/auto-approve"), body);
}
if (str_eq(clean, EL_STR("/api/connectors/remove"))) {
return connectd_post(EL_STR("/mcp/servers/remove"), body);
}
if (str_eq(clean, EL_STR("/api/connectors/secret"))) {
return connectd_post(EL_STR("/mcp/servers/secret"), body);
}
if (str_eq(clean, EL_STR("/api/connectors/oauth/start"))) {
return connectd_post(EL_STR("/mcp/oauth/start"), body);
}
return EL_STR("{\"ok\":false,\"error\":\"unknown connectors route\"}");
return 0;
}
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
el_val_t clean = strip_query(path);
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/dharma/recv"))) {
@@ -277,7 +346,7 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
el_val_t edges_raw = json_get_raw(snap, EL_STR("edges"));
return ({ el_val_t _if_result_10 = 0; if (str_eq(edges_raw, EL_STR(""))) { _if_result_10 = (EL_STR("[]")); } else { _if_result_10 = (edges_raw); } _if_result_10; });
return ({ el_val_t _if_result_11 = 0; if (str_eq(edges_raw, EL_STR(""))) { _if_result_11 = (EL_STR("[]")); } else { _if_result_11 = (edges_raw); } _if_result_11; });
}
if (str_eq(clean, EL_STR("/api/chat"))) {
return handle_chat(body);
@@ -349,6 +418,9 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_starts_with(clean, EL_STR("/api/neuron/recall"))) {
return handle_api_recall(method, path, body);
}
if (str_starts_with(clean, EL_STR("/api/connectors"))) {
return handle_connectors(method, clean, body);
}
return err_404(clean);
}
if (str_eq(method, EL_STR("POST"))) {
@@ -365,11 +437,20 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
return handle_elp_chat(body);
}
if (str_eq(clean, EL_STR("/api/chat"))) {
el_val_t agentic_flag = json_get_bool(body, EL_STR("agentic"));
el_val_t reply = ({ el_val_t _if_result_11 = 0; if (agentic_flag) { _if_result_11 = (handle_chat_agentic(body)); } else { _if_result_11 = (handle_chat(body)); } _if_result_11; });
auto_persist(body, reply);
el_val_t agentic_flag = flag_true(body, EL_STR("agentic"));
el_val_t reply = ({ el_val_t _if_result_12 = 0; if (agentic_flag) { _if_result_12 = (handle_chat_agentic(body)); } else { _if_result_12 = (handle_chat(body)); } _if_result_12; });
el_val_t is_pending = str_contains(reply, EL_STR("\"status\":\"tool_pending\""));
if (!is_pending) {
auto_persist(body, reply);
}
return reply;
}
if (str_starts_with(clean, EL_STR("/api/sessions/")) && str_ends_with(clean, EL_STR("/approve"))) {
el_val_t sid_start = str_len(EL_STR("/api/sessions/"));
el_val_t sid_end = (str_len(clean) - str_len(EL_STR("/approve")));
el_val_t sid = str_slice(clean, sid_start, sid_end);
return handle_session_approve(sid, body);
}
if (str_eq(clean, EL_STR("/api/see"))) {
return handle_see(body);
}
@@ -406,6 +487,9 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_starts_with(clean, EL_STR("/api/imprints"))) {
return axon_post(clean, body);
}
if (str_starts_with(clean, EL_STR("/api/connectors"))) {
return handle_connectors(method, clean, body);
}
if (str_eq(clean, EL_STR("/api/neuron/session/begin"))) {
return handle_api_begin_session(body);
}
+89 -4
View File
@@ -13,6 +13,15 @@ fn strip_query(path: String) -> String {
return str_slice(path, 0, q)
}
// Truthy flag test tolerant of BOTH the boolean form (Kotlin UI sends true) and
// the integer form (el-src UI sends 1). json_get_bool only recognizes literal
// `true`, so without this an "agentic":1 request silently routes to the
// non-agentic, tool-less path which is exactly how the UI ended up with no
// tools.
fn flag_true(body: String, key: String) -> Bool {
return json_get_bool(body, key) || json_get_int(body, key) > 0
}
fn err_404(path: String) -> String {
return "{\"error\":\"not found\",\"path\":\"" + path + "\"}"
}
@@ -142,13 +151,18 @@ fn handle_dharma_recv(body: String) -> String {
} else {
eff_payload
}
let agentic_flag: Bool = json_get_bool(eff_payload, "agentic")
let agentic_flag: Bool = flag_true(eff_payload, "agentic")
let reply: String = if agentic_flag {
handle_chat_agentic(chat_body)
} else {
handle_chat(chat_body)
}
auto_persist(chat_body, reply)
// A paused tool loop is not a completed exchange the approve
// handler persists once the loop finishes.
let is_pending: Bool = str_contains(reply, "\"status\":\"tool_pending\"")
if !is_pending {
auto_persist(chat_body, reply)
}
return reply
}
@@ -203,6 +217,58 @@ fn route_sessions() -> String {
return results
}
// Connectors API (Phase 4)
// Thin proxy from the UI to the neuron-connectd bridge on 127.0.0.1:7771, so the
// app talks to ONE origin (the soul) and never reaches the bridge directly. The
// bridge owns all MCP/config complexity; these handlers just forward + relay.
fn connectd_get(suffix: String) -> String {
let out: String = exec_capture("curl -s --max-time 5 http://127.0.0.1:7771" + suffix)
if str_eq(out, "") {
return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}"
}
return out
}
// POST passthrough: the request body is written to a temp file and handed to curl
// via -d @file, so arbitrary JSON can never reach the shell as an argument.
fn connectd_post(suffix: String, body: String) -> String {
let eff: String = if str_eq(body, "") { "{}" } else { body }
let tmp: String = "/tmp/neuron-connectors-req.json"
fs_write(tmp, eff)
let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp)
if str_eq(out, "") {
return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}"
}
return out
}
fn handle_connectors(method: String, clean: String, body: String) -> String {
if str_eq(method, "GET") {
// /api/connectors -> each configured server with status, tools, auth, auto-approve.
return connectd_get("/mcp/servers")
}
if str_eq(clean, "/api/connectors/add") {
return connectd_post("/mcp/servers/add", body)
}
if str_eq(clean, "/api/connectors/toggle") {
return connectd_post("/mcp/servers/toggle", body)
}
if str_eq(clean, "/api/connectors/auto-approve") {
return connectd_post("/mcp/servers/auto-approve", body)
}
if str_eq(clean, "/api/connectors/remove") {
return connectd_post("/mcp/servers/remove", body)
}
if str_eq(clean, "/api/connectors/secret") {
return connectd_post("/mcp/servers/secret", body)
}
if str_eq(clean, "/api/connectors/oauth/start") {
return connectd_post("/mcp/oauth/start", body)
}
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
}
fn handle_request(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
@@ -301,6 +367,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_starts_with(clean, "/api/neuron/recall") {
return handle_api_recall(method, path, body)
}
if str_starts_with(clean, "/api/connectors") {
return handle_connectors(method, clean, body)
}
return err_404(clean)
}
@@ -318,15 +387,28 @@ fn handle_request(method: String, path: String, body: String) -> String {
return handle_elp_chat(body)
}
if str_eq(clean, "/api/chat") {
let agentic_flag: Bool = json_get_bool(body, "agentic")
let agentic_flag: Bool = flag_true(body, "agentic")
let reply: String = if agentic_flag {
handle_chat_agentic(body)
} else {
handle_chat(body)
}
auto_persist(body, reply)
// A paused tool loop is not a completed exchange the approve
// handler persists once the loop finishes.
let is_pending: Bool = str_contains(reply, "\"status\":\"tool_pending\"")
if !is_pending {
auto_persist(body, reply)
}
return reply
}
// The desktop UI's tool-approval flow: resume a parked agentic loop
// with the user's allow/deny decision on the pending tool call.
if str_starts_with(clean, "/api/sessions/") && str_ends_with(clean, "/approve") {
let sid_start: Int = str_len("/api/sessions/")
let sid_end: Int = str_len(clean) - str_len("/approve")
let sid: String = str_slice(clean, sid_start, sid_end)
return handle_session_approve(sid, body)
}
if str_eq(clean, "/api/see") {
return handle_see(body)
}
@@ -363,6 +445,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_starts_with(clean, "/api/imprints") {
return axon_post(clean, body)
}
if str_starts_with(clean, "/api/connectors") {
return handle_connectors(method, clean, body)
}
// Neuron cognitive API POST endpoints
if str_eq(clean, "/api/neuron/session/begin") {
return handle_api_begin_session(body)