v0.0.82 - Move model/task persistence to kind 30078, add finish_reason stall diagnostics, and remove local config/tasks file coupling

This commit is contained in:
Your Name
2026-03-18 15:58:33 -04:00
parent 253aa4b5fc
commit c221104bc0
11 changed files with 432 additions and 193 deletions

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. 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.81 ## Current Status — v0.0.82
**Active build — this project is barely working. Experiment at your own risk.** **Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.81Add loop progress controls and admin limit diagnostics for DM, HTTP API, and triggers > Last release update: v0.0.82Move model/task persistence to kind 30078, add finish_reason stall diagnostics, and remove local config/tasks file coupling
- Connects to configured relays with auto-reconnect and relay state transition logging - Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected - Publishes configured startup events per relay as each relay becomes connected

View File

@@ -4,16 +4,23 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_BINARY="$SCRIPT_DIR/didactyl_static_x86_64" 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 if ! command -v rsync >/dev/null 2>&1; then
echo "ERROR: rsync is not installed or not in PATH" echo "ERROR: rsync is not installed or not in PATH"
exit 1 exit 1
fi fi
if [ ! -f "$SOURCE_BINARY" ]; then if ! command -v ssh >/dev/null 2>&1; then
echo "ERROR: Source binary not found: $SOURCE_BINARY" echo "ERROR: ssh is not installed or not in PATH"
echo "Build it first so didactyl_static_x86_64 exists."
exit 1 exit 1
fi fi
@@ -25,6 +32,20 @@ if [ ! -f "$SOURCE_BINARY" ]; then
exit 1 exit 1
fi fi
echo "Deploying $SOURCE_BINARY to $REMOTE_TARGET" REMOTE_STAGE="/tmp/didactyl_static_x86_64"
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_TARGET"
echo "Deployment complete." 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."

View File

@@ -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

View File

