2357 lines
85 KiB
C
2357 lines
85 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 "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"
|
|
|
|
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 d_tag[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 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;
|
|
}
|
|
|
|
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 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 (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(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;
|
|
char* conversation_role = NULL;
|
|
char* conversation_content = NULL;
|
|
|
|
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;
|
|
}
|
|
|
|
if (section && strcmp(section, "conversation") == 0) {
|
|
free(conversation_role);
|
|
free(conversation_content);
|
|
conversation_role = strdup(role_s);
|
|
conversation_content = strdup(content_s);
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (conversation_role && conversation_content) {
|
|
(void)appendf(&out,
|
|
&cap,
|
|
&used,
|
|
"# conversation | role=%s\n\n%s\n\n",
|
|
conversation_role,
|
|
conversation_content);
|
|
}
|
|
|
|
free(chat);
|
|
free(latest_admin_line);
|
|
free(conversation_role);
|
|
free(conversation_content);
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
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 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 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_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);
|
|
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 d_tag_val[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(d_tag_val, sizeof(d_tag_val), "%s", d_tag->valuestring);
|
|
}
|
|
cJSON_Delete(tags);
|
|
}
|
|
if (d_tag_val[0] == '\0') {
|
|
snprintf(d_tag_val, sizeof(d_tag_val), "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].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val);
|
|
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 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 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_d_tag,
|
|
const char* skill_content,
|
|
cJSON* triggering_event,
|
|
const char* relay_url) {
|
|
if (!g_cfg || !g_system_context || !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: ";
|
|
|
|
size_t system_len = strlen(g_system_context) + 2 + strlen(trigger_prefix) + strlen(skill_d_tag) +
|
|
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_d_tag,
|
|
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);
|
|
|
|
char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
|
|
if (!tools_json) {
|
|
free(system_prompt);
|
|
free(user_prompt);
|
|
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", system_prompt) != 0 ||
|
|
append_simple_message(messages, "user", user_prompt) != 0) {
|
|
cJSON_Delete(messages);
|
|
free(tools_json);
|
|
free(system_prompt);
|
|
free(user_prompt);
|
|
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", user_prompt);
|
|
free(system_prompt);
|
|
free(user_prompt);
|
|
|
|
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_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(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 || !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;
|
|
g_tools_ctx.template_trigger_event_json = NULL;
|
|
|
|
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_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 (g_trigger_manager) {
|
|
int dm_fired = trigger_manager_fire_dm(g_trigger_manager,
|
|
sender_pubkey_hex,
|
|
message,
|
|
tier,
|
|
"dm");
|
|
if (dm_fired > 0) {
|
|
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 : 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;
|
|
|
|
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_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;
|
|
}
|
|
|
|
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);
|
|
(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);
|
|
}
|
|
|
|
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;
|
|
}
|