338 lines
11 KiB
Markdown
338 lines
11 KiB
Markdown
# 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()` (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
|