#define _POSIX_C_SOURCE 200809L #include "agent.h" #include #include #include #include #include #include #include #include #include #include #include "llm.h" #include "nostr_handler.h" #include "trigger_manager.h" #include "tools/tools.h" #include "prompt_template.h" #include "cjson/cJSON.h" #include "debug.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" #include "context_format.h" #include "context_roles.h" #include "json_to_markdown.h" static didactyl_config_t* g_cfg = NULL; static tools_context_t g_tools_ctx; static struct trigger_manager* g_trigger_manager = NULL; typedef enum { CONTEXT_DEBUG_OFF = 0, CONTEXT_DEBUG_INIT = 1, CONTEXT_DEBUG_FULL = 2 } context_debug_mode_t; static context_debug_mode_t g_context_debug_mode = CONTEXT_DEBUG_OFF; static char g_context_debug_run_stamp[32] = {0}; static int g_context_debug_turn = 0; #define AGENT_CONTEXT_PART_NAMES_MAX 512 static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX]; static int g_context_part_names_count = 0; static pthread_mutex_t g_context_part_names_mutex = PTHREAD_MUTEX_INITIALIZER; #define AGENT_DEBOUNCE_WINDOW_SECONDS 5 #define AGENT_DEBOUNCE_CACHE_SIZE 256 #define AGENT_HISTORY_TURNS 12 #define AGENT_HISTORY_QUERY_LIMIT 200 #define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 300 #define AGENT_ADOPTED_SKILLS_MAX 24 #define AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS 1800 #define AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS 7200 typedef struct { uint64_t fingerprint; time_t seen_at; } agent_seen_msg_t; typedef struct { int kind; char author_pubkey_hex[65]; char d_tag[65]; char* content; int has_trigger; trigger_type_t trigger_type; char filter_json[TRIGGER_FILTER_JSON_MAX]; } agent_adopted_skill_t; static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE]; static int g_seen_msgs_count = 0; static int g_seen_msgs_next = 0; static pthread_mutex_t g_seen_msgs_mutex = PTHREAD_MUTEX_INITIALIZER; static agent_adopted_skill_t g_adopted_skills[AGENT_ADOPTED_SKILLS_MAX]; static int g_adopted_skills_count = 0; static time_t g_adopted_skills_last_refresh_at = 0; static pthread_mutex_t g_adopted_skills_mutex = PTHREAD_MUTEX_INITIALIZER; static int refresh_adopted_skills_cache_if_needed(void); static uint64_t fnv1a64(const char* s) { uint64_t h = 1469598103934665603ULL; if (!s) return h; while (*s) { h ^= (unsigned char)(*s++); h *= 1099511628211ULL; } return h; } static uint64_t message_fingerprint(const char* sender_pubkey_hex, const char* message) { uint64_t a = fnv1a64(sender_pubkey_hex); uint64_t b = fnv1a64(message); return a ^ (b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2)); } static uint64_t tool_calls_fingerprint(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(tc->name ? tc->name : ""); uint64_t args_h = fnv1a64(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(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_cfg || g_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_cfg->admin.pubkey, diag); } static int agent_message_is_debounced(const char* sender_pubkey_hex, const char* message) { time_t now = time(NULL); uint64_t fp = message_fingerprint(sender_pubkey_hex, message); int duplicate = 0; pthread_mutex_lock(&g_seen_msgs_mutex); for (int i = 0; i < g_seen_msgs_count; i++) { if (g_seen_msgs[i].fingerprint == fp && g_seen_msgs[i].seen_at > 0 && (now - g_seen_msgs[i].seen_at) <= AGENT_DEBOUNCE_WINDOW_SECONDS) { duplicate = 1; break; } } if (!duplicate) { int slot = 0; if (g_seen_msgs_count < AGENT_DEBOUNCE_CACHE_SIZE) { slot = g_seen_msgs_count; g_seen_msgs_count++; } else { slot = g_seen_msgs_next; g_seen_msgs_next = (g_seen_msgs_next + 1) % AGENT_DEBOUNCE_CACHE_SIZE; } g_seen_msgs[slot].fingerprint = fp; g_seen_msgs[slot].seen_at = now; } pthread_mutex_unlock(&g_seen_msgs_mutex); return duplicate; } static int append_simple_message(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(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(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 void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload); static char* local_json_error(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 int is_space_char(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(*s)) s++; return s ? s : ""; } static char* build_slash_args_json(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 int append_textf_local(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_local(char** buf, size_t* cap, size_t* used, int depth) { for (int i = 0; i < depth; i++) { if (append_textf_local(buf, cap, used, " ") != 0) { return -1; } } return 0; } static int append_cjson_markdown_local(char** buf, size_t* cap, size_t* used, cJSON* node, int depth) { if (!node) { return append_markdown_indent_local(buf, cap, used, depth) == 0 && append_textf_local(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_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- **%s:** %s\n", key, child->valuestring ? child->valuestring : "") != 0) { return -1; } } else if (cJSON_IsBool(child)) { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- **%s:** %s\n", key, cJSON_IsTrue(child) ? "true" : "false") != 0) { return -1; } } else if (cJSON_IsNumber(child)) { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- **%s:** %g\n", key, child->valuedouble) != 0) { return -1; } } else if (cJSON_IsNull(child)) { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- **%s:** null\n", key) != 0) { return -1; } } else { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- **%s:**\n", key) != 0) { return -1; } if (append_cjson_markdown_local(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_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- %s\n", item->valuestring ? item->valuestring : "") != 0) { return -1; } } else if (cJSON_IsBool(item)) { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- %s\n", cJSON_IsTrue(item) ? "true" : "false") != 0) { return -1; } } else if (cJSON_IsNumber(item)) { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- %g\n", item->valuedouble) != 0) { return -1; } } else if (cJSON_IsNull(item)) { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "- null\n") != 0) { return -1; } } else { if (append_markdown_indent_local(buf, cap, used, depth) != 0 || append_textf_local(buf, cap, used, "-\n") != 0) { return -1; } if (append_cjson_markdown_local(buf, cap, used, item, depth + 1) != 0) { return -1; } } } return 0; } if (cJSON_IsString(node)) { return append_markdown_indent_local(buf, cap, used, depth) == 0 && append_textf_local(buf, cap, used, "- %s\n", node->valuestring ? node->valuestring : "") == 0 ? 0 : -1; } if (cJSON_IsBool(node)) { return append_markdown_indent_local(buf, cap, used, depth) == 0 && append_textf_local(buf, cap, used, "- %s\n", cJSON_IsTrue(node) ? "true" : "false") == 0 ? 0 : -1; } if (cJSON_IsNumber(node)) { return append_markdown_indent_local(buf, cap, used, depth) == 0 && append_textf_local(buf, cap, used, "- %g\n", node->valuedouble) == 0 ? 0 : -1; } return append_markdown_indent_local(buf, cap, used, depth) == 0 && append_textf_local(buf, cap, used, "- null\n") == 0 ? 0 : -1; } static char* format_result_markdown_local(const char* raw_result) { if (!raw_result) { return strdup("No output."); } cJSON* root = cJSON_Parse(raw_result); if (!root) { return strdup(raw_result); } if (cJSON_IsObject(root)) { cJSON* content = cJSON_GetObjectItemCaseSensitive(root, "content"); if (content && cJSON_IsString(content) && content->valuestring) { char* direct = strdup(content->valuestring); cJSON_Delete(root); return direct ? direct : 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_local(&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(void) { size_t cap = 2048U; size_t used = 0U; char* out = (char*)malloc(cap); if (!out) return NULL; out[0] = '\0'; if (append_textf_local(&out, &cap, &used, "AVAILABLE TOOLS\n") != 0) { free(out); return NULL; } if (append_textf_local(&out, &cap, &used, "(run / -h for full JSON args template)\n") != 0) { free(out); return NULL; } char* tool_list_json = tools_execute(&g_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_local(&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_local(&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_local(&out, &cap, &used, "\nAVAILABLE SKILLS\n") != 0) { free(out); return NULL; } char* skill_list_json = tools_execute(&g_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_local(&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_local(&out, &cap, &used, "- (none)\n"); } cJSON_Delete(skill_root); free(skill_list_json); if (append_textf_local(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) { free(out); return NULL; } char* adopted_json = tools_execute(&g_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_local(&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_local(&out, &cap, &used, "- (none)\n"); } cJSON_Delete(adoption_events); cJSON_Delete(adopted_root); free(adopted_json); return out; } static cJSON* build_example_value_for_type_local(const char* type_s) { if (!type_s || type_s[0] == '\0') return cJSON_CreateString(""); if (strcmp(type_s, "string") == 0) return cJSON_CreateString(""); if (strcmp(type_s, "integer") == 0) return cJSON_CreateNumber(0); if (strcmp(type_s, "number") == 0) return cJSON_CreateNumber(0.0); if (strcmp(type_s, "boolean") == 0) return cJSON_CreateBool(0); if (strcmp(type_s, "array") == 0) return cJSON_CreateArray(); if (strcmp(type_s, "object") == 0) return cJSON_CreateObject(); return cJSON_CreateString(""); } static cJSON* build_example_args_from_parameters_local(cJSON* params) { cJSON* example = cJSON_CreateObject(); if (!example) return NULL; cJSON* props = params ? cJSON_GetObjectItemCaseSensitive(params, "properties") : NULL; if (!props || !cJSON_IsObject(props)) { return example; } cJSON* prop = NULL; cJSON_ArrayForEach(prop, props) { if (!prop->string || prop->string[0] == '\0') continue; cJSON* enum_vals = cJSON_GetObjectItemCaseSensitive(prop, "enum"); if (enum_vals && cJSON_IsArray(enum_vals) && cJSON_GetArraySize(enum_vals) > 0) { cJSON* first = cJSON_GetArrayItem(enum_vals, 0); cJSON* dup = cJSON_Duplicate(first, 1); if (dup) { cJSON_AddItemToObject(example, prop->string, dup); continue; } } cJSON* type = cJSON_GetObjectItemCaseSensitive(prop, "type"); const char* type_s = (type && cJSON_IsString(type) && type->valuestring) ? type->valuestring : NULL; cJSON* val = build_example_value_for_type_local(type_s); if (!val) val = cJSON_CreateString(""); if (!val) { cJSON_Delete(example); return NULL; } cJSON_AddItemToObject(example, prop->string, val); } return example; } static char* build_slash_help_tool_json(const char* tool_name) { if (!tool_name || tool_name[0] == '\0') { return local_json_error("missing tool name"); } char* schema_json = tools_build_openai_schema_json(&g_tools_ctx); if (!schema_json) { return local_json_error("failed to build tool schema"); } cJSON* schema = cJSON_Parse(schema_json); free(schema_json); if (!schema || !cJSON_IsArray(schema)) { cJSON_Delete(schema); return local_json_error("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 local_json_error("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)); cJSON* params = cJSON_GetObjectItemCaseSensitive(matched_fn, "parameters"); cJSON* example_args = build_example_args_from_parameters_local(params); if (example_args) { char* example_json = cJSON_PrintUnformatted(example_args); if (example_json) { cJSON_AddStringToObject(out, "example_args_json", example_json); size_t usage_len = strlen(tool_name) + strlen(example_json) + 4U; char* usage = (char*)malloc(usage_len); if (usage) { snprintf(usage, usage_len, "/%s %s", tool_name, example_json); cJSON_AddStringToObject(out, "example_slash", usage); free(usage); } free(example_json); } cJSON_Delete(example_args); } char* result = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(schema); return result; } static int handle_slash_command(const char* sender_pubkey_hex, const char* message) { if (!sender_pubkey_hex || !message || message[0] != '/') { return 0; } const char* p = message + 1; while (*p && !is_space_char(*p)) p++; size_t tool_len = (size_t)(p - (message + 1)); if (tool_len == 0 || tool_len >= 128U) { (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, "{\"success\":false,\"error\":\"invalid slash command\"}", DIDACTYL_DM_HISTORY_TOOL_RESPONSE); return 1; } 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)) { result_json = build_slash_help_tool_json(tool_name); } else if (strcmp(tool_name, "help") == 0) { if (!raw_args || raw_args[0] == '\0') { result_json = build_slash_help_all_json(); } else { char help_tool[128]; size_t i = 0; while (raw_args[i] && !is_space_char(raw_args[i]) && i < sizeof(help_tool) - 1U) { help_tool[i] = raw_args[i]; i++; } help_tool[i] = '\0'; result_json = build_slash_help_tool_json(help_tool); } } else { char* args_json = build_slash_args_json(raw_args); if (!args_json) { result_json = local_json_error("failed to parse slash args"); } else { result_json = tools_execute(&g_tools_ctx, tool_name, args_json); free(args_json); } } if (!result_json) { result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}"); } size_t tool_name_len = strlen(tool_name); int send_raw_json = (tool_name_len >= 4U && strcmp(tool_name + tool_name_len - 4U, "_get") == 0); char* markdown_result = send_raw_json ? NULL : format_result_markdown_local(result_json); const char* dm_payload = send_raw_json ? result_json : (markdown_result ? markdown_result : result_json); size_t log_cap = strlen(message) + strlen(result_json ? result_json : "") + strlen(dm_payload ? dm_payload : "") + 192U; char* log_payload = (char*)malloc(log_cap); if (log_payload) { snprintf(log_payload, log_cap, "slash=%s\nresult_json=%s\nresult_markdown=%s", message, result_json ? result_json : "", dm_payload ? dm_payload : ""); append_context_log(sender_pubkey_hex, "direct_tool_exec", log_payload); free(log_payload); } (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, dm_payload ? dm_payload : "Command executed with no output.", DIDACTYL_DM_HISTORY_TOOL_RESPONSE); free(markdown_result); free(result_json); return 1; } static const char* get_context_part_name_copy(int idx); static const char* detect_context_section(const char* role, const char* content, int idx) { const char* c = content ? content : ""; const char* r = role ? role : ""; if (idx == 0 && strcmp(r, "system") == 0) return "system_prompt"; if (strncmp(c, "Administrator Context", 21) == 0) { return "admin_context"; } if (strncmp(c, "Agent identity", 14) == 0 || strncmp(c, "Agent Identity", 14) == 0) { return "agent_identity"; } if (strncmp(c, "Sender verification", 19) == 0 || strncmp(c, "This message has been cryptographically verified", 47) == 0 || strncmp(c, "This message is from a web-of-trust contact", 43) == 0) { return "sender_context"; } if (strncmp(c, "Startup events memory", 21) == 0 || strncmp(c, "Startup Events Memory", 21) == 0) { return "startup_events"; } if (strncmp(c, "Adopted skills memory", 21) == 0 || strncmp(c, "Adopted Skills Memory", 21) == 0) { return "adopted_skills"; } if (strncmp(c, "### Current Tasks", 17) == 0) { return "agent_tasks"; } if (strncmp(c, "Administrator recent public notes", 33) == 0) { return "admin_notes"; } if (strcmp(r, "user") == 0 || strcmp(r, "assistant") == 0) { return "dm_history"; } return "context_part"; } const char* agent_classify_message_part(cJSON* msg, int idx) { const char* mapped = get_context_part_name_copy(idx); if (mapped) { return mapped; } cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL; cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL; const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : ""; const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : ""; return detect_context_section(role_s, content_s, idx); } static char* render_messages_json_as_markdown(const char* messages_json) { if (!messages_json || messages_json[0] == '\0') { return NULL; } cJSON* root = cJSON_Parse(messages_json); if (!root || !cJSON_IsArray(root)) { cJSON_Delete(root); return NULL; } char* out = (char*)malloc(4096); if (!out) { cJSON_Delete(root); return NULL; } size_t cap = 4096; size_t used = 0; out[0] = '\0'; int n = cJSON_GetArraySize(root); for (int i = 0; i < n; i++) { cJSON* msg = cJSON_GetArrayItem(root, i); if (!msg || !cJSON_IsObject(msg)) { continue; } cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role"); const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "message"; char* msg_json = cJSON_PrintUnformatted(msg); char* msg_md = msg_json ? json_to_markdown(msg_json, 8) : NULL; const char* body = msg_md ? msg_md : (msg_json ? msg_json : ""); if (append_textf_local(&out, &cap, &used, "### Message %d (%s)\n\n%s\n\n", i + 1, role_s, body) != 0) { free(msg_md); free(msg_json); free(out); cJSON_Delete(root); return NULL; } free(msg_md); free(msg_json); } cJSON_Delete(root); return out; } static void clear_context_part_names_locked(void) { for (int i = 0; i < g_context_part_names_count; i++) { free(g_context_part_names[i]); g_context_part_names[i] = NULL; } g_context_part_names_count = 0; } static void clear_context_part_names(void) { pthread_mutex_lock(&g_context_part_names_mutex); clear_context_part_names_locked(); pthread_mutex_unlock(&g_context_part_names_mutex); } static void set_context_part_name(int idx, const char* name) { if (idx < 0 || idx >= AGENT_CONTEXT_PART_NAMES_MAX) return; pthread_mutex_lock(&g_context_part_names_mutex); if (idx >= g_context_part_names_count) { for (int i = g_context_part_names_count; i <= idx && i < AGENT_CONTEXT_PART_NAMES_MAX; i++) { g_context_part_names[i] = NULL; } g_context_part_names_count = idx + 1; } free(g_context_part_names[idx]); g_context_part_names[idx] = strdup(name ? name : "context_part"); pthread_mutex_unlock(&g_context_part_names_mutex); } static const char* get_context_part_name_copy(int idx) { static __thread char tls_name[128]; tls_name[0] = '\0'; pthread_mutex_lock(&g_context_part_names_mutex); if (idx >= 0 && idx < g_context_part_names_count && g_context_part_names[idx]) { snprintf(tls_name, sizeof(tls_name), "%s", g_context_part_names[idx]); } pthread_mutex_unlock(&g_context_part_names_mutex); return tls_name[0] ? tls_name : NULL; } static __attribute__((unused)) void template_emit_hook(const char* section_name, int message_index, void* user_data) { (void)user_data; set_context_part_name(message_index, section_name ? section_name : "context_part"); } static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) { (void)sender_pubkey_hex; if (!phase || g_context_debug_mode == CONTEXT_DEBUG_OFF) { return; } int is_initial_phase = (strcmp(phase, "llm_chat_with_tools_messages") == 0 || strcmp(phase, "llm_trigger") == 0); int is_turn_phase = (strcmp(phase, "llm_chat_with_tools_turn") == 0 || strcmp(phase, "llm_trigger_with_tools_turn") == 0); if (!is_initial_phase && !is_turn_phase) { return; } if (g_context_debug_mode == CONTEXT_DEBUG_INIT && !is_initial_phase) { return; } const char* safe_payload = context_payload ? context_payload : ""; char* turn_markdown = NULL; if (is_turn_phase && context_payload && context_payload[0] == '[') { turn_markdown = render_messages_json_as_markdown(context_payload); if (turn_markdown) { safe_payload = turn_markdown; } } int entry_len = snprintf(NULL, 0, "%s\n\n", safe_payload); if (entry_len < 0) { return; } char* entry = (char*)malloc((size_t)entry_len + 1U); if (!entry) { return; } snprintf(entry, (size_t)entry_len + 1U, "%s\n\n", safe_payload); char* old_data = NULL; size_t old_len = 0; FILE* in = fopen("context.log.md", "rb"); if (in) { if (fseek(in, 0, SEEK_END) == 0) { long sz = ftell(in); if (sz > 0 && fseek(in, 0, SEEK_SET) == 0) { old_len = (size_t)sz; old_data = (char*)malloc(old_len); if (old_data) { size_t n = fread(old_data, 1, old_len, in); if (n != old_len) { free(old_data); old_data = NULL; old_len = 0; } } else { old_len = 0; } } } fclose(in); } FILE* out = fopen("context.log.md", "wb"); if (!out) { free(old_data); free(entry); return; } (void)fwrite(entry, 1, (size_t)entry_len, out); if (old_data && old_len > 0) { (void)fwrite(old_data, 1, old_len, out); } fclose(out); free(old_data); if (mkdir("context.logs", 0755) != 0 && errno != EEXIST) { free(entry); return; } time_t now = time(NULL); struct tm tm_info; localtime_r(&now, &tm_info); if (g_context_debug_run_stamp[0] == '\0') { strftime(g_context_debug_run_stamp, sizeof(g_context_debug_run_stamp), "%Y%m%dT%H%M%S", &tm_info); g_context_debug_turn = 0; } char snapshot_path[256] = {0}; if (is_initial_phase) { snprintf(snapshot_path, sizeof(snapshot_path), "context.logs/%s_init.md", g_context_debug_run_stamp); } else { g_context_debug_turn++; snprintf(snapshot_path, sizeof(snapshot_path), "context.logs/%s_t%03d.md", g_context_debug_run_stamp, g_context_debug_turn); } FILE* snapshot = fopen(snapshot_path, "wb"); if (snapshot) { (void)fwrite(safe_payload, 1, strlen(safe_payload), snapshot); fclose(snapshot); } free(entry); free(turn_markdown); } int agent_set_context_debug_mode(const char* mode) { if (!mode || mode[0] == '\0' || strcmp(mode, "off") == 0) { g_context_debug_mode = CONTEXT_DEBUG_OFF; } else if (strcmp(mode, "init") == 0) { g_context_debug_mode = CONTEXT_DEBUG_INIT; } else if (strcmp(mode, "full") == 0) { g_context_debug_mode = CONTEXT_DEBUG_FULL; } else { return -1; } g_context_debug_run_stamp[0] = '\0'; g_context_debug_turn = 0; return 0; } void agent_append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) { append_context_log(sender_pubkey_hex, phase, context_payload); } static __attribute__((unused)) char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) { if (sender_tier == DIDACTYL_SENDER_ADMIN) { return strdup("This message has been cryptographically verified as coming from your administrator."); } if (sender_tier == DIDACTYL_SENDER_WOT) { return strdup("This message is from a web-of-trust contact (not the administrator)."); } return strdup("Sender tier is unknown."); } static __attribute__((unused)) int append_recent_admin_dm_history(cJSON* messages, const char* current_message) { if (!messages || !g_cfg) { return -1; } char* history_json = nostr_handler_get_dm_history_json(g_cfg->admin.pubkey, AGENT_HISTORY_TURNS); if (!history_json) { return 0; } cJSON* items = cJSON_Parse(history_json); free(history_json); if (!items || !cJSON_IsArray(items)) { cJSON_Delete(items); return 0; } const char* last_appended_role = NULL; const char* last_appended_content = NULL; int n = cJSON_GetArraySize(items); for (int i = 0; i < n; i++) { cJSON* item = cJSON_GetArrayItem(items, i); cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL; cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL; cJSON* created_at = item ? cJSON_GetObjectItemCaseSensitive(item, "created_at") : NULL; if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) || !role->valuestring || !content->valuestring) { continue; } const char* role_s = role->valuestring; const char* content_s = content->valuestring; int role_is_user = (strcmp(role_s, "user") == 0); if (!role_is_user && strcmp(role_s, "assistant") != 0) { continue; } if (i == n - 1 && role_is_user && current_message && strcmp(content_s, current_message) == 0) { continue; } if (last_appended_role && last_appended_content && strcmp(last_appended_role, role_s) == 0 && strcmp(last_appended_content, content_s) == 0) { continue; } if (append_simple_message(messages, role_s, content_s) != 0) { cJSON_Delete(items); return -1; } cJSON* appended = cJSON_GetArrayItem(messages, cJSON_GetArraySize(messages) - 1); if (appended && cJSON_IsObject(appended) && created_at && cJSON_IsNumber(created_at)) { cJSON_AddNumberToObject(appended, "_ts", created_at->valuedouble); } last_appended_role = role_s; last_appended_content = content_s; } cJSON_Delete(items); return 0; } static cJSON* find_tag_value_string_local(cJSON* tags, const char* key) { if (!tags || !key || !cJSON_IsArray(tags)) { return NULL; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; 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) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) { continue; } if (strcmp(k->valuestring, key) == 0) { return v; } } return NULL; } static int parse_skill_address_tag_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) { if (!addr || !out_kind || !out_pubkey_hex || !out_d_tag) { return -1; } const char* p1 = strchr(addr, ':'); if (!p1) return -1; const char* p2 = strchr(p1 + 1, ':'); if (!p2) return -1; char kind_buf[16] = {0}; size_t kind_len = (size_t)(p1 - addr); if (kind_len == 0 || kind_len >= sizeof(kind_buf)) { return -1; } memcpy(kind_buf, addr, kind_len); int kind = atoi(kind_buf); if (kind != 31123 && kind != 31124) { return -1; } size_t pub_len = (size_t)(p2 - (p1 + 1)); if (pub_len != 64U) { return -1; } memcpy(out_pubkey_hex, p1 + 1, 64U); out_pubkey_hex[64] = '\0'; size_t d_tag_len = strlen(p2 + 1); if (d_tag_len == 0 || d_tag_len >= 65U) { return -1; } memcpy(out_d_tag, p2 + 1, d_tag_len + 1U); *out_kind = kind; return 0; } static int dm_filter_matches_tier_local(const char* filter_json, didactyl_sender_tier_t tier) { if (!filter_json || filter_json[0] == '\0') { return 0; } cJSON* root = cJSON_Parse(filter_json); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return 0; } cJSON* from = cJSON_GetObjectItemCaseSensitive(root, "from"); const char* from_s = (from && cJSON_IsString(from) && from->valuestring) ? from->valuestring : "admin"; int match = 0; if (strcmp(from_s, "any") == 0) { match = 1; } else if (strcmp(from_s, "admin") == 0) { match = (tier == DIDACTYL_SENDER_ADMIN); } else if (strcmp(from_s, "wot") == 0) { match = (tier == DIDACTYL_SENDER_WOT); } cJSON_Delete(root); return match; } static const char* adopted_skill_content_lookup_by_d_tag_locked(const char* d_tag) { if (!d_tag || d_tag[0] == '\0') { return NULL; } for (int i = 0; i < g_adopted_skills_count; i++) { if (strcmp(g_adopted_skills[i].d_tag, d_tag) == 0) { return g_adopted_skills[i].content; } } return NULL; } static const char* template_skill_lookup_callback(void* user_data, const char* d_tag) { (void)user_data; if (!d_tag || d_tag[0] == '\0') { return NULL; } (void)refresh_adopted_skills_cache_if_needed(); const char* out = NULL; pthread_mutex_lock(&g_adopted_skills_mutex); out = adopted_skill_content_lookup_by_d_tag_locked(d_tag); pthread_mutex_unlock(&g_adopted_skills_mutex); return out; } static char* resolve_skill_references_local(const char* input) { return prompt_template_resolve_inline_variables(input ? input : "", &g_tools_ctx); } static char* build_context_from_triggers(trigger_type_t trigger_type, const char* trigger_filter, cJSON* trigger_event, const char* relay_url) { (void)relay_url; (void)trigger_filter; if (!g_cfg) { return strdup("You are an AI agent. Respond to the message."); } (void)refresh_adopted_skills_cache_if_needed(); char* title = context_build_title(&g_tools_ctx); size_t cap = 4096; size_t used = 0; char* out = (char*)malloc(cap); if (!out) { free(title); return NULL; } out[0] = '\0'; if (title) { size_t title_len = strlen(title); if (title_len + 1U > cap) { cap = title_len + 1024; char* grown = (char*)realloc(out, cap); if (!grown) { free(title); free(out); return NULL; } out = grown; } memcpy(out, title, title_len); used = title_len; out[used] = '\0'; free(title); } int matched = 0; didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER; if (trigger_event && cJSON_IsObject(trigger_event)) { cJSON* tier_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "tier"); const char* tier_s = (tier_j && cJSON_IsString(tier_j) && tier_j->valuestring) ? tier_j->valuestring : "stranger"; if (strcmp(tier_s, "admin") == 0) tier = DIDACTYL_SENDER_ADMIN; else if (strcmp(tier_s, "wot") == 0) tier = DIDACTYL_SENDER_WOT; } char* matched_skill_contents[AGENT_ADOPTED_SKILLS_MAX]; int matched_skill_count = 0; memset(matched_skill_contents, 0, sizeof(matched_skill_contents)); pthread_mutex_lock(&g_adopted_skills_mutex); for (int i = 0; i < g_adopted_skills_count && matched_skill_count < AGENT_ADOPTED_SKILLS_MAX; i++) { const agent_adopted_skill_t* s = &g_adopted_skills[i]; if (!s->has_trigger || s->trigger_type != trigger_type) { continue; } if (trigger_type == TRIGGER_TYPE_DM && !dm_filter_matches_tier_local(s->filter_json, tier)) { continue; } matched_skill_contents[matched_skill_count] = strdup(s->content ? s->content : ""); if (matched_skill_contents[matched_skill_count]) { matched_skill_count++; } } pthread_mutex_unlock(&g_adopted_skills_mutex); for (int i = 0; i < matched_skill_count; i++) { char* expanded = resolve_skill_references_local(matched_skill_contents[i] ? matched_skill_contents[i] : ""); const char* skill_text = expanded ? expanded : (matched_skill_contents[i] ? matched_skill_contents[i] : ""); char* bumped = context_bump_headings(skill_text); const char* final_text = bumped ? bumped : skill_text; size_t need = strlen("\n\n---\n\n") + strlen(final_text) + 1U; if (used + need >= cap) { size_t next = cap; while (used + need >= next) next *= 2U; char* grown = (char*)realloc(out, next); if (!grown) { free(bumped); free(expanded); for (int j = i; j < matched_skill_count; j++) { free(matched_skill_contents[j]); } free(out); return NULL; } out = grown; cap = next; } if (matched > 0) { memcpy(out + used, "\n\n---\n\n", 7); used += 7; } size_t sl = strlen(final_text); memcpy(out + used, final_text, sl); used += sl; out[used] = '\0'; matched++; free(bumped); free(expanded); free(matched_skill_contents[i]); } if (matched == 0) { const char* fallback = "## Default Instructions\n\nYou are an AI agent. Respond to the message."; const char* sep = (used > 0) ? "\n\n---\n\n" : ""; size_t sep_len = strlen(sep); size_t fb_len = strlen(fallback); size_t need = used + sep_len + fb_len + 1U; if (need > cap) { char* grown = (char*)realloc(out, need); if (!grown) { free(out); return NULL; } out = grown; cap = need; } if (sep_len > 0) { memcpy(out + used, sep, sep_len); used += sep_len; } memcpy(out + used, fallback, fb_len); used += fb_len; out[used] = '\0'; } return out; } static void clear_adopted_skills_cache_locked(void) { for (int i = 0; i < g_adopted_skills_count; i++) { free(g_adopted_skills[i].content); g_adopted_skills[i].content = NULL; g_adopted_skills[i].kind = 0; g_adopted_skills[i].author_pubkey_hex[0] = '\0'; g_adopted_skills[i].d_tag[0] = '\0'; g_adopted_skills[i].has_trigger = 0; g_adopted_skills[i].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION; g_adopted_skills[i].filter_json[0] = '\0'; } g_adopted_skills_count = 0; } static int refresh_adopted_skills_cache_if_needed(void) { if (!g_cfg) { return -1; } time_t now = time(NULL); pthread_mutex_lock(&g_adopted_skills_mutex); int cache_fresh = (g_adopted_skills_last_refresh_at > 0) && ((now - g_adopted_skills_last_refresh_at) < AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS); pthread_mutex_unlock(&g_adopted_skills_mutex); if (cache_fresh) { return 0; } agent_adopted_skill_t tmp[AGENT_ADOPTED_SKILLS_MAX]; memset(tmp, 0, sizeof(tmp)); int tmp_count = 0; char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123); cJSON* adoption_events = adoption_json ? cJSON_Parse(adoption_json) : NULL; free(adoption_json); if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) { cJSON* list_event = cJSON_GetArrayItem(adoption_events, 0); cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL; if (list_tags && cJSON_IsArray(list_tags)) { int tn = cJSON_GetArraySize(list_tags); for (int i = 0; i < tn && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) { cJSON* tag = cJSON_GetArrayItem(list_tags, i); if (!tag || !cJSON_IsArray(tag)) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring || strcmp(key->valuestring, "a") != 0) { continue; } int skill_kind = 0; char skill_author[65] = {0}; char skill_d_tag[65] = {0}; if (parse_skill_address_tag_local(val->valuestring, &skill_kind, skill_author, skill_d_tag) != 0) { continue; } cJSON* skill_events = NULL; if (strcmp(skill_author, g_cfg->keys.public_key_hex) == 0) { char* skill_json = nostr_handler_get_self_events_by_kind_json(skill_kind); if (skill_json) { cJSON* all_events = cJSON_Parse(skill_json); free(skill_json); if (all_events && cJSON_IsArray(all_events)) { skill_events = cJSON_CreateArray(); if (skill_events) { int all_n = cJSON_GetArraySize(all_events); for (int ai = 0; ai < all_n; ai++) { cJSON* ev = cJSON_GetArrayItem(all_events, ai); cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL; cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; cJSON* ev_d = find_tag_value_string_local(ev_tags, "d"); if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring || strcmp(ev_pubkey->valuestring, skill_author) != 0 || !ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring || strcmp(ev_d->valuestring, skill_d_tag) != 0) { continue; } cJSON* dup = cJSON_Duplicate(ev, 1); if (dup) { cJSON_AddItemToArray(skill_events, dup); } } } } cJSON_Delete(all_events); } } else { cJSON* skill_filter = cJSON_CreateObject(); cJSON* sk_kinds = cJSON_CreateArray(); cJSON* sk_authors = cJSON_CreateArray(); cJSON* d_values = cJSON_CreateArray(); if (!skill_filter || !sk_kinds || !sk_authors || !d_values) { cJSON_Delete(skill_filter); cJSON_Delete(sk_kinds); cJSON_Delete(sk_authors); cJSON_Delete(d_values); continue; } cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(skill_kind)); cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds); cJSON_AddItemToArray(sk_authors, cJSON_CreateString(skill_author)); cJSON_AddItemToObject(skill_filter, "authors", sk_authors); cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_d_tag)); cJSON_AddItemToObject(skill_filter, "#d", d_values); cJSON_AddNumberToObject(skill_filter, "limit", 1); char* skill_json = nostr_handler_query_json(skill_filter, 2000); cJSON_Delete(skill_filter); if (skill_json) { skill_events = cJSON_Parse(skill_json); free(skill_json); } } if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) { cJSON_Delete(skill_events); continue; } cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0); cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL; cJSON* skill_tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL; if (!content || !cJSON_IsString(content) || !content->valuestring || !skill_tags || !cJSON_IsArray(skill_tags)) { cJSON_Delete(skill_events); continue; } cJSON* enabled_tag = find_tag_value_string_local(skill_tags, "enabled"); const char* enabled_s = (enabled_tag && cJSON_IsString(enabled_tag) && enabled_tag->valuestring) ? enabled_tag->valuestring : "true"; int is_enabled = !(strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0); if (!is_enabled) { cJSON_Delete(skill_events); continue; } tmp[tmp_count].kind = skill_kind; snprintf(tmp[tmp_count].author_pubkey_hex, sizeof(tmp[tmp_count].author_pubkey_hex), "%s", skill_author); snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", skill_d_tag); tmp[tmp_count].content = strdup(content->valuestring); tmp[tmp_count].has_trigger = 0; tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION; tmp[tmp_count].filter_json[0] = '\0'; cJSON* trigger_tag = find_tag_value_string_local(skill_tags, "trigger"); cJSON* filter_tag = find_tag_value_string_local(skill_tags, "filter"); if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' && filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') { tmp[tmp_count].has_trigger = 1; tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring); snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring); } if (tmp[tmp_count].content) { tmp_count++; } cJSON_Delete(skill_events); } } } cJSON_Delete(adoption_events); pthread_mutex_lock(&g_adopted_skills_mutex); clear_adopted_skills_cache_locked(); for (int i = 0; i < tmp_count; i++) { g_adopted_skills[g_adopted_skills_count] = tmp[i]; tmp[i].content = NULL; g_adopted_skills_count++; } g_adopted_skills_last_refresh_at = now; pthread_mutex_unlock(&g_adopted_skills_mutex); for (int i = 0; i < tmp_count; i++) { free(tmp[i].content); } return 0; } static char* flatten_skill_content_to_text(const char* skill_content) { if (!skill_content || skill_content[0] == '\0') { return strdup(""); } cJSON* root = cJSON_Parse(skill_content); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return strdup(skill_content); } cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name"); cJSON* description = cJSON_GetObjectItemCaseSensitive(root, "description"); cJSON* event_kind = cJSON_GetObjectItemCaseSensitive(root, "event_kind"); cJSON* procedure = cJSON_GetObjectItemCaseSensitive(root, "procedure"); size_t cap = strlen(skill_content) + 512U; char* out = (char*)malloc(cap); if (!out) { cJSON_Delete(root); return NULL; } out[0] = '\0'; if (name && cJSON_IsString(name) && name->valuestring) { snprintf(out + strlen(out), cap - strlen(out), "Name: %s\n", name->valuestring); } if (description && cJSON_IsString(description) && description->valuestring) { snprintf(out + strlen(out), cap - strlen(out), "Description: %s\n", description->valuestring); } if (event_kind && cJSON_IsNumber(event_kind)) { snprintf(out + strlen(out), cap - strlen(out), "Event kind: %d\n", (int)event_kind->valuedouble); } if (procedure && cJSON_IsArray(procedure) && cJSON_GetArraySize(procedure) > 0) { strcat(out, "Procedure:\n"); int pn = cJSON_GetArraySize(procedure); for (int i = 0; i < pn; i++) { cJSON* step = cJSON_GetArrayItem(procedure, i); if (!step || !cJSON_IsString(step) || !step->valuestring) continue; snprintf(out + strlen(out), cap - strlen(out), " %d. %s\n", i + 1, step->valuestring); } } if (out[0] == '\0') { snprintf(out, cap, "%s", skill_content); } cJSON_Delete(root); return out; } static __attribute__((unused)) int append_adopted_skills_context(cJSON* messages) { if (!messages || !g_cfg) { return -1; } (void)refresh_adopted_skills_cache_if_needed(); pthread_mutex_lock(&g_adopted_skills_mutex); int cached_count = g_adopted_skills_count; if (cached_count <= 0) { pthread_mutex_unlock(&g_adopted_skills_mutex); return 0; } size_t capacity = (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS + 2048U; char* payload = (char*)malloc(capacity); if (!payload) { pthread_mutex_unlock(&g_adopted_skills_mutex); return -1; } size_t used = 0; int n = snprintf(payload, capacity, "## Adopted Skills Memory (source: adoption list + startup fallback)\n\nAdopted skills memory (always-on behavioral instructions): follow these skill instructions when relevant and never violate explicit constraints.\n"); if (n < 0) { free(payload); pthread_mutex_unlock(&g_adopted_skills_mutex); return -1; } used = (size_t)n < capacity ? (size_t)n : capacity - 1U; size_t total_skill_chars = 0; int included = 0; for (int i = 0; i < cached_count && used + 64U < capacity; i++) { char* flattened_skill = flatten_skill_content_to_text(g_adopted_skills[i].content ? g_adopted_skills[i].content : ""); const char* skill_content = flattened_skill ? flattened_skill : (g_adopted_skills[i].content ? g_adopted_skills[i].content : ""); size_t content_len = strlen(skill_content); size_t clipped_len = content_len; if (clipped_len > (size_t)AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS) { clipped_len = (size_t)AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS; } if (total_skill_chars + clipped_len > (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS) { size_t remaining = (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS - total_skill_chars; if (remaining == 0) { break; } clipped_len = remaining; } int head_n = snprintf(payload + used, capacity - used, "\n\n---\n\n### Skill\nSkill d_tag: %s\nSkill address: %d:%s:%s\n\nInstructions:\n", g_adopted_skills[i].d_tag, g_adopted_skills[i].kind, g_adopted_skills[i].author_pubkey_hex, g_adopted_skills[i].d_tag); if (head_n < 0 || (size_t)head_n >= (capacity - used)) { break; } used += (size_t)head_n; size_t write_len = clipped_len; if (write_len >= (capacity - used)) { write_len = capacity - used - 1U; } memcpy(payload + used, skill_content, write_len); used += write_len; payload[used] = '\0'; int truncated = (content_len > write_len); if (truncated && used + 24U < capacity) { int tail_n = snprintf(payload + used, capacity - used, "\n...[truncated]\n"); if (tail_n > 0 && (size_t)tail_n < (capacity - used)) { used += (size_t)tail_n; } } else if (used + 1U < capacity) { payload[used++] = '\n'; payload[used] = '\0'; } total_skill_chars += write_len; included++; if (total_skill_chars >= (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS) { break; } free(flattened_skill); } if (included < cached_count && used + 96U < capacity) { int extra_n = snprintf(payload + used, capacity - used, "\nAdditional adopted skills omitted due to context budget limits (%d/%d included).\n", included, cached_count); if (extra_n > 0 && (size_t)extra_n < (capacity - used)) { used += (size_t)extra_n; } } pthread_mutex_unlock(&g_adopted_skills_mutex); int rc = append_simple_message(messages, "system", payload); free(payload); return rc; } static __attribute__((unused)) cJSON* build_recent_admin_dm_history_messages(const char* current_message) { cJSON* messages = cJSON_CreateArray(); if (!messages) { return NULL; } if (append_recent_admin_dm_history(messages, current_message) != 0) { cJSON_Delete(messages); return NULL; } return messages; } int agent_init(didactyl_config_t* config) { if (!config) { return -1; } g_cfg = config; if (tools_init(&g_tools_ctx, g_cfg) != 0) { g_cfg = NULL; return -1; } memset(g_seen_msgs, 0, sizeof(g_seen_msgs)); g_seen_msgs_count = 0; g_seen_msgs_next = 0; pthread_mutex_lock(&g_adopted_skills_mutex); clear_adopted_skills_cache_locked(); g_adopted_skills_last_refresh_at = 0; pthread_mutex_unlock(&g_adopted_skills_mutex); g_tools_ctx.template_skill_lookup = template_skill_lookup_callback; g_tools_ctx.template_skill_lookup_user_data = NULL; clear_context_part_names(); return 0; } void agent_set_trigger_manager(struct trigger_manager* trigger_manager) { g_trigger_manager = trigger_manager; g_tools_ctx.trigger_manager = trigger_manager; } void agent_on_trigger(const char* skill_d_tag, const char* skill_content, cJSON* triggering_event, const char* relay_url) { if (!g_cfg || !skill_d_tag || !skill_content || !triggering_event) { return; } char* event_json = cJSON_PrintUnformatted(triggering_event); if (!event_json) { return; } const char* prev_trigger_event_json = g_tools_ctx.template_trigger_event_json; g_tools_ctx.template_trigger_event_json = event_json; const char* relay = (relay_url && relay_url[0] != '\0') ? relay_url : "unknown"; const char* trigger_prefix = "Triggered-skill execution context:\n" "- You are executing a skill because a Nostr event matched its trigger filter.\n" "- Execute the skill instructions below against the triggering event.\n" "- Keep output concise and actionable for the administrator.\n" "- If no action is needed, explicitly say why.\n\n" "Skill d_tag: "; char* composed_context = build_context_from_triggers(TRIGGER_TYPE_NOSTR_SUBSCRIPTION, NULL, triggering_event, relay_url); if (!composed_context) { composed_context = strdup("You are an AI agent. Respond to the trigger event."); } size_t full_len = strlen(composed_context ? composed_context : "") + 2 + strlen(trigger_prefix) + strlen(skill_d_tag) + strlen("\nRelay: ") + strlen(relay) + strlen("\n\nSkill instructions:\n") + strlen(skill_content) + strlen("\n\nuser:\nTriggering event JSON:\n") + strlen(event_json) + 1U; char* full_markdown = (char*)malloc(full_len); if (!full_markdown) { free(composed_context); free(event_json); return; } snprintf(full_markdown, full_len, "%s\n\n%s%s\nRelay: %s\n\nSkill instructions:\n%s\n\nuser:\nTriggering event JSON:\n%s", composed_context ? composed_context : "", trigger_prefix, skill_d_tag, relay, skill_content, event_json); free(composed_context); context_roles_t roles; if (context_roles_split(full_markdown, &roles) != 0) { free(full_markdown); free(event_json); return; } char* tools_json = tools_build_openai_schema_json(&g_tools_ctx); if (!tools_json) { context_roles_free(&roles); free(full_markdown); g_tools_ctx.template_trigger_event_json = prev_trigger_event_json; free(event_json); (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: tools unavailable."); return; } cJSON* messages = cJSON_CreateArray(); if (!messages || !cJSON_IsArray(messages) || append_simple_message(messages, "system", roles.system_content) != 0 || append_simple_message(messages, "user", roles.user_content) != 0) { cJSON_Delete(messages); free(tools_json); context_roles_free(&roles); free(full_markdown); g_tools_ctx.template_trigger_event_json = prev_trigger_event_json; free(event_json); (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: prompt initialization failed."); return; } append_context_log(g_cfg->admin.pubkey, "llm_trigger", full_markdown); context_roles_free(&roles); free(full_markdown); int max_turns = g_cfg->tools.trigger_max_turns > 0 ? g_cfg->tools.trigger_max_turns : (g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8); int turns_run = 0; int trigger_completed = 0; for (int turn = 0; turn < max_turns; turn++) { turns_run = turn + 1; char* messages_json = cJSON_PrintUnformatted(messages); if (!messages_json) { break; } append_context_log(g_cfg->admin.pubkey, "llm_trigger_with_tools_turn", messages_json); llm_response_t resp; int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp); free(messages_json); if (rc != 0) { (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: LLM request failed."); cJSON_Delete(messages); free(tools_json); g_tools_ctx.template_trigger_event_json = prev_trigger_event_json; free(event_json); return; } if (resp.tool_call_count <= 0) { trigger_completed = 1; llm_response_free(&resp); break; } if (append_assistant_tool_calls_message(messages, &resp) != 0) { llm_response_free(&resp); break; } 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_tools_ctx, tc->name, tc->arguments_json); if (!tool_result) { tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}"); } if (append_tool_result_message(messages, tc->id ? tc->id : "", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) { free(tool_result); llm_response_free(&resp); cJSON_Delete(messages); free(tools_json); g_tools_ctx.template_trigger_event_json = prev_trigger_event_json; free(event_json); (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: tool result append failed."); return; } free(tool_result); } llm_response_free(&resp); } if (!trigger_completed && turns_run >= max_turns) { notify_admin_limit_diagnostic("trigger_skill_loop", "max_turns_exhausted", skill_d_tag, max_turns, turns_run, g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3, 0, g_cfg->llm.max_tokens, NULL, NULL, ""); } if (g_trigger_manager) { (void)trigger_manager_fire_chains(g_trigger_manager, skill_d_tag, triggering_event, relay_url && relay_url[0] != '\0' ? relay_url : "trigger"); } cJSON_Delete(messages); free(tools_json); g_tools_ctx.template_trigger_event_json = prev_trigger_event_json; free(event_json); } int agent_build_admin_messages_json(const char* current_user_message, didactyl_sender_tier_t sender_tier, char** out_messages_json) { if (!out_messages_json || !g_cfg) { return -1; } *out_messages_json = NULL; clear_context_part_names(); cJSON* dm_event = cJSON_CreateObject(); if (!dm_event) { return -1; } cJSON_AddStringToObject(dm_event, "type", "dm"); cJSON_AddStringToObject(dm_event, "sender_pubkey", g_cfg->admin.pubkey); cJSON_AddStringToObject(dm_event, "message", current_user_message ? current_user_message : ""); cJSON_AddStringToObject(dm_event, "tier", sender_tier == DIDACTYL_SENDER_ADMIN ? "admin" : (sender_tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger")); cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL)); char* composed_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "api"); cJSON_Delete(dm_event); if (!composed_context) { composed_context = strdup("You are an AI agent. Respond to the message."); } cJSON* messages = cJSON_CreateArray(); if (!messages || append_simple_message(messages, "system", composed_context ? composed_context : "You are an AI agent. Respond to the message.") != 0 || append_simple_message(messages, "user", current_user_message ? current_user_message : "") != 0) { free(composed_context); cJSON_Delete(messages); return -1; } free(composed_context); int n = cJSON_GetArraySize(messages); for (int i = 0; i < n; i++) { cJSON* msg = cJSON_GetArrayItem(messages, i); cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL; cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL; const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : ""; const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : ""; set_context_part_name(i, detect_context_section(role_s, content_s, i)); } char* messages_json = cJSON_PrintUnformatted(messages); cJSON_Delete(messages); if (!messages_json) { return -1; } *out_messages_json = messages_json; return 0; } tools_context_t* agent_tools_context(void) { return &g_tools_ctx; } void agent_on_message(const char* sender_pubkey_hex, const char* message, didactyl_sender_tier_t tier, void* user_data) { (void)user_data; if (!g_cfg || !sender_pubkey_hex || !message) { return; } if (tier == DIDACTYL_SENDER_STRANGER) { return; } fprintf(stdout, "[didactyl] incoming message from %.16s... tier=%d\n", sender_pubkey_hex, (int)tier); if (agent_message_is_debounced(sender_pubkey_hex, message)) { fprintf(stdout, "[didactyl] debounced duplicate inbound message from %.16s...\n", sender_pubkey_hex); return; } int allow_tools = (tier == DIDACTYL_SENDER_ADMIN) && g_cfg->tools.enabled && g_cfg->security.admin.tools_enabled; int inbound_role = (message[0] == '/') ? DIDACTYL_DM_HISTORY_TOOL_REQUEST : DIDACTYL_DM_HISTORY_USER; (void)nostr_handler_dm_history_remember(sender_pubkey_hex, message, (didactyl_dm_history_role_t)inbound_role); if (message[0] == '/') { if (!allow_tools) { const char* denied = "{\"success\":false,\"error\":\"slash commands are disabled for this sender tier\"}"; append_context_log(sender_pubkey_hex, "direct_tool_exec", denied); (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, denied, DIDACTYL_DM_HISTORY_TOOL_RESPONSE); return; } if (handle_slash_command(sender_pubkey_hex, message)) { return; } } cJSON* dm_event = cJSON_CreateObject(); if (!dm_event) { return; } cJSON_AddStringToObject(dm_event, "type", "dm"); cJSON_AddStringToObject(dm_event, "sender_pubkey", sender_pubkey_hex); cJSON_AddStringToObject(dm_event, "message", message); cJSON_AddStringToObject(dm_event, "tier", tier == DIDACTYL_SENDER_ADMIN ? "admin" : (tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger")); cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL)); char* dm_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "dm"); cJSON_Delete(dm_event); if (!dm_context) { dm_context = strdup("You are an AI agent. Respond to the message."); } size_t full_len = strlen(dm_context) + strlen("\n\nuser:\n") + strlen(message) + 1U; char* full_markdown = (char*)malloc(full_len); if (!full_markdown) { free(dm_context); return; } snprintf(full_markdown, full_len, "%s\n\nuser:\n%s", dm_context, message); free(dm_context); context_roles_t roles; if (context_roles_split(full_markdown, &roles) != 0) { free(full_markdown); return; } if (!allow_tools) { const char* tier_prefix = (tier == DIDACTYL_SENDER_WOT) ? "You are responding to a web-of-trust contact. Keep the response helpful and concise. Tool use is disabled for this tier." : "You are responding in chat-only mode. Tool use is disabled."; size_t ctx_len = strlen(roles.system_content ? roles.system_content : "") + strlen("\n\n") + strlen(tier_prefix) + 1U; char* system_for_chat = (char*)malloc(ctx_len); if (!system_for_chat) { context_roles_free(&roles); free(full_markdown); return; } snprintf(system_for_chat, ctx_len, "%s\n\n%s", roles.system_content ? roles.system_content : "", tier_prefix); append_context_log(sender_pubkey_hex, "llm_chat", full_markdown); char* response = llm_chat(system_for_chat, roles.user_content ? roles.user_content : message); free(system_for_chat); context_roles_free(&roles); free(full_markdown); if (!response) { const char* fallback = "I could not get a response from the LLM right now."; fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n"); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, fallback); return; } fprintf(stdout, "[didactyl] llm response: %.240s%s\n", response, strlen(response) > 240 ? "..." : ""); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, response); free(response); return; } char* tools_json = tools_build_openai_schema_json(&g_tools_ctx); if (!tools_json) { context_roles_free(&roles); free(full_markdown); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Tool schema generation failed."); return; } cJSON* messages = cJSON_CreateArray(); if (!messages || append_simple_message(messages, "system", roles.system_content ? roles.system_content : "You are an AI agent. Respond to the message.") != 0 || append_simple_message(messages, "user", roles.user_content ? roles.user_content : message) != 0) { cJSON_Delete(messages); free(tools_json); context_roles_free(&roles); free(full_markdown); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation messages."); return; } cJSON* live_user_msg = cJSON_GetArrayItem(messages, cJSON_GetArraySize(messages) - 1); if (live_user_msg && cJSON_IsObject(live_user_msg)) { cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL)); } int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 20; int stall_repeat_threshold = g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3; uint64_t last_tool_fp = 0; int repeated_tool_turns = 0; int exited_on_stall = 0; int stall_due_to_length = 0; char* final_answer_owned = NULL; int turns_run = 0; append_context_log(sender_pubkey_hex, "llm_chat_with_tools_messages", full_markdown); for (int turn = 0; turn < max_turns; turn++) { turns_run = turn + 1; char* messages_json = cJSON_PrintUnformatted(messages); if (!messages_json) { break; } append_context_log(sender_pubkey_hex, "llm_chat_with_tools_turn", messages_json); llm_response_t resp; int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp); free(messages_json); if (rc != 0) { (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "LLM request failed."); cJSON_Delete(messages); free(tools_json); context_roles_free(&roles); free(full_markdown); return; } if (resp.tool_call_count <= 0) { const char* answer = resp.content ? resp.content : "No response content."; fprintf(stdout, "[didactyl] llm response (no tool call): %.240s%s\n", answer, strlen(answer) > 240 ? "..." : ""); final_answer_owned = strdup(answer); llm_response_free(&resp); break; } uint64_t current_tool_fp = tool_calls_fingerprint(&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(messages, &resp) != 0) { llm_response_free(&resp); break; } for (int i = 0; i < resp.tool_call_count; i++) { llm_tool_call_t* tc = &resp.tool_calls[i]; fprintf(stdout, "[didactyl] executing tool call: %s\n", tc->name ? tc->name : ""); DEBUG_TRACE("[didactyl] tool call args: %s", tc->arguments_json ? tc->arguments_json : "{}"); char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json); if (!tool_result) { tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}"); } DEBUG_TRACE("[didactyl] tool call result: %s", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}"); if (append_tool_result_message(messages, tc->id ? tc->id : "", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) { free(tool_result); llm_response_free(&resp); cJSON_Delete(messages); free(tools_json); context_roles_free(&roles); free(full_markdown); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to append tool result."); return; } free(tool_result); } llm_response_free(&resp); } if (!final_answer_owned) { char* messages_json = cJSON_PrintUnformatted(messages); 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_answer_owned = strdup(forced_answer); } llm_response_free(&final_resp); } } } int exhausted_on_max_turns = (!final_answer_owned && !exited_on_stall && turns_run >= max_turns); if (!final_answer_owned) { final_answer_owned = strdup(exited_on_stall ? "I stopped repeated tool calls after detecting a loop and could not produce a final summary." : "I hit my tool-use limit for this request."); } const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request."; if (exited_on_stall || exhausted_on_max_turns) { notify_admin_limit_diagnostic("dm_agent_loop", exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted", sender_pubkey_hex, max_turns, turns_run, stall_repeat_threshold, repeated_tool_turns, g_cfg->llm.max_tokens, (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_answer); } fprintf(stdout, "[didactyl] final response: %.240s%s\n", final_answer, strlen(final_answer) > 240 ? "..." : ""); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, final_answer); free(final_answer_owned); cJSON_Delete(messages); free(tools_json); context_roles_free(&roles); free(full_markdown); } void agent_cleanup(void) { tools_cleanup(&g_tools_ctx); g_cfg = NULL; g_trigger_manager = NULL; memset(g_seen_msgs, 0, sizeof(g_seen_msgs)); g_seen_msgs_count = 0; g_seen_msgs_next = 0; pthread_mutex_lock(&g_adopted_skills_mutex); clear_adopted_skills_cache_locked(); g_adopted_skills_last_refresh_at = 0; pthread_mutex_unlock(&g_adopted_skills_mutex); clear_context_part_names(); }