Files
didactyl/src/agent.c
T

2391 lines
82 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 "llm.h"
#include "nostr_handler.h"
#include "tools.h"
#include "prompt_template.h"
#include "cjson/cJSON.h"
#include "debug.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static didactyl_config_t* g_cfg = NULL;
static char* g_system_context = NULL;
static tools_context_t g_tools_ctx;
static struct trigger_manager* g_trigger_manager = NULL;
static prompt_template_t g_prompt_template;
static int g_has_prompt_template = 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 slug[65];
char* content;
} 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 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 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);
}
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;
}
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;
if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
(void)append_textf_local(&out, &cap, &used, "- (none)\n");
} else {
int sn = cJSON_GetArraySize(skills);
for (int i = 0; i < sn; i++) {
cJSON* row = cJSON_GetArrayItem(skills, i);
cJSON* slug = row ? cJSON_GetObjectItemCaseSensitive(row, "slug") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
const char* slug_s = (slug && cJSON_IsString(slug) && slug->valuestring) ? slug->valuestring : NULL;
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
if (!slug_s || slug_s[0] == '\0') {
continue;
}
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", slug_s, desc_s[0] ? " — " : "", desc_s) != 0) {
cJSON_Delete(skill_root);
free(skill_list_json);
free(out);
return NULL;
}
}
}
cJSON_Delete(skill_root);
free(skill_list_json);
return out;
}
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));
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(sender_pubkey_hex, "{\"success\":false,\"error\":\"invalid slash command\"}");
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 (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\"}");
}
char* markdown_result = format_result_markdown_local(result_json);
const char* dm_payload = 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(sender_pubkey_hex, dm_payload ? dm_payload : "Command executed with no output.");
free(markdown_result);
free(result_json);
return 1;
}
static const char* get_context_part_name_copy(int idx);
static int appendf(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 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 const char* admin_label_for_log(void) {
static __thread char name_buf[96];
snprintf(name_buf, sizeof(name_buf), "%s", "Administrator");
char* kind0 = nostr_handler_get_admin_kind0_context();
if (!kind0 || kind0[0] == '\0') {
free(kind0);
return name_buf;
}
cJSON* root = cJSON_Parse(kind0);
free(kind0);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return name_buf;
}
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(root, "display_name");
cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name");
const char* chosen = NULL;
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
chosen = display_name->valuestring;
} else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
chosen = name->valuestring;
}
if (chosen) {
snprintf(name_buf, sizeof(name_buf), "%s", chosen);
}
cJSON_Delete(root);
return name_buf;
}
static char* demote_markdown_headings_for_log(const char* content) {
const char* src = content ? content : "";
size_t n = strlen(src);
char* out = (char*)malloc(n * 2U + 1U);
if (!out) {
return NULL;
}
size_t w = 0;
int line_start = 1;
for (size_t i = 0; i < n; i++) {
char c = src[i];
if (line_start && c == '#') {
out[w++] = '#';
}
out[w++] = c;
if (c == '\n') {
line_start = 1;
} else if (line_start && (c == ' ' || c == '\t' || c == '\r')) {
line_start = 1;
} else {
line_start = 0;
}
}
out[w] = '\0';
return out;
}
static void format_chat_time_for_log(time_t ts, char out[16]) {
if (!out) return;
out[0] = '\0';
if (ts <= 0) {
snprintf(out, 16, "--:--");
return;
}
struct tm tm_info;
if (!localtime_r(&ts, &tm_info)) {
snprintf(out, 16, "--:--");
return;
}
strftime(out, 16, "%H:%M", &tm_info);
}
static char* format_context_payload_for_log(const char* context_payload) {
const char* payload = context_payload ? context_payload : "";
cJSON* root = cJSON_Parse(payload);
if (!root || !cJSON_IsArray(root)) {
cJSON_Delete(root);
return strdup(payload);
}
size_t cap = strlen(payload) + 2048U;
if (cap < 4096U) cap = 4096U;
char* out = (char*)malloc(cap);
if (!out) {
cJSON_Delete(root);
return strdup(payload);
}
out[0] = '\0';
size_t used = 0;
int n = cJSON_GetArraySize(root);
(void)appendf(&out, &cap, &used, "Sections: %d\n\n", n);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(root, 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
: "unknown";
const char* content_s = "";
if (content && cJSON_IsString(content) && content->valuestring) {
content_s = content->valuestring;
} else if (content && cJSON_IsNull(content)) {
content_s = "<null>";
}
const char* section = agent_classify_message_part(msg, i);
if (section && strcmp(section, "dm_history") == 0) {
continue;
}
char* display_content = demote_markdown_headings_for_log(content_s);
const char* safe_display_content = display_content ? display_content : content_s;
if (appendf(&out,
&cap,
&used,
"# %s | role=%s\n\n"
"%s\n\n",
section ? section : "context_part",
role_s,
safe_display_content) != 0) {
free(display_content);
free(out);
cJSON_Delete(root);
return strdup(payload);
}
free(display_content);
}
const char* admin_label = admin_label_for_log();
size_t chat_cap = 4096U;
char* chat = (char*)malloc(chat_cap);
size_t chat_used = 0;
char* latest_admin_line = NULL;
if (chat) {
chat[0] = '\0';
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(root, i);
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
cJSON* ts = msg ? cJSON_GetObjectItemCaseSensitive(msg, "_ts") : 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 : "";
const char* section = agent_classify_message_part(msg, i);
if (!section || strcmp(section, "dm_history") != 0) {
continue;
}
time_t created_at = 0;
if (ts && cJSON_IsNumber(ts)) {
created_at = (time_t)ts->valuedouble;
}
char time_buf[16] = {0};
format_chat_time_for_log(created_at, time_buf);
const char* who = (strcmp(role_s, "user") == 0) ? admin_label : "Agent";
if (appendf(&chat, &chat_cap, &chat_used, "%s %s %s\n", time_buf, who, content_s) != 0) {
free(chat);
chat = NULL;
break;
}
if (strcmp(role_s, "user") == 0) {
free(latest_admin_line);
int line_len = snprintf(NULL, 0, "%s %s %s", time_buf, who, content_s);
if (line_len >= 0) {
latest_admin_line = (char*)malloc((size_t)line_len + 1U);
if (latest_admin_line) {
snprintf(latest_admin_line, (size_t)line_len + 1U, "%s %s %s", time_buf, who, content_s);
}
}
}
}
if (chat && chat_used > 0) {
(void)appendf(&out, &cap, &used, "# dm_history | role=chat\n\n%s\n", chat);
}
}
free(chat);
free(latest_admin_line);
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 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) {
time_t now = time(NULL);
struct tm tm_info;
localtime_r(&now, &tm_info);
char timestamp[32] = {0};
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
char* pretty_payload = format_context_payload_for_log(context_payload);
const char* safe_phase = phase ? phase : "unknown";
const char* safe_sender = sender_pubkey_hex ? sender_pubkey_hex : "unknown";
const char* safe_payload = pretty_payload ? pretty_payload : "";
size_t context_bytes = context_payload ? strlen(context_payload) : 0U;
size_t approx_tokens = context_bytes / 4U;
llm_config_t active_llm_cfg = {0};
const char* safe_model = (g_cfg && g_cfg->llm.model[0] != '\0') ? g_cfg->llm.model : "unknown";
if (llm_get_config(&active_llm_cfg) == 0 && active_llm_cfg.model[0] != '\0') {
safe_model = active_llm_cfg.model;
}
int entry_len = snprintf(NULL,
0,
"```text\n"
"Context Log - not seen by model\n"
"timestamp=%s\n"
"phase=%s\n"
"sender=%s\n"
"model=%s\n"
"context_bytes=%zu\n"
"approx_tokens=%zu\n"
"```\n\n"
"%s\n\n---\n\n",
timestamp,
safe_phase,
safe_sender,
safe_model,
context_bytes,
approx_tokens,
safe_payload);
if (entry_len < 0) {
free(pretty_payload);
return;
}
char* entry = (char*)malloc((size_t)entry_len + 1U);
if (!entry) {
free(pretty_payload);
return;
}
snprintf(entry,
(size_t)entry_len + 1U,
"```text\n"
"Context Log - not seen by model\n"
"timestamp=%s\n"
"phase=%s\n"
"sender=%s\n"
"model=%s\n"
"context_bytes=%zu\n"
"approx_tokens=%zu\n"
"```\n\n"
"%s\n\n---\n\n",
timestamp,
safe_phase,
safe_sender,
safe_model,
context_bytes,
approx_tokens,
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);
free(pretty_payload);
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);
free(entry);
free(pretty_payload);
}
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);
}
typedef struct {
time_t created_at;
int role_is_user;
char* content;
} agent_history_item_t;
static int extract_first_p_tag_local(cJSON* tags, char out_pubkey_hex[65]) {
if (!tags || !cJSON_IsArray(tags) || !out_pubkey_hex) {
return -1;
}
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* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !val->valuestring) {
continue;
}
if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) {
memcpy(out_pubkey_hex, val->valuestring, 64U);
out_pubkey_hex[64] = '\0';
return 0;
}
}
return -1;
}
static int history_item_cmp_created_at(const void* a, const void* b) {
const agent_history_item_t* ia = (const agent_history_item_t*)a;
const agent_history_item_t* ib = (const agent_history_item_t*)b;
if (ia->created_at < ib->created_at) return -1;
if (ia->created_at > ib->created_at) return 1;
return 0;
}
static void free_history_items(agent_history_item_t* items, int count) {
if (!items) return;
for (int i = 0; i < count; i++) {
free(items[i].content);
}
free(items);
}
static int append_startup_events_context(cJSON* messages) {
if (!messages || !g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
return 0;
}
cJSON* arr = cJSON_CreateArray();
if (!arr) {
return -1;
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (!se || se->kind == 31120) {
continue;
}
cJSON* item = cJSON_CreateObject();
if (!item) {
cJSON_Delete(arr);
return -1;
}
cJSON_AddNumberToObject(item, "kind", se->kind);
cJSON_AddStringToObject(item, "content", se->content ? se->content : "");
cJSON* tags = NULL;
if (se->tags_json) {
tags = cJSON_Parse(se->tags_json);
}
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(tags);
tags = cJSON_CreateArray();
}
cJSON_AddItemToObject(item, "tags", tags);
cJSON_AddItemToArray(arr, item);
}
char* events_json = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!events_json) {
return -1;
}
const char* prefix = "Startup events memory (source: config.startup_events; kind 31120 excluded): ";
size_t out_len = strlen(prefix) + strlen(events_json) + 1U;
char* payload = (char*)malloc(out_len);
if (!payload) {
free(events_json);
return -1;
}
snprintf(payload, out_len, "%s%s", prefix, events_json);
free(events_json);
int rc = append_simple_message(messages, "system", payload);
free(payload);
return rc;
}
static 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 char* build_admin_profile_plain_text(void) {
char* kind0 = nostr_handler_get_admin_kind0_context();
if (!kind0 || kind0[0] == '\0') {
free(kind0);
return strdup("Administrator profile: unavailable.");
}
cJSON* root = cJSON_Parse(kind0);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
size_t n = strlen("Administrator profile raw JSON: ") + strlen(kind0) + 1U;
char* out = (char*)malloc(n);
if (!out) {
free(kind0);
return NULL;
}
snprintf(out, n, "Administrator profile raw JSON: %s", kind0);
free(kind0);
return out;
}
const char* name = "";
const char* display_name = "";
const char* about = "";
const char* website = "";
const char* picture = "";
cJSON* v = cJSON_GetObjectItemCaseSensitive(root, "name");
if (v && cJSON_IsString(v) && v->valuestring) name = v->valuestring;
v = cJSON_GetObjectItemCaseSensitive(root, "display_name");
if (v && cJSON_IsString(v) && v->valuestring) display_name = v->valuestring;
v = cJSON_GetObjectItemCaseSensitive(root, "about");
if (v && cJSON_IsString(v) && v->valuestring) about = v->valuestring;
v = cJSON_GetObjectItemCaseSensitive(root, "website");
if (v && cJSON_IsString(v) && v->valuestring) website = v->valuestring;
v = cJSON_GetObjectItemCaseSensitive(root, "picture");
if (v && cJSON_IsString(v) && v->valuestring) picture = v->valuestring;
const char* fmt = "Administrator profile: name=%s; display_name=%s; about=%s; website=%s; picture=%s";
int len = snprintf(NULL, 0, fmt, name, display_name, about, website, picture);
char* out = (char*)malloc((size_t)len + 1U);
if (out) {
snprintf(out, (size_t)len + 1U, fmt, name, display_name, about, website, picture);
}
cJSON_Delete(root);
free(kind0);
return out;
}
static char* build_admin_relay_list_plain_text(void) {
char* kind10002 = nostr_handler_get_admin_kind10002_context();
if (!kind10002 || kind10002[0] == '\0') {
free(kind10002);
return strdup("Administrator relay list: unavailable.");
}
cJSON* arr = cJSON_Parse(kind10002);
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(arr);
size_t n = strlen("Administrator relay list raw JSON: ") + strlen(kind10002) + 1U;
char* out = (char*)malloc(n);
if (!out) {
free(kind10002);
return NULL;
}
snprintf(out, n, "Administrator relay list raw JSON: %s", kind10002);
free(kind10002);
return out;
}
size_t cap = strlen("Administrator relay list: ") + strlen(kind10002) + 64U;
char* out = (char*)malloc(cap);
if (!out) {
cJSON_Delete(arr);
free(kind10002);
return NULL;
}
out[0] = '\0';
strcat(out, "Administrator relay list: ");
int n = cJSON_GetArraySize(arr);
int added = 0;
for (int i = 0; i < n; i++) {
cJSON* it = cJSON_GetArrayItem(arr, i);
if (!it || !cJSON_IsString(it) || !it->valuestring || it->valuestring[0] == '\0') continue;
if (added > 0) strcat(out, ", ");
strcat(out, it->valuestring);
added++;
}
if (added == 0) {
strcat(out, "none");
}
cJSON_Delete(arr);
free(kind10002);
return out;
}
static char* build_admin_recent_posts_text(void) {
char* notes = nostr_handler_get_admin_kind1_notes_context();
if (!notes || notes[0] == '\0') {
free(notes);
return strdup("Recent posts: unavailable.");
}
const char* prefix = "Administrator recent public notes:\n";
const char* body = notes;
if (strncmp(notes, prefix, strlen(prefix)) == 0) {
body = notes + strlen(prefix);
}
size_t n = strlen("Recent posts:\n") + strlen(body) + 1U;
char* out = (char*)malloc(n);
if (!out) {
free(notes);
return NULL;
}
snprintf(out, n, "Recent posts:\n%s", body);
free(notes);
return out;
}
static int append_admin_identity_context(cJSON* messages, didactyl_sender_tier_t sender_tier) {
if (!messages || !g_cfg) {
return -1;
}
char* sender_verification = build_sender_verification_text(sender_tier);
char* profile_plain = build_admin_profile_plain_text();
char* relay_plain = build_admin_relay_list_plain_text();
char* posts_plain = build_admin_recent_posts_text();
if (!sender_verification || !profile_plain || !relay_plain || !posts_plain) {
free(sender_verification);
free(profile_plain);
free(relay_plain);
free(posts_plain);
return -1;
}
const char* fmt =
"Sender verification: %s\n"
"Administrator pubkey (hex): %s\n"
"%s\n"
"%s\n"
"%s";
int len = snprintf(NULL,
0,
fmt,
sender_verification,
g_cfg->admin.pubkey[0] != '\0' ? g_cfg->admin.pubkey : "unknown",
profile_plain,
relay_plain,
posts_plain);
if (len < 0) {
free(sender_verification);
free(profile_plain);
free(relay_plain);
free(posts_plain);
return -1;
}
char* payload = (char*)malloc((size_t)len + 1U);
if (!payload) {
free(sender_verification);
free(profile_plain);
free(relay_plain);
free(posts_plain);
return -1;
}
snprintf(payload,
(size_t)len + 1U,
fmt,
sender_verification,
g_cfg->admin.pubkey[0] != '\0' ? g_cfg->admin.pubkey : "unknown",
profile_plain,
relay_plain,
posts_plain);
int rc = append_simple_message(messages, "system", payload);
free(payload);
free(sender_verification);
free(profile_plain);
free(relay_plain);
free(posts_plain);
return rc;
}
static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
if (!messages || !g_cfg) {
return -1;
}
unsigned char admin_pubkey[32];
if (nostr_hex_to_bytes(g_cfg->admin.pubkey, admin_pubkey, 32) != 0) {
return -1;
}
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->admin.pubkey));
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", AGENT_HISTORY_QUERY_LIMIT);
char* events_json = nostr_handler_query_json(filter, 1500);
cJSON_Delete(filter);
if (!events_json) {
return 0;
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return 0;
}
agent_history_item_t* items = NULL;
int item_count = 0;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
cJSON* pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
if (!kind || !pubkey || !content || !tags || !created_at ||
!cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) ||
!cJSON_IsString(content) || !cJSON_IsArray(tags) || !cJSON_IsNumber(created_at) ||
(int)kind->valuedouble != 4 || !pubkey->valuestring || !content->valuestring) {
continue;
}
char p_tag_pubkey[65] = {0};
if (extract_first_p_tag_local(tags, p_tag_pubkey) != 0) {
continue;
}
int role_is_user = 0;
if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0 &&
strcmp(p_tag_pubkey, g_cfg->keys.public_key_hex) == 0) {
role_is_user = 1;
} else if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0 &&
strcmp(p_tag_pubkey, g_cfg->admin.pubkey) == 0) {
role_is_user = 0;
} else {
continue;
}
char* plaintext = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
if (!plaintext) {
continue;
}
plaintext[0] = '\0';
if (nostr_nip04_decrypt(g_cfg->keys.private_key,
admin_pubkey,
content->valuestring,
plaintext,
NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
free(plaintext);
continue;
}
agent_history_item_t* grown = (agent_history_item_t*)realloc(items, (size_t)(item_count + 1) * sizeof(agent_history_item_t));
if (!grown) {
free(plaintext);
free_history_items(items, item_count);
cJSON_Delete(events);
return -1;
}
items = grown;
items[item_count].created_at = (time_t)created_at->valuedouble;
items[item_count].role_is_user = role_is_user;
items[item_count].content = plaintext;
item_count++;
}
cJSON_Delete(events);
if (item_count > 1) {
qsort(items, (size_t)item_count, sizeof(agent_history_item_t), history_item_cmp_created_at);
}
int start = item_count > AGENT_HISTORY_TURNS ? item_count - AGENT_HISTORY_TURNS : 0;
const char* last_appended_role = NULL;
const char* last_appended_content = NULL;
for (int i = start; i < item_count; i++) {
if (i == item_count - 1 &&
items[i].role_is_user &&
current_message &&
strcmp(items[i].content, current_message) == 0) {
continue;
}
const char* role = items[i].role_is_user ? "user" : "assistant";
const char* content = items[i].content ? items[i].content : "";
if (last_appended_role && last_appended_content &&
strcmp(last_appended_role, role) == 0 &&
strcmp(last_appended_content, content) == 0) {
continue;
}
if (append_simple_message(messages, role, content) != 0) {
free_history_items(items, item_count);
return -1;
}
cJSON* appended = cJSON_GetArrayItem(messages, cJSON_GetArraySize(messages) - 1);
if (appended && cJSON_IsObject(appended)) {
cJSON_AddNumberToObject(appended, "_ts", (double)items[i].created_at);
}
last_appended_role = role;
last_appended_content = content;
}
free_history_items(items, item_count);
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_slug[65]) {
if (!addr || !out_kind || !out_pubkey_hex || !out_slug) {
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 slug_len = strlen(p2 + 1);
if (slug_len == 0 || slug_len >= 65U) {
return -1;
}
memcpy(out_slug, p2 + 1, slug_len + 1U);
*out_kind = kind;
return 0;
}
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].slug[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_slug[65] = {0};
if (parse_skill_address_tag_local(val->valuestring, &skill_kind, skill_author, skill_slug) != 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_slug) != 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_slug));
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].slug, sizeof(tmp[tmp_count].slug), "%s", skill_slug);
tmp[tmp_count].content = strdup(content->valuestring);
if (tmp[tmp_count].content) {
tmp_count++;
}
cJSON_Delete(skill_events);
}
}
}
cJSON_Delete(adoption_events);
if (tmp_count == 0 && g_cfg->startup_event_count > 0 && g_cfg->startup_events) {
for (int i = 0; i < g_cfg->startup_event_count && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (!se || (se->kind != 31123 && se->kind != 31124) || !se->content || se->content[0] == '\0') {
continue;
}
char slug[65] = {0};
if (se->tags_json) {
cJSON* tags = cJSON_Parse(se->tags_json);
cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') {
snprintf(slug, sizeof(slug), "%s", d_tag->valuestring);
}
cJSON_Delete(tags);
}
if (slug[0] == '\0') {
snprintf(slug, sizeof(slug), "startup-skill-%d", i + 1);
}
tmp[tmp_count].kind = se->kind;
snprintf(tmp[tmp_count].author_pubkey_hex,
sizeof(tmp[tmp_count].author_pubkey_hex),
"%s",
g_cfg->keys.public_key_hex[0] != '\0'
? g_cfg->keys.public_key_hex
: "unknown");
snprintf(tmp[tmp_count].slug, sizeof(tmp[tmp_count].slug), "%s", slug);
tmp[tmp_count].content = strdup(se->content);
if (tmp[tmp_count].content) {
tmp_count++;
}
}
}
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 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 slug: %s\nSkill address: %d:%s:%s\n\nInstructions:\n",
g_adopted_skills[i].slug,
g_adopted_skills[i].kind,
g_adopted_skills[i].author_pubkey_hex,
g_adopted_skills[i].slug);
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 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, const char* system_context) {
if (!config || !system_context) {
return -1;
}
g_cfg = config;
g_system_context = strdup(system_context);
if (!g_system_context) {
return -1;
}
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
free(g_system_context);
g_system_context = NULL;
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);
prompt_template_free(&g_prompt_template);
g_has_prompt_template = (prompt_template_parse(g_system_context, &g_prompt_template) == 0);
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_slug,
const char* skill_content,
cJSON* triggering_event,
const char* relay_url) {
if (!g_cfg || !g_system_context || !skill_slug || !skill_content || !triggering_event) {
return;
}
char* event_json = cJSON_PrintUnformatted(triggering_event);
if (!event_json) {
return;
}
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 slug: ";
size_t system_len = strlen(g_system_context) + 2 + strlen(trigger_prefix) + strlen(skill_slug) +
strlen("\nRelay: ") + strlen(relay) + strlen("\n\nSkill instructions:\n") +
strlen(skill_content) + 1U;
char* system_prompt = (char*)malloc(system_len);
if (!system_prompt) {
free(event_json);
return;
}
snprintf(system_prompt,
system_len,
"%s\n\n%s%s\nRelay: %s\n\nSkill instructions:\n%s",
g_system_context,
trigger_prefix,
skill_slug,
relay,
skill_content);
size_t user_len = strlen("Triggering event JSON:\n") + strlen(event_json) + 1U;
char* user_prompt = (char*)malloc(user_len);
if (!user_prompt) {
free(system_prompt);
free(event_json);
return;
}
snprintf(user_prompt, user_len, "Triggering event JSON:\n%s", event_json);
free(event_json);
append_context_log(g_cfg->admin.pubkey, "llm_trigger", user_prompt);
char* response = llm_chat(system_prompt, user_prompt);
free(system_prompt);
free(user_prompt);
if (!response) {
const char* fallback = "Triggered skill execution failed: LLM unavailable.";
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, fallback);
return;
}
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, response);
free(response);
}
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 || !g_system_context) {
return -1;
}
*out_messages_json = NULL;
clear_context_part_names();
cJSON* messages = NULL;
if (g_has_prompt_template) {
cJSON* dm_history = build_recent_admin_dm_history_messages(current_user_message);
if (!dm_history) {
dm_history = cJSON_CreateArray();
}
llm_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
const char* provider_name = NULL;
if (llm_get_config(&cfg) == 0 && cfg.provider[0] != '\0') {
provider_name = cfg.provider;
}
g_tools_ctx.template_sender_tier = (int)sender_tier;
g_tools_ctx.template_current_user_message = current_user_message;
messages = prompt_template_build_messages(&g_prompt_template,
provider_name,
&g_tools_ctx,
dm_history,
AGENT_HISTORY_TURNS,
template_emit_hook,
NULL);
cJSON_Delete(dm_history);
if (!messages) {
return -1;
}
} else {
messages = cJSON_CreateArray();
if (!messages) {
return -1;
}
char agent_identity[192];
snprintf(agent_identity,
sizeof(agent_identity),
"Agent identity\nYour pubkey (hex): %s",
g_cfg->keys.public_key_hex[0] != '\0' ? g_cfg->keys.public_key_hex : "unknown");
char* sender_verification = build_sender_verification_text(sender_tier);
if (!sender_verification ||
append_simple_message(messages, "system", g_system_context) != 0 ||
append_simple_message(messages, "system", agent_identity) != 0 ||
append_simple_message(messages, "system", sender_verification) != 0 ||
append_admin_identity_context(messages, sender_tier) != 0 ||
append_startup_events_context(messages) != 0 ||
append_adopted_skills_context(messages) != 0) {
free(sender_verification);
cJSON_Delete(messages);
return -1;
}
free(sender_verification);
if (append_recent_admin_dm_history(messages, current_user_message) != 0) {
cJSON_Delete(messages);
return -1;
}
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 || !g_system_context || !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;
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(sender_pubkey_hex, denied);
return;
}
if (handle_slash_command(sender_pubkey_hex, message)) {
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(g_system_context) + strlen("\n\n") + strlen(tier_prefix) + 1U;
char* system_for_chat = (char*)malloc(ctx_len);
if (!system_for_chat) {
return;
}
snprintf(system_for_chat, ctx_len, "%s\n\n%s", g_system_context, tier_prefix);
size_t context_len = strlen("system:\n\nuser:\n") + strlen(system_for_chat) + strlen(message) + 1U;
char* plain_context = (char*)malloc(context_len);
if (plain_context) {
snprintf(plain_context, context_len, "system:\n%s\n\nuser:\n%s", system_for_chat, message);
append_context_log(sender_pubkey_hex, "llm_chat", plain_context);
free(plain_context);
}
char* response = llm_chat(system_for_chat, message);
free(system_for_chat);
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) {
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Tool schema generation failed.");
return;
}
char* base_messages_json = NULL;
if (agent_build_admin_messages_json(message, tier, &base_messages_json) != 0 || !base_messages_json) {
free(tools_json);
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation messages.");
return;
}
cJSON* messages = cJSON_Parse(base_messages_json);
free(base_messages_json);
if (!messages || !cJSON_IsArray(messages)) {
cJSON_Delete(messages);
free(tools_json);
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation state.");
return;
}
if (append_simple_message(messages, "user", message) != 0) {
cJSON_Delete(messages);
free(tools_json);
(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 : 8;
char* final_answer_owned = NULL;
for (int turn = 0; turn < max_turns; turn++) {
char* messages_json = cJSON_PrintUnformatted(messages);
if (!messages_json) {
break;
}
append_context_log(sender_pubkey_hex, "llm_chat_with_tools_messages", 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);
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;
}
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);
(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) {
final_answer_owned = strdup("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.";
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);
}
void agent_cleanup(void) {
tools_cleanup(&g_tools_ctx);
free(g_system_context);
g_system_context = NULL;
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();
prompt_template_free(&g_prompt_template);
g_has_prompt_template = 0;
}