Files
didactyl/plans/skill_tools.md

11 KiB
Raw Blame History

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=<d_tag>
31124 Private skill definition Yes (d-tag) d=<d_tag>
10123 Public skill adoption list Yes (replaceable) a refs to 31123 events

All skill events carry these standard tags:

  • ["d", "<d_tag>"] — 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:

{
  "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:<own_pubkey>:<d_tag>"] 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:

{
  "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:

{
  "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: "<kind>:<pubkey>:<d_tag>"
  4. Query existing kind 10123 event for own pubkey
  5. Check if ["a", "<kind>:<pubkey>:<d_tag>"] 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:

{
  "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: "<kind>:<pubkey>:<d_tag>"
  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

Purpose: Search for skills across the agent's Web of Trust.

OpenAI schema:

{
  "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:

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

// Extract d_tag, kind, scope, description, created_at from a skill event JSON
static cJSON* extract_skill_summary(cJSON* event);

Flow diagram

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() (t22t26)
  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 164 characters
  • Contain only lowercase letters, digits, and hyphens
  • Not start or end with a hyphen
  • Not contain consecutive hyphens
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 helpersvalidate_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