Fix agentic tool loop: El scoping rules, json_get_raw for array/object fields, result truncation

This commit is contained in:
Will Anderson
2026-05-03 12:36:42 -05:00
parent af2ce3ddf3
commit d500415316
9 changed files with 179 additions and 48 deletions
Vendored
+104 -7
View File
@@ -24,6 +24,9 @@ el_val_t clean_llm_response(el_val_t s);
el_val_t handle_chat(el_val_t body);
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 dispatch_tool(el_val_t tool_name, el_val_t tool_input);
el_val_t handle_chat_agentic(el_val_t body);
el_val_t handle_chat_as_soul(el_val_t body);
el_val_t auto_persist(el_val_t req, el_val_t resp);
@@ -171,6 +174,51 @@ el_val_t studio_tools_json(void) {
return 0;
}
el_val_t agentic_api_key(void) {
el_val_t k1 = env(EL_STR("ANTHROPIC_API_KEY"));
if (!str_eq(k1, EL_STR(""))) {
return k1;
}
return env(EL_STR("NEURON_LLM_0_KEY"));
return 0;
}
el_val_t agentic_tools_literal(void) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("["), EL_STR("{\"name\":\"read_file\",\"description\":\"Read contents of a file from disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute file path\"}},\"required\":[\"path\"]}},")), EL_STR("{\"name\":\"write_file\",\"description\":\"Write content to a file on disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"content\":{\"type\":\"string\"}},\"required\":[\"path\",\"content\"]}},")), EL_STR("{\"name\":\"web_get\",\"description\":\"Fetch content from a URL.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"]}},")), EL_STR("{\"name\":\"search_memory\",\"description\":\"Search engram memory for relevant nodes.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"]}},")), EL_STR("{\"name\":\"run_command\",\"description\":\"Run a shell command and capture output.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\"}},\"required\":[\"command\"]}}")), 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"));
el_val_t content = fs_read(path);
return json_safe(content);
}
if (str_eq(tool_name, EL_STR("write_file"))) {
el_val_t path = json_get(tool_input, EL_STR("path"));
el_val_t content = json_get(tool_input, EL_STR("content"));
fs_write(path, content);
return EL_STR("{\\\"ok\\\":true}");
}
if (str_eq(tool_name, EL_STR("web_get"))) {
el_val_t url = json_get(tool_input, EL_STR("url"));
el_val_t result = http_get(url);
return json_safe(result);
}
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);
return json_safe(result);
}
if (str_eq(tool_name, EL_STR("run_command"))) {
el_val_t cmd = json_get(tool_input, EL_STR("command"));
el_val_t result = exec_capture(cmd);
return json_safe(result);
}
return el_str_concat(EL_STR("unknown tool: "), tool_name);
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(""))) {
@@ -181,12 +229,61 @@ el_val_t handle_chat_agentic(el_val_t body) {
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 tools = studio_tools_json();
el_val_t text = llm_call_agentic(model, system, message, tools);
if (str_eq(text, EL_STR(""))) {
el_val_t api_key = agentic_api_key();
el_val_t tools_json = agentic_tools_literal();
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 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_19 = 0; if (str_eq(content_arr, EL_STR(""))) { _if_result_19 = (EL_STR("[]")); } else { _if_result_19 = (content_arr); } _if_result_19; });
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_20 = 0; if (str_eq(btype, EL_STR("text"))) { _if_result_20 = (el_str_concat(text_out, json_get(block, EL_STR("text")))); } else { _if_result_20 = (text_out); } _if_result_20; });
el_val_t is_new_tool = (str_eq(btype, EL_STR("tool_use")) && !has_tool);
has_tool = ({ el_val_t _if_result_21 = 0; if (is_new_tool) { _if_result_21 = (1); } else { _if_result_21 = (has_tool); } _if_result_21; });
tool_id = ({ el_val_t _if_result_22 = 0; if (is_new_tool) { _if_result_22 = (json_get(block, EL_STR("id"))); } else { _if_result_22 = (tool_id); } _if_result_22; });
tool_name = ({ el_val_t _if_result_23 = 0; if (is_new_tool) { _if_result_23 = (json_get(block, EL_STR("name"))); } else { _if_result_23 = (tool_name); } _if_result_23; });
tool_input = ({ el_val_t _if_result_24 = 0; if (is_new_tool) { _if_result_24 = (json_get_raw(block, EL_STR("input"))); } else { _if_result_24 = (tool_input); } _if_result_24; });
ci = (ci + 1);
}
el_val_t tool_result_raw = ({ el_val_t _if_result_25 = 0; if (has_tool) { _if_result_25 = (dispatch_tool(tool_name, tool_input)); } else { _if_result_25 = (EL_STR("")); } _if_result_25; });
el_val_t tool_result = ({ el_val_t _if_result_26 = 0; if ((str_len(tool_result_raw) > 6000)) { _if_result_26 = (el_str_concat(str_slice(tool_result_raw, 0, 6000), EL_STR("...[truncated]"))); } else { _if_result_26 = (tool_result_raw); } _if_result_26; });
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 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_27 = 0; if (is_tool_turn) { _if_result_27 = (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_27 = (messages); } _if_result_27; });
final_text = ({ el_val_t _if_result_28 = 0; if (!is_tool_turn) { _if_result_28 = (text_out); } else { _if_result_28 = (final_text); } _if_result_28; });
keep_going = ({ el_val_t _if_result_29 = 0; if (!is_tool_turn) { _if_result_29 = (0); } else { _if_result_29 = (keep_going); } _if_result_29; });
iteration = (iteration + 1);
}
if (str_eq(final_text, EL_STR(""))) {
return EL_STR("{\"error\":\"no response\",\"reply\":\"\"}");
}
el_val_t safe_text = json_safe(text);
el_val_t safe_text = json_safe(final_text);
return 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}"));
return 0;
}
@@ -202,12 +299,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_19 = 0; if (str_eq(message, EL_STR(""))) { _if_result_19 = (transcript); } else { _if_result_19 = (message); } _if_result_19; });
el_val_t eff_message = ({ el_val_t _if_result_30 = 0; if (str_eq(message, EL_STR(""))) { _if_result_30 = (transcript); } else { _if_result_30 = (message); } _if_result_30; });
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_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_31 = 0; if (str_eq(req_model, EL_STR(""))) { _if_result_31 = (chat_default_model()); } else { _if_result_31 = (req_model); } _if_result_31; });
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) {
@@ -222,7 +319,7 @@ el_val_t handle_chat_as_soul(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_21 = 0; if (str_eq(reply, EL_STR(""))) { _if_result_21 = (json_get(resp, EL_STR("reply"))); } else { _if_result_21 = (reply); } _if_result_21; });
el_val_t reply2 = ({ el_val_t _if_result_32 = 0; if (str_eq(reply, EL_STR(""))) { _if_result_32 = (json_get(resp, EL_STR("reply"))); } else { _if_result_32 = (reply); } _if_result_32; });
if (str_eq(message, EL_STR(""))) {
return EL_STR("");
}