Files
didactyl/src/agent.c

2437 lines
88 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "agent.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <pthread.h>
#include <stdarg.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>
#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 /<tool> -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("<value>");
if (strcmp(type_s, "string") == 0) return cJSON_CreateString("<string>");
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("<value>");
}
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("<value>");
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 : "<null>");
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();
}