Compare commits

...
6 Commits
23 changed files with 883 additions and 253 deletions
+2 -2
View File
@@ -55,11 +55,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.0.80
## Current Status — v0.0.86
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.80Fix wizard/runtime config persistence and add --admin CLI override for agent_config updates
> Last release update: v0.0.86Return masked api_key field in model_get/model_set responses
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+5 -4
View File
@@ -40,10 +40,11 @@
// triggered skills, HTTP API prompt runs, and local HTTP fetch tool.
"tools": {
"enabled": true,
"max_turns": 8, // default max turns for normal agent tool loops
"trigger_max_turns": 6, // max turns for triggered-skill executions
"api_default_max_turns": 4, // default when HTTP body omits max_turns
"api_max_turns_ceiling": 16, // hard cap applied to API-provided max_turns
"max_turns": 20, // default max turns for normal agent tool loops
"trigger_max_turns": 12, // max turns for triggered-skill executions
"api_default_max_turns": 8, // default when HTTP body omits max_turns
"api_max_turns_ceiling": 32, // hard cap applied to API-provided max_turns
"stall_repeat_threshold": 3, // stop early when identical tool-call turns repeat this many times
"local_http_fetch_default_timeout_seconds": 20,
"local_http_fetch_max_timeout_seconds": 120,
"shell": {
+28 -7
View File
@@ -4,16 +4,23 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_BINARY="$SCRIPT_DIR/didactyl_static_x86_64"
REMOTE_TARGET="ubuntu@laantungir.net:~/didactyl"
REMOTE_HOST="ubuntu@laantungir.net"
REMOTE_TARGETS=(
"/home/simon/didactyl"
"/home/didactyl/didactyl"
)
REMOTE_SERVICES=(
"simon.service"
"didactyl.service"
)
if ! command -v rsync >/dev/null 2>&1; then
echo "ERROR: rsync is not installed or not in PATH"
exit 1
fi
if [ ! -f "$SOURCE_BINARY" ]; then
echo "ERROR: Source binary not found: $SOURCE_BINARY"
echo "Build it first so didactyl_static_x86_64 exists."
if ! command -v ssh >/dev/null 2>&1; then
echo "ERROR: ssh is not installed or not in PATH"
exit 1
fi
@@ -25,6 +32,20 @@ if [ ! -f "$SOURCE_BINARY" ]; then
exit 1
fi
echo "Deploying $SOURCE_BINARY to $REMOTE_TARGET"
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_TARGET"
echo "Deployment complete."
REMOTE_STAGE="/tmp/didactyl_static_x86_64"
echo "Uploading $SOURCE_BINARY to staging path $REMOTE_HOST:$REMOTE_STAGE"
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_HOST:$REMOTE_STAGE"
for target in "${REMOTE_TARGETS[@]}"; do
echo "Installing staged binary to $target via sudo"
ssh "$REMOTE_HOST" "sudo install -m 0755 '$REMOTE_STAGE' '$target'"
done
echo "Restarting services on $REMOTE_HOST: ${REMOTE_SERVICES[*]}"
ssh "$REMOTE_HOST" "sudo systemctl restart ${REMOTE_SERVICES[*]}"
echo "Cleaning up remote staging file $REMOTE_STAGE"
ssh "$REMOTE_HOST" "rm -f '$REMOTE_STAGE'"
echo "Deployment complete for all targets and services."
@@ -0,0 +1,146 @@
# Fix: Local File Persistence & Stall Diagnostic Improvements
## Context
The agent got stuck in a stall loop when trying to create a skill via DM. The root cause is `max_tokens=512` truncating the LLM's tool call JSON. The admin tried to fix it with `/model_set {"max_tokens": 10000}` but got `"failed to persist llm config to config file"` because `model_set` tries to write to a local config file that doesn't exist in the Nostr-native deployment model.
Investigation revealed two tools that incorrectly persist state to local files instead of kind 30078 Nostr events, plus missing diagnostic information in the stall detector.
## Hardcoded File Locations to Remove
### tool_model.c
- `ctx->cfg->config_path` — references a local config file (genesis.jsonc or config.jsonc)
- The entire `persist_llm_config()` function (lines 82-139) reads/writes this file
- The `read_entire_file_local()` helper (lines 42-80) exists only for this purpose
### tool_task.c
- `"tasks.json"` — hardcoded at line 188: `build_tool_path_local(ctx, "tasks.json", ...)`
- `tasks_load_root_local()` (lines 82-141) reads from this file
- `tasks_save_root_local()` (lines 143-159) writes to this file
### tools_schema.c
- Line 985: `"Update active LLM configuration and persist it to config.jsonc"` — mentions config.jsonc
- Line 1285: `"Build current task list context block from tasks.json"` — mentions tasks.json
- Line 1314: `"Manage agent short-term task memory stored in tasks.json"` — mentions tasks.json
## Fixes
### Fix 1: model_set — Persist to Kind 30078
**File:** `src/tools/tool_model.c`
Replace `persist_llm_config()` (lines 82-139) with a function that:
1. Builds a JSON object with LLM config fields (provider, model, base_url, max_tokens, temperature — NOT api_key for security)
2. NIP-44 self-encrypts it using the same pattern as `config_store` in `tool_config.c`
3. Publishes as kind 30078 with tags `["d", "llm_config"]` and `["app", "didactyl"]`
Delete the `read_entire_file_local()` helper (lines 42-80) and the `json_object_set_string()` helper (lines 33-40) — they only exist for the file-based persist.
Also update `execute_model_set()` (line 234) to call the new Nostr-based persist, and return success with a warning if persist fails (since runtime update already succeeded at line 229).
### Fix 2: model_set — Don't Fail When Only Persist Fails
**File:** `src/tools/tool_model.c`, lines 229-236
Currently:
```c
if (llm_set_config(&cfg) != 0) {
return json_error_local("failed to update runtime llm config");
}
ctx->cfg->llm = cfg;
if (persist_llm_config(ctx, &cfg) != 0) {
return json_error_local("failed to persist llm config to config file");
}
```
Change to: return success with a `"persist_warning"` field if persist fails, since the runtime update already took effect.
### Fix 3: task_manage — Persist to Kind 30078
**File:** `src/tools/tool_task.c`
Replace `tasks_load_root_local()` and `tasks_save_root_local()` with:
- `tasks_load_root_nostr()` — queries kind 30078 `d=tasks` from self, NIP-44 decrypts, parses JSON
- `tasks_save_root_nostr()` — serializes JSON, NIP-44 self-encrypts, publishes kind 30078 `d=tasks`
This follows the exact same pattern as `config_store`/`config_recall` in `tool_config.c` and `memory_save`/`memory_recall` in `tool_memory.c`.
Remove the hardcoded `"tasks.json"` string at line 188 and the `build_tool_path_local()` call for it.
Remove `tasks_path` from the response JSON at line 424.
### Fix 4: Update Schema Descriptions
**File:** `src/tools/tools_schema.c`
- Line 985: Change `"Update active LLM configuration and persist it to config.jsonc"` to `"Update active LLM configuration and persist it to Nostr"`
- Line 1285: Change `"Build current task list context block from tasks.json"` to `"Build current task list context block from agent task memory"`
- Line 1314: Change `"Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)"` to `"Manage agent short-term task memory stored on Nostr (list/add/update/remove/clear/replace)"`
### Fix 5: Add finish_reason to LLM Response
**File:** `src/llm.h`
Add `char* finish_reason;` to `llm_response_t`:
```c
typedef struct {
char* content;
llm_tool_call_t* tool_calls;
int tool_call_count;
char* finish_reason;
} llm_response_t;
```
**File:** `src/llm.c`
In `parse_llm_response()` (line 276), after extracting `msg` from `choices[0]`, extract `finish_reason`:
```c
cJSON* fr = first ? cJSON_GetObjectItemCaseSensitive(first, "finish_reason") : NULL;
if (fr && cJSON_IsString(fr) && fr->valuestring) {
out->finish_reason = strdup(fr->valuestring);
}
```
In `llm_response_free()`, add `free(response->finish_reason);`.
### Fix 6: Add max_tokens Truncation Hint to Stall Diagnostic
**File:** `src/agent.c`
In the stall detection block at line 2217, before freeing `resp`, check `finish_reason`:
```c
if (repeated_tool_turns >= stall_repeat_threshold) {
int truncated = resp.finish_reason && strcmp(resp.finish_reason, "length") == 0;
exited_on_stall = 1;
llm_response_free(&resp);
break;
}
```
In `notify_admin_limit_diagnostic()` (line 106), add fields:
- `possible_cause` — "max_tokens_truncation" when finish_reason was "length"
- `current_max_tokens` — the current max_tokens value
- `hint` — "Increase max_tokens: /model_set {\"max_tokens\": 4096}"
**File:** `src/http_api.c`
Same changes in the HTTP API tool loop stall detection at line 966.
## Execution Order
1. Fix 5 (finish_reason in llm_response_t) — no dependencies
2. Fix 6 (stall diagnostic) — depends on Fix 5
3. Fix 1 (model_set persist to Nostr) — independent
4. Fix 2 (model_set graceful failure) — can be done with Fix 1
5. Fix 3 (task_manage persist to Nostr) — independent
6. Fix 4 (schema descriptions) — do last, trivial
## Files Modified
- `src/llm.h` — add finish_reason field
- `src/llm.c` — parse finish_reason, free it
- `src/agent.c` — stall diagnostic with truncation hint
- `src/http_api.c` — stall diagnostic with truncation hint
- `src/tools/tool_model.c` — replace file persist with kind 30078, graceful failure
- `src/tools/tool_task.c` — replace file I/O with kind 30078
- `src/tools/tools_schema.c` — update 3 descriptions
+164 -7
View File
@@ -79,6 +79,72 @@ static uint64_t message_fingerprint(const char* sender_pubkey_hex, const char* m
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);
@@ -371,6 +437,15 @@ static char* format_result_markdown_local(const char* raw_result) {
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;
@@ -508,10 +583,15 @@ static char* build_slash_help_all_json(void) {
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;
cJSON* adoption_events = (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring)
? cJSON_Parse(adoption_events_json->valuestring)
: 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) {
@@ -661,8 +741,11 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}");
}
char* markdown_result = format_result_markdown_local(result_json);
const char* dm_payload = markdown_result ? markdown_result : result_json;
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);
@@ -1815,7 +1898,10 @@ void agent_on_trigger(const char* skill_d_tag,
? 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;
@@ -1836,6 +1922,7 @@ void agent_on_trigger(const char* skill_d_tag,
}
if (resp.tool_call_count <= 0) {
trigger_completed = 1;
llm_response_free(&resp);
break;
}
@@ -1871,6 +1958,20 @@ void agent_on_trigger(const char* skill_d_tag,
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,
@@ -2097,10 +2198,17 @@ 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 : 8;
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;
@@ -2128,6 +2236,21 @@ void agent_on_message(const char* sender_pubkey_hex,
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;
@@ -2162,10 +2285,44 @@ void agent_on_message(const char* sender_pubkey_hex,
}
if (!final_answer_owned) {
final_answer_owned = strdup("I hit my tool-use limit for this request.");
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 ? "..." : "");
+1 -1
View File
@@ -4,7 +4,7 @@
#include "config.h"
#include "nostr_handler.h"
#include "cjson/cJSON.h"
#include "tools.h"
#include "tools/tools.h"
struct trigger_manager;
+12 -4
View File
@@ -225,6 +225,7 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
cJSON* trigger_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "trigger_max_turns");
cJSON* api_default_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "api_default_max_turns");
cJSON* api_max_turns_ceiling = cJSON_GetObjectItemCaseSensitive(tools, "api_max_turns_ceiling");
cJSON* stall_repeat_threshold = cJSON_GetObjectItemCaseSensitive(tools, "stall_repeat_threshold");
cJSON* local_http_fetch_default_timeout_seconds =
cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds");
cJSON* local_http_fetch_max_timeout_seconds =
@@ -244,6 +245,9 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
if (api_max_turns_ceiling && cJSON_IsNumber(api_max_turns_ceiling)) {
config->tools.api_max_turns_ceiling = (int)api_max_turns_ceiling->valuedouble;
}
if (stall_repeat_threshold && cJSON_IsNumber(stall_repeat_threshold)) {
config->tools.stall_repeat_threshold = (int)stall_repeat_threshold->valuedouble;
}
if (local_http_fetch_default_timeout_seconds && cJSON_IsNumber(local_http_fetch_default_timeout_seconds)) {
config->tools.local_http_fetch_default_timeout_seconds =
(int)local_http_fetch_default_timeout_seconds->valuedouble;
@@ -294,6 +298,9 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
if (config->tools.api_default_max_turns > config->tools.api_max_turns_ceiling) {
config->tools.api_default_max_turns = config->tools.api_max_turns_ceiling;
}
if (config->tools.stall_repeat_threshold < 2) {
config->tools.stall_repeat_threshold = 3;
}
if (config->tools.local_http_fetch_default_timeout_seconds < 1) {
config->tools.local_http_fetch_default_timeout_seconds = 20;
}
@@ -1124,10 +1131,11 @@ int config_load(const char* path, didactyl_config_t* config) {
config->dm_protocol = DM_PROTOCOL_NIP04;
config->tools.enabled = 1;
config->tools.max_turns = 8;
config->tools.trigger_max_turns = 6;
config->tools.api_default_max_turns = 4;
config->tools.api_max_turns_ceiling = 16;
config->tools.max_turns = 20;
config->tools.trigger_max_turns = 12;
config->tools.api_default_max_turns = 8;
config->tools.api_max_turns_ceiling = 32;
config->tools.stall_repeat_threshold = 3;
config->tools.local_http_fetch_default_timeout_seconds = 20;
config->tools.local_http_fetch_max_timeout_seconds = 120;
config->tools.shell.enabled = 1;
+1
View File
@@ -50,6 +50,7 @@ typedef struct {
int trigger_max_turns;
int api_default_max_turns;
int api_max_turns_ceiling;
int stall_repeat_threshold;
int local_http_fetch_default_timeout_seconds;
int local_http_fetch_max_timeout_seconds;
shell_tools_config_t shell;
+147 -5
View File
@@ -5,6 +5,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include "agent.h"
@@ -81,6 +82,82 @@ static int estimate_tokens_from_chars(int chars) {
return (chars + 3) / 4;
}
static uint64_t fnv1a64_local(const char* s) {
uint64_t h = 1469598103934665603ULL;
if (!s) return h;
while (*s) {
h ^= (unsigned char)(*s++);
h *= 1099511628211ULL;
}
return h;
}
static uint64_t tool_calls_fingerprint_local(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_local(tc->name ? tc->name : "");
uint64_t args_h = fnv1a64_local(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_http(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_api_ctx.cfg || g_api_ctx.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_api_ctx.cfg->admin.pubkey, diag);
}
static int uri_extract_after_prefix(const struct mg_str* uri,
const char* prefix,
char* out,
@@ -649,10 +726,15 @@ static char* build_slash_help_all_json_local(void) {
char* adopted_json = tools_execute(g_api_ctx.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;
cJSON* adoption_events = (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring)
? cJSON_Parse(adoption_events_json->valuestring)
: 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) {
@@ -844,9 +926,20 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
return NULL;
}
int stall_repeat_threshold = 3;
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.stall_repeat_threshold > 1) {
stall_repeat_threshold = g_api_ctx.cfg->tools.stall_repeat_threshold;
}
char* final_response = NULL;
uint64_t last_tool_fp = 0;
int repeated_tool_turns = 0;
int exited_on_stall = 0;
int stall_due_to_length = 0;
int turns_run = 0;
for (int turn = 0; turn < max_turns; turn++) {
turns_run = turn + 1;
char* messages_json = cJSON_PrintUnformatted(convo);
if (!messages_json) break;
@@ -877,6 +970,21 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
break;
}
uint64_t current_tool_fp = tool_calls_fingerprint_local(&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_local(convo, &resp) != 0) {
llm_response_free(&resp);
final_response = strdup("Failed to append assistant tool calls.");
@@ -917,9 +1025,43 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
llm_response_free(&resp);
}
done:
done:;
int hit_max_turn_limit = (!final_response && !exited_on_stall && turns_run >= max_turns);
if (!final_response) {
final_response = strdup(tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run.");
char* messages_json = cJSON_PrintUnformatted(convo);
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_response = strdup(forced_answer);
}
llm_response_free(&final_resp);
}
}
}
if (!final_response) {
final_response = strdup(exited_on_stall
? "Stopped repeated tool calls after detecting a loop, and no final summary was produced."
: (tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run."));
}
if (exited_on_stall || hit_max_turn_limit) {
notify_admin_limit_diagnostic_http("http_api_tool_loop",
exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
log_sender,
max_turns,
turns_run,
stall_repeat_threshold,
repeated_tool_turns,
g_api_ctx.cfg ? g_api_ctx.cfg->llm.max_tokens : 0,
(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_response ? final_response : "");
}
cJSON_AddBoolToObject(root, "success", 1);
+1 -1
View File
@@ -2,7 +2,7 @@
#define DIDACTYL_HTTP_API_H
#include "config.h"
#include "tools.h"
#include "tools/tools.h"
struct trigger_manager;
+6
View File
@@ -289,6 +289,11 @@ static int parse_llm_response(const char* json, llm_response_t* out) {
return -1;
}
cJSON* finish_reason = first ? cJSON_GetObjectItemCaseSensitive(first, "finish_reason") : NULL;
if (finish_reason && cJSON_IsString(finish_reason) && finish_reason->valuestring) {
out->finish_reason = strdup(finish_reason->valuestring);
}
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
if (content && cJSON_IsString(content) && content->valuestring) {
out->content = strdup(content->valuestring);
@@ -459,6 +464,7 @@ int llm_chat_with_tools(const char* system_prompt,
void llm_response_free(llm_response_t* response) {
if (!response) return;
free(response->content);
free(response->finish_reason);
for (int i = 0; i < response->tool_call_count; i++) {
free(response->tool_calls[i].id);
free(response->tool_calls[i].name);
+1
View File
@@ -13,6 +13,7 @@ typedef struct {
char* content;
llm_tool_call_t* tool_calls;
int tool_call_count;
char* finish_reason;
} llm_response_t;
int llm_init(const llm_config_t* config);
+29 -19
View File
@@ -813,24 +813,29 @@ static int publish_encrypted_self_config(const didactyl_config_t* cfg, const cha
return rc;
}
static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg) {
static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int persist_llm_config) {
if (!cfg) return -1;
cJSON* llm = cJSON_CreateObject();
if (!llm) return -1;
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
int llm_rc = 0;
if (persist_llm_config) {
cJSON* llm = cJSON_CreateObject();
if (!llm) return -1;
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
char* llm_json = cJSON_PrintUnformatted(llm);
cJSON_Delete(llm);
if (!llm_json) return -1;
char* llm_json = cJSON_PrintUnformatted(llm);
cJSON_Delete(llm);
if (!llm_json) return -1;
int llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
free(llm_json);
llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
free(llm_json);
} else {
DEBUG_INFO("[didactyl] startup phase: skipping llm_config publish because it was recalled from Nostr");
}
cJSON* agent = cJSON_CreateObject();
if (!agent) return llm_rc;
@@ -846,15 +851,18 @@ static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg) {
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
}
static void recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
if (!cfg) {
return;
return 0;
}
int llm_recalled_from_nostr = 0;
if (!llm_config_is_complete(&cfg->llm)) {
char* llm_plaintext = NULL;
if (fetch_self_config_plaintext(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
if (apply_recalled_llm_config(cfg, llm_plaintext) == 0) {
llm_recalled_from_nostr = 1;
DEBUG_INFO("[didactyl] startup phase: recovered llm_config from Nostr");
} else {
DEBUG_WARN("[didactyl] startup phase: failed to apply recalled llm_config");
@@ -885,6 +893,8 @@ static void recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
}
free(agent_plaintext);
return llm_recalled_from_nostr;
}
int main(int argc, char** argv) {
@@ -1129,8 +1139,8 @@ int main(int argc, char** argv) {
}
startup_step_begin(2, "Recover runtime config from Nostr");
recover_missing_runtime_config_from_nostr(&cfg);
startup_step_ok(2, "Recover runtime config from Nostr", NULL);
int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg);
startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "llm_config recalled" : NULL);
startup_step_begin(3, "Validate LLM config");
if (!llm_config_is_complete(&cfg.llm)) {
@@ -1184,7 +1194,7 @@ int main(int argc, char** argv) {
}
}
if (persist_runtime_config_to_nostr(&cfg) != 0) {
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
} else {
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
+2 -2
View File
@@ -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 0
#define DIDACTYL_VERSION_PATCH 80
#define DIDACTYL_VERSION "v0.0.80"
#define DIDACTYL_VERSION_PATCH 86
#define DIDACTYL_VERSION "v0.0.86"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+89 -1
View File
@@ -3802,6 +3802,55 @@ char* nostr_handler_get_admin_kind1_notes_context(void) {
return out;
}
static char* extract_latest_kind0_content_from_events_json(const char* events_json) {
if (!events_json || events_json[0] == '\0') {
return NULL;
}
cJSON* root = cJSON_Parse(events_json);
if (!root || !cJSON_IsArray(root)) {
cJSON_Delete(root);
return NULL;
}
char* best_content = NULL;
time_t best_created_at = 0;
int n = cJSON_GetArraySize(root);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(root, i);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 0 ||
!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
continue;
}
time_t ev_created_at = 0;
if (created_at && cJSON_IsNumber(created_at)) {
ev_created_at = (time_t)created_at->valuedouble;
}
if (!best_content || ev_created_at >= best_created_at) {
char* dup = strdup(content->valuestring);
if (!dup) {
continue;
}
free(best_content);
best_content = dup;
best_created_at = ev_created_at;
}
}
cJSON_Delete(root);
return best_content;
}
char* nostr_handler_get_agent_kind0_context(void) {
if (!g_cfg) {
return NULL;
@@ -3810,7 +3859,46 @@ char* nostr_handler_get_agent_kind0_context(void) {
pthread_mutex_lock(&g_admin_ctx_mutex);
char* out = g_agent_kind0_json ? strdup(g_agent_kind0_json) : NULL;
pthread_mutex_unlock(&g_admin_ctx_mutex);
return out;
if (out && out[0] != '\0') {
return out;
}
free(out);
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 NULL;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 8);
char* events_json = nostr_handler_query_json(filter, 3000);
cJSON_Delete(filter);
if (!events_json) {
return NULL;
}
char* fetched_kind0 = extract_latest_kind0_content_from_events_json(events_json);
free(events_json);
if (!fetched_kind0 || fetched_kind0[0] == '\0') {
free(fetched_kind0);
return NULL;
}
pthread_mutex_lock(&g_admin_ctx_mutex);
free(g_agent_kind0_json);
g_agent_kind0_json = strdup(fetched_kind0);
pthread_mutex_unlock(&g_admin_ctx_mutex);
return fetched_kind0;
}
char* nostr_handler_get_agent_kind3_context(void) {
+1 -1
View File
@@ -2,7 +2,7 @@
#define DIDACTYL_PROMPT_TEMPLATE_H
#include "cjson/cJSON.h"
#include "tools.h"
#include "tools/tools.h"
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
+5 -4
View File
@@ -271,10 +271,11 @@ static void config_set_defaults(didactyl_config_t* cfg) {
cfg->dm_protocol = DM_PROTOCOL_NIP04;
cfg->tools.enabled = 1;
cfg->tools.max_turns = 8;
cfg->tools.trigger_max_turns = 6;
cfg->tools.api_default_max_turns = 4;
cfg->tools.api_max_turns_ceiling = 16;
cfg->tools.max_turns = 20;
cfg->tools.trigger_max_turns = 12;
cfg->tools.api_default_max_turns = 8;
cfg->tools.api_max_turns_ceiling = 32;
cfg->tools.stall_repeat_threshold = 3;
cfg->tools.local_http_fetch_default_timeout_seconds = 20;
cfg->tools.local_http_fetch_max_timeout_seconds = 120;
cfg->tools.shell.enabled = 1;
-6
View File
@@ -47,8 +47,6 @@ char* execute_admin_identity(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_pubkey", ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown");
cJSON_AddStringToObject(out, "sender_verification", tier_text);
char content[768];
snprintf(content,
@@ -82,7 +80,6 @@ char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_kind0_json", profile_json);
size_t content_len = strlen("## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
char* content = (char*)malloc(content_len);
@@ -121,7 +118,6 @@ char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json)
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_kind3_json", contacts_json);
size_t content_len = strlen("## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): ") + strlen(contacts_json) + 1U;
char* content = (char*)malloc(content_len);
@@ -160,7 +156,6 @@ char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_kind10002_json", relays_json);
size_t content_len = strlen("## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): ") + strlen(relays_json) + 1U;
char* content = (char*)malloc(content_len);
@@ -199,7 +194,6 @@ char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "admin_notes_content", notes_text);
size_t content_len = strlen("## Administrator Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
char* content = (char*)malloc(content_len);
-8
View File
@@ -62,8 +62,6 @@ char* execute_agent_identity(tools_context_t* ctx, const char* args_json) {
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "agent_pubkey", ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown");
cJSON_AddStringToObject(out, "agent_npub", npub);
char content[384];
snprintf(content,
@@ -95,7 +93,6 @@ char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "agent_kind0_json", profile_json);
size_t content_len = strlen("## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
char* content = (char*)malloc(content_len);
@@ -134,7 +131,6 @@ char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json)
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "agent_kind3_json", contacts_json);
size_t content_len = strlen("## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent contacts (JSON): ") + strlen(contacts_json) + 1U;
char* content = (char*)malloc(content_len);
@@ -173,7 +169,6 @@ char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "agent_kind10002_json", relays_json);
size_t content_len = strlen("## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent relay list (JSON): ") + strlen(relays_json) + 1U;
char* content = (char*)malloc(content_len);
@@ -212,7 +207,6 @@ char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "agent_notes_content", notes_text);
size_t content_len = strlen("## Agent Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
char* content = (char*)malloc(content_len);
@@ -267,7 +261,6 @@ char* execute_adopted_skills(tools_context_t* ctx, const char* args_json) {
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "content", (events_json && events_json[0]) ? events_json : "[]");
cJSON_AddStringToObject(out, "adoption_events_json", (events_json && events_json[0]) ? events_json : "[]");
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
@@ -302,7 +295,6 @@ char* execute_trigger_event(tools_context_t* ctx, const char* args_json) {
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "content", trigger_json);
cJSON_AddStringToObject(out, "trigger_event_json", trigger_json);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
+83 -91
View File
@@ -30,113 +30,72 @@ static cJSON* parse_args_local(const char* args_json) {
return args;
}
static int json_object_set_string(cJSON* obj, const char* key, const char* value) {
if (!obj || !key || !value) return -1;
cJSON_DeleteItemFromObjectCaseSensitive(obj, key);
cJSON* v = cJSON_CreateString(value);
if (!v) return -1;
cJSON_AddItemToObject(obj, key, v);
return 0;
}
static char* read_entire_file_local(const char* file_path, size_t* out_bytes) {
if (!file_path) return NULL;
FILE* fp = fopen(file_path, "rb");
if (!fp) return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cfg, char** out_error) {
if (!ctx || !ctx->cfg || !cfg) {
if (out_error) *out_error = strdup("invalid persist context");
return -1;
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return NULL;
if (out_error) *out_error = NULL;
cJSON* args = cJSON_CreateObject();
cJSON* content = cJSON_CreateObject();
if (!args || !content) {
cJSON_Delete(args);
cJSON_Delete(content);
if (out_error) *out_error = strdup("allocation failure while building llm_config payload");
return -1;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NULL;
cJSON_AddStringToObject(args, "d_tag", "llm_config");
cJSON_AddStringToObject(content, "provider", cfg->provider);
cJSON_AddStringToObject(content, "api_key", cfg->api_key);
cJSON_AddStringToObject(content, "model", cfg->model);
cJSON_AddStringToObject(content, "base_url", cfg->base_url);
cJSON_AddNumberToObject(content, "max_tokens", cfg->max_tokens);
cJSON_AddNumberToObject(content, "temperature", cfg->temperature);
cJSON_AddItemToObject(args, "content", content);
char* store_args_json = cJSON_PrintUnformatted(args);
cJSON_Delete(args);
if (!store_args_json) {
if (out_error) *out_error = strdup("failed to serialize llm_config payload");
return -1;
}
char* buf = (char*)malloc((size_t)len + 1U);
if (!buf) {
fclose(fp);
return NULL;
char* store_result = execute_config_store(ctx, store_args_json);
free(store_args_json);
if (!store_result) {
if (out_error) *out_error = strdup("config_store returned no response");
return -1;
}
size_t n = fread(buf, 1, (size_t)len, fp);
fclose(fp);
if (n != (size_t)len) {
free(buf);
return NULL;
}
buf[len] = '\0';
if (out_bytes) *out_bytes = (size_t)len;
return buf;
}
static int persist_llm_config(tools_context_t* ctx, const llm_config_t* cfg) {
if (!ctx || !ctx->cfg || !cfg) return -1;
if (ctx->cfg->config_path[0] == '\0') return -1;
size_t src_len = 0;
char* raw = read_entire_file_local(ctx->cfg->config_path, &src_len);
if (!raw) return -1;
char* src = jsonc_strip_comments(raw, src_len);
free(raw);
if (!src) return -1;
cJSON* root = cJSON_ParseWithLength(src, strlen(src));
free(src);
cJSON* root = cJSON_Parse(store_result);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
if (out_error) *out_error = strdup("config_store returned invalid JSON");
free(store_result);
return -1;
}
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
if (!llm || !cJSON_IsObject(llm)) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "llm");
llm = cJSON_CreateObject();
if (!llm) {
cJSON_Delete(root);
return -1;
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
if (!success || !cJSON_IsBool(success) || !cJSON_IsTrue(success)) {
cJSON* err = cJSON_GetObjectItemCaseSensitive(root, "error");
if (out_error) {
if (err && cJSON_IsString(err) && err->valuestring) {
*out_error = strdup(err->valuestring);
} else {
*out_error = strdup("config_store failed");
}
}
cJSON_AddItemToObject(root, "llm", llm);
}
if (json_object_set_string(llm, "provider", cfg->provider) != 0 ||
json_object_set_string(llm, "api_key", cfg->api_key) != 0 ||
json_object_set_string(llm, "model", cfg->model) != 0 ||
json_object_set_string(llm, "base_url", cfg->base_url) != 0) {
cJSON_Delete(root);
free(store_result);
return -1;
}
cJSON_DeleteItemFromObjectCaseSensitive(llm, "max_tokens");
cJSON_AddNumberToObject(llm, "max_tokens", cfg->max_tokens);
cJSON_DeleteItemFromObjectCaseSensitive(llm, "temperature");
cJSON_AddNumberToObject(llm, "temperature", cfg->temperature);
char* out = cJSON_Print(root);
cJSON_Delete(root);
if (!out) return -1;
FILE* fp = fopen(ctx->cfg->config_path, "wb");
if (!fp) {
free(out);
return -1;
}
size_t out_len = strlen(out);
size_t n = fwrite(out, 1, out_len, fp);
fclose(fp);
free(out);
return n == out_len ? 0 : -1;
free(store_result);
return 0;
}
static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* changed) {
@@ -149,6 +108,25 @@ static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* cha
return 0;
}
static void mask_api_key_local(const char* api_key, char* out, size_t out_size) {
if (!out || out_size == 0) return;
out[0] = '\0';
if (!api_key || api_key[0] == '\0') {
return;
}
size_t api_len = strlen(api_key);
if (api_len >= 8U) {
snprintf(out,
out_size,
"%.4s...%s",
api_key,
api_key + api_len - 4U);
} else {
snprintf(out, out_size, "%s", "(set)");
}
}
char* execute_model_get(const char* args_json) {
cJSON* args = parse_args_local(args_json);
if (!args) return json_error_local("invalid arguments JSON");
@@ -161,10 +139,15 @@ char* execute_model_get(const char* args_json) {
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
char api_key_masked[64] = {0};
mask_api_key_local(cfg.api_key, api_key_masked, sizeof(api_key_masked));
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "provider", cfg.provider);
cJSON_AddStringToObject(out, "model", cfg.model);
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
cJSON_AddStringToObject(out, "api_key", api_key_masked);
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
@@ -231,21 +214,30 @@ char* execute_model_set(tools_context_t* ctx, const char* args_json) {
}
ctx->cfg->llm = cfg;
if (persist_llm_config(ctx, &cfg) != 0) {
return json_error_local("failed to persist llm config to config file");
}
char* persist_error = NULL;
int persisted = (persist_llm_config_nostr(ctx, &cfg, &persist_error) == 0) ? 1 : 0;
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
char api_key_masked[64] = {0};
mask_api_key_local(cfg.api_key, api_key_masked, sizeof(api_key_masked));
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "provider", cfg.provider);
cJSON_AddStringToObject(out, "model", cfg.model);
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
cJSON_AddStringToObject(out, "api_key", api_key_masked);
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
cJSON_AddBoolToObject(out, "persisted", persisted ? 1 : 0);
if (!persisted && persist_error && persist_error[0] != '\0') {
cJSON_AddStringToObject(out, "persist_warning", persist_error);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(persist_error);
return json;
}
+150 -86
View File
@@ -2,14 +2,18 @@
#include "tools_internal.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "cjson/cJSON.h"
#include "../nostr_handler.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#define TASKS_KIND 30078
#define TASKS_D_TAG "tasks"
#define TASKS_APP_TAG "didactyl"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
@@ -31,33 +35,6 @@ static cJSON* parse_args_local(const char* args_json) {
return args;
}
static int is_safe_relative_path_local(const char* path) {
if (!path || path[0] == '\0') return 0;
if (path[0] == '/') return 0;
if (strstr(path, "..") != NULL) return 0;
if (strchr(path, '\\') != NULL) return 0;
return 1;
}
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
if (!is_safe_relative_path_local(rel_path)) return -1;
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
? ctx->cfg->tools.shell.working_directory
: ".";
int n = 0;
if (strcmp(cwd, ".") == 0) {
n = snprintf(out, out_size, "%s", rel_path);
} else {
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
}
if (n < 0 || (size_t)n >= out_size) return -1;
return 0;
}
static cJSON* tasks_create_empty_root_local(void) {
cJSON* root = cJSON_CreateObject();
cJSON* tasks = cJSON_CreateArray();
@@ -79,47 +56,118 @@ static const char* normalize_task_status_local(const char* status) {
return NULL;
}
static cJSON* tasks_load_root_local(const char* path) {
if (!path) return NULL;
static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext, char** out_ciphertext) {
if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
FILE* fp = fopen(path, "rb");
if (!fp) {
if (access(path, F_OK) == 0) {
return NULL;
}
const char* plain = plaintext ? plaintext : "";
size_t out_cap = (strlen(plain) * 4U) + 1024U;
char* ciphertext = (char*)malloc(out_cap);
if (!ciphertext) return -1;
int rc = nostr_nip44_encrypt(ctx->cfg->keys.private_key,
ctx->cfg->keys.public_key,
plain,
ciphertext,
out_cap);
if (rc != NOSTR_SUCCESS) {
free(ciphertext);
return -1;
}
*out_ciphertext = ciphertext;
return 0;
}
static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext, char** out_plaintext) {
if (!ctx || !ctx->cfg || !ciphertext || !out_plaintext) return -1;
size_t out_cap = strlen(ciphertext) + 1024U;
char* plaintext = (char*)malloc(out_cap);
if (!plaintext) return -1;
int rc = nostr_nip44_decrypt(ctx->cfg->keys.private_key,
ctx->cfg->keys.public_key,
ciphertext,
plaintext,
out_cap);
if (rc != NOSTR_SUCCESS) {
free(plaintext);
return -1;
}
*out_plaintext = plaintext;
return 0;
}
static int query_tasks_ciphertext_local(tools_context_t* ctx, char** out_ciphertext) {
if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
cJSON* d_vals = cJSON_CreateArray();
if (!filter || !kinds || !authors || !d_vals) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
cJSON_Delete(d_vals);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(TASKS_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddItemToArray(d_vals, cJSON_CreateString(TASKS_D_TAG));
cJSON_AddItemToObject(filter, "#d", d_vals);
cJSON_AddNumberToObject(filter, "limit", 1);
char* events_json = nostr_handler_query_json(filter, 4000);
cJSON_Delete(filter);
if (!events_json) {
return -1;
}
cJSON* arr = cJSON_Parse(events_json);
free(events_json);
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
cJSON_Delete(arr);
*out_ciphertext = strdup("");
return (*out_ciphertext != NULL) ? 0 : -1;
}
cJSON* ev = cJSON_GetArrayItem(arr, 0);
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
const char* cipher = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
*out_ciphertext = strdup(cipher);
cJSON_Delete(arr);
return (*out_ciphertext != NULL) ? 0 : -1;
}
static cJSON* tasks_load_root_nostr_local(tools_context_t* ctx) {
char* ciphertext = NULL;
if (query_tasks_ciphertext_local(ctx, &ciphertext) != 0) {
return NULL;
}
if (!ciphertext || ciphertext[0] == '\0') {
free(ciphertext);
return tasks_create_empty_root_local();
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return NULL;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
char* plaintext = NULL;
if (nip44_decrypt_self_local(ctx, ciphertext, &plaintext) != 0) {
free(ciphertext);
return NULL;
}
free(ciphertext);
char* buf = (char*)malloc((size_t)len + 1U);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)len, fp);
fclose(fp);
if (n != (size_t)len) {
free(buf);
return NULL;
}
buf[len] = '\0';
cJSON* root = cJSON_Parse(buf);
free(buf);
cJSON* root = cJSON_Parse(plaintext);
free(plaintext);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return NULL;
@@ -140,22 +188,45 @@ static cJSON* tasks_load_root_local(const char* path) {
return root;
}
static int tasks_save_root_local(const char* path, cJSON* root) {
if (!path || !root) return -1;
char* raw = cJSON_PrintUnformatted(root);
if (!raw) return -1;
static int tasks_save_root_nostr_local(tools_context_t* ctx, cJSON* root) {
if (!ctx || !ctx->cfg || !root) return -1;
FILE* fp = fopen(path, "wb");
if (!fp) {
free(raw);
char* plaintext = cJSON_PrintUnformatted(root);
if (!plaintext) return -1;
char* ciphertext = NULL;
if (nip44_encrypt_self_local(ctx, plaintext, &ciphertext) != 0) {
free(plaintext);
return -1;
}
free(plaintext);
cJSON* tags = cJSON_CreateArray();
cJSON* d_tag = cJSON_CreateArray();
cJSON* app_tag = cJSON_CreateArray();
if (!tags || !d_tag || !app_tag) {
cJSON_Delete(tags);
cJSON_Delete(d_tag);
cJSON_Delete(app_tag);
free(ciphertext);
return -1;
}
size_t len = strlen(raw);
size_t n = fwrite(raw, 1, len, fp);
fclose(fp);
free(raw);
return (n == len) ? 0 : -1;
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
cJSON_AddItemToArray(d_tag, cJSON_CreateString(TASKS_D_TAG));
cJSON_AddItemToArray(tags, d_tag);
cJSON_AddItemToArray(app_tag, cJSON_CreateString("app"));
cJSON_AddItemToArray(app_tag, cJSON_CreateString(TASKS_APP_TAG));
cJSON_AddItemToArray(tags, app_tag);
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(TASKS_KIND, ciphertext, tags, &publish_result);
cJSON_Delete(tags);
free(ciphertext);
nostr_handler_publish_result_free(&publish_result);
return rc == 0 ? 0 : -1;
}
static cJSON* task_find_by_id_local(cJSON* tasks, int id, int* out_index) {
@@ -184,16 +255,10 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
return json_error_local("task_manage requires string action");
}
char tasks_path[PATH_MAX];
if (build_tool_path_local(ctx, "tasks.json", tasks_path, sizeof(tasks_path)) != 0) {
cJSON_Delete(args);
return json_error_local("failed to resolve tasks file path");
}
cJSON* root = tasks_load_root_local(tasks_path);
cJSON* root = tasks_load_root_nostr_local(ctx);
if (!root) {
cJSON_Delete(args);
return json_error_local("failed to load tasks file");
return json_error_local("failed to load tasks memory");
}
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
@@ -405,10 +470,10 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
if (mutated) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id");
cJSON_AddNumberToObject(root, "next_id", next_id);
if (tasks_save_root_local(tasks_path, root) != 0) {
if (tasks_save_root_nostr_local(ctx, root) != 0) {
cJSON_Delete(args);
cJSON_Delete(root);
return json_error_local("failed to save tasks file");
return json_error_local("failed to persist tasks memory");
}
}
@@ -421,7 +486,6 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "action", action_s);
cJSON_AddStringToObject(out, "path", tasks_path);
cJSON_AddBoolToObject(out, "mutated", mutated ? 1 : 0);
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(tasks));
+1 -1
View File
@@ -1,7 +1,7 @@
#ifndef DIDACTYL_TOOLS_H
#define DIDACTYL_TOOLS_H
#include "config.h"
#include "../config.h"
struct trigger_manager;
+9 -3
View File
@@ -687,6 +687,9 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON* p_skill_create_llm = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_create_llm, "type", "string");
cJSON_AddStringToObject(p_skill_create_llm,
"description",
"Model to use when this trigger fires (e.g. 'gpt-4o-mini'); overrides the agent default model for this triggered skill execution only.");
cJSON_AddItemToObject(t22_props, "llm", p_skill_create_llm);
cJSON* p_skill_create_tools = cJSON_CreateObject();
@@ -840,6 +843,9 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON* p_skill_edit_llm = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_llm, "type", "string");
cJSON_AddStringToObject(p_skill_edit_llm,
"description",
"Model to use when this trigger fires (e.g. 'gpt-4o-mini'); overrides the agent default model for this triggered skill execution only.");
cJSON_AddItemToObject(t25b_props, "llm", p_skill_edit_llm);
cJSON* p_skill_edit_tools = cJSON_CreateObject();
@@ -976,7 +982,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddStringToObject(t31, "type", "function");
cJSON_AddStringToObject(t31_fn, "name", "model_set");
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to config.jsonc");
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)");
cJSON_AddStringToObject(t31_params, "type", "object");
cJSON_AddItemToObject(t31_params, "properties", t31_props);
@@ -1276,7 +1282,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddStringToObject(t42, "type", "function");
cJSON_AddStringToObject(t42_fn, "name", "task_list");
cJSON_AddStringToObject(t42_fn, "description", "Build current task list context block from tasks.json");
cJSON_AddStringToObject(t42_fn, "description", "Build current task list context block from agent task memory on Nostr");
cJSON_AddStringToObject(t42_params, "type", "object");
cJSON_AddItemToObject(t42_params, "properties", t42_props);
cJSON_AddItemToObject(t42_fn, "parameters", t42_params);
@@ -1305,7 +1311,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddStringToObject(t45, "type", "function");
cJSON_AddStringToObject(t45_fn, "name", "task_manage");
cJSON_AddStringToObject(t45_fn, "description", "Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)");
cJSON_AddStringToObject(t45_fn, "description", "Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace");
cJSON_AddStringToObject(t45_params, "type", "object");
cJSON_AddItemToObject(t45_params, "properties", t45_props);
cJSON_AddItemToObject(t45_params, "required", t45_required);