@@ -106,12 +106,15 @@ static void notify_admin_limit_diagnostic(const char* source,
int turns_run, int turns_run,
int stall_repeat_threshold, int stall_repeat_threshold,
int repeated_tool_turns, int repeated_tool_turns,
int current_max_tokens,
const char* possible_cause,
const char* hint,
const char* final_answer) { const char* final_answer) {
if (!g_cfg || g_cfg->admin.pubkey[0] == '\0') { if (!g_cfg || g_cfg->admin.pubkey[0] == '\0') {
return; return;
} }
char diag[1400]; char diag[1800];
snprintf(diag, snprintf(diag,
sizeof(diag), sizeof(diag),
"⚠️ Didactyl limit diagnostic\n" "⚠️ Didactyl limit diagnostic\n"
@@ -122,6 +125,9 @@ static void notify_admin_limit_diagnostic(const char* source,
"turns_run=%d\n" "turns_run=%d\n"
"stall_repeat_threshold=%d\n" "stall_repeat_threshold=%d\n"
"repeated_tool_turns=%d\n" "repeated_tool_turns=%d\n"
"current_max_tokens=%d\n"
"possible_cause=%s\n"
"hint=%s\n"
"final_answer_preview=%.*s", "final_answer_preview=%.*s",
source ? source : "unknown", source ? source : "unknown",
reason ? reason : "unknown", reason ? reason : "unknown",
@@ -130,6 +136,9 @@ static void notify_admin_limit_diagnostic(const char* source,
turns_run, turns_run,
stall_repeat_threshold, stall_repeat_threshold,
repeated_tool_turns, repeated_tool_turns,
current_max_tokens,
possible_cause ? possible_cause : "n/a",
hint ? hint : "n/a",
360, 360,
final_answer ? final_answer : ""); final_answer ? final_answer : "");
@@ -1940,6 +1949,9 @@ void agent_on_trigger(const char* skill_d_tag,
turns_run, turns_run,
g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3, g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3,
0, 0,
g_cfg->llm.max_tokens,
NULL,
NULL,
""); "");
} }
@@ -2174,6 +2186,7 @@ void agent_on_message(const char* sender_pubkey_hex,
uint64_t last_tool_fp = 0; uint64_t last_tool_fp = 0;
int repeated_tool_turns = 0; int repeated_tool_turns = 0;
int exited_on_stall = 0; int exited_on_stall = 0;
int stall_due_to_length = 0;
char* final_answer_owned = NULL; char* final_answer_owned = NULL;
int turns_run = 0; int turns_run = 0;
@@ -2216,6 +2229,7 @@ void agent_on_message(const char* sender_pubkey_hex,
if (repeated_tool_turns >= stall_repeat_threshold) { if (repeated_tool_turns >= stall_repeat_threshold) {
exited_on_stall = 1; exited_on_stall = 1;
stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
llm_response_free(&resp); llm_response_free(&resp);
break; break;
} }
@@ -2287,6 +2301,9 @@ void agent_on_message(const char* sender_pubkey_hex,
turns_run, turns_run,
stall_repeat_threshold, stall_repeat_threshold,
repeated_tool_turns, 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); final_answer);
} }
fprintf(stdout, "[didactyl] final response: %.240s%s\n", fprintf(stdout, "[didactyl] final response: %.240s%s\n",

View File

@@ -119,12 +119,15 @@ static void notify_admin_limit_diagnostic_http(const char* source,
int turns_run, int turns_run,
int stall_repeat_threshold, int stall_repeat_threshold,
int repeated_tool_turns, int repeated_tool_turns,
int current_max_tokens,
const char* possible_cause,
const char* hint,
const char* final_answer) { const char* final_answer) {
if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') { if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') {
return; return;
} }
char diag[1400]; char diag[1800];
snprintf(diag, snprintf(diag,
sizeof(diag), sizeof(diag),
"⚠️ Didactyl limit diagnostic\n" "⚠️ Didactyl limit diagnostic\n"
@@ -135,6 +138,9 @@ static void notify_admin_limit_diagnostic_http(const char* source,
"turns_run=%d\n" "turns_run=%d\n"
"stall_repeat_threshold=%d\n" "stall_repeat_threshold=%d\n"
"repeated_tool_turns=%d\n" "repeated_tool_turns=%d\n"
"current_max_tokens=%d\n"
"possible_cause=%s\n"
"hint=%s\n"
"final_answer_preview=%.*s", "final_answer_preview=%.*s",
source ? source : "unknown", source ? source : "unknown",
reason ? reason : "unknown", reason ? reason : "unknown",
@@ -143,6 +149,9 @@ static void notify_admin_limit_diagnostic_http(const char* source,
turns_run, turns_run,
stall_repeat_threshold, stall_repeat_threshold,
repeated_tool_turns, repeated_tool_turns,
current_max_tokens,
possible_cause ? possible_cause : "n/a",
hint ? hint : "n/a",
360, 360,
final_answer ? final_answer : ""); final_answer ? final_answer : "");
@@ -921,6 +930,7 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
uint64_t last_tool_fp = 0; uint64_t last_tool_fp = 0;
int repeated_tool_turns = 0; int repeated_tool_turns = 0;
int exited_on_stall = 0; int exited_on_stall = 0;
int stall_due_to_length = 0;
int turns_run = 0; int turns_run = 0;
for (int turn = 0; turn < max_turns; turn++) { for (int turn = 0; turn < max_turns; turn++) {
@@ -965,6 +975,7 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
if (repeated_tool_turns >= stall_repeat_threshold) { if (repeated_tool_turns >= stall_repeat_threshold) {
exited_on_stall = 1; exited_on_stall = 1;
stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
llm_response_free(&resp); llm_response_free(&resp);
break; break;
} }
@@ -1042,6 +1053,9 @@ done:;
turns_run, turns_run,
stall_repeat_threshold, stall_repeat_threshold,
repeated_tool_turns, 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 : ""); final_response ? final_response : "");
} }

View File

@@ -289,6 +289,11 @@ static int parse_llm_response(const char* json, llm_response_t* out) {
return -1; 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"); cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
if (content && cJSON_IsString(content) && content->valuestring) { if (content && cJSON_IsString(content) && content->valuestring) {
out->content = strdup(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) { void llm_response_free(llm_response_t* response) {
if (!response) return; if (!response) return;
free(response->content); free(response->content);
free(response->finish_reason);
for (int i = 0; i < response->tool_call_count; i++) { for (int i = 0; i < response->tool_call_count; i++) {
free(response->tool_calls[i].id); free(response->tool_calls[i].id);
free(response->tool_calls[i].name); free(response->tool_calls[i].name);

View File

@@ -13,6 +13,7 @@ typedef struct {
char* content; char* content;
llm_tool_call_t* tool_calls; llm_tool_call_t* tool_calls;
int tool_call_count; int tool_call_count;
char* finish_reason;
} llm_response_t; } llm_response_t;
int llm_init(const llm_config_t* config); int llm_init(const llm_config_t* config);

View File

@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0 #define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 81 #define DIDACTYL_VERSION_PATCH 82
#define DIDACTYL_VERSION "v0.0.81" #define DIDACTYL_VERSION "v0.0.82"
// Agent metadata // Agent metadata
#define DIDACTYL_NAME "Didactyl" #define DIDACTYL_NAME "Didactyl"

View File

@@ -30,113 +30,72 @@ static cJSON* parse_args_local(const char* args_json) {
return args; return args;
} }
static int json_object_set_string(cJSON* obj, const char* key, const char* value) { static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cfg, char** out_error) {
if (!obj || !key || !value) return -1; if (!ctx || !ctx->cfg || !cfg) {
cJSON_DeleteItemFromObjectCaseSensitive(obj, key); if (out_error) *out_error = strdup("invalid persist context");
cJSON* v = cJSON_CreateString(value); return -1;
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;
} }
long len = ftell(fp); if (out_error) *out_error = NULL;
if (len < 0) {
fclose(fp); cJSON* args = cJSON_CreateObject();
return NULL; 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) { cJSON_AddStringToObject(args, "d_tag", "llm_config");
fclose(fp); cJSON_AddStringToObject(content, "provider", cfg->provider);
return NULL; 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); char* store_result = execute_config_store(ctx, store_args_json);
if (!buf) { free(store_args_json);
fclose(fp); if (!store_result) {
return NULL; if (out_error) *out_error = strdup("config_store returned no response");
return -1;
} }
size_t n = fread(buf, 1, (size_t)len, fp); cJSON* root = cJSON_Parse(store_result);
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);
if (!root || !cJSON_IsObject(root)) { if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root); cJSON_Delete(root);
if (out_error) *out_error = strdup("config_store returned invalid JSON");
free(store_result);
return -1; return -1;
} }
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm"); cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
if (!llm || !cJSON_IsObject(llm)) { if (!success || !cJSON_IsBool(success) || !cJSON_IsTrue(success)) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "llm"); cJSON* err = cJSON_GetObjectItemCaseSensitive(root, "error");
llm = cJSON_CreateObject(); if (out_error) {
if (!llm) { if (err && cJSON_IsString(err) && err->valuestring) {
cJSON_Delete(root); *out_error = strdup(err->valuestring);
return -1; } 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); cJSON_Delete(root);
free(store_result);
return -1; 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); cJSON_Delete(root);
if (!out) return -1; free(store_result);
return 0;
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;
} }
static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* changed) { static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* changed) {
@@ -231,9 +190,9 @@ char* execute_model_set(tools_context_t* ctx, const char* args_json) {
} }
ctx->cfg->llm = cfg; 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(); cJSON* out = cJSON_CreateObject();
if (!out) return NULL; if (!out) return NULL;
@@ -243,9 +202,14 @@ char* execute_model_set(tools_context_t* ctx, const char* args_json) {
cJSON_AddStringToObject(out, "base_url", cfg.base_url); cJSON_AddStringToObject(out, "base_url", cfg.base_url);
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens); cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
cJSON_AddNumberToObject(out, "temperature", cfg.temperature); 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); char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out); cJSON_Delete(out);
free(persist_error);
return json; return json;
} }

