# Skill Tools — Architecture Plan ## Overview Add a family of five skill-management tools to Didactyl so the agent can **create, list, adopt, remove, and discover** skills at runtime — all through the existing LLM tool-calling loop. Skills are Nostr events. The tools are thin orchestration wrappers over the existing `nostr_handler_publish_kind_event()` and `nostr_handler_query_json()` primitives. --- ## Nostr Kind Reference | Kind | Purpose | Replaceable? | Key tag | |---|---|---|---| | `31123` | Public skill definition | Yes (d-tag) | `d=` | | `31124` | Private skill definition | Yes (d-tag) | `d=` | | `10123` | Public skill adoption list | Yes (replaceable) | `a` refs to 31123 events | All skill events carry these standard tags: - `["d", ""]` — unique identifier within the author's pubkey - `["app", "didactyl"]` — app namespace - `["scope", "public"]` or `["scope", "private"]` --- ## Tool Family ### 1. `skill_create` **Purpose:** Create or update a skill definition and optionally auto-adopt it. **OpenAI schema:** ```json { "name": "skill_create", "description": "Create or update a skill definition (kind 31123 public / 31124 private) and optionally auto-adopt it", "parameters": { "type": "object", "properties": { "d_tag": { "type": "string", "description": "Unique skill identifier (lowercase, hyphens allowed)" }, "content": { "type": "string", "description": "Skill body — markdown instructions or structured JSON" }, "scope": { "type": "string", "description": "public (kind 31123) or private (kind 31124). Default: public" }, "description": { "type": "string", "description": "Short one-line description for the skill" }, "auto_adopt": { "type": "boolean", "description": "Automatically add to adoption list (kind 10123). Default: true" } }, "required": ["d_tag", "content"] } } ``` **Execution logic (`execute_skill_create`):** 1. Validate `d_tag` — must be non-empty, lowercase alphanumeric + hyphens, no spaces 2. Determine kind: `31123` if scope is `"public"` or absent; `31124` if `"private"` 3. Build tags array: - `["d", d_tag]` - `["app", "didactyl"]` - `["scope", scope]` - `["description", description]` if provided 4. Call `nostr_handler_publish_kind_event(kind, content, tags, &result)` 5. If `auto_adopt` is true (default), update the kind `10123` adoption list: - Query existing `10123` event for own pubkey (same pattern as `execute_nostr_list_manage`) - Add `["a", "31123::"]` tag if not already present - Republish the updated `10123` event 6. Return JSON with `success`, `event_id`, `naddr_uri`, `d_tag`, `adopted` **Key design decisions:** - Auto-adopt defaults to `true` — creating a skill you don't adopt is unusual - Private skills (31124) are NOT added to the public adoption list (10123) - Republishing with the same d_tag replaces the previous version (replaceable event) --- ### 2. `skill_list` **Purpose:** List the agent's own published skills. **OpenAI schema:** ```json { "name": "skill_list", "description": "List skills published by this agent, optionally filtered by scope", "parameters": { "type": "object", "properties": { "scope": { "type": "string", "description": "Filter by public or private. Omit for both." } } } } ``` **Execution logic (`execute_skill_list`):** 1. Build filter based on scope: - Both: `{"kinds": [31123, 31124], "authors": [own_pubkey]}` - Public only: `{"kinds": [31123], "authors": [own_pubkey]}` - Private only: `{"kinds": [31124], "authors": [own_pubkey]}` 2. Call `nostr_handler_query_json(filter, 8000)` 3. Parse results, extract for each event: - `d_tag` (from d-tag) - `kind` - `scope` (from scope tag) - `description` (from description tag, if present) - `created_at` timestamp - `content` preview (first 200 chars) 4. Return JSON array of skill summaries --- ### 3. `skill_adopt` **Purpose:** Adopt a skill published by another author (or self) into the agent's adoption list. **OpenAI schema:** ```json { "name": "skill_adopt", "description": "Add a skill to the agent's public adoption list (kind 10123)", "parameters": { "type": "object", "properties": { "pubkey": { "type": "string", "description": "Hex pubkey of the skill author" }, "d_tag": { "type": "string", "description": "Skill d_tag (d-tag value)" }, "kind": { "type": "integer", "description": "Skill kind (31123 or 31124). Default: 31123" } }, "required": ["pubkey", "d_tag"] } } ``` **Execution logic (`execute_skill_adopt`):** 1. Validate pubkey (64-char hex) and d_tag (non-empty) 2. Default kind to 31123 if not provided 3. Build the `a`-tag value: `"::"` 4. Query existing kind `10123` event for own pubkey 5. Check if `["a", "::"]` already exists — if so, return success with `already_adopted: true` 6. Add the tag, republish `10123` 7. Return JSON with `success`, `adopted_address`, `event_id` --- ### 4. `skill_remove` **Purpose:** Remove a skill from the agent's adoption list. **OpenAI schema:** ```json { "name": "skill_remove", "description": "Remove a skill from the agent's public adoption list (kind 10123)", "parameters": { "type": "object", "properties": { "pubkey": { "type": "string", "description": "Hex pubkey of the skill author. Defaults to own pubkey." }, "d_tag": { "type": "string", "description": "Skill d_tag to remove" }, "kind": { "type": "integer", "description": "Skill kind (31123 or 31124). Default: 31123" } }, "required": ["d_tag"] } } ``` **Execution logic (`execute_skill_remove`):** 1. Default pubkey to own pubkey if not provided 2. Default kind to 31123 3. Build the `a`-tag value: `"::"` 4. Query existing kind `10123` event for own pubkey 5. Find and remove matching `["a", ...]` tag 6. Republish `10123` 7. Return JSON with `success`, `removed_address`, `event_id` --- ### 5. `skill_search` **Purpose:** Search for skills across the agent's Web of Trust. **OpenAI schema:** ```json { "name": "skill_search", "description": "Search for skills adopted by Web of Trust contacts, or query public skill definitions", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Optional keyword to filter skill slugs or descriptions" }, "pubkey": { "type": "string", "description": "Search skills by a specific author pubkey" }, "popular": { "type": "boolean", "description": "If true, query WoT adoption lists to find most-adopted skills" } } } } ``` **Execution logic (`execute_skill_search`):** 1. If `popular` is true: - Query `{"kinds": [10123]}` from relays (with reasonable limit) - Parse all `a`-tags from results - Count occurrences of each `a`-tag address - Sort by adoption count descending - Return top N skill addresses with counts 2. If `pubkey` is provided: - Query `{"kinds": [31123], "authors": [pubkey]}` - Return skill summaries 3. If `query` is provided (keyword search): - Query `{"kinds": [31123]}` with limit - Filter results client-side by matching `query` against d_tag, description tag, or content - Return matching skill summaries 4. Default (no params): return own adopted skills from `10123` --- ## Implementation Architecture ### Shared helper: adoption list update Since `skill_create`, `skill_adopt`, and `skill_remove` all modify the kind `10123` list, extract a shared helper: ```c // Fetch current 10123 event, return duplicated tags array (or empty array if none exists) static cJSON* fetch_adoption_list_tags(tools_context_t* ctx); // Publish updated 10123 event with new tags static int publish_adoption_list(tools_context_t* ctx, cJSON* tags, nostr_publish_result_t* result); ``` This is essentially the same pattern already used in `execute_nostr_list_manage()` but specialized for kind `10123`. ### Shared helper: skill summary extraction ```c // Extract d_tag, kind, scope, description, created_at from a skill event JSON static cJSON* extract_skill_summary(cJSON* event); ``` ### Flow diagram ```mermaid flowchart TD SC[skill_create] --> PUB[nostr_handler_publish_kind_event] SC --> ADOPT_HELPER[update adoption list helper] SL[skill_list] --> QUERY[nostr_handler_query_json] SL --> SUMMARY[extract_skill_summary] SA[skill_adopt] --> ADOPT_HELPER SR[skill_remove] --> ADOPT_HELPER SS[skill_search] --> QUERY SS --> SUMMARY ADOPT_HELPER --> QUERY ADOPT_HELPER --> PUB ``` --- ## Changes Required ### `src/tools.c` 1. **Schema registration** — Add 5 new tool definitions in `tools_build_openai_schema_json()` (t22–t26) 2. **Execution functions** — Add 5 new `execute_skill_*()` static functions 3. **Dispatch** — Add 5 new `strcmp` branches in `tools_execute()` 4. **Shared helpers** — Add `fetch_adoption_list_tags()`, `publish_adoption_list()`, `extract_skill_summary()`, and `validate_skill_d_tag()` ### `README.md` 1. Add skill tools to the Tooling Interface section under a new "Skill management" category ### No changes needed to: - `src/tools.h` — the `tools_context_t` already has `cfg` which provides `keys.public_key_hex` - `src/nostr_handler.h` — all needed APIs already exist - `src/config.h` — no new config fields needed --- ## Slug Validation Rules A valid skill d_tag must: - Be 1–64 characters - Contain only lowercase letters, digits, and hyphens - Not start or end with a hyphen - Not contain consecutive hyphens ```c static int validate_skill_d_tag(const char* d_tag) { if (!d_tag || d_tag[0] == '\0' || strlen(d_tag) > 64) return 0; if (d_tag[0] == '-') return 0; int prev_dash = 0; for (size_t i = 0; d_tag[i]; i++) { char c = d_tag[i]; if (c == '-') { if (prev_dash) return 0; prev_dash = 1; } else if (islower(c) || isdigit(c)) { prev_dash = 0; } else { return 0; } } if (d_tag[strlen(d_tag) - 1] == '-') return 0; return 1; } ``` --- ## Implementation Order 1. **Shared helpers** — `validate_skill_d_tag`, `fetch_adoption_list_tags`, `publish_adoption_list`, `extract_skill_summary` 2. **`skill_create`** — most important tool, enables the agent to author skills 3. **`skill_list`** — lets the agent see what it has published 4. **`skill_adopt`** — adopt skills from other authors 5. **`skill_remove`** — remove skills from adoption list 6. **`skill_search`** — discover skills across WoT 7. **Schema registration** — add all 5 tools to `tools_build_openai_schema_json()` 8. **Dispatch wiring** — add all 5 to `tools_execute()` 9. **README update** — document the new tools 10. **Build and test** — verify compilation and basic tool execution --- ## Security Considerations - **Admin-only**: Skill tools inherit the existing ADMIN tier restriction — only the admin can trigger tool calls - **Slug validation**: Prevents injection of malformed d-tags - **No arbitrary kind**: `skill_create` only publishes kind 31123 or 31124, not arbitrary kinds - **Adoption list integrity**: The helpers always fetch-then-update to avoid clobbering existing adoption entries - **Content size**: No explicit limit on skill content size — relies on relay limits and LLM context window constraints