11 KiB
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):
- Validate
d_tag— must be non-empty, lowercase alphanumeric + hyphens, no spaces - Determine kind:
31123if scope is"public"or absent;31124if"private" - Build tags array:
["d", d_tag]["app", "didactyl"]["scope", scope]["description", description]if provided
- Call
nostr_handler_publish_kind_event(kind, content, tags, &result) - If
auto_adoptis true (default), update the kind10123adoption list:- Query existing
10123event for own pubkey (same pattern asexecute_nostr_list_manage) - Add
["a", "31123:<own_pubkey>:<d_tag>"]tag if not already present - Republish the updated
10123event
- Query existing
- 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):
- 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]}
- Both:
- Call
nostr_handler_query_json(filter, 8000) - Parse results, extract for each event:
d_tag(from d-tag)kindscope(from scope tag)description(from description tag, if present)created_attimestampcontentpreview (first 200 chars)
- 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):
- Validate pubkey (64-char hex) and d_tag (non-empty)
- Default kind to 31123 if not provided
- Build the
a-tag value:"<kind>:<pubkey>:<d_tag>" - Query existing kind
10123event for own pubkey - Check if
["a", "<kind>:<pubkey>:<d_tag>"]already exists — if so, return success withalready_adopted: true - Add the tag, republish
10123 - 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):
- Default pubkey to own pubkey if not provided
- Default kind to 31123
- Build the
a-tag value:"<kind>:<pubkey>:<d_tag>" - Query existing kind
10123event for own pubkey - Find and remove matching
["a", ...]tag - Republish
10123 - Return JSON with
success,removed_address,event_id
5. skill_search
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):
- If
popularis 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
- Query
- If
pubkeyis provided:- Query
{"kinds": [31123], "authors": [pubkey]} - Return skill summaries
- Query
- If
queryis provided (keyword search):- Query
{"kinds": [31123]}with limit - Filter results client-side by matching
queryagainst d_tag, description tag, or content - Return matching skill summaries
- Query
- 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
- Schema registration — Add 5 new tool definitions in
tools_build_openai_schema_json()(t22–t26) - Execution functions — Add 5 new
execute_skill_*()static functions - Dispatch — Add 5 new
strcmpbranches intools_execute() - Shared helpers — Add
fetch_adoption_list_tags(),publish_adoption_list(),extract_skill_summary(), andvalidate_skill_d_tag()
README.md
- Add skill tools to the Tooling Interface section under a new "Skill management" category
No changes needed to:
src/tools.h— thetools_context_talready hascfgwhich provideskeys.public_key_hexsrc/nostr_handler.h— all needed APIs already existsrc/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
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
- Shared helpers —
validate_skill_d_tag,fetch_adoption_list_tags,publish_adoption_list,extract_skill_summary skill_create— most important tool, enables the agent to author skillsskill_list— lets the agent see what it has publishedskill_adopt— adopt skills from other authorsskill_remove— remove skills from adoption listskill_search— discover skills across WoT- Schema registration — add all 5 tools to
tools_build_openai_schema_json() - Dispatch wiring — add all 5 to
tools_execute() - README update — document the new tools
- 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_createonly 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