From b9526c3a200fab17e36be69e068e815437d32bf9 Mon Sep 17 00:00:00 2001 From: Didactyl User Date: Tue, 7 Apr 2026 06:33:15 -0400 Subject: [PATCH] v0.2.28 - Updated LT and Anvil genesis configs: unified DM history context as startup skill, migrated default skills into startup_events, rotated admin npub to npub1rmz9..., and redeployed/restarted didactyl+simon+anvil --- README.md | 4 +- genesis.jsonc.example | 8 +-- genesis.lt.didactyl.jsonc | 80 ++++++++++++++++++++++++++++++ genesis.lt.simon.jsonc | 101 ++++++++++++++++++++++++++++++++++++++ src/agent.c | 18 +++++-- src/config.c | 16 +++++- src/llm.c | 57 ++++++++++++++++----- src/main.c | 7 +++ src/main.h | 4 +- src/setup_wizard.c | 9 +++- src/tools/tool_model.c | 21 ++++++++ 11 files changed, 300 insertions(+), 25 deletions(-) create mode 100644 genesis.lt.didactyl.jsonc create mode 100644 genesis.lt.simon.jsonc diff --git a/README.md b/README.md index 80b7aef..e1407c1 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.2.27 +## Current Status — v0.2.28 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.27 — Remove markdown context snapshot logging; keep only raw provider request JSON logging and simplify context debug plumbing +> Last release update: v0.2.28 — Updated LT and Anvil genesis configs: unified DM history context as startup skill, migrated default skills into startup_events, rotated admin npub to npub1rmz9..., and redeployed/restarted didactyl+simon+anvil - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/genesis.jsonc.example b/genesis.jsonc.example index 263b86a..f51368a 100644 --- a/genesis.jsonc.example +++ b/genesis.jsonc.example @@ -24,7 +24,7 @@ { "kind": 30078, "d_tag": "user-settings", - "content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"openai\",\"api_key\":\"sk-REPLACE_WITH_API_KEY\",\"model\":\"gpt-4o-mini\",\"base_url\":\"https://api.openai.com/v1\",\"max_tokens\":512,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1REPLACE_WITH_ADMIN_PUBKEY\",\"dm_protocol\":\"nip04\"}}" + "content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"openai\",\"api_key\":\"sk-REPLACE_WITH_API_KEY\",\"model\":\"gpt-4o-mini\",\"base_url\":\"https://api.openai.com/v1\",\"max_tokens\":512,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1REPLACE_WITH_ADMIN_PUBKEY\",\"dm_protocol\":\"nip04\",\"max_turns\":40}}" } ], @@ -74,12 +74,12 @@ }, { "kind": 31124, - "content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}", + "content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}", "tags": [ - ["d", "dm_history"], + ["d", "dm_history_context"], ["app", "didactyl"], ["scope", "private"], - ["description", "DM conversation history for context continuity"], + ["description", "DM history context"], ["trigger", "dm"], ["filter", "{\"from\":\"admin\"}"] ] diff --git a/genesis.lt.didactyl.jsonc b/genesis.lt.didactyl.jsonc new file mode 100644 index 0000000..7c6a0bc --- /dev/null +++ b/genesis.lt.didactyl.jsonc @@ -0,0 +1,80 @@ +{ + // ─── Agent Identity Keys ─────────────────────────────────────────── + // Keep nsec private; npub/hex fields can be used for verification/debugging. + "key": { + "nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e" + }, + + // ─── Administrator ───────────────────────────────────────────────── + // Admin pubkey (npub or hex) controls privileged interactions. + "admin": { + "pubkey": "npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06" + }, + + // ─── DM Protocol ────────────────────────────────────────────────── + // Supported values: "nip04", "nip17", or "both". + "dm_protocol": "nip04", + + // ─── HTTP Admin API ─────────────────────────────────────────────── + // Local API for model/context inspection and runtime controls. + "api": { + "enabled": true, + "port": 8484, + "bind_address": "127.0.0.1" + }, + + // ─── Encrypted Startup Config Events ────────────────────────────── + // These are published as NIP-44 encrypted kind 30078 self-events on first run. + "encrypted_events": [ + { + "kind": 30078, + "d_tag": "user-settings", + "content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"ppq\",\"api_key\":\"sk-LshAWvFC0KOFgrUYiP6NmT\",\"model\":\"claude-opus-4.6\",\"base_url\":\"https://api.ppq.ai\",\"max_tokens\":200000,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06\",\"dm_protocol\":\"nip04\",\"max_turns\":40}}" + } + ], + + // ─── Startup Events ─────────────────────────────────────────────── + // Minimal relay list required for relay pool initialization. + "startup_events": [ + { + "kind": 10002, + "content": "", + "tags": [ + ["r", "wss://relay.damus.io"], + ["r", "wss://relay.primal.net"], + ["r", "wss://relay.laantungir.net"] + ] + }, + { + "kind": 31124, + "content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}", + "tags": [ + ["d", "dm_history_context"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "DM history context"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + }, + { + "kind": 31124, + "content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true", + "tags": [ + [ + "d", + "didactyl-default" + ], + [ + "app", + "didactyl" + ], + [ + "scope", + "private" + ] + ] + } + + ] +} diff --git a/genesis.lt.simon.jsonc b/genesis.lt.simon.jsonc new file mode 100644 index 0000000..2b34c82 --- /dev/null +++ b/genesis.lt.simon.jsonc @@ -0,0 +1,101 @@ +{ + // ─── Agent Identity Keys ─────────────────────────────────────────── + // Keep nsec private; npub/hex fields can be used for verification/debugging. + "key": { + "nsec": "nsec17yzm0lyr479z73sgnpf9yuxv0nwclpaff0k45npsh49nddpc3fhqmhh2ph" + }, + // ─── Administrator ───────────────────────────────────────────────── + // Admin pubkey (npub or hex) controls privileged interactions. + "admin": { + "pubkey": "npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06" + }, + // ─── DM Protocol ────────────────────────────────────────────────── + // Supported values: "nip04", "nip17", or "both". + "dm_protocol": "nip04", + // ─── HTTP Admin API ─────────────────────────────────────────────── + // Local API for model/context inspection and runtime controls. + "api": { + "enabled": true, + "port": 8485, + "bind_address": "127.0.0.1" + }, + // ─── Encrypted Startup Config Events ────────────────────────────── + // These are published as NIP-44 encrypted kind 30078 self-events on first run. + "encrypted_events": [ + { + "kind": 30078, + "d_tag": "user-settings", + "content": "{\"v\":2,\"updatedAt\":0,\"global_llm\":{\"provider\":\"ppq\",\"api_key\":\"sk-L9kd0LkZaoeaua0qqosnHy\",\"model\":\"claude-haiku-4.5\",\"base_url\":\"https://api.ppq.ai\",\"max_tokens\":512,\"temperature\":0.7},\"didactyl\":{\"admin_pubkey\":\"npub1rmz9gu6de0m0u4ysrn39crrud099ahvfgs6pvasl4hpjr5ud7yus54xv06\",\"dm_protocol\":\"nip04\",\"max_turns\":40}}" + } + ], + // ─── Startup Events ─────────────────────────────────────────────── + // Minimal relay list required for relay pool initialization. + "startup_events": [ + { + "kind": 10002, + "content": "", + "tags": [ + [ + "r", + "wss://relay.damus.io" + ], + [ + "r", + "wss://relay.primal.net" + ], + [ + "r", + "wss://relay.laantungir.net" + ] + ] + }, + { + "kind": 31124, + "content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}", + "tags": [ + [ + "d", + "dm_history_context" + ], + [ + "app", + "didactyl" + ], + [ + "scope", + "private" + ], + [ + "description", + "DM history context" + ], + [ + "trigger", + "dm" + ], + [ + "filter", + "{\"from\":\"admin\"}" + ] + ] + }, + { + "kind": 31124, + "content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true", + "tags": [ + [ + "d", + "didactyl-default" + ], + [ + "app", + "didactyl" + ], + [ + "scope", + "private" + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/agent.c b/src/agent.c index 20621fa..9661003 100644 --- a/src/agent.c +++ b/src/agent.c @@ -2055,7 +2055,7 @@ void agent_on_message(const char* sender_pubkey_hex, 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 max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 40; 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; @@ -2162,10 +2162,22 @@ void agent_on_message(const char* sender_pubkey_hex, 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."); + : "I reached the tool-turn limit before getting a final response from the model."); } - const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request."; + if (exhausted_on_max_turns) { + const char* base = final_answer_owned ? final_answer_owned : "I reached the tool-turn limit before getting a final response from the model."; + const char* suffix = "\n\nI have paused to avoid running forever. Reply with 'continue' and I will continue from the current context."; + size_t need = strlen(base) + strlen(suffix) + 1U; + char* with_prompt = (char*)malloc(need); + if (with_prompt) { + snprintf(with_prompt, need, "%s%s", base, suffix); + free(final_answer_owned); + final_answer_owned = with_prompt; + } + } + + const char* final_answer = final_answer_owned ? final_answer_owned : "I reached the tool-turn limit before getting a final response from the model."; if (exited_on_stall || exhausted_on_max_turns) { notify_admin_limit_diagnostic("dm_agent_loop", diff --git a/src/config.c b/src/config.c index 401eabc..3333338 100644 --- a/src/config.c +++ b/src/config.c @@ -294,7 +294,7 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) { } if (config->tools.max_turns < 1) { - config->tools.max_turns = 8; + config->tools.max_turns = 40; } if (config->tools.trigger_max_turns < 1) { config->tools.trigger_max_turns = config->tools.max_turns; @@ -564,6 +564,18 @@ static int parse_agent_payload_object(cJSON* agent_obj, didactyl_config_t* confi } } + cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(agent_obj, "max_turns"); + if (max_turns) { + if (!cJSON_IsNumber(max_turns)) { + return -1; + } + int parsed = (int)max_turns->valuedouble; + if (parsed < 1) { + parsed = 40; + } + config->tools.max_turns = parsed; + } + return 0; } @@ -1424,7 +1436,7 @@ int config_load(const char* path, didactyl_config_t* config) { config->dm_protocol = DM_PROTOCOL_NIP04; config->tools.enabled = 1; - config->tools.max_turns = 20; + config->tools.max_turns = 40; config->tools.trigger_max_turns = 12; config->tools.api_default_max_turns = 8; config->tools.api_max_turns_ceiling = 32; diff --git a/src/llm.c b/src/llm.c index 79fe6ef..f8d638e 100644 --- a/src/llm.c +++ b/src/llm.c @@ -22,33 +22,46 @@ static int g_initialized = 0; static pthread_mutex_t g_llm_request_log_mutex = PTHREAD_MUTEX_INITIALIZER; static unsigned long g_llm_request_log_seq = 0; -static void log_provider_request_body_local(const char* body) { - if (!body || body[0] == '\0') { - return; +static int provider_log_next_stamp_seq_local(char* stamp, size_t stamp_size, unsigned long* seq_out) { + if (!stamp || stamp_size == 0 || !seq_out) { + return -1; } + stamp[0] = '\0'; + *seq_out = 0; + if (mkdir("context.logs", 0755) != 0 && errno != EEXIST) { - return; + return -1; } - char stamp[32] = {0}; time_t now = time(NULL); struct tm tm_info; localtime_r(&now, &tm_info); - strftime(stamp, sizeof(stamp), "%Y%m%dT%H%M%S", &tm_info); + strftime(stamp, stamp_size, "%Y%m%dT%H%M%S", &tm_info); pthread_mutex_lock(&g_llm_request_log_mutex); - unsigned long seq = ++g_llm_request_log_seq; + *seq_out = ++g_llm_request_log_seq; pthread_mutex_unlock(&g_llm_request_log_mutex); + return 0; +} + +static void log_provider_payload_local(const char* payload, + const char* stamp, + unsigned long seq, + const char* phase) { + if (!payload || payload[0] == '\0' || !stamp || stamp[0] == '\0' || !phase || phase[0] == '\0') { + return; + } + char path[256] = {0}; - snprintf(path, sizeof(path), "context.logs/%s_req%04lu.json", stamp, seq); + snprintf(path, sizeof(path), "context.logs/%s_%s%04lu.json", stamp, phase, seq); FILE* out = fopen(path, "wb"); if (!out) { return; } - (void)fwrite(body, 1, strlen(body), out); + (void)fwrite(payload, 1, strlen(payload), out); fclose(out); } @@ -118,8 +131,17 @@ static char* perform_http_request(const char* url, const char* body, int is_post DEBUG_INFO("[didactyl] llm request: method=GET url=%s", url); } - if (is_post && body) { - log_provider_request_body_local(body); + char provider_log_stamp[32] = {0}; + unsigned long provider_log_seq = 0; + int provider_log_has_pair = 0; + + if (is_post && body && body[0] != '\0') { + if (provider_log_next_stamp_seq_local(provider_log_stamp, + sizeof(provider_log_stamp), + &provider_log_seq) == 0) { + log_provider_payload_local(body, provider_log_stamp, provider_log_seq, "req"); + provider_log_has_pair = 1; + } } nostr_http_response_t resp; @@ -129,6 +151,19 @@ static char* perform_http_request(const char* url, const char* body, int is_post return NULL; } + if (resp.body && resp.body_len > 0) { + if (!provider_log_has_pair) { + if (provider_log_next_stamp_seq_local(provider_log_stamp, + sizeof(provider_log_stamp), + &provider_log_seq) == 0) { + provider_log_has_pair = 1; + } + } + if (provider_log_has_pair) { + log_provider_payload_local(resp.body, provider_log_stamp, provider_log_seq, "res"); + } + } + if (resp.status_code < 200 || resp.status_code >= 300) { DEBUG_ERROR("[didactyl] llm http request failed: status=%ld", resp.status_code); if (resp.status_code == 101) { diff --git a/src/main.c b/src/main.c index c35b9a8..50c7744 100644 --- a/src/main.c +++ b/src/main.c @@ -747,6 +747,7 @@ static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plai cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); + cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || @@ -784,6 +785,11 @@ static int apply_recalled_user_settings(didactyl_config_t* cfg, const char* plai dm_protocol_from_string(dm_protocol->valuestring, cfg); + if (max_turns && cJSON_IsNumber(max_turns)) { + int parsed = (int)max_turns->valuedouble; + cfg->tools.max_turns = (parsed > 0) ? parsed : 40; + } + cJSON_Delete(root); return 0; } @@ -864,6 +870,7 @@ static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int per cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey); cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol)); + cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40); cJSON_AddItemToObject(user_settings, "didactyl", didactyl); char* user_settings_json = cJSON_PrintUnformatted(user_settings); diff --git a/src/main.h b/src/main.h index 9b43920..f1d2368 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 2 -#define DIDACTYL_VERSION_PATCH 27 -#define DIDACTYL_VERSION "v0.2.27" +#define DIDACTYL_VERSION_PATCH 28 +#define DIDACTYL_VERSION "v0.2.28" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 3cc3f88..7920173 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -271,7 +271,7 @@ static void config_set_defaults(didactyl_config_t* cfg) { cfg->dm_protocol = DM_PROTOCOL_NIP04; cfg->tools.enabled = 1; - cfg->tools.max_turns = 20; + cfg->tools.max_turns = 40; cfg->tools.trigger_max_turns = 12; cfg->tools.api_default_max_turns = 8; cfg->tools.api_max_turns_ceiling = 32; @@ -1006,6 +1006,7 @@ static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const cha cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey"); cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol"); + cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns"); if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || @@ -1049,6 +1050,11 @@ static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const cha cfg->dm_protocol = DM_PROTOCOL_NIP04; } + if (max_turns && cJSON_IsNumber(max_turns)) { + int parsed = (int)max_turns->valuedouble; + cfg->tools.max_turns = (parsed > 0) ? parsed : 40; + } + cJSON_Delete(root); return 0; } @@ -1166,6 +1172,7 @@ static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg) cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey); cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol)); + cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40); cJSON_AddItemToObject(user_settings, "didactyl", didactyl); char* settings_json = cJSON_PrintUnformatted(user_settings); diff --git a/src/tools/tool_model.c b/src/tools/tool_model.c index 0562be5..a51c85e 100644 --- a/src/tools/tool_model.c +++ b/src/tools/tool_model.c @@ -136,6 +136,27 @@ static int persist_global_llm_user_settings_nostr(tools_context_t* ctx, cJSON_DeleteItemFromObjectCaseSensitive(global_llm, "temperature"); cJSON_AddNumberToObject(global_llm, "temperature", cfg->temperature); + cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(user_settings, "didactyl"); + if (didactyl && !cJSON_IsObject(didactyl)) { + cJSON_DeleteItemFromObjectCaseSensitive(user_settings, "didactyl"); + didactyl = NULL; + } + if (!didactyl) { + cJSON* new_didactyl = cJSON_CreateObject(); + if (!new_didactyl) { + cJSON_Delete(user_settings); + if (out_error) *out_error = strdup("allocation failure while preparing didactyl payload"); + return -1; + } + cJSON_AddItemToObject(user_settings, "didactyl", new_didactyl); + didactyl = new_didactyl; + } + + cJSON_DeleteItemFromObjectCaseSensitive(didactyl, "max_turns"); + cJSON_AddNumberToObject(didactyl, + "max_turns", + (ctx->cfg->tools.max_turns > 0) ? ctx->cfg->tools.max_turns : 40); + cJSON* store_args = cJSON_CreateObject(); if (!store_args) { cJSON_Delete(user_settings);