Files
didactyl/plans/skill_tools.md
T

338 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:**
```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:<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:**
```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: `"<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:**
```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: `"<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`
---
### 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()` (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
```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