diff --git a/README.md b/README.md index 0822f93..a4f7d80 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa 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.52 +## Current Status — v0.0.53 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.52 — Added dm_history expand section to soul template so history is injected into context +> Last release update: v0.0.53 — Add skill_create trigger schema fields and startup cheerleader triggered skill example - 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/config.jsonc.example b/config.jsonc.example index d2ae894..0ef6caf 100644 --- a/config.jsonc.example +++ b/config.jsonc.example @@ -213,6 +213,23 @@ ] }, + // Kind 31123: Public triggered skill — cheerleader + // Watches for admin kind-1 notes and sends an encouraging DM. + { + "kind": 31123, + "content": "You are my personal cheerleader. When the admin posts a kind 1 note, read their note and send them a short DM that cheers them on, praises their effort, tells them they are good looking, and encourages them to keep going. Be warm, playful, and positive. Mention something specific from their note so it feels personal. Keep it to 2-3 sentences.", + "tags": [ + ["d", "cheerleader"], + ["app", "didactyl"], + ["scope", "public"], + ["description", "Cheer on admin whenever they post a kind 1 note"], + ["trigger", "nostr-subscription"], + ["filter", "{\"kinds\":[1],\"authors\":[\"REPLACE_WITH_ADMIN_HEX_PUBKEY\"]}"], + ["action", "llm"], + ["enabled", "true"] + ] + }, + // Kind 31124: Private skill — admin_ops // Private operational procedures (admin-only). { @@ -237,6 +254,7 @@ "tags": [ ["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"], ["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"], + ["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:cheerleader"], ["app", "didactyl"], ["scope", "public"] ] diff --git a/plans/cheerleader_triggered_skill.md b/plans/cheerleader_triggered_skill.md new file mode 100644 index 0000000..1ba445c --- /dev/null +++ b/plans/cheerleader_triggered_skill.md @@ -0,0 +1,162 @@ +# Cheerleader Triggered Skill — Implementation Plan + +## Goal + +Create the first triggered skill: whenever the admin posts a kind 1 note on Nostr, the agent reads the note content and sends a DM back to the admin cheering them on, praising them, and telling them how good looking they are. + +--- + +## Current State + +The entire triggered-skill infrastructure is **already built and functional**: + +| Component | File | Status | +|---|---|---| +| Trigger manager (poll, cooldown, add/remove) | `src/trigger_manager.c` | ✅ Complete | +| LLM-mediated trigger execution | `src/agent.c` `agent_on_trigger()` | ✅ Complete | +| `skill_create` tool with trigger support | `src/tools.c` `execute_skill_create()` | ✅ Complete | +| Trigger polling in main loop | `src/main.c` | ✅ Complete | +| Config parsing for triggers section | `src/config.c` | ✅ Complete | +| `trigger_list` tool | `src/tools.c` | ✅ Complete | + +### Gap Found + +The `skill_create` tool **schema** (what the LLM sees) only exposes 5 parameters: `slug`, `content`, `scope`, `description`, `auto_adopt`. The execution function already handles `trigger`, `filter`, `action`, and `enabled` — but these are **not declared in the tool schema**, so the LLM will never pass them. + +--- + +## Implementation Steps + +### 1. Fix `skill_create` Tool Schema + +**File:** `src/tools.c` lines ~1607-1641 + +Add four new properties to the `skill_create` tool definition so the LLM can see and use them: + +```c +// After auto_adopt property (line ~1634): + +cJSON* p_skill_create_trigger = cJSON_CreateObject(); +cJSON_AddStringToObject(p_skill_create_trigger, "type", "string"); +cJSON_AddStringToObject(p_skill_create_trigger, "description", + "Trigger type. Use nostr-subscription to activate on matching Nostr events"); +cJSON_AddItemToObject(t22_props, "trigger", p_skill_create_trigger); + +cJSON* p_skill_create_filter = cJSON_CreateObject(); +cJSON_AddStringToObject(p_skill_create_filter, "type", "string"); +cJSON_AddStringToObject(p_skill_create_filter, "description", + "Nostr subscription filter JSON for the trigger, e.g. {\"kinds\":[1],\"authors\":[\"\"]}"); +cJSON_AddItemToObject(t22_props, "filter", p_skill_create_filter); + +cJSON* p_skill_create_action = cJSON_CreateObject(); +cJSON_AddStringToObject(p_skill_create_action, "type", "string"); +cJSON_AddStringToObject(p_skill_create_action, "description", + "Action type: llm (default, full LLM reasoning) or template (fast interpolation)"); +cJSON_AddItemToObject(t22_props, "action", p_skill_create_action); + +cJSON* p_skill_create_enabled = cJSON_CreateObject(); +cJSON_AddStringToObject(p_skill_create_enabled, "type", "boolean"); +cJSON_AddStringToObject(p_skill_create_enabled, "description", + "Whether the trigger is active. Default: true"); +cJSON_AddItemToObject(t22_props, "enabled", p_skill_create_enabled); +``` + +This is ~20 lines of code. No changes needed to the execution function — it already handles all four parameters. + +### 2. Add Cheerleader Skill as Startup Event + +**File:** `config.jsonc.example` + +Add a new kind 31123 skill event with trigger tags. Insert before the kind 10123 adoption list event: + +```jsonc +// Kind 31123: Public triggered skill — cheerleader +// Watches for admin kind 1 notes and sends encouraging DMs. +{ + "kind": 31123, + "content": "You are the admin's biggest fan and personal cheerleader. When the admin posts a note on Nostr, read the note content carefully and send them a DM that:\n\n1. References what they actually wrote about\n2. Cheers them on enthusiastically\n3. Praises their intelligence and insight\n4. Tells them how good looking they are\n5. Encourages them to keep posting\n\nBe genuine, warm, and over-the-top positive. Use their name from the admin profile if available. Keep it to 2-3 sentences max.", + "tags": [ + ["d", "cheerleader"], + ["app", "didactyl"], + ["scope", "public"], + ["description", "Cheer on the admin whenever they post a kind 1 note"], + ["trigger", "nostr-subscription"], + ["filter", "{\"kinds\":[1],\"authors\":[\"ADMIN_PUBKEY_HEX\"]}"], + ["action", "llm"], + ["enabled", "true"] + ] +} +``` + +> **Note:** The `ADMIN_PUBKEY_HEX` placeholder in the filter must be replaced with the actual admin pubkey from the config. Since this is a static config example, we use a placeholder. At runtime, the user replaces it with their admin pubkey. + +### 3. Add to Adoption List + +**File:** `config.jsonc.example` + +Add the cheerleader skill address to the kind 10123 adoption list tags: + +```jsonc +["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:cheerleader"] +``` + +### 4. Verify Trigger Filter + +The Nostr filter for this trigger: + +```json +{"kinds": [1], "authors": [""]} +``` + +This matches: +- **Kind 1** — text notes only (not DMs, not reactions, not reposts) +- **Authors** — only the admin's pubkey (not anyone else's notes) + +The trigger manager polls every 10 seconds (`trigger_manager_poll` checks `now - last_poll_at < 10`), applies the `since` parameter to only fetch events newer than the last seen, and enforces the configured cooldown (default 60 seconds) between firings. + +--- + +## Data Flow + +```mermaid +sequenceDiagram + participant Admin as Admin Client + participant Relay as Nostr Relay + participant TM as Trigger Manager + participant Agent as agent_on_trigger + participant LLM as LLM API + + Admin->>Relay: Publish kind 1 note + Note over TM: Poll every 10s + TM->>Relay: Query filter: kinds=1, authors=admin + Relay-->>TM: New event found + TM->>TM: Check cooldown, check last_seen_created_at + TM->>Agent: agent_on_trigger with skill content + event + Agent->>Agent: Build system prompt with soul + skill instructions + Agent->>LLM: llm_chat with system + triggering event JSON + LLM-->>Agent: Cheerful response + Agent->>Relay: DM to admin via nostr_handler_send_dm_auto + Relay-->>Admin: Encrypted DM with encouragement +``` + +--- + +## File Changes Summary + +| File | Change | Lines | +|---|---|---| +| `src/tools.c` | Add trigger/filter/action/enabled to skill_create schema | ~20 lines added | +| `config.jsonc.example` | Add cheerleader skill startup event | ~20 lines added | +| `config.jsonc.example` | Add cheerleader to adoption list | 1 line added | + +--- + +## Testing + +1. Build with `make` +2. Start the agent +3. Verify trigger loads on startup via `trigger_list` tool or HTTP API `/status` +4. Post a kind 1 note from the admin account +5. Wait up to ~10 seconds for the poll cycle +6. Receive a cheerful DM from the agent +7. Verify cooldown works — posting again within 60s should not trigger a second DM diff --git a/src/agent.c b/src/agent.c index 3f88fce..d7bba57 100644 --- a/src/agent.c +++ b/src/agent.c @@ -805,6 +805,8 @@ static char* format_context_payload_for_log(const char* context_payload) { } out[0] = '\0'; size_t used = 0; + char* conversation_role = NULL; + char* conversation_content = NULL; int n = cJSON_GetArraySize(root); (void)appendf(&out, &cap, &used, "Sections: %d\n\n", n); @@ -830,6 +832,14 @@ static char* format_context_payload_for_log(const char* context_payload) { continue; } + if (section && strcmp(section, "conversation") == 0) { + free(conversation_role); + free(conversation_content); + conversation_role = strdup(role_s); + conversation_content = strdup(content_s); + continue; + } + char* display_content = demote_markdown_headings_for_log(content_s); const char* safe_display_content = display_content ? display_content : content_s; @@ -901,8 +911,19 @@ static char* format_context_payload_for_log(const char* context_payload) { } } + if (conversation_role && conversation_content) { + (void)appendf(&out, + &cap, + &used, + "# conversation | role=%s\n\n%s\n\n", + conversation_role, + conversation_content); + } + free(chat); free(latest_admin_line); + free(conversation_role); + free(conversation_content); cJSON_Delete(root); return out; diff --git a/src/main.h b/src/main.h index a603a87..f6fcaeb 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 0 -#define DIDACTYL_VERSION_PATCH 52 -#define DIDACTYL_VERSION "v0.0.52" +#define DIDACTYL_VERSION_PATCH 53 +#define DIDACTYL_VERSION "v0.0.53" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/tools.c b/src/tools.c index f2f4ce2..0bccf93 100644 --- a/src/tools.c +++ b/src/tools.c @@ -1633,6 +1633,22 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) { cJSON_AddStringToObject(p_skill_create_auto, "type", "boolean"); cJSON_AddItemToObject(t22_props, "auto_adopt", p_skill_create_auto); + cJSON* p_skill_create_trigger = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_trigger, "type", "string"); + cJSON_AddItemToObject(t22_props, "trigger", p_skill_create_trigger); + + cJSON* p_skill_create_filter = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_filter, "type", "string"); + cJSON_AddItemToObject(t22_props, "filter", p_skill_create_filter); + + cJSON* p_skill_create_action = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_action, "type", "string"); + cJSON_AddItemToObject(t22_props, "action", p_skill_create_action); + + cJSON* p_skill_create_enabled = cJSON_CreateObject(); + cJSON_AddStringToObject(p_skill_create_enabled, "type", "boolean"); + cJSON_AddItemToObject(t22_props, "enabled", p_skill_create_enabled); + cJSON_AddItemToArray(t22_required, cJSON_CreateString("slug")); cJSON_AddItemToArray(t22_required, cJSON_CreateString("content"));