Files
didactyl/src/main.c
T

399 lines
13 KiB
C

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#include "main.h"
#include "agent.h"
#include "config.h"
#include "llm.h"
#include "nostr_handler.h"
#include "debug.h"
#include "trigger_manager.h"
#include "tools.h"
#include "cjson/cJSON.h"
#include "http_api.h"
static volatile sig_atomic_t g_running = 1;
typedef struct {
trigger_manager_t* trigger_manager;
int loaded_once;
} deferred_trigger_load_ctx_t;
static void on_self_skill_eose_load_triggers(int event_count, void* user_data) {
deferred_trigger_load_ctx_t* ctx = (deferred_trigger_load_ctx_t*)user_data;
if (!ctx || !ctx->trigger_manager) {
return;
}
if (ctx->loaded_once) {
DEBUG_TRACE("[didactyl] deferred trigger load skipped (already loaded once) event_count=%d", event_count);
return;
}
DEBUG_INFO("[didactyl] deferred trigger load begin after self-skill EOSE (event_count=%d)", event_count);
if (trigger_manager_load_from_skills(ctx->trigger_manager) != 0) {
DEBUG_WARN("[didactyl] deferred trigger load failed after self-skill EOSE");
return;
}
ctx->loaded_once = 1;
char* status = trigger_manager_status_json(ctx->trigger_manager);
if (status) {
DEBUG_INFO("[didactyl] deferred trigger load status: %s", status);
free(status);
}
}
static void signal_handler(int signum) {
(void)signum;
g_running = 0;
}
static void print_usage(const char* prog) {
fprintf(stderr,
"Usage: %s [-h|--help] [-v|--version] [--config <path>] [--debug <0-5>] [--dump-schemas] [--test-tool <name> <args_json>]\n",
prog ? prog : "didactyl");
}
static int tool_result_success(const char* json) {
if (!json) {
return 0;
}
cJSON* root = cJSON_Parse(json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return 0;
}
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
int ok = (success && cJSON_IsBool(success) && cJSON_IsTrue(success)) ? 1 : 0;
cJSON_Delete(root);
return ok;
}
static int wait_for_connected_relays(int timeout_ms) {
if (timeout_ms <= 0) {
return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
}
const int step_ms = 100;
int elapsed_ms = 0;
while (elapsed_ms < timeout_ms) {
if (nostr_handler_connected_relay_count() > 0) {
return 1;
}
(void)nostr_handler_poll(step_ms);
elapsed_ms += step_ms;
}
return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
}
int main(int argc, char** argv) {
const char* config_path = "./config.jsonc";
int debug_level = DEBUG_LEVEL_TRACE;
int dump_schemas = 0;
const char* test_tool_name = NULL;
const char* test_tool_args = "{}";
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
return 0;
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
printf("%s %s\n", DIDACTYL_NAME, DIDACTYL_VERSION);
return 0;
} else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
config_path = argv[++i];
} else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) {
debug_level = atoi(argv[++i]);
} else if (strcmp(argv[i], "--dump-schemas") == 0) {
dump_schemas = 1;
} else if (strcmp(argv[i], "--test-tool") == 0 && i + 2 < argc) {
test_tool_name = argv[++i];
test_tool_args = argv[++i];
} else {
print_usage(argv[0]);
return 1;
}
}
debug_init(debug_level);
DEBUG_INFO("%s %s starting", DIDACTYL_NAME, DIDACTYL_VERSION);
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize nostr core\n");
return 1;
}
didactyl_config_t cfg;
if (config_load(config_path, &cfg) != 0) {
fprintf(stderr, "Failed to load config: %s\n", config_path);
fprintf(stderr, "Config error: %s\n", config_last_error());
nostr_cleanup();
return 1;
}
if (dump_schemas || test_tool_name) {
if (llm_init(&cfg.llm) != 0) {
fprintf(stderr, "Failed to initialize llm client\n");
config_free(&cfg);
nostr_cleanup();
return 1;
}
if (nostr_handler_init(&cfg) != 0) {
fprintf(stderr, "Failed to initialize nostr handler\n");
config_free(&cfg);
llm_cleanup();
nostr_cleanup();
return 1;
}
tools_context_t tools_ctx;
if (tools_init(&tools_ctx, &cfg) != 0) {
fprintf(stderr, "Failed to initialize tools context\n");
nostr_handler_cleanup();
config_free(&cfg);
llm_cleanup();
nostr_cleanup();
return 1;
}
int exit_code = 0;
if (test_tool_name) {
const int timeout_ms = 15000;
if (!wait_for_connected_relays(timeout_ms)) {
fprintf(stderr,
"No relay connection established within %d ms; refusing to run --test-tool '%s'\n",
timeout_ms,
test_tool_name);
tools_cleanup(&tools_ctx);
nostr_handler_cleanup();
config_free(&cfg);
llm_cleanup();
nostr_cleanup();
return 1;
}
}
if (dump_schemas) {
char* schemas = tools_build_openai_schema_json(&tools_ctx);
if (!schemas) {
fprintf(stderr, "Failed to build tool schemas\n");
exit_code = 1;
} else {
printf("%s\n", schemas);
free(schemas);
}
} else {
char* result = tools_execute(&tools_ctx, test_tool_name, test_tool_args);
if (!result) {
fprintf(stderr, "Failed to execute tool\n");
exit_code = 1;
} else {
printf("%s\n", result);
exit_code = tool_result_success(result) ? 0 : 1;
free(result);
}
}
tools_cleanup(&tools_ctx);
nostr_handler_cleanup();
config_free(&cfg);
llm_cleanup();
nostr_cleanup();
return exit_code;
}
if (llm_init(&cfg.llm) != 0) {
fprintf(stderr, "Failed to initialize llm client\n");
config_free(&cfg);
nostr_cleanup();
return 1;
}
if (nostr_handler_init(&cfg) != 0) {
fprintf(stderr, "Failed to initialize nostr handler\n");
llm_cleanup();
config_free(&cfg);
nostr_cleanup();
return 1;
}
if (nostr_handler_reconcile_startup_events() != 0) {
DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)");
}
const char* system_context = nostr_handler_get_system_context();
if (!system_context || system_context[0] == '\0') {
system_context = "You are Didactyl, a sovereign AI agent living on Nostr.";
}
if (agent_init(&cfg, system_context) != 0) {
fprintf(stderr, "Failed to initialize agent\n");
nostr_handler_cleanup();
llm_cleanup();
config_free(&cfg);
nostr_cleanup();
return 1;
}
trigger_manager_t trigger_manager;
if (trigger_manager_init(&trigger_manager, &cfg) != 0) {
fprintf(stderr, "Failed to initialize trigger manager\n");
agent_cleanup();
nostr_handler_cleanup();
llm_cleanup();
config_free(&cfg);
nostr_cleanup();
return 1;
}
agent_set_trigger_manager(&trigger_manager);
nostr_handler_set_trigger_manager(&trigger_manager);
deferred_trigger_load_ctx_t deferred_trigger_ctx = {
.trigger_manager = &trigger_manager,
.loaded_once = 0
};
nostr_handler_set_self_skill_eose_callback(on_self_skill_eose_load_triggers, &deferred_trigger_ctx);
DEBUG_INFO("[didactyl] startup phase: deferred trigger load will run after self-skill EOSE");
DEBUG_INFO("[didactyl] startup phase: startup-config trigger load begin");
if (trigger_manager_load_from_startup_events(&trigger_manager) != 0) {
DEBUG_WARN("[didactyl] startup phase: startup-config trigger load failed (continuing)");
}
DEBUG_INFO("[didactyl] startup phase: startup-config trigger load end");
DEBUG_INFO("[didactyl] startup phase: subscribe admin context begin");
if (nostr_handler_subscribe_admin_context() != 0) {
DEBUG_WARN("[didactyl] startup phase: subscribe admin context failed (continuing)");
}
DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
if (nostr_handler_subscribe_self_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
}
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
char startup_dm[768];
const char* startup_name = nostr_handler_get_startup_display_name();
int connected_relays = nostr_handler_connected_relay_count();
time_t startup_now = time(NULL);
struct tm startup_tm;
char startup_time_str[32] = {0};
if (localtime_r(&startup_now, &startup_tm)) {
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
} else {
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
}
snprintf(startup_dm,
sizeof(startup_dm),
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
startup_time_str,
DIDACTYL_VERSION,
connected_relays,
cfg.relay_count);
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
}
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed");
fprintf(stderr, "Failed to subscribe to DMs\n");
agent_cleanup();
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();
return 1;
}
DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGPIPE, SIG_IGN);
int http_api_started = 0;
if (cfg.api.enabled) {
http_api_context_t http_ctx;
memset(&http_ctx, 0, sizeof(http_ctx));
http_ctx.cfg = &cfg;
http_ctx.tools_ctx = agent_tools_context();
http_ctx.trigger_manager = &trigger_manager;
if (http_api_init(&http_ctx) != 0) {
fprintf(stderr,
"Failed to initialize HTTP API on %s:%d\n",
cfg.api.bind_address,
cfg.api.port);
agent_cleanup();
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();
return 1;
}
http_api_started = 1;
DEBUG_INFO("[didactyl] HTTP API listening at http://%s:%d",
cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
cfg.api.port > 0 ? cfg.api.port : 8484);
DEBUG_INFO("[didactyl] HTTP API endpoints: http://%s:%d/api/context/current http://%s:%d/api/context/parts",
cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
cfg.api.port > 0 ? cfg.api.port : 8484,
cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
cfg.api.port > 0 ? cfg.api.port : 8484);
} else {
DEBUG_INFO("[didactyl] HTTP API disabled (set api.enabled=true in config to enable)");
}
nostr_handler_refresh_relay_statuses();
DEBUG_INFO("[didactyl] entering main poll loop");
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
while (g_running) {
(void)nostr_handler_poll(100);
(void)trigger_manager_poll(&trigger_manager);
if (http_api_started) {
(void)http_api_poll(0);
}
struct timespec ts = {0, 10 * 1000 * 1000};
nanosleep(&ts, NULL);
}
DEBUG_INFO("[didactyl] shutting down");
if (http_api_started) {
http_api_cleanup();
}
agent_cleanup();
nostr_handler_cleanup();
llm_cleanup();
trigger_manager_cleanup(&trigger_manager);
config_free(&cfg);
nostr_cleanup();
return 0;
}