Files
didactyl/src/http_api.c

1681 lines
58 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "http_api.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include "agent.h"
#include "llm.h"
#include "main.h"
#include "nostr_handler.h"
#include "trigger_manager.h"
#include "tools/tools.h"
#include "cjson/cJSON.h"
#include "debug.h"
#include "mongoose.h"
static http_api_context_t g_api_ctx;
static int g_api_initialized = 0;
static struct mg_mgr g_mgr;
static struct mg_connection* g_listener = NULL;
static int g_api_tls_enabled = 0;
static struct mg_str g_tls_cert = {NULL, 0};
static struct mg_str g_tls_key = {NULL, 0};
static const char* k_json_headers =
"Content-Type: application/json\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Access-Control-Allow-Private-Network: true\r\n";
static int method_is(const struct mg_http_message* hm, const char* method) {
size_t n = strlen(method);
return hm && hm->method.len == n && strncmp(hm->method.buf, method, n) == 0;
}
static void reply_json(struct mg_connection* c, int status, cJSON* root) {
char* out = cJSON_PrintUnformatted(root);
if (!out) {
mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"json serialization failed\"}");
return;
}
mg_http_reply(c, status, k_json_headers, "%s", out);
free(out);
}
static void reply_error(struct mg_connection* c, int status, const char* err) {
cJSON* root = cJSON_CreateObject();
if (!root) {
mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"oom\"}");
return;
}
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root, "error", err ? err : "error");
reply_json(c, status, root);
cJSON_Delete(root);
}
static cJSON* parse_body_json(const struct mg_http_message* hm) {
if (!hm || hm->body.len == 0) {
return cJSON_CreateObject();
}
char* buf = (char*)malloc(hm->body.len + 1U);
if (!buf) {
return NULL;
}
memcpy(buf, hm->body.buf, hm->body.len);
buf[hm->body.len] = '\0';
cJSON* root = cJSON_Parse(buf);
free(buf);
return root;
}
static int estimate_tokens_from_chars(int chars) {
if (chars <= 0) return 0;
return (chars + 3) / 4;
}
static uint64_t fnv1a64_local(const char* s) {
uint64_t h = 1469598103934665603ULL;
if (!s) return h;
while (*s) {
h ^= (unsigned char)(*s++);
h *= 1099511628211ULL;
}
return h;
}
static uint64_t tool_calls_fingerprint_local(const llm_response_t* resp) {
if (!resp || resp->tool_call_count <= 0 || !resp->tool_calls) {
return 1469598103934665603ULL;
}
uint64_t h = 1469598103934665603ULL;
for (int i = 0; i < resp->tool_call_count; i++) {
const llm_tool_call_t* tc = &resp->tool_calls[i];
uint64_t name_h = fnv1a64_local(tc->name ? tc->name : "");
uint64_t args_h = fnv1a64_local(tc->arguments_json ? tc->arguments_json : "{}");
uint64_t part = name_h ^ (args_h + 0x9e3779b97f4a7c15ULL + (name_h << 6) + (name_h >> 2));
h ^= part + (uint64_t)(i + 1) * 1099511628211ULL;
h *= 1099511628211ULL;
}
h ^= (uint64_t)resp->tool_call_count;
h *= 1099511628211ULL;
return h;
}
static void notify_admin_limit_diagnostic_http(const char* source,
const char* reason,
const char* subject,
int max_turns,
int turns_run,
int stall_repeat_threshold,
int repeated_tool_turns,
int current_max_tokens,
const char* possible_cause,
const char* hint,
const char* final_answer) {
if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') {
return;
}
char diag[1800];
snprintf(diag,
sizeof(diag),
"⚠️ Didactyl limit diagnostic\n"
"source=%s\n"
"reason=%s\n"
"subject=%s\n"
"max_turns=%d\n"
"turns_run=%d\n"
"stall_repeat_threshold=%d\n"
"repeated_tool_turns=%d\n"
"current_max_tokens=%d\n"
"possible_cause=%s\n"
"hint=%s\n"
"final_answer_preview=%.*s",
source ? source : "unknown",
reason ? reason : "unknown",
subject ? subject : "n/a",
max_turns,
turns_run,
stall_repeat_threshold,
repeated_tool_turns,
current_max_tokens,
possible_cause ? possible_cause : "n/a",
hint ? hint : "n/a",
360,
final_answer ? final_answer : "");
(void)nostr_handler_send_dm_auto(g_api_ctx.cfg->admin.pubkey, diag);
}
static int uri_extract_after_prefix(const struct mg_str* uri,
const char* prefix,
char* out,
size_t out_sz) {
if (!uri || !prefix || !out || out_sz == 0) {
return -1;
}
out[0] = '\0';
size_t prefix_len = strlen(prefix);
if (uri->len <= prefix_len || strncmp(uri->buf, prefix, prefix_len) != 0) {
return -1;
}
size_t rem = uri->len - prefix_len;
if (rem == 0 || rem >= out_sz) {
return -1;
}
memcpy(out, uri->buf + prefix_len, rem);
out[rem] = '\0';
return 0;
}
static void handle_trigger_webhook(struct mg_connection* c,
const struct mg_http_message* hm,
const char* d_tag) {
if (!g_api_ctx.trigger_manager) {
reply_error(c, 503, "trigger manager unavailable");
return;
}
active_trigger_t trigger;
if (trigger_manager_find(g_api_ctx.trigger_manager, d_tag, &trigger) != 0) {
reply_error(c, 404, "active trigger not found for d_tag");
return;
}
if (trigger.trigger_type != TRIGGER_TYPE_WEBHOOK) {
reply_error(c, 400, "trigger type is not webhook");
return;
}
if (!trigger.enabled) {
reply_error(c, 400, "webhook trigger is disabled");
return;
}
cJSON* payload = parse_body_json(hm);
if (!payload || !cJSON_IsObject(payload)) {
cJSON_Delete(payload);
reply_error(c, 400, "invalid JSON body");
return;
}
cJSON* event = cJSON_CreateObject();
if (!event) {
cJSON_Delete(payload);
reply_error(c, 500, "oom");
return;
}
cJSON_AddStringToObject(event, "type", "webhook");
cJSON_AddStringToObject(event, "skill_d_tag", d_tag);
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
cJSON_AddItemToObject(event, "payload", payload);
int fired = trigger_manager_fire(g_api_ctx.trigger_manager, d_tag, event, "webhook");
cJSON_Delete(event);
if (fired < 0) {
reply_error(c, 404, "active trigger not found for d_tag");
return;
}
cJSON* out = cJSON_CreateObject();
if (!out) {
reply_error(c, 500, "oom");
return;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "d_tag", d_tag);
cJSON_AddBoolToObject(out, "fired", fired > 0 ? 1 : 0);
reply_json(c, 200, out);
cJSON_Delete(out);
}
static char* maybe_model_override_begin(cJSON* body, llm_config_t* out_old_cfg, int* out_overridden) {
if (!body || !out_old_cfg || !out_overridden) return NULL;
*out_overridden = 0;
cJSON* model = cJSON_GetObjectItemCaseSensitive(body, "model");
if (!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0') {
return NULL;
}
if (llm_get_config(out_old_cfg) != 0) {
return "failed to read current llm config";
}
llm_config_t next = *out_old_cfg;
snprintf(next.model, sizeof(next.model), "%s", model->valuestring);
if (llm_set_config(&next) != 0) {
return "failed to set temporary model";
}
*out_overridden = 1;
return NULL;
}
static void maybe_model_override_end(const llm_config_t* old_cfg, int overridden) {
if (overridden && old_cfg) {
(void)llm_set_config(old_cfg);
}
}
static const char* classify_part_name(cJSON* msg, int idx) {
return agent_classify_message_part(msg, idx);
}
static int build_context_parts_response(cJSON* out_root) {
if (!out_root) return -1;
char* messages_json = NULL;
if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) {
return -1;
}
cJSON* arr = cJSON_Parse(messages_json);
free(messages_json);
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(arr);
return -1;
}
cJSON* parts = cJSON_CreateArray();
if (!parts) {
cJSON_Delete(arr);
return -1;
}
int total_chars = 0;
int n = cJSON_GetArraySize(arr);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(arr, i);
if (!msg || !cJSON_IsObject(msg)) continue;
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "unknown";
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
int chars = (int)strlen(content_s);
total_chars += chars;
cJSON* part = cJSON_CreateObject();
if (!part) continue;
cJSON_AddStringToObject(part, "name", classify_part_name(msg, i));
cJSON_AddStringToObject(part, "role", role_s);
cJSON_AddNumberToObject(part, "chars", chars);
cJSON_AddNumberToObject(part, "estimated_tokens", estimate_tokens_from_chars(chars));
cJSON_AddStringToObject(part, "content", content_s);
cJSON_AddItemToArray(parts, part);
}
cJSON_AddBoolToObject(out_root, "success", 1);
cJSON_AddNumberToObject(out_root, "total_chars", total_chars);
cJSON_AddNumberToObject(out_root, "total_estimated_tokens", estimate_tokens_from_chars(total_chars));
cJSON_AddItemToObject(out_root, "parts", parts);
cJSON_AddItemToObject(out_root, "messages", arr);
return 0;
}
static int append_simple_message_local(cJSON* messages, const char* role, const char* content) {
if (!messages || !role) return -1;
cJSON* msg = cJSON_CreateObject();
if (!msg) return -1;
cJSON_AddStringToObject(msg, "role", role);
cJSON_AddStringToObject(msg, "content", content ? content : "");
cJSON_AddItemToArray(messages, msg);
return 0;
}
static int append_assistant_tool_calls_message_local(cJSON* messages, const llm_response_t* resp) {
if (!messages || !resp || resp->tool_call_count <= 0) return -1;
cJSON* assistant = cJSON_CreateObject();
cJSON* tool_calls = cJSON_CreateArray();
if (!assistant || !tool_calls) {
cJSON_Delete(assistant);
cJSON_Delete(tool_calls);
return -1;
}
cJSON_AddStringToObject(assistant, "role", "assistant");
if (resp->content) cJSON_AddStringToObject(assistant, "content", resp->content);
else cJSON_AddNullToObject(assistant, "content");
for (int i = 0; i < resp->tool_call_count; i++) {
const llm_tool_call_t* tc = &resp->tool_calls[i];
cJSON* tc_obj = cJSON_CreateObject();
cJSON* fn_obj = cJSON_CreateObject();
if (!tc_obj || !fn_obj) {
cJSON_Delete(tc_obj);
cJSON_Delete(fn_obj);
cJSON_Delete(assistant);
cJSON_Delete(tool_calls);
return -1;
}
cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : "");
cJSON_AddStringToObject(tc_obj, "type", "function");
cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : "");
cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
cJSON_AddItemToObject(tc_obj, "function", fn_obj);
cJSON_AddItemToArray(tool_calls, tc_obj);
}
cJSON_AddItemToObject(assistant, "tool_calls", tool_calls);
cJSON_AddItemToArray(messages, assistant);
return 0;
}
static int append_tool_result_message_local(cJSON* messages, const char* tool_call_id, const char* tool_result_json) {
if (!messages || !tool_call_id) return -1;
cJSON* msg = cJSON_CreateObject();
if (!msg) return -1;
cJSON_AddStringToObject(msg, "role", "tool");
cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id);
cJSON_AddStringToObject(msg,
"content",
tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}");
cJSON_AddItemToArray(messages, msg);
return 0;
}
static int maybe_append_loaded_skill_instructions_local(cJSON* messages,
const char* tool_name,
const char* tool_result_json) {
if (!messages || !tool_name || strcmp(tool_name, "skill_load") != 0 || !tool_result_json) {
return 0;
}
cJSON* root = cJSON_Parse(tool_result_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return 0;
}
cJSON* ok = cJSON_GetObjectItemCaseSensitive(root, "success");
cJSON* type = cJSON_GetObjectItemCaseSensitive(root, "type");
cJSON* instructions = cJSON_GetObjectItemCaseSensitive(root, "instructions");
int should_apply = (ok && cJSON_IsBool(ok) && cJSON_IsTrue(ok) &&
type && cJSON_IsString(type) && type->valuestring &&
strcmp(type->valuestring, "skill_instructions") == 0 &&
instructions && cJSON_IsString(instructions) && instructions->valuestring &&
instructions->valuestring[0] != '\0');
int rc = 0;
if (should_apply) {
rc = append_simple_message_local(messages, "system", instructions->valuestring);
}
cJSON_Delete(root);
return rc;
}
static int is_space_char_local(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
static const char* skip_spaces_local(const char* s) {
while (s && *s && is_space_char_local(*s)) s++;
return s ? s : "";
}
static char* json_error_string_local(const char* msg) {
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 0);
cJSON_AddStringToObject(out, "error", msg ? msg : "error");
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* build_slash_args_json_local(const char* raw_args) {
const char* s = skip_spaces_local(raw_args);
if (!s || s[0] == '\0') {
return strdup("{}");
}
cJSON* parsed = cJSON_Parse(s);
if (parsed) {
char* out = cJSON_PrintUnformatted(parsed);
cJSON_Delete(parsed);
return out;
}
cJSON* obj = cJSON_CreateObject();
if (!obj) return NULL;
cJSON_AddStringToObject(obj, "input", s);
char* out = cJSON_PrintUnformatted(obj);
cJSON_Delete(obj);
return out;
}
static char* build_run_args_json_local(const char* raw_args) {
const char* s = skip_spaces_local(raw_args);
if (!s || s[0] == '\0') {
return json_error_string_local("/run requires a skill identifier");
}
const char* ident_start = s;
while (*s && !is_space_char_local(*s)) s++;
size_t ident_len = (size_t)(s - ident_start);
if (ident_len == 0 || ident_len >= 256U) {
return json_error_string_local("/run skill identifier is invalid");
}
char ident[256];
memcpy(ident, ident_start, ident_len);
ident[ident_len] = '\0';
const char* remaining = skip_spaces_local(s);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
const char* c1 = strchr(ident, ':');
const char* c2 = c1 ? strchr(c1 + 1, ':') : NULL;
if (c1 && c2 && c1 != ident && (size_t)(c2 - c1) == 65U && c2[1] != '\0') {
size_t pubkey_len = (size_t)(c2 - (c1 + 1));
char pubkey[65];
memcpy(pubkey, c1 + 1, pubkey_len);
pubkey[pubkey_len] = '\0';
cJSON_AddStringToObject(out, "pubkey", pubkey);
cJSON_AddStringToObject(out, "d_tag", c2 + 1);
} else {
cJSON_AddStringToObject(out, "d_tag", ident);
}
if (remaining && remaining[0] != '\0') {
cJSON_AddStringToObject(out, "args", remaining);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static int append_textf_http(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
if (!buf || !cap || !used || !fmt) return -1;
while (1) {
if (*used + 128U >= *cap) {
size_t next_cap = (*cap) * 2U;
char* grown = (char*)realloc(*buf, next_cap);
if (!grown) return -1;
*buf = grown;
*cap = next_cap;
}
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap);
va_end(ap);
if (n < 0) return -1;
if ((size_t)n < (*cap - *used)) {
*used += (size_t)n;
return 0;
}
size_t next_cap = (*cap) * 2U + (size_t)n + 64U;
char* grown = (char*)realloc(*buf, next_cap);
if (!grown) return -1;
*buf = grown;
*cap = next_cap;
}
}
static int append_markdown_indent_http(char** buf, size_t* cap, size_t* used, int depth) {
for (int i = 0; i < depth; i++) {
if (append_textf_http(buf, cap, used, " ") != 0) {
return -1;
}
}
return 0;
}
static int append_cjson_markdown_http(char** buf, size_t* cap, size_t* used, cJSON* node, int depth) {
if (!node) {
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1;
}
if (cJSON_IsObject(node)) {
cJSON* child = NULL;
cJSON_ArrayForEach(child, node) {
const char* key = child->string ? child->string : "item";
if (cJSON_IsString(child)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- **%s:** %s\n", key,
child->valuestring ? child->valuestring : "") != 0) {
return -1;
}
} else if (cJSON_IsBool(child)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- **%s:** %s\n", key,
cJSON_IsTrue(child) ? "true" : "false") != 0) {
return -1;
}
} else if (cJSON_IsNumber(child)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- **%s:** %g\n", key, child->valuedouble) != 0) {
return -1;
}
} else if (cJSON_IsNull(child)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- **%s:** null\n", key) != 0) {
return -1;
}
} else {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- **%s:**\n", key) != 0) {
return -1;
}
if (append_cjson_markdown_http(buf, cap, used, child, depth + 1) != 0) {
return -1;
}
}
}
return 0;
}
if (cJSON_IsArray(node)) {
int n = cJSON_GetArraySize(node);
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(node, i);
if (cJSON_IsString(item)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- %s\n", item->valuestring ? item->valuestring : "") != 0) {
return -1;
}
} else if (cJSON_IsBool(item)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(item) ? "true" : "false") != 0) {
return -1;
}
} else if (cJSON_IsNumber(item)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- %g\n", item->valuedouble) != 0) {
return -1;
}
} else if (cJSON_IsNull(item)) {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "- null\n") != 0) {
return -1;
}
} else {
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
append_textf_http(buf, cap, used, "-\n") != 0) {
return -1;
}
if (append_cjson_markdown_http(buf, cap, used, item, depth + 1) != 0) {
return -1;
}
}
}
return 0;
}
if (cJSON_IsString(node)) {
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
append_textf_http(buf, cap, used, "- %s\n", node->valuestring ? node->valuestring : "") == 0 ? 0 : -1;
}
if (cJSON_IsBool(node)) {
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(node) ? "true" : "false") == 0 ? 0 : -1;
}
if (cJSON_IsNumber(node)) {
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
append_textf_http(buf, cap, used, "- %g\n", node->valuedouble) == 0 ? 0 : -1;
}
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1;
}
static char* format_result_markdown_http(const char* raw_result) {
if (!raw_result) {
return strdup("No output.");
}
cJSON* root = cJSON_Parse(raw_result);
if (!root) {
return strdup(raw_result);
}
size_t cap = strlen(raw_result) * 2U + 512U;
if (cap < 2048U) cap = 2048U;
size_t used = 0U;
char* out = (char*)malloc(cap);
if (!out) {
cJSON_Delete(root);
return strdup(raw_result);
}
out[0] = '\0';
if (append_cjson_markdown_http(&out, &cap, &used, root, 0) != 0) {
cJSON_Delete(root);
free(out);
return strdup(raw_result);
}
cJSON_Delete(root);
return out;
}
static char* build_slash_help_all_json_local(void) {
size_t cap = 2048U;
size_t used = 0U;
char* out = (char*)malloc(cap);
if (!out) return NULL;
out[0] = '\0';
if (append_textf_http(&out, &cap, &used, "AVAILABLE TOOLS\n") != 0) {
free(out);
return NULL;
}
char* tool_list_json = tools_execute(g_api_ctx.tools_ctx, "tool_list", "{}");
cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL;
cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL;
if (!tools || !cJSON_IsArray(tools) || cJSON_GetArraySize(tools) <= 0) {
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
} else {
int tn = cJSON_GetArraySize(tools);
int* used_idx = (int*)calloc((size_t)tn, sizeof(int));
if (!used_idx) {
cJSON_Delete(tool_root);
free(tool_list_json);
free(out);
return NULL;
}
for (int k = 0; k < tn; k++) {
int best = -1;
const char* best_name = NULL;
for (int i = 0; i < tn; i++) {
if (used_idx[i]) continue;
cJSON* row = cJSON_GetArrayItem(tools, i);
cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL;
const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown";
if (best < 0 || strcmp(name_s, best_name) < 0) {
best = i;
best_name = name_s;
}
}
if (best < 0) {
break;
}
used_idx[best] = 1;
cJSON* row = cJSON_GetArrayItem(tools, best);
cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown";
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", name_s, desc_s[0] ? "" : "", desc_s) != 0) {
free(used_idx);
cJSON_Delete(tool_root);
free(tool_list_json);
free(out);
return NULL;
}
}
free(used_idx);
}
cJSON_Delete(tool_root);
free(tool_list_json);
if (append_textf_http(&out, &cap, &used, "\nAVAILABLE SKILLS\n") != 0) {
free(out);
return NULL;
}
char* skill_list_json = tools_execute(g_api_ctx.tools_ctx, "skill_list", "{}");
cJSON* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
int available_count = 0;
if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
int sn = cJSON_GetArraySize(skills);
for (int i = 0; i < sn; i++) {
cJSON* row = cJSON_GetArrayItem(skills, i);
cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
continue;
}
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? "" : "", desc_s) != 0) {
cJSON_Delete(skill_root);
free(skill_list_json);
free(out);
return NULL;
}
available_count++;
}
}
if (available_count <= 0) {
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
}
cJSON_Delete(skill_root);
free(skill_list_json);
if (append_textf_http(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
free(out);
return NULL;
}
char* adopted_json = tools_execute(g_api_ctx.tools_ctx, "adopted_skills", "{}");
cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
cJSON* adoption_events_content = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "content") : NULL;
cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
const char* adoption_events_text = NULL;
if (adoption_events_content && cJSON_IsString(adoption_events_content) && adoption_events_content->valuestring) {
adoption_events_text = adoption_events_content->valuestring;
} else if (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring) {
adoption_events_text = adoption_events_json->valuestring;
}
cJSON* adoption_events = adoption_events_text ? cJSON_Parse(adoption_events_text) : NULL;
int adopted_count = 0;
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
if (tags && cJSON_IsArray(tags)) {
int tn = cJSON_GetArraySize(tags);
for (int i = 0; i < tn; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* k = cJSON_GetArrayItem(tag, 0);
cJSON* v = cJSON_GetArrayItem(tag, 1);
if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
!cJSON_IsString(v) || !v->valuestring) {
continue;
}
if (strcmp(k->valuestring, "a") != 0) {
continue;
}
const char* addr = v->valuestring;
const char* d_tag = strrchr(addr, ':');
const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;
if (append_textf_http(&out, &cap, &used, "- %s\n", label) != 0) {
cJSON_Delete(adoption_events);
cJSON_Delete(adopted_root);
free(adopted_json);
free(out);
return NULL;
}
adopted_count++;
}
}
}
if (adopted_count <= 0) {
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
}
cJSON_Delete(adoption_events);
cJSON_Delete(adopted_root);
free(adopted_json);
return out;
}
static char* build_slash_help_tool_json_local(const char* tool_name) {
if (!tool_name || tool_name[0] == '\0') {
return json_error_string_local("missing tool name");
}
char* schema_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
if (!schema_json) return json_error_string_local("failed to build tool schema");
cJSON* schema = cJSON_Parse(schema_json);
free(schema_json);
if (!schema || !cJSON_IsArray(schema)) {
cJSON_Delete(schema);
return json_error_string_local("tool schema parse failure");
}
cJSON* matched_fn = NULL;
int n = cJSON_GetArraySize(schema);
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(schema, i);
cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
if (name && cJSON_IsString(name) && name->valuestring && strcmp(name->valuestring, tool_name) == 0) {
matched_fn = fn;
break;
}
}
if (!matched_fn) {
cJSON_Delete(schema);
return json_error_string_local("unknown tool");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(schema);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "mode", "slash_help_tool");
cJSON_AddStringToObject(out, "tool", tool_name);
cJSON_AddItemToObject(out, "schema", cJSON_Duplicate(matched_fn, 1));
char* result = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cJSON_Delete(schema);
return result;
}
static char* execute_slash_command_http(const char* message) {
if (!message || message[0] != '/') return NULL;
const char* p = message + 1;
while (*p && !is_space_char_local(*p)) p++;
size_t tool_len = (size_t)(p - (message + 1));
if (tool_len == 0 || tool_len >= 128U) {
return strdup("{\"success\":false,\"error\":\"invalid slash command\"}");
}
char tool_name[128];
memcpy(tool_name, message + 1, tool_len);
tool_name[tool_len] = '\0';
const char* raw_args = skip_spaces_local(p);
char* result_json = NULL;
if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) {
const char* help_name = (strcmp(tool_name, "run") == 0) ? "skill_run" : tool_name;
result_json = build_slash_help_tool_json_local(help_name);
} else if (strcmp(tool_name, "help") == 0) {
if (!raw_args || raw_args[0] == '\0') {
result_json = build_slash_help_all_json_local();
} else {
char help_tool[128];
size_t i = 0;
while (raw_args[i] && !is_space_char_local(raw_args[i]) && i < sizeof(help_tool) - 1U) {
help_tool[i] = raw_args[i];
i++;
}
help_tool[i] = '\0';
const char* help_name = (strcmp(help_tool, "run") == 0) ? "skill_run" : help_tool;
result_json = build_slash_help_tool_json_local(help_name);
}
} else {
char* args_json = NULL;
const char* exec_tool_name = tool_name;
if (strcmp(tool_name, "run") == 0) {
args_json = build_run_args_json_local(raw_args);
exec_tool_name = "skill_run";
} else {
args_json = build_slash_args_json_local(raw_args);
}
if (!args_json) {
result_json = json_error_string_local("failed to parse slash args");
} else {
result_json = tools_execute(g_api_ctx.tools_ctx, exec_tool_name, args_json);
free(args_json);
}
}
if (!result_json) {
result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}");
}
char* markdown_result = format_result_markdown_http(result_json);
free(result_json);
return markdown_result ? markdown_result : strdup("Command executed with no output.");
}
static cJSON* run_prompt_with_tools_convo(cJSON* convo,
int max_turns,
const char* log_sender,
const char* tool_limit_message) {
if (!convo || !cJSON_IsArray(convo)) return NULL;
int max_ceiling = 16;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_max_turns_ceiling > 0) {
max_ceiling = g_api_ctx.cfg->tools.api_max_turns_ceiling;
}
if (max_turns < 1) max_turns = 1;
if (max_turns > max_ceiling) max_turns = max_ceiling;
char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
if (!tools_json) {
return NULL;
}
cJSON* root = cJSON_CreateObject();
cJSON* turns = cJSON_CreateArray();
if (!root || !turns) {
cJSON_Delete(root);
cJSON_Delete(turns);
free(tools_json);
return NULL;
}
int stall_repeat_threshold = 3;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.stall_repeat_threshold > 1) {
stall_repeat_threshold = g_api_ctx.cfg->tools.stall_repeat_threshold;
}
char* final_response = NULL;
uint64_t last_tool_fp = 0;
int repeated_tool_turns = 0;
int exited_on_stall = 0;
int stall_due_to_length = 0;
int turns_run = 0;
for (int turn = 0; turn < max_turns; turn++) {
turns_run = turn + 1;
char* messages_json = cJSON_PrintUnformatted(convo);
if (!messages_json) break;
llm_response_t resp;
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
free(messages_json);
if (rc != 0) {
final_response = strdup("LLM request failed.");
break;
}
cJSON* turn_obj = cJSON_CreateObject();
if (turn_obj) {
cJSON_AddNumberToObject(turn_obj, "turn", turn + 1);
cJSON* tool_calls = cJSON_CreateArray();
if (tool_calls) cJSON_AddItemToObject(turn_obj, "tool_calls", tool_calls);
cJSON_AddItemToArray(turns, turn_obj);
}
if (resp.tool_call_count <= 0) {
final_response = strdup(resp.content ? resp.content : "No response content.");
llm_response_free(&resp);
break;
}
uint64_t current_tool_fp = tool_calls_fingerprint_local(&resp);
if (turn == 0 || current_tool_fp != last_tool_fp) {
repeated_tool_turns = 1;
last_tool_fp = current_tool_fp;
} else {
repeated_tool_turns++;
}
if (repeated_tool_turns >= stall_repeat_threshold) {
exited_on_stall = 1;
stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
llm_response_free(&resp);
break;
}
if (append_assistant_tool_calls_message_local(convo, &resp) != 0) {
llm_response_free(&resp);
final_response = strdup("Failed to append assistant tool calls.");
break;
}
cJSON* current_turn = cJSON_GetArrayItem(turns, cJSON_GetArraySize(turns) - 1);
cJSON* turn_tool_calls = current_turn ? cJSON_GetObjectItemCaseSensitive(current_turn, "tool_calls") : NULL;
for (int i = 0; i < resp.tool_call_count; i++) {
llm_tool_call_t* tc = &resp.tool_calls[i];
char* tool_result = tools_execute(g_api_ctx.tools_ctx, tc->name, tc->arguments_json);
if (!tool_result) {
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
}
if (turn_tool_calls && cJSON_IsArray(turn_tool_calls)) {
cJSON* tr = cJSON_CreateObject();
if (tr) {
cJSON_AddStringToObject(tr, "name", tc->name ? tc->name : "");
cJSON_AddStringToObject(tr, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
cJSON_AddStringToObject(tr, "result", tool_result ? tool_result : "{}");
cJSON_AddItemToArray(turn_tool_calls, tr);
}
}
if (append_tool_result_message_local(convo,
tc->id ? tc->id : "",
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
free(tool_result);
llm_response_free(&resp);
final_response = strdup("Failed to append tool result.");
goto done;
}
if (maybe_append_loaded_skill_instructions_local(convo,
tc->name,
tool_result) != 0) {
free(tool_result);
llm_response_free(&resp);
final_response = strdup("Failed to apply loaded skill instructions.");
goto done;
}
free(tool_result);
}
llm_response_free(&resp);
}
done:;
int hit_max_turn_limit = (!final_response && !exited_on_stall && turns_run >= max_turns);
if (!final_response) {
char* messages_json = cJSON_PrintUnformatted(convo);
if (messages_json) {
llm_response_t final_resp;
int final_rc = llm_chat_with_tools_messages(messages_json, tools_json, "none", &final_resp);
free(messages_json);
if (final_rc == 0) {
const char* forced_answer = final_resp.content ? final_resp.content : "";
if (forced_answer[0] != '\0') {
final_response = strdup(forced_answer);
}
llm_response_free(&final_resp);
}
}
}
if (!final_response) {
final_response = strdup(exited_on_stall
? "Stopped repeated tool calls after detecting a loop, and no final summary was produced."
: (tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run."));
}
if (exited_on_stall || hit_max_turn_limit) {
notify_admin_limit_diagnostic_http("http_api_tool_loop",
exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
log_sender,
max_turns,
turns_run,
stall_repeat_threshold,
repeated_tool_turns,
g_api_ctx.cfg ? g_api_ctx.cfg->llm.max_tokens : 0,
(exited_on_stall && stall_due_to_length) ? "max_tokens_truncation" : NULL,
(exited_on_stall && stall_due_to_length) ? "Increase max_tokens (example: /model_set {\"max_tokens\": 4096})" : NULL,
final_response ? final_response : "");
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "final_response", final_response ? final_response : "");
cJSON_AddItemToObject(root, "turns", turns);
llm_config_t cfg;
if (llm_get_config(&cfg) == 0) {
cJSON_AddStringToObject(root, "model_used", cfg.model);
}
int input_chars = 0;
char* convo_json = cJSON_PrintUnformatted(convo);
if (convo_json) {
input_chars = (int)strlen(convo_json);
free(convo_json);
}
cJSON_AddNumberToObject(root, "total_input_tokens_estimate", estimate_tokens_from_chars(input_chars));
cJSON_AddNumberToObject(root,
"total_output_tokens_estimate",
estimate_tokens_from_chars((int)strlen(final_response ? final_response : "")));
free(final_response);
free(tools_json);
return root;
}
static cJSON* run_prompt_with_tools(cJSON* body) {
if (!body || !cJSON_IsObject(body)) return NULL;
cJSON* messages_node = cJSON_GetObjectItemCaseSensitive(body, "messages");
if (!messages_node || !cJSON_IsArray(messages_node)) return NULL;
cJSON* convo = cJSON_Duplicate(messages_node, 1);
if (!convo || !cJSON_IsArray(convo)) {
cJSON_Delete(convo);
return NULL;
}
int max_turns = 4;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
}
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
max_turns = (int)max_turns_node->valuedouble;
}
cJSON* root = run_prompt_with_tools_convo(convo,
max_turns,
"http_api",
"I hit my tool-use limit for this prompt run.");
cJSON_Delete(convo);
return root;
}
static void handle_status(struct mg_connection* c) {
cJSON* root = cJSON_CreateObject();
if (!root) {
reply_error(c, 500, "oom");
return;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "name", DIDACTYL_NAME);
cJSON_AddStringToObject(root, "version", DIDACTYL_VERSION);
if (g_api_ctx.cfg) {
cJSON_AddStringToObject(root, "pubkey", g_api_ctx.cfg->keys.public_key_hex);
cJSON_AddNumberToObject(root, "relay_count", g_api_ctx.cfg->relay_count);
}
cJSON_AddNumberToObject(root, "connected_relays", nostr_handler_connected_relay_count());
if (g_api_ctx.trigger_manager) {
cJSON_AddNumberToObject(root, "active_triggers", trigger_manager_active_count(g_api_ctx.trigger_manager));
}
reply_json(c, 200, root);
cJSON_Delete(root);
}
static void handle_context_current(struct mg_connection* c) {
char* messages_json = NULL;
if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) {
reply_error(c, 500, "failed to build context messages");
return;
}
cJSON* messages = cJSON_Parse(messages_json);
cJSON* root = cJSON_CreateObject();
if (!root || !messages || !cJSON_IsArray(messages)) {
free(messages_json);
cJSON_Delete(messages);
cJSON_Delete(root);
reply_error(c, 500, "failed to serialize context");
return;
}
int chars = (int)strlen(messages_json);
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddNumberToObject(root, "total_chars", chars);
cJSON_AddNumberToObject(root, "total_estimated_tokens", estimate_tokens_from_chars(chars));
cJSON_AddItemToObject(root, "messages", messages);
reply_json(c, 200, root);
free(messages_json);
cJSON_Delete(root);
}
static void handle_context_parts(struct mg_connection* c) {
cJSON* root = cJSON_CreateObject();
if (!root) {
reply_error(c, 500, "oom");
return;
}
if (build_context_parts_response(root) != 0) {
cJSON_Delete(root);
reply_error(c, 500, "failed to build context parts");
return;
}
reply_json(c, 200, root);
cJSON_Delete(root);
}
static void handle_prompt_run_simple(struct mg_connection* c, const struct mg_http_message* hm) {
cJSON* body = parse_body_json(hm);
if (!body || !cJSON_IsObject(body)) {
cJSON_Delete(body);
reply_error(c, 400, "invalid JSON body");
return;
}
cJSON* system = cJSON_GetObjectItemCaseSensitive(body, "system");
cJSON* user = cJSON_GetObjectItemCaseSensitive(body, "user");
if (!system || !user || !cJSON_IsString(system) || !cJSON_IsString(user) ||
!system->valuestring || !user->valuestring) {
cJSON_Delete(body);
reply_error(c, 400, "system and user strings are required");
return;
}
llm_config_t old_cfg;
int overridden = 0;
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
if (model_err) {
cJSON_Delete(body);
reply_error(c, 400, model_err);
return;
}
char* response = llm_chat(system->valuestring, user->valuestring);
maybe_model_override_end(&old_cfg, overridden);
if (!response) {
cJSON_Delete(body);
reply_error(c, 500, "llm request failed");
return;
}
cJSON* root = cJSON_CreateObject();
if (!root) {
free(response);
cJSON_Delete(body);
reply_error(c, 500, "oom");
return;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "response", response);
llm_config_t cfg;
if (llm_get_config(&cfg) == 0) {
cJSON_AddStringToObject(root, "model_used", cfg.model);
}
int in_chars = (int)strlen(system->valuestring) + (int)strlen(user->valuestring);
cJSON_AddNumberToObject(root, "input_tokens_estimate", estimate_tokens_from_chars(in_chars));
cJSON_AddNumberToObject(root, "output_tokens_estimate", estimate_tokens_from_chars((int)strlen(response)));
free(response);
cJSON_Delete(body);
reply_json(c, 200, root);
cJSON_Delete(root);
}
static void handle_prompt_run(struct mg_connection* c, const struct mg_http_message* hm) {
cJSON* body = parse_body_json(hm);
if (!body || !cJSON_IsObject(body)) {
cJSON_Delete(body);
reply_error(c, 400, "invalid JSON body");
return;
}
llm_config_t old_cfg;
int overridden = 0;
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
if (model_err) {
cJSON_Delete(body);
reply_error(c, 400, model_err);
return;
}
cJSON* result = run_prompt_with_tools(body);
maybe_model_override_end(&old_cfg, overridden);
cJSON_Delete(body);
if (!result) {
reply_error(c, 500, "failed to run prompt");
return;
}
reply_json(c, 200, result);
cJSON_Delete(result);
}
static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_message* hm) {
cJSON* body = parse_body_json(hm);
if (!body || !cJSON_IsObject(body)) {
cJSON_Delete(body);
reply_error(c, 400, "invalid JSON body");
return;
}
cJSON* message = cJSON_GetObjectItemCaseSensitive(body, "message");
if (!message || !cJSON_IsString(message) || !message->valuestring) {
cJSON_Delete(body);
reply_error(c, 400, "message string is required");
return;
}
char* slash_result = execute_slash_command_http(message->valuestring);
if (slash_result) {
cJSON* out = cJSON_CreateObject();
cJSON* turns = cJSON_CreateArray();
if (!out || !turns) {
free(slash_result);
cJSON_Delete(out);
cJSON_Delete(turns);
cJSON_Delete(body);
reply_error(c, 500, "oom");
return;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "final_response", slash_result);
cJSON_AddItemToObject(out, "turns", turns);
cJSON_AddNumberToObject(out, "total_input_tokens_estimate", estimate_tokens_from_chars((int)strlen(message->valuestring)));
cJSON_AddNumberToObject(out, "total_output_tokens_estimate", estimate_tokens_from_chars((int)strlen(slash_result)));
free(slash_result);
cJSON_Delete(body);
reply_json(c, 200, out);
cJSON_Delete(out);
return;
}
int max_turns = 4;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
}
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
max_turns = (int)max_turns_node->valuedouble;
}
llm_config_t old_cfg;
int overridden = 0;
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
if (model_err) {
cJSON_Delete(body);
reply_error(c, 400, model_err);
return;
}
char* base_messages_json = NULL;
cJSON* convo = NULL;
cJSON* result = NULL;
if (agent_build_admin_messages_json(message->valuestring,
DIDACTYL_SENDER_ADMIN,
&base_messages_json) != 0 || !base_messages_json) {
maybe_model_override_end(&old_cfg, overridden);
cJSON_Delete(body);
reply_error(c, 500, "failed to build agent context");
return;
}
convo = cJSON_Parse(base_messages_json);
free(base_messages_json);
if (!convo || !cJSON_IsArray(convo)) {
cJSON_Delete(convo);
maybe_model_override_end(&old_cfg, overridden);
cJSON_Delete(body);
reply_error(c, 500, "failed to initialize conversation messages");
return;
}
if (append_simple_message_local(convo, "user", message->valuestring) != 0) {
cJSON_Delete(convo);
maybe_model_override_end(&old_cfg, overridden);
cJSON_Delete(body);
reply_error(c, 500, "failed to append user message");
return;
}
cJSON* live_user_msg = cJSON_GetArrayItem(convo, cJSON_GetArraySize(convo) - 1);
if (live_user_msg && cJSON_IsObject(live_user_msg)) {
cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL));
}
result = run_prompt_with_tools_convo(convo,
max_turns,
"http_api_agent",
"I hit my tool-use limit for this request.");
cJSON_Delete(convo);
maybe_model_override_end(&old_cfg, overridden);
cJSON_Delete(body);
if (!result) {
reply_error(c, 500, "failed to run prompt");
return;
}
reply_json(c, 200, result);
cJSON_Delete(result);
}
static cJSON* run_variant(cJSON* variant) {
if (!variant || !cJSON_IsObject(variant)) return NULL;
cJSON* body_copy = cJSON_Duplicate(variant, 1);
if (!body_copy) return NULL;
llm_config_t old_cfg;
int overridden = 0;
char* model_err = maybe_model_override_begin(body_copy, &old_cfg, &overridden);
if (model_err) {
cJSON* err = cJSON_CreateObject();
if (err) {
cJSON_AddBoolToObject(err, "success", 0);
cJSON_AddStringToObject(err, "error", model_err);
}
cJSON_Delete(body_copy);
return err;
}
cJSON* out = run_prompt_with_tools(body_copy);
maybe_model_override_end(&old_cfg, overridden);
cJSON_Delete(body_copy);
return out;
}
static void handle_prompt_compare(struct mg_connection* c, const struct mg_http_message* hm) {
cJSON* body = parse_body_json(hm);
if (!body || !cJSON_IsObject(body)) {
cJSON_Delete(body);
reply_error(c, 400, "invalid JSON body");
return;
}
cJSON* a = cJSON_GetObjectItemCaseSensitive(body, "variant_a");
cJSON* b = cJSON_GetObjectItemCaseSensitive(body, "variant_b");
if (!a || !b || !cJSON_IsObject(a) || !cJSON_IsObject(b)) {
cJSON_Delete(body);
reply_error(c, 400, "variant_a and variant_b objects are required");
return;
}
cJSON* out_a = run_variant(a);
cJSON* out_b = run_variant(b);
if (!out_a || !out_b) {
cJSON_Delete(out_a);
cJSON_Delete(out_b);
cJSON_Delete(body);
reply_error(c, 500, "failed to run one or more variants");
return;
}
cJSON* root = cJSON_CreateObject();
if (!root) {
cJSON_Delete(out_a);
cJSON_Delete(out_b);
cJSON_Delete(body);
reply_error(c, 500, "oom");
return;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddItemToObject(root, "variant_a", out_a);
cJSON_AddItemToObject(root, "variant_b", out_b);
cJSON_Delete(body);
reply_json(c, 200, root);
cJSON_Delete(root);
}
static void http_handler(struct mg_connection* c, int ev, void* ev_data) {
if (ev == MG_EV_ACCEPT) {
if (g_api_tls_enabled) {
struct mg_tls_opts opts;
memset(&opts, 0, sizeof(opts));
opts.cert = g_tls_cert;
opts.key = g_tls_key;
mg_tls_init(c, &opts);
}
return;
}
if (ev != MG_EV_HTTP_MSG) {
return;
}
struct mg_http_message* hm = (struct mg_http_message*)ev_data;
if (method_is(hm, "OPTIONS")) {
mg_http_reply(c, 204, k_json_headers, "");
return;
}
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/status"), NULL)) {
handle_status(c);
return;
}
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/current"), NULL)) {
handle_context_current(c);
return;
}
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/parts"), NULL)) {
handle_context_parts(c);
return;
}
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run-simple"), NULL)) {
handle_prompt_run_simple(c, hm);
return;
}
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run"), NULL)) {
handle_prompt_run(c, hm);
return;
}
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/agent"), NULL)) {
handle_prompt_agent(c, hm);
return;
}
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/compare"), NULL)) {
handle_prompt_compare(c, hm);
return;
}
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/trigger/*"), NULL)) {
char d_tag[TRIGGER_SKILL_D_TAG_MAX] = {0};
if (uri_extract_after_prefix(&hm->uri, "/api/trigger/", d_tag, sizeof(d_tag)) != 0) {
reply_error(c, 400, "missing or invalid d_tag");
return;
}
handle_trigger_webhook(c, hm, d_tag);
return;
}
reply_error(c, 404, "not found");
}
int http_api_init(const http_api_context_t* ctx) {
if (!ctx || !ctx->cfg || !ctx->tools_ctx) {
return -1;
}
g_api_ctx = *ctx;
mg_mgr_init(&g_mgr);
g_api_tls_enabled = 0;
g_tls_cert = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/cert.pem");
g_tls_key = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/key.pem");
if (g_tls_cert.buf && g_tls_cert.len > 0 && g_tls_key.buf && g_tls_key.len > 0) {
g_api_tls_enabled = 1;
} else {
if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf);
if (g_tls_key.buf) mg_free((void*)g_tls_key.buf);
g_tls_cert.buf = NULL;
g_tls_cert.len = 0;
g_tls_key.buf = NULL;
g_tls_key.len = 0;
}
char listen_url[320];
snprintf(listen_url,
sizeof(listen_url),
"%s://%s:%d",
g_api_tls_enabled ? "https" : "http",
g_api_ctx.cfg->api.bind_address[0] ? g_api_ctx.cfg->api.bind_address : "127.0.0.1",
g_api_ctx.cfg->api.port > 0 ? g_api_ctx.cfg->api.port : 8484);
g_listener = mg_http_listen(&g_mgr, listen_url, http_handler, NULL);
if (!g_listener) {
mg_mgr_free(&g_mgr);
memset(&g_mgr, 0, sizeof(g_mgr));
return -1;
}
g_api_initialized = 1;
DEBUG_INFO("[didactyl] http api listening on %s", listen_url);
return 0;
}
int http_api_poll(int timeout_ms) {
if (!g_api_initialized) {
return 0;
}
if (timeout_ms < 0) timeout_ms = 0;
mg_mgr_poll(&g_mgr, timeout_ms);
return 0;
}
void http_api_cleanup(void) {
if (!g_api_initialized) {
return;
}
mg_mgr_free(&g_mgr);
memset(&g_mgr, 0, sizeof(g_mgr));
g_listener = NULL;
if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf);
if (g_tls_key.buf) mg_free((void*)g_tls_key.buf);
g_tls_cert.buf = NULL;
g_tls_cert.len = 0;
g_tls_key.buf = NULL;
g_tls_key.len = 0;
g_api_tls_enabled = 0;
memset(&g_api_ctx, 0, sizeof(g_api_ctx));
g_api_initialized = 0;
}