1730 lines
56 KiB
C
1730 lines
56 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "trigger_manager.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <ctype.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "debug.h"
|
|
#include "nostr_handler.h"
|
|
#include "llm.h"
|
|
|
|
void agent_on_trigger(const char* skill_d_tag,
|
|
const char* skill_content,
|
|
cJSON* triggering_event,
|
|
const char* relay_url);
|
|
|
|
typedef struct {
|
|
trigger_manager_t* mgr;
|
|
char skill_d_tag[TRIGGER_SKILL_D_TAG_MAX];
|
|
} trigger_subscription_ctx_t;
|
|
|
|
static int clamp_enabled(int enabled) {
|
|
return enabled ? 1 : 0;
|
|
}
|
|
|
|
trigger_type_t trigger_type_from_string(const char* s) {
|
|
if (!s) {
|
|
return TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
|
}
|
|
if (strcmp(s, "webhook") == 0) {
|
|
return TRIGGER_TYPE_WEBHOOK;
|
|
}
|
|
if (strcmp(s, "cron") == 0) {
|
|
return TRIGGER_TYPE_CRON;
|
|
}
|
|
if (strcmp(s, "chain") == 0) {
|
|
return TRIGGER_TYPE_CHAIN;
|
|
}
|
|
if (strcmp(s, "dm") == 0) {
|
|
return TRIGGER_TYPE_DM;
|
|
}
|
|
return TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
|
}
|
|
|
|
const char* trigger_type_to_string(trigger_type_t t) {
|
|
switch (t) {
|
|
case TRIGGER_TYPE_WEBHOOK:
|
|
return "webhook";
|
|
case TRIGGER_TYPE_CRON:
|
|
return "cron";
|
|
case TRIGGER_TYPE_CHAIN:
|
|
return "chain";
|
|
case TRIGGER_TYPE_DM:
|
|
return "dm";
|
|
case TRIGGER_TYPE_NOSTR_SUBSCRIPTION:
|
|
default:
|
|
return "nostr-subscription";
|
|
}
|
|
}
|
|
|
|
static int is_int_in_csv_list(const char* text, int value) {
|
|
if (!text || text[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
const char* p = text;
|
|
while (*p) {
|
|
while (*p == ' ') p++;
|
|
char* endptr = NULL;
|
|
long n = strtol(p, &endptr, 10);
|
|
if (endptr == p) {
|
|
return 0;
|
|
}
|
|
if ((int)n == value) {
|
|
return 1;
|
|
}
|
|
p = endptr;
|
|
while (*p == ' ') p++;
|
|
if (*p == ',') {
|
|
p++;
|
|
continue;
|
|
}
|
|
if (*p == '\0') {
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cron_field_token_matches(const char* token, int value, int min_v, int max_v) {
|
|
if (!token || token[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(token, "*") == 0) {
|
|
return 1;
|
|
}
|
|
|
|
const char* slash = strchr(token, '/');
|
|
int step = 0;
|
|
char base[64];
|
|
if (slash) {
|
|
size_t base_len = (size_t)(slash - token);
|
|
if (base_len == 0 || base_len >= sizeof(base)) {
|
|
return 0;
|
|
}
|
|
memcpy(base, token, base_len);
|
|
base[base_len] = '\0';
|
|
|
|
char* endptr = NULL;
|
|
long step_l = strtol(slash + 1, &endptr, 10);
|
|
if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
|
|
return 0;
|
|
}
|
|
step = (int)step_l;
|
|
} else {
|
|
snprintf(base, sizeof(base), "%s", token);
|
|
}
|
|
|
|
int range_start = min_v;
|
|
int range_end = max_v;
|
|
|
|
if (strcmp(base, "*") == 0) {
|
|
range_start = min_v;
|
|
range_end = max_v;
|
|
} else {
|
|
const char* dash = strchr(base, '-');
|
|
if (dash) {
|
|
char left[32];
|
|
char right[32];
|
|
size_t left_len = (size_t)(dash - base);
|
|
size_t right_len = strlen(dash + 1);
|
|
if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
|
|
return 0;
|
|
}
|
|
memcpy(left, base, left_len);
|
|
left[left_len] = '\0';
|
|
memcpy(right, dash + 1, right_len + 1U);
|
|
|
|
char* e1 = NULL;
|
|
char* e2 = NULL;
|
|
long a = strtol(left, &e1, 10);
|
|
long b = strtol(right, &e2, 10);
|
|
if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
|
|
return 0;
|
|
}
|
|
range_start = (int)a;
|
|
range_end = (int)b;
|
|
} else if (strchr(base, ',')) {
|
|
if (!is_int_in_csv_list(base, value)) {
|
|
return 0;
|
|
}
|
|
if (step <= 1) {
|
|
return 1;
|
|
}
|
|
return ((value - min_v) % step) == 0;
|
|
} else {
|
|
char* e = NULL;
|
|
long n = strtol(base, &e, 10);
|
|
if (!e || *e != '\0') {
|
|
return 0;
|
|
}
|
|
range_start = (int)n;
|
|
range_end = (int)n;
|
|
}
|
|
}
|
|
|
|
if (range_start < min_v || range_end > max_v || range_start > range_end) {
|
|
return 0;
|
|
}
|
|
if (value < range_start || value > range_end) {
|
|
return 0;
|
|
}
|
|
if (step > 1 && ((value - range_start) % step) != 0) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int cron_field_matches(const char* field, int value, int min_v, int max_v) {
|
|
if (!field || field[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
char tmp[128];
|
|
snprintf(tmp, sizeof(tmp), "%s", field);
|
|
|
|
char* saveptr = NULL;
|
|
char* token = strtok_r(tmp, ",", &saveptr);
|
|
while (token) {
|
|
while (*token == ' ') token++;
|
|
if (cron_field_token_matches(token, value, min_v, max_v)) {
|
|
return 1;
|
|
}
|
|
token = strtok_r(NULL, ",", &saveptr);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cron_normalize_expression(const char* expr, char* out, size_t out_sz, int* out_expanded) {
|
|
if (out_expanded) {
|
|
*out_expanded = 0;
|
|
}
|
|
if (!expr || !out || out_sz == 0) {
|
|
return -1;
|
|
}
|
|
|
|
while (*expr && isspace((unsigned char)*expr)) {
|
|
expr++;
|
|
}
|
|
if (*expr == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
const char* mapped = NULL;
|
|
if (strcmp(expr, "*") == 0) {
|
|
mapped = "* * * * *";
|
|
} else if (strcmp(expr, "@yearly") == 0 || strcmp(expr, "@annually") == 0) {
|
|
mapped = "0 0 1 1 *";
|
|
} else if (strcmp(expr, "@monthly") == 0) {
|
|
mapped = "0 0 1 * *";
|
|
} else if (strcmp(expr, "@weekly") == 0) {
|
|
mapped = "0 0 * * 0";
|
|
} else if (strcmp(expr, "@daily") == 0 || strcmp(expr, "@midnight") == 0) {
|
|
mapped = "0 0 * * *";
|
|
} else if (strcmp(expr, "@hourly") == 0) {
|
|
mapped = "0 * * * *";
|
|
}
|
|
|
|
const char* src = mapped ? mapped : expr;
|
|
int n = snprintf(out, out_sz, "%s", src);
|
|
if (n < 0 || (size_t)n >= out_sz) {
|
|
return -1;
|
|
}
|
|
|
|
if (mapped && out_expanded) {
|
|
*out_expanded = 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int cron_field_token_valid(const char* token, int min_v, int max_v) {
|
|
if (!token || token[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(token, "*") == 0) {
|
|
return 1;
|
|
}
|
|
|
|
const char* slash = strchr(token, '/');
|
|
int step = 0;
|
|
char base[64];
|
|
if (slash) {
|
|
size_t base_len = (size_t)(slash - token);
|
|
if (base_len == 0 || base_len >= sizeof(base)) {
|
|
return 0;
|
|
}
|
|
memcpy(base, token, base_len);
|
|
base[base_len] = '\0';
|
|
|
|
char* endptr = NULL;
|
|
long step_l = strtol(slash + 1, &endptr, 10);
|
|
if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
|
|
return 0;
|
|
}
|
|
step = (int)step_l;
|
|
} else {
|
|
int n = snprintf(base, sizeof(base), "%s", token);
|
|
if (n < 0 || (size_t)n >= sizeof(base)) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (strcmp(base, "*") == 0) {
|
|
return step == 0 || step <= (max_v - min_v + 1);
|
|
}
|
|
|
|
const char* dash = strchr(base, '-');
|
|
if (dash) {
|
|
char left[32];
|
|
char right[32];
|
|
size_t left_len = (size_t)(dash - base);
|
|
size_t right_len = strlen(dash + 1);
|
|
if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
|
|
return 0;
|
|
}
|
|
memcpy(left, base, left_len);
|
|
left[left_len] = '\0';
|
|
memcpy(right, dash + 1, right_len + 1U);
|
|
|
|
char* e1 = NULL;
|
|
char* e2 = NULL;
|
|
long a = strtol(left, &e1, 10);
|
|
long b = strtol(right, &e2, 10);
|
|
if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
|
|
return 0;
|
|
}
|
|
if ((int)a < min_v || (int)b > max_v || (int)a > (int)b) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
char* e = NULL;
|
|
long n = strtol(base, &e, 10);
|
|
if (!e || *e != '\0') {
|
|
return 0;
|
|
}
|
|
if ((int)n < min_v || (int)n > max_v) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int cron_field_valid(const char* field, int min_v, int max_v) {
|
|
if (!field || field[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
char tmp[128];
|
|
int n = snprintf(tmp, sizeof(tmp), "%s", field);
|
|
if (n < 0 || (size_t)n >= sizeof(tmp)) {
|
|
return 0;
|
|
}
|
|
|
|
int tokens = 0;
|
|
char* saveptr = NULL;
|
|
char* token = strtok_r(tmp, ",", &saveptr);
|
|
while (token) {
|
|
while (*token == ' ') token++;
|
|
if (!cron_field_token_valid(token, min_v, max_v)) {
|
|
return 0;
|
|
}
|
|
tokens++;
|
|
token = strtok_r(NULL, ",", &saveptr);
|
|
}
|
|
|
|
return tokens > 0;
|
|
}
|
|
|
|
static int cron_expr_is_valid(const char* expr, char* normalized_out, size_t normalized_out_size) {
|
|
if (!normalized_out || normalized_out_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (cron_normalize_expression(expr, normalized_out, normalized_out_size, NULL) != 0) {
|
|
return 0;
|
|
}
|
|
|
|
char parse_buf[TRIGGER_FILTER_JSON_MAX];
|
|
int n = snprintf(parse_buf, sizeof(parse_buf), "%s", normalized_out);
|
|
if (n < 0 || (size_t)n >= sizeof(parse_buf)) {
|
|
return 0;
|
|
}
|
|
|
|
char* fields[5] = {0};
|
|
int nf = 0;
|
|
char* saveptr = NULL;
|
|
char* tok = strtok_r(parse_buf, " \t", &saveptr);
|
|
while (tok && nf < 5) {
|
|
fields[nf++] = tok;
|
|
tok = strtok_r(NULL, " \t", &saveptr);
|
|
}
|
|
if (nf != 5 || tok != NULL) {
|
|
return 0;
|
|
}
|
|
|
|
if (!cron_field_valid(fields[0], 0, 59)) return 0;
|
|
if (!cron_field_valid(fields[1], 0, 23)) return 0;
|
|
if (!cron_field_valid(fields[2], 1, 31)) return 0;
|
|
if (!cron_field_valid(fields[3], 1, 12)) return 0;
|
|
if (!cron_field_valid(fields[4], 0, 6)) return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int cron_matches_now(const char* expr, time_t now_ts) {
|
|
if (!expr || expr[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
char normalized[TRIGGER_FILTER_JSON_MAX];
|
|
if (!cron_expr_is_valid(expr, normalized, sizeof(normalized))) {
|
|
return 0;
|
|
}
|
|
|
|
char parse_buf[TRIGGER_FILTER_JSON_MAX];
|
|
snprintf(parse_buf, sizeof(parse_buf), "%s", normalized);
|
|
|
|
char* fields[5] = {0};
|
|
int nf = 0;
|
|
char* saveptr = NULL;
|
|
char* tok = strtok_r(parse_buf, " \t", &saveptr);
|
|
while (tok && nf < 5) {
|
|
fields[nf++] = tok;
|
|
tok = strtok_r(NULL, " \t", &saveptr);
|
|
}
|
|
if (nf != 5 || tok != NULL) {
|
|
return 0;
|
|
}
|
|
|
|
struct tm tm_now;
|
|
localtime_r(&now_ts, &tm_now);
|
|
|
|
int minute = tm_now.tm_min;
|
|
int hour = tm_now.tm_hour;
|
|
int mday = tm_now.tm_mday;
|
|
int month = tm_now.tm_mon + 1;
|
|
int wday = tm_now.tm_wday;
|
|
|
|
if (!cron_field_matches(fields[0], minute, 0, 59)) return 0;
|
|
if (!cron_field_matches(fields[1], hour, 0, 23)) return 0;
|
|
if (!cron_field_matches(fields[2], mday, 1, 31)) return 0;
|
|
if (!cron_field_matches(fields[3], month, 1, 12)) return 0;
|
|
if (!cron_field_matches(fields[4], wday, 0, 6)) return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int ensure_capacity(trigger_manager_t* mgr, int needed) {
|
|
if (!mgr || needed <= 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (mgr->capacity >= needed) {
|
|
return 0;
|
|
}
|
|
|
|
int new_cap = mgr->capacity > 0 ? mgr->capacity : TRIGGER_DEFAULT_MAX_ACTIVE;
|
|
while (new_cap < needed) {
|
|
if (new_cap > 1024) {
|
|
return -1;
|
|
}
|
|
new_cap *= 2;
|
|
}
|
|
|
|
active_trigger_t* grown = (active_trigger_t*)realloc(mgr->triggers, (size_t)new_cap * sizeof(active_trigger_t));
|
|
if (!grown) {
|
|
return -1;
|
|
}
|
|
|
|
if (new_cap > mgr->capacity) {
|
|
memset(&grown[mgr->capacity], 0, (size_t)(new_cap - mgr->capacity) * sizeof(active_trigger_t));
|
|
}
|
|
|
|
mgr->triggers = grown;
|
|
mgr->capacity = new_cap;
|
|
return 0;
|
|
}
|
|
|
|
static int find_trigger_index_locked(trigger_manager_t* mgr, const char* skill_d_tag) {
|
|
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < mgr->count; i++) {
|
|
if (strncmp(mgr->triggers[i].skill_d_tag, skill_d_tag, TRIGGER_SKILL_D_TAG_MAX) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static cJSON* find_tag_value_string(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 && strcmp(k->valuestring, key) == 0) {
|
|
return v;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static __attribute__((unused)) int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
|
|
if (!addr || !out_kind || !out_pubkey || !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, p1 + 1, 64U);
|
|
out_pubkey[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 const char* trim_ws_local(const char* s) {
|
|
while (s && *s && isspace((unsigned char)*s)) s++;
|
|
return s ? s : "";
|
|
}
|
|
|
|
static void parse_trigger_runtime_tags(cJSON* tags,
|
|
const char** out_llm,
|
|
const char** out_tools,
|
|
int* out_has_max_tokens,
|
|
int* out_max_tokens,
|
|
int* out_has_temperature,
|
|
double* out_temperature,
|
|
int* out_has_seed,
|
|
int* out_seed) {
|
|
if (out_llm) *out_llm = NULL;
|
|
if (out_tools) *out_tools = NULL;
|
|
if (out_has_max_tokens) *out_has_max_tokens = 0;
|
|
if (out_max_tokens) *out_max_tokens = 0;
|
|
if (out_has_temperature) *out_has_temperature = 0;
|
|
if (out_temperature) *out_temperature = 0.0;
|
|
if (out_has_seed) *out_has_seed = 0;
|
|
if (out_seed) *out_seed = 0;
|
|
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
return;
|
|
}
|
|
|
|
cJSON* llm = find_tag_value_string(tags, "llm");
|
|
cJSON* tools = find_tag_value_string(tags, "tools");
|
|
cJSON* max_tokens = find_tag_value_string(tags, "max_tokens");
|
|
cJSON* temperature = find_tag_value_string(tags, "temperature");
|
|
cJSON* seed = find_tag_value_string(tags, "seed");
|
|
|
|
if (out_llm && llm && cJSON_IsString(llm) && llm->valuestring && llm->valuestring[0] != '\0') {
|
|
*out_llm = llm->valuestring;
|
|
}
|
|
if (out_tools && tools && cJSON_IsString(tools) && tools->valuestring && tools->valuestring[0] != '\0') {
|
|
*out_tools = tools->valuestring;
|
|
}
|
|
|
|
if (out_has_max_tokens && out_max_tokens && max_tokens && cJSON_IsString(max_tokens) && max_tokens->valuestring) {
|
|
char* endptr = NULL;
|
|
long n = strtol(trim_ws_local(max_tokens->valuestring), &endptr, 10);
|
|
if (endptr && *trim_ws_local(endptr) == '\0' && n > 0 && n <= 1000000) {
|
|
*out_has_max_tokens = 1;
|
|
*out_max_tokens = (int)n;
|
|
}
|
|
}
|
|
|
|
if (out_has_temperature && out_temperature && temperature && cJSON_IsString(temperature) && temperature->valuestring) {
|
|
char* endptr = NULL;
|
|
double n = strtod(trim_ws_local(temperature->valuestring), &endptr);
|
|
if (endptr && *trim_ws_local(endptr) == '\0' && n >= 0.0 && n <= 2.0) {
|
|
*out_has_temperature = 1;
|
|
*out_temperature = n;
|
|
}
|
|
}
|
|
|
|
if (out_has_seed && out_seed && seed && cJSON_IsString(seed) && seed->valuestring) {
|
|
char* endptr = NULL;
|
|
long n = strtol(trim_ws_local(seed->valuestring), &endptr, 10);
|
|
if (endptr && *trim_ws_local(endptr) == '\0') {
|
|
*out_has_seed = 1;
|
|
*out_seed = (int)n;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
|
|
if (!mgr || !mgr->cfg) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(mgr->cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 300);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 8000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
DEBUG_WARN("[didactyl] trigger reconcile skipped: relay query returned no data");
|
|
return -1;
|
|
}
|
|
|
|
cJSON* events = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!events || !cJSON_IsArray(events)) {
|
|
cJSON_Delete(events);
|
|
DEBUG_WARN("[didactyl] trigger reconcile skipped: invalid JSON from relay query");
|
|
return -1;
|
|
}
|
|
|
|
int considered = 0;
|
|
int loaded = 0;
|
|
|
|
int event_count = cJSON_GetArraySize(events);
|
|
for (int i = 0; i < event_count; i++) {
|
|
cJSON* skill_event = cJSON_GetArrayItem(events, i);
|
|
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
|
|
cJSON* tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
|
|
if (!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* d = find_tag_value_string(tags, "d");
|
|
cJSON* trigger = find_tag_value_string(tags, "trigger");
|
|
cJSON* filter_j = find_tag_value_string(tags, "filter");
|
|
cJSON* action = find_tag_value_string(tags, "action");
|
|
|
|
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
|
|
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
|
|
const char* filter_s = (filter_j && cJSON_IsString(filter_j) && filter_j->valuestring) ? filter_j->valuestring : NULL;
|
|
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
|
|
|
|
considered++;
|
|
if (!d_tag || d_tag[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
int trigger_supported = trigger_s &&
|
|
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
|
strcmp(trigger_s, "webhook") == 0 ||
|
|
strcmp(trigger_s, "cron") == 0 ||
|
|
strcmp(trigger_s, "chain") == 0 ||
|
|
strcmp(trigger_s, "dm") == 0);
|
|
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(action_s, "template") == 0) {
|
|
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
|
|
}
|
|
|
|
const char* llm_s = NULL;
|
|
const char* tools_s = NULL;
|
|
int has_max_tokens = 0;
|
|
int max_tokens = 0;
|
|
int has_temperature = 0;
|
|
double temperature = 0.0;
|
|
int has_seed = 0;
|
|
int seed = 0;
|
|
parse_trigger_runtime_tags(tags,
|
|
&llm_s,
|
|
&tools_s,
|
|
&has_max_tokens,
|
|
&max_tokens,
|
|
&has_temperature,
|
|
&temperature,
|
|
&has_seed,
|
|
&seed);
|
|
|
|
if (trigger_manager_add(mgr,
|
|
d_tag,
|
|
content->valuestring,
|
|
filter_s,
|
|
TRIGGER_ACTION_LLM,
|
|
trigger_s,
|
|
1,
|
|
llm_s,
|
|
tools_s,
|
|
has_max_tokens,
|
|
max_tokens,
|
|
has_temperature,
|
|
temperature,
|
|
has_seed,
|
|
seed) == 0) {
|
|
loaded++;
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(events);
|
|
DEBUG_INFO("[didactyl] trigger reconcile complete: loaded=%d considered=%d", loaded, considered);
|
|
return 0;
|
|
}
|
|
|
|
static void apply_trigger_runtime_to_llm_config(const active_trigger_t* t, llm_config_t* cfg) {
|
|
if (!t || !cfg) return;
|
|
|
|
if (t->llm_spec[0] != '\0') {
|
|
const char* spec = t->llm_spec;
|
|
while (*spec && isspace((unsigned char)*spec)) spec++;
|
|
|
|
const char* comma = strchr(spec, ',');
|
|
size_t len = comma ? (size_t)(comma - spec) : strlen(spec);
|
|
while (len > 0 && isspace((unsigned char)spec[len - 1])) len--;
|
|
|
|
const char* slash = memchr(spec, '/', len);
|
|
if (slash) {
|
|
size_t provider_len = (size_t)(slash - spec);
|
|
const char* model = slash + 1;
|
|
size_t model_len = len - (size_t)(model - spec);
|
|
|
|
while (provider_len > 0 && isspace((unsigned char)spec[provider_len - 1])) provider_len--;
|
|
while (model_len > 0 && isspace((unsigned char)*model)) {
|
|
model++;
|
|
model_len--;
|
|
}
|
|
while (model_len > 0 && isspace((unsigned char)model[model_len - 1])) model_len--;
|
|
|
|
if (provider_len > 0 && provider_len < sizeof(cfg->provider)) {
|
|
snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec);
|
|
}
|
|
if (model_len > 0 && model_len < sizeof(cfg->model)) {
|
|
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)model_len, model);
|
|
}
|
|
} else if (len > 0 && len < sizeof(cfg->model)) {
|
|
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)len, spec);
|
|
}
|
|
}
|
|
|
|
if (t->has_max_tokens && t->max_tokens > 0) {
|
|
cfg->max_tokens = t->max_tokens;
|
|
}
|
|
if (t->has_temperature) {
|
|
cfg->temperature = t->temperature;
|
|
}
|
|
}
|
|
|
|
static void execute_llm_action(const active_trigger_t* t, cJSON* event, const char* relay_url) {
|
|
if (!t || !event) {
|
|
return;
|
|
}
|
|
|
|
llm_config_t old_cfg;
|
|
int had_old_cfg = (llm_get_config(&old_cfg) == 0);
|
|
int overridden = 0;
|
|
|
|
if (had_old_cfg &&
|
|
(t->llm_spec[0] != '\0' || t->has_max_tokens || t->has_temperature)) {
|
|
llm_config_t next = old_cfg;
|
|
apply_trigger_runtime_to_llm_config(t, &next);
|
|
if (llm_set_config(&next) == 0) {
|
|
overridden = 1;
|
|
}
|
|
}
|
|
|
|
agent_on_trigger(t->skill_d_tag, t->skill_content, event, relay_url);
|
|
|
|
if (overridden) {
|
|
(void)llm_set_config(&old_cfg);
|
|
}
|
|
}
|
|
|
|
static int maybe_fire_trigger_locked(trigger_manager_t* mgr, int index, cJSON* event, const char* relay_url) {
|
|
active_trigger_t* t = &mgr->triggers[index];
|
|
if (!t->enabled) {
|
|
return 0;
|
|
}
|
|
|
|
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
|
|
time_t created_ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : 0;
|
|
if (created_ts > 0 && created_ts <= t->last_seen_created_at) {
|
|
return 0;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
int cooldown = mgr->cfg->triggers.cooldown_seconds;
|
|
if (cooldown < 0) cooldown = 0;
|
|
if (cooldown > 0 && t->last_fired > 0 && (now - t->last_fired) < cooldown) {
|
|
return 0;
|
|
}
|
|
|
|
t->last_fired = now;
|
|
if (created_ts > t->last_seen_created_at) {
|
|
t->last_seen_created_at = created_ts;
|
|
}
|
|
|
|
active_trigger_t trigger_copy = *t;
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
(void)mgr;
|
|
execute_llm_action(&trigger_copy, event, relay_url);
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
return 1;
|
|
}
|
|
|
|
static void on_trigger_subscription_eose(cJSON** events, int event_count, void* user_data) {
|
|
(void)events;
|
|
(void)event_count;
|
|
(void)user_data;
|
|
}
|
|
|
|
static void close_trigger_subscription_locked(active_trigger_t* t) {
|
|
if (!t) {
|
|
return;
|
|
}
|
|
|
|
if (t->subscription) {
|
|
(void)nostr_handler_close_subscription(t->subscription);
|
|
t->subscription = NULL;
|
|
}
|
|
|
|
if (t->subscription_ctx) {
|
|
free(t->subscription_ctx);
|
|
t->subscription_ctx = NULL;
|
|
}
|
|
}
|
|
|
|
static void on_trigger_subscription_event(cJSON* event, const char* relay_url, void* user_data) {
|
|
trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)user_data;
|
|
if (!ctx || !ctx->mgr || !event) {
|
|
return;
|
|
}
|
|
|
|
(void)trigger_manager_fire(ctx->mgr, ctx->skill_d_tag, event, relay_url);
|
|
}
|
|
|
|
static int register_trigger_subscription_locked(trigger_manager_t* mgr, active_trigger_t* t) {
|
|
if (!mgr || !t) {
|
|
return -1;
|
|
}
|
|
|
|
close_trigger_subscription_locked(t);
|
|
|
|
if (t->trigger_type != TRIGGER_TYPE_NOSTR_SUBSCRIPTION) {
|
|
return 0;
|
|
}
|
|
|
|
if (!t->enabled || t->filter_json[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
cJSON* filter = cJSON_Parse(t->filter_json);
|
|
if (!filter || !cJSON_IsObject(filter)) {
|
|
cJSON_Delete(filter);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* since = cJSON_GetObjectItemCaseSensitive(filter, "since");
|
|
if (!since || !cJSON_IsNumber(since)) {
|
|
time_t now = time(NULL);
|
|
time_t default_since = now > 30 ? now - 30 : 0;
|
|
cJSON_AddNumberToObject(filter, "since", (double)default_since);
|
|
}
|
|
|
|
cJSON* limit = cJSON_GetObjectItemCaseSensitive(filter, "limit");
|
|
if (!limit || !cJSON_IsNumber(limit)) {
|
|
cJSON_AddNumberToObject(filter, "limit", 200);
|
|
}
|
|
|
|
trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)calloc(1, sizeof(*ctx));
|
|
if (!ctx) {
|
|
cJSON_Delete(filter);
|
|
return -1;
|
|
}
|
|
|
|
ctx->mgr = mgr;
|
|
snprintf(ctx->skill_d_tag, sizeof(ctx->skill_d_tag), "%s", t->skill_d_tag);
|
|
|
|
nostr_pool_subscription_t* sub = nostr_handler_subscribe_with_filter(
|
|
filter,
|
|
on_trigger_subscription_event,
|
|
on_trigger_subscription_eose,
|
|
ctx,
|
|
0,
|
|
0,
|
|
NOSTR_POOL_EOSE_FULL_SET,
|
|
30,
|
|
120);
|
|
|
|
cJSON_Delete(filter);
|
|
|
|
if (!sub) {
|
|
free(ctx);
|
|
return -1;
|
|
}
|
|
|
|
t->subscription = sub;
|
|
t->subscription_ctx = ctx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg) {
|
|
if (!mgr || !cfg) {
|
|
return -1;
|
|
}
|
|
|
|
memset(mgr, 0, sizeof(*mgr));
|
|
mgr->cfg = cfg;
|
|
|
|
if (pthread_mutex_init(&mgr->mutex, NULL) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
int initial_cap = cfg->triggers.max_active > 0 ? cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE;
|
|
mgr->capacity = initial_cap;
|
|
mgr->triggers = (active_trigger_t*)calloc((size_t)mgr->capacity, sizeof(active_trigger_t));
|
|
if (!mgr->triggers) {
|
|
pthread_mutex_destroy(&mgr->mutex);
|
|
memset(mgr, 0, sizeof(*mgr));
|
|
return -1;
|
|
}
|
|
|
|
mgr->last_poll_at = 0;
|
|
mgr->last_reconcile_at = 0;
|
|
|
|
DEBUG_INFO("[didactyl] trigger manager initialized (capacity=%d)", mgr->capacity);
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
|
if (!mgr || !mgr->cfg) {
|
|
return -1;
|
|
}
|
|
|
|
int loaded = 0;
|
|
int considered = 0;
|
|
const int kinds[2] = {31123, 31124};
|
|
|
|
for (int k = 0; k < 2; k++) {
|
|
char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]);
|
|
if (!skill_json) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* skill_events = cJSON_Parse(skill_json);
|
|
free(skill_json);
|
|
if (!skill_events || !cJSON_IsArray(skill_events)) {
|
|
cJSON_Delete(skill_events);
|
|
continue;
|
|
}
|
|
|
|
int sn = cJSON_GetArraySize(skill_events);
|
|
for (int i = 0; i < sn; i++) {
|
|
cJSON* skill_event = cJSON_GetArrayItem(skill_events, i);
|
|
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
|
|
cJSON* tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
|
|
if (!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* d = find_tag_value_string(tags, "d");
|
|
cJSON* trigger = find_tag_value_string(tags, "trigger");
|
|
cJSON* filter = find_tag_value_string(tags, "filter");
|
|
cJSON* action = find_tag_value_string(tags, "action");
|
|
|
|
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
|
|
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
|
|
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
|
|
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
|
|
|
|
considered++;
|
|
if (!d_tag || d_tag[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
int trigger_supported = trigger_s &&
|
|
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
|
strcmp(trigger_s, "webhook") == 0 ||
|
|
strcmp(trigger_s, "cron") == 0 ||
|
|
strcmp(trigger_s, "chain") == 0 ||
|
|
strcmp(trigger_s, "dm") == 0);
|
|
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(action_s, "template") == 0) {
|
|
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
|
|
}
|
|
|
|
const char* llm_s = NULL;
|
|
const char* tools_s = NULL;
|
|
int has_max_tokens = 0;
|
|
int max_tokens = 0;
|
|
int has_temperature = 0;
|
|
double temperature = 0.0;
|
|
int has_seed = 0;
|
|
int seed = 0;
|
|
parse_trigger_runtime_tags(tags,
|
|
&llm_s,
|
|
&tools_s,
|
|
&has_max_tokens,
|
|
&max_tokens,
|
|
&has_temperature,
|
|
&temperature,
|
|
&has_seed,
|
|
&seed);
|
|
|
|
if (trigger_manager_add(mgr,
|
|
d_tag,
|
|
content->valuestring,
|
|
filter_s,
|
|
TRIGGER_ACTION_LLM,
|
|
trigger_s,
|
|
1,
|
|
llm_s,
|
|
tools_s,
|
|
has_max_tokens,
|
|
max_tokens,
|
|
has_temperature,
|
|
temperature,
|
|
has_seed,
|
|
seed) == 0) {
|
|
loaded++;
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(skill_events);
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from self skills (considered=%d)", loaded, considered);
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) {
|
|
if (!mgr || !mgr->cfg) {
|
|
return -1;
|
|
}
|
|
|
|
int loaded = 0;
|
|
int considered = 0;
|
|
|
|
for (int i = 0; i < mgr->cfg->startup_event_count; i++) {
|
|
startup_event_t* ev = &mgr->cfg->startup_events[i];
|
|
if (!ev) {
|
|
continue;
|
|
}
|
|
|
|
if (ev->kind != 31123 && ev->kind != 31124) {
|
|
continue;
|
|
}
|
|
|
|
considered++;
|
|
|
|
if (!ev->content || ev->content[0] == '\0' || !ev->tags_json || ev->tags_json[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
cJSON* tags = cJSON_Parse(ev->tags_json);
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
cJSON_Delete(tags);
|
|
DEBUG_WARN("[didactyl] startup trigger skip: invalid tags_json for startup event index=%d", i);
|
|
continue;
|
|
}
|
|
|
|
cJSON* d = find_tag_value_string(tags, "d");
|
|
cJSON* trigger = find_tag_value_string(tags, "trigger");
|
|
cJSON* filter = find_tag_value_string(tags, "filter");
|
|
cJSON* action = find_tag_value_string(tags, "action");
|
|
cJSON* enabled = find_tag_value_string(tags, "enabled");
|
|
|
|
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
|
|
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
|
|
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
|
|
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
|
|
const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true";
|
|
|
|
if (!d_tag || d_tag[0] == '\0') {
|
|
cJSON_Delete(tags);
|
|
DEBUG_WARN("[didactyl] startup trigger skip: missing d tag for startup event index=%d", i);
|
|
continue;
|
|
}
|
|
|
|
int trigger_supported = trigger_s &&
|
|
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
|
strcmp(trigger_s, "webhook") == 0 ||
|
|
strcmp(trigger_s, "cron") == 0 ||
|
|
strcmp(trigger_s, "chain") == 0 ||
|
|
strcmp(trigger_s, "dm") == 0);
|
|
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
|
|
cJSON_Delete(tags);
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(action_s, "template") == 0) {
|
|
DEBUG_WARN("[didactyl] startup trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
|
|
}
|
|
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
|
|
|
|
const char* llm_s = NULL;
|
|
const char* tools_s = NULL;
|
|
int has_max_tokens = 0;
|
|
int max_tokens = 0;
|
|
int has_temperature = 0;
|
|
double temperature = 0.0;
|
|
int has_seed = 0;
|
|
int seed = 0;
|
|
parse_trigger_runtime_tags(tags,
|
|
&llm_s,
|
|
&tools_s,
|
|
&has_max_tokens,
|
|
&max_tokens,
|
|
&has_temperature,
|
|
&temperature,
|
|
&has_seed,
|
|
&seed);
|
|
|
|
if (trigger_manager_add(mgr,
|
|
d_tag,
|
|
ev->content,
|
|
filter_s,
|
|
TRIGGER_ACTION_LLM,
|
|
trigger_s,
|
|
is_enabled,
|
|
llm_s,
|
|
tools_s,
|
|
has_max_tokens,
|
|
max_tokens,
|
|
has_temperature,
|
|
temperature,
|
|
has_seed,
|
|
seed) == 0) {
|
|
loaded++;
|
|
DEBUG_INFO("[didactyl] startup trigger registered d_tag=%s action=%s enabled=%d", d_tag, "llm", is_enabled);
|
|
} else {
|
|
DEBUG_WARN("[didactyl] startup trigger failed d_tag=%s", d_tag);
|
|
}
|
|
|
|
cJSON_Delete(tags);
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from startup config (considered=%d)", loaded, considered);
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_add(trigger_manager_t* mgr,
|
|
const char* skill_d_tag,
|
|
const char* content,
|
|
const char* filter_json,
|
|
trigger_action_type_t action_type,
|
|
const char* trigger_type_str,
|
|
int enabled,
|
|
const char* llm_spec,
|
|
const char* tools_policy,
|
|
int has_max_tokens,
|
|
int max_tokens,
|
|
int has_temperature,
|
|
double temperature,
|
|
int has_seed,
|
|
int seed) {
|
|
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
|
|
return -1;
|
|
}
|
|
|
|
if (action_type == TRIGGER_ACTION_TEMPLATE) {
|
|
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", skill_d_tag);
|
|
action_type = TRIGGER_ACTION_LLM;
|
|
}
|
|
if (action_type != TRIGGER_ACTION_LLM) {
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
|
|
int existing = find_trigger_index_locked(mgr, skill_d_tag);
|
|
if (existing >= 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return trigger_manager_update(mgr,
|
|
skill_d_tag,
|
|
content,
|
|
filter_json,
|
|
action_type,
|
|
trigger_type_str,
|
|
enabled,
|
|
llm_spec,
|
|
tools_policy,
|
|
has_max_tokens,
|
|
max_tokens,
|
|
has_temperature,
|
|
temperature,
|
|
has_seed,
|
|
seed);
|
|
}
|
|
|
|
int max_active = mgr->cfg ? mgr->cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE;
|
|
if (max_active < 1) max_active = TRIGGER_DEFAULT_MAX_ACTIVE;
|
|
if (mgr->count >= max_active) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
DEBUG_WARN("[didactyl] trigger add rejected: max_active reached (%d)", max_active);
|
|
return -1;
|
|
}
|
|
|
|
if (ensure_capacity(mgr, mgr->count + 1) != 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return -1;
|
|
}
|
|
|
|
active_trigger_t* t = &mgr->triggers[mgr->count];
|
|
memset(t, 0, sizeof(*t));
|
|
snprintf(t->skill_d_tag, sizeof(t->skill_d_tag), "%s", skill_d_tag);
|
|
snprintf(t->skill_content, sizeof(t->skill_content), "%s", content);
|
|
snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json);
|
|
t->action_type = action_type;
|
|
t->enabled = clamp_enabled(enabled);
|
|
t->trigger_type = trigger_type_from_string(trigger_type_str);
|
|
t->last_fired = 0;
|
|
t->last_seen_created_at = 0;
|
|
t->last_cron_fire = 0;
|
|
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
|
char normalized_cron[sizeof(t->cron_expr)] = {0};
|
|
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
DEBUG_WARN("[didactyl] trigger add rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
|
|
memset(t, 0, sizeof(*t));
|
|
return -1;
|
|
}
|
|
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
|
|
}
|
|
snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
|
|
snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
|
|
t->has_max_tokens = has_max_tokens ? 1 : 0;
|
|
t->max_tokens = max_tokens;
|
|
t->has_temperature = has_temperature ? 1 : 0;
|
|
t->temperature = temperature;
|
|
t->has_seed = has_seed ? 1 : 0;
|
|
t->seed = seed;
|
|
t->subscription = NULL;
|
|
t->subscription_ctx = NULL;
|
|
|
|
if (register_trigger_subscription_locked(mgr, t) != 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
DEBUG_WARN("[didactyl] trigger add rejected: failed to create subscription d_tag=%s", skill_d_tag);
|
|
memset(t, 0, sizeof(*t));
|
|
return -1;
|
|
}
|
|
|
|
mgr->count++;
|
|
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
DEBUG_INFO("[didactyl] trigger added d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_d_tag) {
|
|
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
|
|
int idx = find_trigger_index_locked(mgr, skill_d_tag);
|
|
if (idx < 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return 0;
|
|
}
|
|
|
|
close_trigger_subscription_locked(&mgr->triggers[idx]);
|
|
|
|
if (idx < mgr->count - 1) {
|
|
memmove(&mgr->triggers[idx],
|
|
&mgr->triggers[idx + 1],
|
|
(size_t)(mgr->count - idx - 1) * sizeof(active_trigger_t));
|
|
}
|
|
|
|
mgr->count--;
|
|
memset(&mgr->triggers[mgr->count], 0, sizeof(active_trigger_t));
|
|
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
DEBUG_INFO("[didactyl] trigger removed d_tag=%s", skill_d_tag);
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_find(trigger_manager_t* mgr, const char* skill_d_tag, active_trigger_t* out) {
|
|
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !out) {
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
int idx = find_trigger_index_locked(mgr, skill_d_tag);
|
|
if (idx < 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return -1;
|
|
}
|
|
|
|
*out = mgr->triggers[idx];
|
|
out->subscription = NULL;
|
|
out->subscription_ctx = NULL;
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_fire(trigger_manager_t* mgr,
|
|
const char* skill_d_tag,
|
|
cJSON* event,
|
|
const char* source_label) {
|
|
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !event) {
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
int idx = find_trigger_index_locked(mgr, skill_d_tag);
|
|
if (idx < 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return -1;
|
|
}
|
|
|
|
int fired = maybe_fire_trigger_locked(mgr, idx, event, source_label);
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return fired;
|
|
}
|
|
|
|
int trigger_manager_fire_chains(trigger_manager_t* mgr,
|
|
const char* source_skill_d_tag,
|
|
cJSON* source_event,
|
|
const char* source_label) {
|
|
if (!mgr || !source_skill_d_tag || source_skill_d_tag[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
static __thread int s_chain_depth = 0;
|
|
if (s_chain_depth >= 5) {
|
|
DEBUG_WARN("[didactyl] chain trigger depth limit reached for source=%s", source_skill_d_tag);
|
|
return 0;
|
|
}
|
|
|
|
int fired_count = 0;
|
|
s_chain_depth++;
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
int count_snapshot = mgr->count;
|
|
for (int i = 0; i < count_snapshot; i++) {
|
|
active_trigger_t* t = &mgr->triggers[i];
|
|
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_CHAIN) {
|
|
continue;
|
|
}
|
|
if (t->filter_json[0] == '\0' || strcmp(t->filter_json, source_skill_d_tag) != 0) {
|
|
continue;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
int cooldown = mgr->cfg ? mgr->cfg->triggers.cooldown_seconds : 0;
|
|
if (cooldown < 0) cooldown = 0;
|
|
if (cooldown > 0 && t->last_fired > 0 && (now - t->last_fired) < cooldown) {
|
|
continue;
|
|
}
|
|
|
|
t->last_fired = now;
|
|
|
|
active_trigger_t trigger_copy = *t;
|
|
trigger_copy.subscription = NULL;
|
|
trigger_copy.subscription_ctx = NULL;
|
|
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
cJSON* event = cJSON_CreateObject();
|
|
if (event) {
|
|
cJSON_AddStringToObject(event, "type", "chain");
|
|
cJSON_AddStringToObject(event, "source_d_tag", source_skill_d_tag);
|
|
cJSON_AddStringToObject(event, "source_label", source_label ? source_label : "chain");
|
|
cJSON_AddNumberToObject(event, "created_at", (double)now);
|
|
if (source_event) {
|
|
cJSON* dup = cJSON_Duplicate(source_event, 1);
|
|
if (dup) {
|
|
cJSON_AddItemToObject(event, "source_event", dup);
|
|
}
|
|
}
|
|
|
|
(void)mgr;
|
|
execute_llm_action(&trigger_copy, event, "chain");
|
|
cJSON_Delete(event);
|
|
fired_count++;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
}
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
s_chain_depth--;
|
|
return fired_count;
|
|
}
|
|
|
|
int trigger_manager_update(trigger_manager_t* mgr,
|
|
const char* skill_d_tag,
|
|
const char* content,
|
|
const char* filter_json,
|
|
trigger_action_type_t action_type,
|
|
const char* trigger_type_str,
|
|
int enabled,
|
|
const char* llm_spec,
|
|
const char* tools_policy,
|
|
int has_max_tokens,
|
|
int max_tokens,
|
|
int has_temperature,
|
|
double temperature,
|
|
int has_seed,
|
|
int seed) {
|
|
if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
|
|
return -1;
|
|
}
|
|
|
|
if (action_type == TRIGGER_ACTION_TEMPLATE) {
|
|
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", skill_d_tag);
|
|
action_type = TRIGGER_ACTION_LLM;
|
|
}
|
|
if (action_type != TRIGGER_ACTION_LLM) {
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
|
|
int idx = find_trigger_index_locked(mgr, skill_d_tag);
|
|
if (idx < 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
return trigger_manager_add(mgr,
|
|
skill_d_tag,
|
|
content,
|
|
filter_json,
|
|
action_type,
|
|
trigger_type_str,
|
|
enabled,
|
|
llm_spec,
|
|
tools_policy,
|
|
has_max_tokens,
|
|
max_tokens,
|
|
has_temperature,
|
|
temperature,
|
|
has_seed,
|
|
seed);
|
|
}
|
|
|
|
active_trigger_t* t = &mgr->triggers[idx];
|
|
snprintf(t->skill_content, sizeof(t->skill_content), "%s", content);
|
|
snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json);
|
|
t->action_type = action_type;
|
|
t->enabled = clamp_enabled(enabled);
|
|
t->trigger_type = trigger_type_from_string(trigger_type_str);
|
|
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
|
char normalized_cron[sizeof(t->cron_expr)] = {0};
|
|
if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
DEBUG_WARN("[didactyl] trigger update rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
|
|
return -1;
|
|
}
|
|
snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
|
|
} else {
|
|
t->cron_expr[0] = '\0';
|
|
}
|
|
snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
|
|
snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
|
|
t->has_max_tokens = has_max_tokens ? 1 : 0;
|
|
t->max_tokens = max_tokens;
|
|
t->has_temperature = has_temperature ? 1 : 0;
|
|
t->temperature = temperature;
|
|
t->has_seed = has_seed ? 1 : 0;
|
|
t->seed = seed;
|
|
|
|
if (register_trigger_subscription_locked(mgr, t) != 0) {
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
DEBUG_WARN("[didactyl] trigger update failed to (re)subscribe d_tag=%s", skill_d_tag);
|
|
return -1;
|
|
}
|
|
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
DEBUG_INFO("[didactyl] trigger updated d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
|
|
return 0;
|
|
}
|
|
|
|
int trigger_manager_active_count(trigger_manager_t* mgr) {
|
|
if (!mgr) {
|
|
return 0;
|
|
}
|
|
|
|
int active = 0;
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
for (int i = 0; i < mgr->count; i++) {
|
|
if (mgr->triggers[i].enabled) {
|
|
active++;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
return active;
|
|
}
|
|
|
|
int trigger_manager_poll(trigger_manager_t* mgr) {
|
|
if (!mgr) {
|
|
return -1;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
if (now <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
|
|
return 0;
|
|
}
|
|
mgr->last_poll_at = now;
|
|
|
|
int fired = 0;
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
int count_snapshot = mgr->count;
|
|
for (int i = 0; i < count_snapshot; i++) {
|
|
active_trigger_t* t = &mgr->triggers[i];
|
|
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_CRON) {
|
|
continue;
|
|
}
|
|
|
|
const char* expr = (t->cron_expr[0] != '\0') ? t->cron_expr : t->filter_json;
|
|
char normalized_expr[64] = {0};
|
|
if (!cron_expr_is_valid(expr, normalized_expr, sizeof(normalized_expr))) {
|
|
DEBUG_WARN("[didactyl] cron trigger skipped: invalid expression d_tag=%s expr=%s", t->skill_d_tag, expr ? expr : "");
|
|
continue;
|
|
}
|
|
if (!cron_matches_now(normalized_expr, now)) {
|
|
continue;
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] cron trigger matched d_tag=%s expr=%s", t->skill_d_tag, normalized_expr);
|
|
|
|
if (t->last_cron_fire > 0 && (now - t->last_cron_fire) < 50) {
|
|
continue;
|
|
}
|
|
|
|
t->last_cron_fire = now;
|
|
|
|
active_trigger_t trigger_copy = *t;
|
|
trigger_copy.subscription = NULL;
|
|
trigger_copy.subscription_ctx = NULL;
|
|
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
cJSON* event = cJSON_CreateObject();
|
|
if (event) {
|
|
cJSON_AddStringToObject(event, "type", "cron");
|
|
cJSON_AddStringToObject(event, "d_tag", trigger_copy.skill_d_tag);
|
|
cJSON_AddStringToObject(event, "cron_expr", normalized_expr);
|
|
cJSON_AddNumberToObject(event, "created_at", (double)now);
|
|
(void)mgr;
|
|
execute_llm_action(&trigger_copy, event, "cron");
|
|
cJSON_Delete(event);
|
|
fired++;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
}
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
if (mgr->last_reconcile_at <= 0 || (now - mgr->last_reconcile_at) >= 900) {
|
|
mgr->last_reconcile_at = now;
|
|
(void)trigger_manager_reconcile_from_relays(mgr);
|
|
}
|
|
|
|
return fired;
|
|
}
|
|
|
|
static int dm_filter_matches_tier(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;
|
|
}
|
|
|
|
int trigger_manager_get_dm_skills(trigger_manager_t* mgr,
|
|
const char* sender_pubkey_hex,
|
|
didactyl_sender_tier_t tier,
|
|
char** out_d_tags,
|
|
int max_d_tags) {
|
|
(void)sender_pubkey_hex;
|
|
|
|
if (!mgr || !out_d_tags || max_d_tags <= 0) {
|
|
return -1;
|
|
}
|
|
|
|
int count = 0;
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
int count_snapshot = mgr->count;
|
|
for (int i = 0; i < count_snapshot && count < max_d_tags; i++) {
|
|
active_trigger_t* t = &mgr->triggers[i];
|
|
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_DM) {
|
|
continue;
|
|
}
|
|
if (!dm_filter_matches_tier(t->filter_json, tier)) {
|
|
continue;
|
|
}
|
|
|
|
out_d_tags[count++] = t->skill_d_tag;
|
|
}
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
return count;
|
|
}
|
|
|
|
char* trigger_manager_status_json(trigger_manager_t* mgr) {
|
|
if (!mgr) {
|
|
cJSON* err = cJSON_CreateObject();
|
|
if (!err) {
|
|
return NULL;
|
|
}
|
|
cJSON_AddBoolToObject(err, "success", 0);
|
|
cJSON_AddStringToObject(err, "error", "trigger manager unavailable");
|
|
char* out = cJSON_PrintUnformatted(err);
|
|
cJSON_Delete(err);
|
|
return out;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON* arr = cJSON_CreateArray();
|
|
if (!root || !arr) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddNumberToObject(root, "count", mgr->count);
|
|
|
|
int active = 0;
|
|
for (int i = 0; i < mgr->count; i++) {
|
|
active_trigger_t* t = &mgr->triggers[i];
|
|
if (t->enabled) {
|
|
active++;
|
|
}
|
|
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
|
|
cJSON_AddStringToObject(item, "skill_d_tag", t->skill_d_tag);
|
|
cJSON_AddStringToObject(item, "filter_json", t->filter_json);
|
|
cJSON_AddStringToObject(item, "type", trigger_type_to_string(t->trigger_type));
|
|
cJSON_AddStringToObject(item, "action", "llm");
|
|
cJSON_AddBoolToObject(item, "enabled", t->enabled ? 1 : 0);
|
|
cJSON_AddBoolToObject(item, "subscribed", t->subscription ? 1 : 0);
|
|
cJSON_AddStringToObject(item, "llm", t->llm_spec);
|
|
cJSON_AddStringToObject(item, "tools", t->tools_policy);
|
|
cJSON_AddBoolToObject(item, "has_max_tokens", t->has_max_tokens ? 1 : 0);
|
|
if (t->has_max_tokens) cJSON_AddNumberToObject(item, "max_tokens", t->max_tokens);
|
|
cJSON_AddBoolToObject(item, "has_temperature", t->has_temperature ? 1 : 0);
|
|
if (t->has_temperature) cJSON_AddNumberToObject(item, "temperature", t->temperature);
|
|
cJSON_AddBoolToObject(item, "has_seed", t->has_seed ? 1 : 0);
|
|
if (t->has_seed) cJSON_AddNumberToObject(item, "seed", t->seed);
|
|
cJSON_AddNumberToObject(item, "last_fired", (double)t->last_fired);
|
|
cJSON_AddNumberToObject(item, "last_seen_created_at", (double)t->last_seen_created_at);
|
|
if (t->trigger_type == TRIGGER_TYPE_CRON) {
|
|
cJSON_AddStringToObject(item, "cron_expr", t->cron_expr);
|
|
cJSON_AddNumberToObject(item, "last_cron_fire", (double)t->last_cron_fire);
|
|
}
|
|
cJSON_AddItemToArray(arr, item);
|
|
}
|
|
|
|
cJSON_AddNumberToObject(root, "active", active);
|
|
cJSON_AddItemToObject(root, "triggers", arr);
|
|
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
return out;
|
|
}
|
|
|
|
void trigger_manager_cleanup(trigger_manager_t* mgr) {
|
|
if (!mgr) {
|
|
return;
|
|
}
|
|
|
|
pthread_mutex_lock(&mgr->mutex);
|
|
for (int i = 0; i < mgr->count; i++) {
|
|
close_trigger_subscription_locked(&mgr->triggers[i]);
|
|
}
|
|
free(mgr->triggers);
|
|
mgr->triggers = NULL;
|
|
mgr->count = 0;
|
|
mgr->capacity = 0;
|
|
mgr->cfg = NULL;
|
|
mgr->last_poll_at = 0;
|
|
pthread_mutex_unlock(&mgr->mutex);
|
|
|
|
pthread_mutex_destroy(&mgr->mutex);
|
|
memset(mgr, 0, sizeof(*mgr));
|
|
|
|
DEBUG_INFO("[didactyl] trigger manager cleaned up");
|
|
}
|