View File

@@ -2,14 +2,18 @@
#include "tools_internal.h" #include "tools_internal.h"
#include <limits.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#include "cjson/cJSON.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) { static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject(); cJSON* root = cJSON_CreateObject();
@@ -31,33 +35,6 @@ static cJSON* parse_args_local(const char* args_json) {
return args; 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) { static cJSON* tasks_create_empty_root_local(void) {
cJSON* root = cJSON_CreateObject(); cJSON* root = cJSON_CreateObject();
cJSON* tasks = cJSON_CreateArray(); cJSON* tasks = cJSON_CreateArray();
@@ -79,47 +56,118 @@ static const char* normalize_task_status_local(const char* status) {
return NULL; return NULL;
} }
static cJSON* tasks_load_root_local(const char* path) { static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext, char** out_ciphertext) {
if (!path) return NULL; if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
FILE* fp = fopen(path, "rb"); const char* plain = plaintext ? plaintext : "";
if (!fp) { size_t out_cap = (strlen(plain) * 4U) + 1024U;
if (access(path, F_OK) == 0) { char* ciphertext = (char*)malloc(out_cap);
return NULL; 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(); return tasks_create_empty_root_local();
} }
if (fseek(fp, 0, SEEK_END) != 0) { char* plaintext = NULL;
fclose(fp); if (nip44_decrypt_self_local(ctx, ciphertext, &plaintext) != 0) {
return NULL; free(ciphertext);
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return NULL;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NULL; return NULL;
} }
free(ciphertext);
char* buf = (char*)malloc((size_t)len + 1U); cJSON* root = cJSON_Parse(plaintext);
if (!buf) { free(plaintext);
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);
if (!root || !cJSON_IsObject(root)) { if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root); cJSON_Delete(root);
return NULL; return NULL;
@@ -140,22 +188,45 @@ static cJSON* tasks_load_root_local(const char* path) {
return root; return root;
} }
static int tasks_save_root_local(const char* path, cJSON* root) { static int tasks_save_root_nostr_local(tools_context_t* ctx, cJSON* root) {
if (!path || !root) return -1; if (!ctx || !ctx->cfg || !root) return -1;
char* raw = cJSON_PrintUnformatted(root);
if (!raw) return -1;
FILE* fp = fopen(path, "wb"); char* plaintext = cJSON_PrintUnformatted(root);
if (!fp) { if (!plaintext) return -1;
free(raw);
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; return -1;
} }
size_t len = strlen(raw); cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
size_t n = fwrite(raw, 1, len, fp); cJSON_AddItemToArray(d_tag, cJSON_CreateString(TASKS_D_TAG));
fclose(fp); cJSON_AddItemToArray(tags, d_tag);
free(raw);
return (n == len) ? 0 : -1; 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) { 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"); return json_error_local("task_manage requires string action");
} }
char tasks_path[PATH_MAX]; cJSON* root = tasks_load_root_nostr_local(ctx);
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);
if (!root) { if (!root) {
cJSON_Delete(args); 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"); cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
@@ -405,10 +470,10 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
if (mutated) { if (mutated) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id"); cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id");
cJSON_AddNumberToObject(root, "next_id", 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(args);
cJSON_Delete(root); 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_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "action", action_s); cJSON_AddStringToObject(out, "action", action_s);
cJSON_AddStringToObject(out, "path", tasks_path);
cJSON_AddBoolToObject(out, "mutated", mutated ? 1 : 0); cJSON_AddBoolToObject(out, "mutated", mutated ? 1 : 0);
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(tasks)); cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(tasks));

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* p_skill_create_llm = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_create_llm, "type", "string"); 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_AddItemToObject(t22_props, "llm", p_skill_create_llm);
cJSON* p_skill_create_tools = cJSON_CreateObject(); 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* p_skill_edit_llm = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_edit_llm, "type", "string"); 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_AddItemToObject(t25b_props, "llm", p_skill_edit_llm);
cJSON* p_skill_edit_tools = cJSON_CreateObject(); 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, "type", "function");
cJSON_AddStringToObject(t31_fn, "name", "model_set"); 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_AddStringToObject(t31_params, "type", "object");
cJSON_AddItemToObject(t31_params, "properties", t31_props); 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, "type", "function");
cJSON_AddStringToObject(t42_fn, "name", "task_list"); 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_AddStringToObject(t42_params, "type", "object");
cJSON_AddItemToObject(t42_params, "properties", t42_props); cJSON_AddItemToObject(t42_params, "properties", t42_props);
cJSON_AddItemToObject(t42_fn, "parameters", t42_params); 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, "type", "function");
cJSON_AddStringToObject(t45_fn, "name", "task_manage"); 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_AddStringToObject(t45_params, "type", "object");
cJSON_AddItemToObject(t45_params, "properties", t45_props); cJSON_AddItemToObject(t45_params, "properties", t45_props);
cJSON_AddItemToObject(t45_params, "required", t45_required); cJSON_AddItemToObject(t45_params, "required", t45_required);