2437 lines
88 KiB
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();
|
|
}
|