diff --git a/README.md b/README.md index 9aa7658..5e0a17b 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.2.12 +## Current Status — v0.2.13 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.12 — Fix nostr_core_lib EOSE_FIRST semantics and relay pool subscription cleanup +> Last release update: v0.2.13 — Unify startup skills, rename identity skill, add dm_history text format, and enforce startup publish success - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/docs/GENESIS.md b/docs/GENESIS.md index a8f4607..88e1415 100644 --- a/docs/GENESIS.md +++ b/docs/GENESIS.md @@ -6,7 +6,7 @@ See also: [CONTEXT.md](CONTEXT.md) · [SKILLS.md](SKILLS.md) · [README.md](../R `genesis.jsonc` is the first-run bootstrap document for a Didactyl agent. -It defines initial identity, admin policy, startup events, default skill trigger behavior, and baseline runtime settings so the agent can publish itself onto Nostr. +It defines initial identity, admin policy, startup events (including startup skills), and baseline runtime settings so the agent can publish itself onto Nostr. After bootstrap, the long-term direction is **nsec-only startup** with state recovered from Nostr events. @@ -30,7 +30,7 @@ Typical optional sections: - `security` - `admin_context` - `api` -- `default_skill` (published as private kind `31124`, default `d=default_admin_dm`) +- Startup skills in `startup_events` (typically private kind `31124`, e.g. `d=identity_and_rules` and `d=dm_history`) --- @@ -121,10 +121,11 @@ That relay list is used as the initial network attachment for querying existing ## Migration Notes (v0.2.0) - Legacy `config.jsonc` and template-DSL context files have been removed from the active startup model. -- Default DM handling now relies on `default_skill` with `d_tag: default_admin_dm` and trigger tags: - - `["trigger", "dm"]` - - `["filter", "{\"from\":\"admin\"}"]` -- Default skill content is plain markdown with inline variables such as `{{my_kind0_profile}}` and `{{my_npub}}`. +- Startup skills are defined directly in `startup_events` as kind `31123`/`31124` events. +- Typical default DM stack is two startup skills with tags: + - `d=identity_and_rules` with `trigger=dm` and `filter={"from":"admin"}` + - `d=dm_history` with `trigger=dm` and `filter={"from":"admin"}` +- Skill content is plain markdown with inline variables such as `{{my_kind0_profile}}`, `{{my_npub}}`, and `{{nostr_dm_history({"format":"text","limit":12})}}`. --- diff --git a/genesis.jsonc.example b/genesis.jsonc.example index ea15f61..0530353 100644 --- a/genesis.jsonc.example +++ b/genesis.jsonc.example @@ -61,20 +61,30 @@ ["r", "wss://relay.primal.net"] ] } - ], - - // ─── Default Skill ──────────────────────────────────────────────── - // Keep this generic and non-sensitive for repository examples. - "default_skill": { - "d_tag": "default_admin_dm", - "kind": 31124, - "content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone", - "tags": [ - ["app", "didactyl"], - ["scope", "private"], - ["description", "Default admin DM handler"], - ["trigger", "dm"], - ["filter", "{\"from\":\"admin\"}"] - ] - } + }, + { + "kind": 31124, + "content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone", + "tags": [ + ["d", "identity_and_rules"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "Agent identity and behavioral rules"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + }, + { + "kind": 31124, + "content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}", + "tags": [ + ["d", "dm_history"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "DM conversation history for context continuity"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + } + ] } diff --git a/plans/dm_history_context_cleanup.md b/plans/dm_history_context_cleanup.md new file mode 100644 index 0000000..f24afa7 --- /dev/null +++ b/plans/dm_history_context_cleanup.md @@ -0,0 +1,246 @@ +# DM History Context Cleanup, Skill Rename & Startup Events Unification + +## Problems + +### 1. DM History as JSON in System Prompt +The `{{nostr_dm_history}}` template variable resolves to a raw JSON array string pasted into the system prompt. The LLM receives conversation history as JSON-within-text, wasting tokens and forcing it to parse structured data embedded in prose. + +### 2. Misleading Skill Name +The skill `default_admin_dm` is really an identity and rules skill. Rename to `identity_and_rules`. + +### 3. `default_skill` Config Section Is Redundant +The `default_skill` section in genesis.jsonc is a special-case config path that converts a skill into a startup event and auto-adopts it. This is unnecessary — skills should just be startup events like everything else. The program should treat all startup events equally and halt if any fail to publish. + +## Solution + +Three coordinated changes: + +### Part A: DM History Text Format +Add a `format` parameter to the `nostr_dm_history` tool. Create a separate `dm_history` skill. + +### Part B: Skill Rename +Rename `default_admin_dm` → `identity_and_rules` everywhere. + +### Part C: Eliminate `default_skill` Config Section +Move skills into `startup_events`. Auto-adopt kind 31123/31124 startup events. Make startup event publishing mandatory (halt on failure). + +--- + +## Implementation Steps + +### Part A: DM History Text Format + +#### A1. Add `format` parameter to `execute_nostr_dm_history()` + +**File:** `src/tools/tool_agent.c` — line 265 + +Add a `format` parameter: `"json"` (default, current behavior) or `"text"`. + +When `format` is `"text"`, after the filtering loop (lines 320-360), iterate the filtered array and build plain text: + +``` +User: Good morning +Assistant: Good morning! ☀️ How can I help you today? +User: What is the capital of England? +Assistant: London! 🇬🇧 +``` + +Existing `include_current=false` default already deduplicates the current live message (line 339). + +#### A2. Add `format` to the tool schema + +**File:** `src/tools/tools_schema.c` — around line 1493 + +Add `format` property with description. Update tool description to mention the parameter. + +--- + +### Part B: Skill Rename + +#### B1. Update `src/default_events.h` + +- `DIDACTYL_DEFAULT_SKILL_D_TAG`: `"default_admin_dm"` → `"identity_and_rules"` +- `DIDACTYL_DEFAULT_SKILL_TAGS_JSON`: update d-tag and description +- `DIDACTYL_DEFAULT_SKILL_TEMPLATE`: keep as-is (already clean, no `{{nostr_dm_history}}`) + +#### B2. Update tool description examples in `src/tools/tools_schema.c` + +- Line 854: `skill_get` example +- Line 883: `skill_view` example +- Line 911: `skill_set` example + +#### B3. Update `docs/GENESIS.md` + +- Line 33 and 124: `default_admin_dm` → `identity_and_rules` + +--- + +### Part C: Eliminate `default_skill` Config Section + +#### C1. Auto-adopt kind 31123/31124 startup events + +**File:** `src/config.c` + +Replace `config_ensure_default_skill_startup_events()` with a new function `config_ensure_startup_skill_adoption()` that: + +1. Scans all startup events for kind 31123 and 31124 +2. For each skill found, extracts its `d_tag` from tags +3. Builds the skill address (`kind:pubkey:d_tag`) +4. Ensures a kind 10123 adoption list startup event exists with all skill addresses +5. If no 10123 event exists, creates one + +This replaces the single-skill logic with a generic "adopt all startup skills" approach. + +#### C2. Remove `default_skill` config parsing + +**File:** `src/config.c` + +- Remove `parse_default_skill_config()` function (lines 733-816) +- Remove the call at line 1369 +- Keep backward compatibility: if `default_skill` section exists in genesis.jsonc, convert it to a startup event during parsing (migration path) — OR just remove support and let users update their genesis.jsonc + +#### C3. Remove `default_skill_config_t` from config struct + +**File:** `src/config.h` + +- Remove `default_skill_config_t` typedef (lines 67-72) +- Remove `default_skill` field from `didactyl_config_t` (line 134) + +#### C4. Update `src/main.c` + +- Line 1060: Replace `config_ensure_default_skill_startup_events()` call with `config_ensure_startup_skill_adoption()` + +#### C5. Make startup event publishing mandatory + +**File:** `src/nostr_handler.c` + +In the startup publish flow (around line 3150), after all relays have been tried: +- Check that each startup event was published to at least one relay +- If any event failed on all relays, log an error and return failure +- The caller in `main.c` should halt on this failure + +#### C6. Update setup wizard + +**File:** `src/setup_wizard.c` + +- `configure_default_skill_for_agent()` (line 498): Instead of populating `cfg->default_skill`, append the identity skill as a startup event directly +- Add the `dm_history` skill as a second startup event +- The auto-adoption logic (C1) handles the adoption list + +#### C7. Update `default_events.h` + +Add the `dm_history` skill template: + +```c +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_D_TAG "dm_history" + +static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE = + "## Recent Conversation\n\n" + "{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}"; +``` + +--- + +### Part D: Genesis Config Updates + +#### D1. Update `genesis.jsonc.example` + +Remove `default_skill` section. Add skills as startup events: + +```jsonc +"startup_events": [ + { + "kind": 0, + "content_fields": { + "name": "Didactyl", + "about": "I am a Didactyl agent living on Nostr" + }, + "tags": [] + }, + { + "kind": 3, + "content": "", + "tags": [["p", "ADMIN_HEX_PUBKEY"]] + }, + { + "kind": 10002, + "content": "", + "tags": [ + ["r", "wss://relay.damus.io"], + ["r", "wss://relay.primal.net"] + ] + }, + { + "kind": 31124, + "content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone", + "tags": [ + ["d", "identity_and_rules"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "Agent identity and behavioral rules"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + }, + { + "kind": 31124, + "content": "## Recent Conversation\n\n{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}", + "tags": [ + ["d", "dm_history"], + ["app", "didactyl"], + ["scope", "private"], + ["description", "DM conversation history for context continuity"], + ["trigger", "dm"], + ["filter", "{\"from\":\"admin\"}"] + ] + } + ] +``` + +#### D2. Update live `genesis.jsonc` + +Same changes as D1 but for the actual deployment config. + +--- + +## Result + +### Before + +```json +[ + {"role": "system", "content": "You are {...}\n\nYour npub: npub1...\n\n## Rules\n...\n[{\"role\":\"user\",\"content\":\"Good morning\",...}]"}, + {"role": "user", "content": "And Fiji?"} +] +``` + +### After + +```json +[ + {"role": "system", "content": "# Didactyl Agent\n\nYou are {...}\n\nYour npub: npub1...\n\n## Rules\n...\n\n---\n\n## Recent Conversation\n\nUser: Good morning\nAssistant: Good morning! ☀️ How can I help you today?\nUser: What is the capital of England?\nAssistant: London! 🇬🇧\nUser: How about France?\nAssistant: Paris! 🇫🇷"}, + {"role": "user", "content": "And Fiji?"} +] +``` + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `src/tools/tool_agent.c` | Add `format` parameter to `execute_nostr_dm_history()` | +| `src/tools/tools_schema.c` | Add `format` to schema; update example d-tags | +| `src/default_events.h` | Rename d-tag; add `dm_history` skill template | +| `src/config.h` | Remove `default_skill_config_t` and field | +| `src/config.c` | Remove `parse_default_skill_config()`; replace `config_ensure_default_skill_startup_events()` with `config_ensure_startup_skill_adoption()` | +| `src/main.c` | Update call to new adoption function | +| `src/nostr_handler.c` | Make startup event publishing mandatory (halt on total failure) | +| `src/setup_wizard.c` | Put skills in startup_events; add dm_history | +| `genesis.jsonc.example` | Remove `default_skill`; add skills as startup events | +| `genesis.jsonc` | Same as example | +| `docs/GENESIS.md` | Update references | + +## Migration Note + +For backward compatibility, consider keeping `parse_default_skill_config()` as a migration shim that converts `default_skill` into a startup event with a deprecation warning. This way existing genesis.jsonc files with `default_skill` still work but users are prompted to update. diff --git a/src/agent.c b/src/agent.c index b44ee78..9d0a5d0 100644 --- a/src/agent.c +++ b/src/agent.c @@ -778,7 +778,9 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa size_t tool_len = (size_t)(p - (message + 1)); if (tool_len == 0 || tool_len >= 128U) { - (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "{\"success\":false,\"error\":\"invalid slash command\"}"); + (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, + "{\"success\":false,\"error\":\"invalid slash command\"}", + DIDACTYL_DM_HISTORY_TOOL_RESPONSE); return 1; } @@ -835,7 +837,9 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa free(log_payload); } - (void)nostr_handler_send_dm_auto(sender_pubkey_hex, dm_payload ? dm_payload : "Command executed with no output."); + (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, + dm_payload ? dm_payload : "Command executed with no output.", + DIDACTYL_DM_HISTORY_TOOL_RESPONSE); free(markdown_result); free(result_json); return 1; @@ -2039,12 +2043,16 @@ void agent_on_message(const char* sender_pubkey_hex, } int allow_tools = (tier == DIDACTYL_SENDER_ADMIN) && g_cfg->tools.enabled && g_cfg->security.admin.tools_enabled; + int inbound_role = (message[0] == '/') ? DIDACTYL_DM_HISTORY_TOOL_REQUEST : DIDACTYL_DM_HISTORY_USER; + (void)nostr_handler_dm_history_remember(sender_pubkey_hex, message, (didactyl_dm_history_role_t)inbound_role); if (message[0] == '/') { if (!allow_tools) { const char* denied = "{\"success\":false,\"error\":\"slash commands are disabled for this sender tier\"}"; append_context_log(sender_pubkey_hex, "direct_tool_exec", denied); - (void)nostr_handler_send_dm_auto(sender_pubkey_hex, denied); + (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex, + denied, + DIDACTYL_DM_HISTORY_TOOL_RESPONSE); return; } if (handle_slash_command(sender_pubkey_hex, message)) { diff --git a/src/config.c b/src/config.c index 670a981..562acc0 100644 --- a/src/config.c +++ b/src/config.c @@ -692,6 +692,9 @@ static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* ta return 0; } +static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag); +static int append_startup_event(didactyl_config_t* config, int kind, const char* content, const char* tags_json); + static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) { if (!item || !tags || !cJSON_IsArray(tags)) { return 0; @@ -730,7 +733,7 @@ static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) { return set_tag_value_string(tags, "d", d_tag); } -static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) { +static int parse_legacy_default_skill_to_startup_events(cJSON* root, didactyl_config_t* config) { if (!root || !config) { return -1; } @@ -746,19 +749,20 @@ static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) { cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(def, "content_fields"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(def, "tags"); - if (!kind || !cJSON_IsNumber(kind)) { + if (!kind || !cJSON_IsNumber(kind) || !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') { return -1; } + int kind_i = (int)kind->valuedouble; if (kind_i != 31123 && kind_i != 31124) { return -1; } - if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') { - return -1; - } - if (strlen(d_tag->valuestring) >= sizeof(config->default_skill.d_tag)) { - return -1; + for (int i = 0; i < config->startup_event_count; i++) { + startup_event_t* se = &config->startup_events[i]; + if (se->kind == kind_i && startup_event_has_d_tag(se, d_tag->valuestring)) { + return 0; + } } char* content_text = NULL; @@ -806,13 +810,10 @@ static int parse_default_skill_config(cJSON* root, didactyl_config_t* config) { return -1; } - free(config->default_skill.content); - free(config->default_skill.tags_json); - config->default_skill.kind = kind_i; - snprintf(config->default_skill.d_tag, sizeof(config->default_skill.d_tag), "%s", d_tag->valuestring); - config->default_skill.content = content_text; - config->default_skill.tags_json = tags_json; - return 0; + int rc = append_startup_event(config, kind_i, content_text, tags_json); + free(content_text); + free(tags_json); + return rc; } static int parse_startup_events(cJSON* root, didactyl_config_t* config) { @@ -1094,42 +1095,48 @@ static int append_startup_event(didactyl_config_t* config, int kind, const char* return 0; } -int config_ensure_default_skill_startup_events(didactyl_config_t* config) { - if (!config || !config->default_skill.content || config->default_skill.content[0] == '\0' || - config->default_skill.d_tag[0] == '\0') { +int config_ensure_startup_skill_adoption(didactyl_config_t* config) { + if (!config || config->keys.public_key_hex[0] == '\0') { return 0; } - int has_default_skill_event = 0; + cJSON* needed_addrs = cJSON_CreateArray(); + if (!needed_addrs) { + return -1; + } + for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; - if (se->kind == config->default_skill.kind && startup_event_has_d_tag(se, config->default_skill.d_tag)) { - has_default_skill_event = 1; - break; + if (!se || (se->kind != 31123 && se->kind != 31124) || !se->tags_json) { + continue; } + + cJSON* tags = cJSON_Parse(se->tags_json); + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(tags); + continue; + } + + cJSON* d_val = find_tag_value_string(tags, "d"); + if (d_val && cJSON_IsString(d_val) && d_val->valuestring && d_val->valuestring[0] != '\0') { + char addr[256]; + snprintf(addr, + sizeof(addr), + "%d:%s:%s", + se->kind, + config->keys.public_key_hex, + d_val->valuestring); + cJSON_AddItemToArray(needed_addrs, cJSON_CreateString(addr)); + } + + cJSON_Delete(tags); } - if (!has_default_skill_event) { - if (append_startup_event(config, - config->default_skill.kind, - config->default_skill.content, - config->default_skill.tags_json ? config->default_skill.tags_json : "[]") != 0) { - return -1; - } - } - - if (config->keys.public_key_hex[0] == '\0') { + if (cJSON_GetArraySize(needed_addrs) == 0) { + cJSON_Delete(needed_addrs); return 0; } - char addr[256]; - snprintf(addr, - sizeof(addr), - "%d:%s:%s", - config->default_skill.kind, - config->keys.public_key_hex, - config->default_skill.d_tag); - for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; if (se->kind != 10123 || !se->tags_json) { @@ -1142,35 +1149,49 @@ int config_ensure_default_skill_startup_events(didactyl_config_t* config) { continue; } - if (adoption_tags_has_addr(tags, addr)) { - cJSON_Delete(tags); - return 0; - } - - if (append_adoption_addr_tag(tags, addr) != 0) { - cJSON_Delete(tags); - return -1; + int needed_n = cJSON_GetArraySize(needed_addrs); + for (int j = 0; j < needed_n; j++) { + cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j); + if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') { + continue; + } + if (!adoption_tags_has_addr(tags, addr_j->valuestring) && append_adoption_addr_tag(tags, addr_j->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(needed_addrs); + return -1; + } } char* updated = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); if (!updated) { + cJSON_Delete(needed_addrs); return -1; } free(se->tags_json); se->tags_json = updated; + cJSON_Delete(needed_addrs); return 0; } cJSON* tags = cJSON_CreateArray(); if (!tags) { + cJSON_Delete(needed_addrs); return -1; } - if (append_adoption_addr_tag(tags, addr) != 0) { - cJSON_Delete(tags); - return -1; + int needed_n = cJSON_GetArraySize(needed_addrs); + for (int j = 0; j < needed_n; j++) { + cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j); + if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') { + continue; + } + if (append_adoption_addr_tag(tags, addr_j->valuestring) != 0) { + cJSON_Delete(tags); + cJSON_Delete(needed_addrs); + return -1; + } } cJSON* app = cJSON_CreateArray(); @@ -1179,17 +1200,19 @@ int config_ensure_default_skill_startup_events(didactyl_config_t* config) { cJSON_Delete(app); cJSON_Delete(scope); cJSON_Delete(tags); + cJSON_Delete(needed_addrs); return -1; } cJSON_AddItemToArray(app, cJSON_CreateString("app")); cJSON_AddItemToArray(app, cJSON_CreateString("didactyl")); cJSON_AddItemToArray(scope, cJSON_CreateString("scope")); - cJSON_AddItemToArray(scope, cJSON_CreateString(config->default_skill.kind == 31124 ? "private" : "public")); + cJSON_AddItemToArray(scope, cJSON_CreateString("private")); cJSON_AddItemToArray(tags, app); cJSON_AddItemToArray(tags, scope); char* tags_json = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); + cJSON_Delete(needed_addrs); if (!tags_json) { return -1; } @@ -1219,9 +1242,6 @@ void config_free(didactyl_config_t* config) { free(config->startup_events); } - free(config->default_skill.content); - free(config->default_skill.tags_json); - free_cashu_wallet_mints(&config->cashu_wallet); memset(config, 0, sizeof(*config)); @@ -1280,11 +1300,6 @@ int config_load(const char* path, didactyl_config_t* config) { config->api.port = 8484; snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1"); - config->default_skill.kind = 31124; - config->default_skill.d_tag[0] = '\0'; - config->default_skill.content = NULL; - config->default_skill.tags_json = NULL; - config->cashu_wallet.enabled = 0; config->cashu_wallet.mint_urls = NULL; config->cashu_wallet.mint_count = 0; @@ -1366,8 +1381,8 @@ int config_load(const char* path, didactyl_config_t* config) { goto cleanup; } - if (parse_default_skill_config(root, config) != 0) { - config_set_error("invalid default_skill configuration"); + if (parse_legacy_default_skill_to_startup_events(root, config) != 0) { + config_set_error("invalid legacy default_skill configuration"); goto cleanup; } diff --git a/src/config.h b/src/config.h index 90d9e38..bac4182 100644 --- a/src/config.h +++ b/src/config.h @@ -64,13 +64,6 @@ typedef struct { char* tags_json; // JSON array string for tags, optional } startup_event_t; -typedef struct { - int kind; - char d_tag[65]; - char* content; - char* tags_json; -} default_skill_config_t; - typedef struct { int enabled; int tools_enabled; @@ -131,12 +124,11 @@ typedef struct { cashu_wallet_config_t cashu_wallet; startup_event_t* startup_events; int startup_event_count; - default_skill_config_t default_skill; char config_path[OW_MAX_URL_LEN]; } didactyl_config_t; int config_load(const char* path, didactyl_config_t* config); -int config_ensure_default_skill_startup_events(didactyl_config_t* config); +int config_ensure_startup_skill_adoption(didactyl_config_t* config); const char* config_last_error(void); void config_free(didactyl_config_t* config); diff --git a/src/default_events.h b/src/default_events.h index 407afa6..0a55199 100644 --- a/src/default_events.h +++ b/src/default_events.h @@ -1,9 +1,13 @@ #ifndef DIDACTYL_DEFAULT_EVENTS_H #define DIDACTYL_DEFAULT_EVENTS_H -#define DIDACTYL_DEFAULT_SKILL_D_TAG "default_admin_dm" +#define DIDACTYL_DEFAULT_SKILL_D_TAG "identity_and_rules" #define DIDACTYL_DEFAULT_SKILL_KIND 31124 -#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]" +#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"identity_and_rules\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Agent identity and behavioral rules\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]" + +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_D_TAG "dm_history" +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND 31124 +#define DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON "[[\"d\",\"dm_history\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"DM conversation history for context continuity\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]" #define DIDACTYL_STARTUP_KIND_PROFILE 0 #define DIDACTYL_STARTUP_KIND_CONTACTS 3 @@ -46,4 +50,8 @@ static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE = "- Never reveal your private key (nsec)\n" "- You may share your public key (npub) with anyone"; +static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE = + "## Recent Conversation\n\n" + "{{nostr_dm_history({\"format\":\"text\",\"limit\":12})}}"; + #endif \ No newline at end of file diff --git a/src/main.c b/src/main.c index 71e6139..dc91cf7 100644 --- a/src/main.c +++ b/src/main.c @@ -1057,8 +1057,8 @@ int main(int argc, char** argv) { DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey); } - if (config_ensure_default_skill_startup_events(&cfg) != 0) { - fprintf(stderr, "Failed to synthesize startup events from default_skill\n"); + if (config_ensure_startup_skill_adoption(&cfg) != 0) { + fprintf(stderr, "Failed to synthesize startup skill adoption events\n"); config_free(&cfg); nostr_cleanup(); return 1; @@ -1248,7 +1248,14 @@ int main(int argc, char** argv) { startup_step_begin(7, "Reconcile/persist startup state"); if (bootstrap_mode || first_run) { if (nostr_handler_reconcile_startup_events() != 0) { - DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)"); + startup_step_fail(7, "Reconcile/persist startup state", "startup event reconciliation failed"); + fprintf(stderr, "Failed to publish startup events; aborting startup\n"); + nostr_block_list_cleanup(); + nostr_handler_cleanup(); + llm_cleanup(); + config_free(&cfg); + nostr_cleanup(); + return 1; } } else { DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run"); diff --git a/src/main.h b/src/main.h index 142ea88..b6f9989 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 2 -#define DIDACTYL_VERSION_PATCH 12 -#define DIDACTYL_VERSION "v0.2.12" +#define DIDACTYL_VERSION_PATCH 13 +#define DIDACTYL_VERSION "v0.2.13" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 02d4920..6abbb53 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -217,7 +217,7 @@ typedef struct { typedef struct { time_t created_at; - int incoming; + didactyl_dm_history_role_t role; char peer_pubkey_hex[65]; char* content; } dm_history_entry_t; @@ -350,14 +350,17 @@ static void dm_history_clear_locked(void) { free(g_dm_history_ring[i].content); g_dm_history_ring[i].content = NULL; g_dm_history_ring[i].created_at = 0; - g_dm_history_ring[i].incoming = 0; + g_dm_history_ring[i].role = DIDACTYL_DM_HISTORY_ASSISTANT; g_dm_history_ring[i].peer_pubkey_hex[0] = '\0'; } g_dm_history_count = 0; g_dm_history_next = 0; } -static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at) { +static void dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role, + time_t created_at) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U || !content) { return; } @@ -381,13 +384,24 @@ static void dm_history_remember(const char* peer_pubkey_hex, const char* content free(g_dm_history_ring[slot].content); g_dm_history_ring[slot].content = dup; g_dm_history_ring[slot].created_at = (created_at > 0) ? created_at : time(NULL); - g_dm_history_ring[slot].incoming = incoming ? 1 : 0; + g_dm_history_ring[slot].role = role; memcpy(g_dm_history_ring[slot].peer_pubkey_hex, peer_pubkey_hex, 64U); g_dm_history_ring[slot].peer_pubkey_hex[64] = '\0'; pthread_mutex_unlock(&g_dm_history_mutex); } +int nostr_handler_dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role) { + if (!peer_pubkey_hex || !content) { + return -1; + } + + dm_history_remember(peer_pubkey_hex, content, role, time(NULL)); + return 0; +} + static const char* relay_status_str(nostr_pool_relay_status_t status) { switch (status) { case NOSTR_POOL_RELAY_DISCONNECTED: @@ -427,7 +441,10 @@ static void register_trigger_from_self_skill_event(cJSON* event); static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data); static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data); static void dm_history_clear_locked(void); -static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at); +static void dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role, + time_t created_at); static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url); static void free_relay_list_local(char** relays, int relay_count); static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count); @@ -1691,11 +1708,6 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) { (int)tier, received_protocol == DM_PROTOCOL_NIP17 ? "nip17" : "nip04"); - dm_history_remember(sender_pubkey_hex, - decrypted, - 1, - time(NULL)); - g_dm_callback(sender_pubkey_hex, decrypted, tier, g_dm_user_data); free(decrypted); } @@ -2761,7 +2773,9 @@ int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adopt return skill_count > 0 ? 0 : -1; } -int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) { +int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) { return -1; } @@ -2843,7 +2857,7 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) if (sent > 0) { dm_history_remember(recipient_pubkey_hex, message, - 0, + role, time(NULL)); } @@ -2852,26 +2866,34 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) return sent > 0 ? 0 : -1; } -int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) { +int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) { + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, + message, + DIDACTYL_DM_HISTORY_ASSISTANT); +} + +int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role) { if (!recipient_pubkey_hex || !message) { return -1; } if (!g_cfg) { - return nostr_handler_send_dm(recipient_pubkey_hex, message); + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) { - return nostr_handler_send_dm(recipient_pubkey_hex, message); + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) { - return nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL); + return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role); } dm_protocol_t target = sender_protocol_lookup(recipient_pubkey_hex); if (target == DM_PROTOCOL_NIP17) { - int rc17 = nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL); + int rc17 = nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role); if (rc17 == 0) { return 0; } @@ -2879,7 +2901,13 @@ int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* mes recipient_pubkey_hex); } - return nostr_handler_send_dm(recipient_pubkey_hex, message); + return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); +} + +int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) { + return nostr_handler_send_dm_auto_with_role(recipient_pubkey_hex, + message, + DIDACTYL_DM_HISTORY_ASSISTANT); } static int publish_kind_event_to_relays(int kind, @@ -3220,6 +3248,7 @@ int nostr_handler_reconcile_startup_events(void) { if (g_startup_publish_tracking_enabled && g_startup_published) { int events_published = 0; int relays_used = 0; + int missing_any_event = 0; for (int r = 0; r < g_cfg->relay_count; r++) { int used = 0; @@ -3257,13 +3286,24 @@ int nostr_handler_reconcile_startup_events(void) { se->kind, startup_kind_label(se->kind), relays_line[0] ? relays_line : ""); + } else { + missing_any_event = 1; + DEBUG_ERROR("[didactyl] startup publish missing: kind=%d (%s) was not published to any relay", + se->kind, + startup_kind_label(se->kind)); } } - DEBUG_INFO("[didactyl] startup publish summary: %d event(s) published to %d/%d relay(s)", + DEBUG_INFO("[didactyl] startup publish summary: %d/%d event(s) published to at least one relay; relays used=%d/%d", events_published, + g_cfg->startup_event_count, relays_used, g_cfg->relay_count); + + if (missing_any_event) { + DEBUG_ERROR("[didactyl] startup reconcile failed: one or more startup events were not published to any relay"); + return -1; + } } return 0; @@ -3682,7 +3722,10 @@ int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int ena return rc; } -int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) { +int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex, + const char* message, + const char* subject, + didactyl_dm_history_role_t role) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') { return -1; } @@ -3772,12 +3815,19 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me dm_history_remember(recipient_pubkey_hex, message, - 0, + role, time(NULL)); return 0; } +int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) { + return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, + message, + subject, + DIDACTYL_DM_HISTORY_ASSISTANT); +} + char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) { return strdup("[]"); @@ -3831,7 +3881,15 @@ char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) if (!item) { continue; } - cJSON_AddStringToObject(item, "role", entry->incoming ? "user" : "assistant"); + const char* role_s = "assistant"; + if (entry->role == DIDACTYL_DM_HISTORY_USER) { + role_s = "user"; + } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_REQUEST) { + role_s = "tool_request"; + } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_RESPONSE) { + role_s = "tool_response"; + } + cJSON_AddStringToObject(item, "role", role_s); cJSON_AddStringToObject(item, "content", entry->content); cJSON_AddNumberToObject(item, "created_at", (double)entry->created_at); cJSON_AddItemToArray(arr, item); diff --git a/src/nostr_handler.h b/src/nostr_handler.h index f2ee8f9..25faa2a 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -13,6 +13,13 @@ typedef enum { DIDACTYL_SENDER_STRANGER = 3 } didactyl_sender_tier_t; +typedef enum { + DIDACTYL_DM_HISTORY_USER = 1, + DIDACTYL_DM_HISTORY_ASSISTANT = 2, + DIDACTYL_DM_HISTORY_TOOL_REQUEST = 3, + DIDACTYL_DM_HISTORY_TOOL_RESPONSE = 4 +} didactyl_dm_history_role_t; + typedef void (*dm_callback_t)(const char* sender_pubkey_hex, const char* message, didactyl_sender_tier_t tier, @@ -42,7 +49,13 @@ void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callb int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data); int nostr_handler_subscribe_wallet_events(void); int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message); +int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role); int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message); +int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex, + const char* message, + didactyl_dm_history_role_t role); int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result); void nostr_handler_publish_result_free(nostr_publish_result_t* result); char* nostr_handler_query_json(cJSON* filter, int timeout_ms); @@ -81,6 +94,13 @@ char* nostr_handler_relay_info_json(const char* relay_url); char* nostr_handler_subscription_status_json(void); int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set); int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject); +int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex, + const char* message, + const char* subject, + didactyl_dm_history_role_t role); +int nostr_handler_dm_history_remember(const char* peer_pubkey_hex, + const char* content, + didactyl_dm_history_role_t role); char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit); void nostr_handler_cleanup(void); diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 30b50ba..014853e 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -372,17 +372,64 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta return 0; } +static int tags_json_get_d_tag_local(const char* tags_json, char* out, size_t out_size) { + if (!tags_json || !out || out_size == 0) { + return -1; + } + + out[0] = '\0'; + cJSON* tags = cJSON_Parse(tags_json); + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(tags); + return -1; + } + + int n = cJSON_GetArraySize(tags); + for (int i = 0; i < n; i++) { + cJSON* tag = cJSON_GetArrayItem(tags, i); + if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue; + cJSON* k = cJSON_GetArrayItem(tag, 0); + cJSON* v = cJSON_GetArrayItem(tag, 1); + if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue; + if (strcmp(k->valuestring, "d") == 0 && v->valuestring[0] != '\0') { + snprintf(out, out_size, "%s", v->valuestring); + cJSON_Delete(tags); + return 0; + } + } + + cJSON_Delete(tags); + return -1; +} + static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) { if (!cfg || !content || !tags_json) { return -1; } + int is_skill_kind = (kind == 31123 || kind == 31124); + char incoming_d[128] = {0}; + int incoming_has_d = (is_skill_kind && tags_json_get_d_tag_local(tags_json, incoming_d, sizeof(incoming_d)) == 0); + for (int i = 0; i < cfg->startup_event_count; i++) { startup_event_t* se = &cfg->startup_events[i]; if (se->kind != kind) { continue; } + if (is_skill_kind) { + if (!incoming_has_d) { + continue; + } + char existing_d[128] = {0}; + if (tags_json_get_d_tag_local(se->tags_json, existing_d, sizeof(existing_d)) != 0) { + continue; + } + if (strcmp(existing_d, incoming_d) != 0) { + continue; + } + } + char* content_dup = strdup(content); char* tags_dup = strdup(tags_json); if (!content_dup || !tags_dup) { @@ -495,35 +542,22 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen return 0; } -static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) { +static int configure_default_skills_for_agent(didactyl_config_t* cfg, const char* agent_name) { if (!cfg || !agent_name || agent_name[0] == '\0') { return -1; } - free(cfg->default_skill.content); - cfg->default_skill.content = NULL; - free(cfg->default_skill.tags_json); - cfg->default_skill.tags_json = NULL; - - cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND; - snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG); - - int needed = snprintf(NULL, 0, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE); - if (needed <= 0) { + if (upsert_startup_event(cfg, + DIDACTYL_DEFAULT_SKILL_KIND, + DIDACTYL_DEFAULT_SKILL_TEMPLATE, + DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) { return -1; } - cfg->default_skill.content = (char*)malloc((size_t)needed + 1U); - if (!cfg->default_skill.content) { - return -1; - } - - snprintf(cfg->default_skill.content, (size_t)needed + 1U, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE); - - cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON); - if (!cfg->default_skill.tags_json) { - free(cfg->default_skill.content); - cfg->default_skill.content = NULL; + if (upsert_startup_event(cfg, + DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND, + DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE, + DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) { return -1; } @@ -2157,8 +2191,8 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t if (prompt_llm_config(cfg) != 0) return -1; } - if (configure_default_skill_for_agent(cfg, agent_name) != 0) { - fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET); + if (configure_default_skills_for_agent(cfg, agent_name) != 0) { + fprintf(stderr, "%sFailed to prepare default startup skill events.%s\n", ANSI_RED, ANSI_RESET); return -1; } diff --git a/src/tools/tool_agent.c b/src/tools/tool_agent.c index e9a05e1..fa1b6a6 100644 --- a/src/tools/tool_agent.c +++ b/src/tools/tool_agent.c @@ -31,6 +31,25 @@ static cJSON* parse_args_local(const char* args_json) { return args; } +static int append_text_local(char** buf, size_t* cap, size_t* used, const char* s) { + if (!buf || !cap || !used || !s) return -1; + size_t n = strlen(s); + if (*used + n + 1U > *cap) { + size_t next = *cap; + while (*used + n + 1U > next) { + next = (next == 0U) ? 256U : (next * 2U); + } + char* grown = (char*)realloc(*buf, next); + if (!grown) return -1; + *buf = grown; + *cap = next; + } + memcpy(*buf + *used, s, n); + *used += n; + (*buf)[*used] = '\0'; + return 0; +} + char* execute_message_current(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); @@ -282,6 +301,14 @@ char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json) { include_current = 1; } + int format_text = 0; + cJSON* format_j = cJSON_GetObjectItemCaseSensitive(args, "format"); + if (format_j && cJSON_IsString(format_j) && format_j->valuestring) { + if (strcmp(format_j->valuestring, "text") == 0) { + format_text = 1; + } + } + const char* peer_pubkey = ctx->cfg->admin.pubkey; cJSON* peer_j = cJSON_GetObjectItemCaseSensitive(args, "peer_pubkey"); if (peer_j && cJSON_IsString(peer_j) && peer_j->valuestring && peer_j->valuestring[0] != '\0') { @@ -361,20 +388,59 @@ char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json) { cJSON_Delete(parsed); - char* filtered_json = cJSON_PrintUnformatted(filtered); - cJSON_Delete(filtered); - if (!filtered_json) { - return json_error_local("failed to serialize dm history"); + char* rendered = NULL; + + if (format_text) { + size_t cap = 256U; + size_t used = 0U; + rendered = (char*)malloc(cap); + if (!rendered) { + cJSON_Delete(filtered); + return json_error_local("failed to serialize dm history"); + } + rendered[0] = '\0'; + + int m = cJSON_GetArraySize(filtered); + for (int i = 0; i < m; i++) { + cJSON* item = cJSON_GetArrayItem(filtered, i); + cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL; + cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL; + if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) || + !role->valuestring || !content->valuestring) { + continue; + } + + const char* prefix = (strcmp(role->valuestring, "user") == 0) ? "User: " : "Assistant: "; + if (append_text_local(&rendered, &cap, &used, prefix) != 0 || + append_text_local(&rendered, &cap, &used, content->valuestring) != 0 || + append_text_local(&rendered, &cap, &used, "\n") != 0) { + free(rendered); + cJSON_Delete(filtered); + return json_error_local("failed to serialize dm history"); + } + } + + if (used > 0U && rendered[used - 1] == '\n') { + rendered[used - 1] = '\0'; + } + } else { + rendered = cJSON_PrintUnformatted(filtered); + if (!rendered) { + cJSON_Delete(filtered); + return json_error_local("failed to serialize dm history"); + } } + cJSON_Delete(filtered); + cJSON* out = cJSON_CreateObject(); if (!out) { - free(filtered_json); + free(rendered); return NULL; } cJSON_AddBoolToObject(out, "success", 1); - cJSON_AddStringToObject(out, "content", filtered_json); - free(filtered_json); + cJSON_AddStringToObject(out, "content", rendered); + free(rendered); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index 433d7ea..b63cda7 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -851,7 +851,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(t24a, "type", "function"); cJSON_AddStringToObject(t24a_fn, "name", "skill_get"); - cJSON_AddStringToObject(t24a_fn, "description", "Get full skill JSON by d tag from local cache; JSON: {\"d\":\"default_admin_dm\"} (slash shorthand also works: /skill_get default_admin_dm)"); + cJSON_AddStringToObject(t24a_fn, "description", "Get full skill JSON by d tag from local cache; JSON: {\"d\":\"identity_and_rules\"} (slash shorthand also works: /skill_get identity_and_rules)"); cJSON_AddStringToObject(t24a_params, "type", "object"); cJSON_AddItemToObject(t24a_params, "properties", t24a_props); cJSON_AddItemToObject(t24a_params, "required", t24a_required); @@ -880,7 +880,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(t24b, "type", "function"); cJSON_AddStringToObject(t24b_fn, "name", "skill_view"); - cJSON_AddStringToObject(t24b_fn, "description", "Compatibility alias for skill_get; JSON: {\"d\":\"default_admin_dm\"}"); + cJSON_AddStringToObject(t24b_fn, "description", "Compatibility alias for skill_get; JSON: {\"d\":\"identity_and_rules\"}"); cJSON_AddStringToObject(t24b_params, "type", "object"); cJSON_AddItemToObject(t24b_params, "properties", t24b_props); cJSON_AddItemToObject(t24b_params, "required", t24b_required); @@ -908,7 +908,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(t24c_fn, "name", "skill_set"); cJSON_AddStringToObject(t24c_fn, "description", - "Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\"skill\":{\"d\":\"default_admin_dm\",\"kind\":31124,\"scope\":\"private\",\"content\":\"...\",\"tags\":[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]}}"); + "Set/publish a full skill JSON payload and refresh runtime cache; JSON: {\"skill\":{\"d\":\"identity_and_rules\",\"kind\":31124,\"scope\":\"private\",\"content\":\"...\",\"tags\":[[\"d\",\"identity_and_rules\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]}}"); cJSON_AddStringToObject(t24c_params, "type", "object"); cJSON_AddItemToObject(t24c_params, "properties", t24c_props); @@ -1487,7 +1487,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(t43_fn, "name", "nostr_dm_history"); cJSON_AddStringToObject(t43_fn, "description", - "Fetch recent DM history for template/context usage; JSON: {\"limit\":12,\"include_current\":false} or with explicit peer {\"peer_pubkey\":\"\",\"limit\":20}; template usage: {{nostr_dm_history({\"limit\":12})}}"); + "Fetch recent DM history for template/context usage; JSON: {\"limit\":12,\"include_current\":false,\"format\":\"json\"} or with explicit peer {\"peer_pubkey\":\"\",\"limit\":20,\"format\":\"text\"}; template usage: {{nostr_dm_history({\"limit\":12,\"format\":\"text\"})}}"); cJSON_AddStringToObject(t43_params, "type", "object"); cJSON_AddItemToObject(t43_params, "properties", t43_props); @@ -1506,6 +1506,13 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(p_dm_history_peer, "description", "Optional peer pubkey hex (defaults to configured admin pubkey)"); cJSON_AddItemToObject(t43_props, "peer_pubkey", p_dm_history_peer); + cJSON* p_dm_history_format = cJSON_CreateObject(); + cJSON_AddStringToObject(p_dm_history_format, "type", "string"); + cJSON_AddStringToObject(p_dm_history_format, + "description", + "Output format: 'json' (default) or 'text' (User:/Assistant: lines)"); + cJSON_AddItemToObject(t43_props, "format", p_dm_history_format); + cJSON_AddItemToObject(t43_fn, "parameters", t43_params); cJSON_AddItemToObject(t43, "function", t43_fn); cJSON_AddItemToArray(tools, t43);