Files
didactyl/docs/SKILLS.md

26 KiB

Skills

See also: CONTEXT.md · TOOLS.md

The Context Window Is Made of Skills

Every time an LLM runs, it receives a context window — the complete set of instructions and information it needs to reason and respond. In this system, the context window is broken up into units called skills.

╔══════════════════════════════════════════╗
║           CONTEXT WINDOW                 ║
║                                          ║
║  ┌────────────────────────────────────┐  ║
║  │ Skill 1: personality               │  ║
║  │                                    │  ║
║  │ You speak concisely and directly.  │  ║
║  │ You favor technical precision.     │  ║
║  │                                    │  ║
║  │ tools: [my_name, my_npub]          │  ║
║  └────────────────────────────────────┘  ║
║  ┌────────────────────────────────────┐  ║
║  │ Skill 2: chat                      │  ║
║  │                                    │  ║
║  │ Respond helpfully to the admin.    │  ║
║  │ Use tools as needed.               │  ║
║  │                                    │  ║
║  │ tools: [nostr_query, nostr_dm]     │  ║
║  └────────────────────────────────────┘  ║
║                                          ║
╚══════════════════════════════════════════╝

Each skill is a block of instructions. The context window is a stack of these blocks. Different events produce different stacks — a DM conversation has one set of skills, a scheduled cron job has a completely different set.

A skill is a set of instructions for an LLM stored as a Nostr event. Skills teach an LLM how to accomplish tasks — the LLM reads the instructions, reasons about them, and uses tools to take action.

Think of it like a woodshop: a skill is knowing how to carve — technique, judgment, decision-making. A tool is the chisel. The skill never directly uses the chisel without the craftsperson (the LLM) in the loop.

Skills are portable, shareable, and discoverable as Nostr events. They are not specific to any single application — any app that can read Nostr events and call an LLM can use skills.


What Is a Skill?

A skill has two orthogonal properties:

  • Triggers — A skill may have trigger tags, or not. If it has triggers, a runtime can fire it automatically when matching events occur. Triggered skills appear in the context window when their trigger matches (layer 1).
  • References — A skill may be referenced by other skills via{{skill_d_tag}} template variables, or not. If referenced, its content is included inside the referencing skill (layer 2).

These properties are independent. A skill can have triggers and be referenced. A skill can have triggers and never be referenced. A skill can have no triggers and only exist to be referenced. A skill can have neither (though that would be inert).


Skill Events

Kind Purpose Replaceable?
31123 Public skill definition Yes, by d-tag
31124 Private skill definition Yes, by d-tag
10123 Skill adoption list Yes, single per pubkey

Skill Content

The content field of a skill event IS the template — markdown instructions that go directly into the context window. No JSON wrapper. The description lives in a tag, not in content.

{
  "kind": 31123,
  "content": "system:\n## Spelling and Grammar Checker\n\nYou are a spelling and grammar checker.\n\n### Rules\n\n- Fix spelling errors\n- Fix grammar errors\n- Preserve original formatting\n- Return **ONLY** the corrected text, no explanations\n\nuser:\n{{message}}",
  "tags": [
    ["d", "spellcheck"],
    ["description", "Check spelling and grammar"],
    ["trigger", "dm"],
    ["filter", "{\"from\":\"admin\"}"],
    ["llm", "openai/gpt-4o-mini, cheap"],
    ["temperature", "0"]
  ]
}
  • content — the template in markdown. May include {{...}} template variables and system: / user: role markers. This is what goes into the context window.
  • Heading Levels — The runtime owns the # (h1) heading for the document title. All headings in skill content are automatically bumped down one level (# becomes ##, ## becomes ###) during context assembly. Skill authors should use ## for their top-level sections.
  • ["description", "..."] — human-readable description for discovery and UI display.
  • Each ["tag", "value"] is a separate tag on the Nostr event.
  • The llm tag uses a CSS font-stack style fallback chain. See LLM Fallback Chain.

Two-Layer Context Model

When a skill executes, the context window is built in two layers:

  • Layer 1: Triggered skills whose trigger matches the current event, ordered by their position in the adoption list (10123). Only triggered skills can be in layer 1 — the trigger system is what puts them there.
  • Layer 2: Skills embedded inside layer 1 skills via{{skill_d_tag}} template references. These are resolved inline, the same way tool-based template variables are resolved.
CONTEXT WINDOW — Admin DM arrives
═══════════════════════════════════════════════════

Layer 1: Triggered skills matching "dm/admin"
(ordered by adoption list)

┌─────────────────────────────────────────────────┐
│ TRIGGERED SKILL: personality                    │
│ trigger: dm, filter: {"from":"admin"}           │
│                                                 │
│   ┌───────────────────────────────────────┐     │
│   │ {{identity}}  (adopted, no trigger)   │     │
│   │ You are Didactyl. npub1abc...xyz      │     │
│   └───────────────────────────────────────┘     │
│                                                 │
│   You speak concisely and directly.             │
│   You favor technical precision.                │
│   You use dry humor sparingly.                  │
│                                                 │
├─────────────────────────────────────────────────┤
│ TRIGGERED SKILL: chat                           │
│ trigger: dm, filter: {"from":"admin"}           │
│                                                 │
│   Respond helpfully. Use tools as needed.       │
│                                                 │
│   tools: [nostr_query, nostr_dm, nostr_post,    │
│           memory_read, memory_write]            │
│                                                 │
├─────────────────────────────────────────────────┤
│ DM CONTENT (always last for dm triggers)        │
│                                                 │
│   "Hey, can you check who mentioned me today?"  │
│                                                 │
└─────────────────────────────────────────────────┘
CONTEXT WINDOW — Cron fires at noon
═══════════════════════════════════════════════════

Layer 1: Triggered skills matching "cron/0 12 * * *"
(ordered by adoption list)

┌─────────────────────────────────────────────────┐
│ TRIGGERED SKILL: readme-monitor                 │
│ trigger: cron, filter: 0 12 * * *               │
│                                                 │
│   ┌───────────────────────────────────────┐     │
│   │ {{identity}}  (adopted, no trigger)   │     │
│   │ You are Didactyl. npub1abc...xyz      │     │
│   └───────────────────────────────────────┘     │
│                                                 │
│   Check the readme at the configured URL.       │
│   Compare with last known version in memory.    │
│   If changed: post it and DM admin a summary.   │
│                                                 │
│   tools: [http_fetch, memory_read,              │
│           memory_write, nostr_post, nostr_dm]   │
│                                                 │
├─────────────────────────────────────────────────┤
│ TRIGGERING EVENT                                │
│                                                 │
│   {"type":"cron","filter":"0 12 * * *",         │
│    "created_at":1742641200}                     │
│                                                 │
└─────────────────────────────────────────────────┘

  personality is NOT here — it has a dm trigger,
  not a cron trigger, so it doesn't match layer 1.

  identity IS here — but only as layer 2 inside
  readme-monitor, because readme-monitor includes
  {{identity}} in its template.

Adoption List (10123)

The adoption list serves two purposes:

  1. Registry — makes skills available for{{skill_d_tag}} resolution (layer 2 inclusion)
  2. Ordering — determines the order of layer 1 triggered skills in the context window
{
  "kind": 10123,
  "tags": [
    ["a", "31124:<pubkey>:identity"],
    ["a", "31124:<pubkey>:personality"],
    ["a", "31123:<pubkey>:chat"],
    ["a", "31123:<pubkey>:readme-monitor"]
  ]
}
  • identity — no trigger, adopted so triggered skills can include it via{{identity}} (layer 2)
  • personality — has["trigger", "dm"], appears in layer 1 for DM events. Also referenceable via{{personality}} by other skills (layer 2).
  • chat — has["trigger", "dm"], appears in layer 1 for DM events after personality (adoption list order)
  • readme-monitor — has["trigger", "cron"], appears in layer 1 for cron events. Its template includes{{identity}} (layer 2).

Skills NOT in this list but with trigger tags are still armed — they fire when their trigger matches, but they execute in isolation (no layer 2 skill references available, only built-in variables).


Template Variables

Template variables resolve through tool execution or skill lookup.

When the engine encounters {{variable_name}}:

  1. Check if it matches a known tool — if so, execute the tool and insert the result
  2. Check if it matches an adopted skill's d-tag — if so, insert that skill's content (layer 2)
  3. If neither matches, resolve to empty (for portability)

Built-in Variables

Variable Resolution Description
{{agent_identity}} agent_identity tool Agent identity block
{{admin_profile}} nostr_admin_profile tool Admin kind 0 profile
{{admin_notes}} nostr_admin_notes tool Admin recent notes
{{admin_relays}} nostr_admin_relays tool Admin relay list
{{dm_history}} (expand directive) Recent DM conversation
{{message}} (built-in) Current user message
{{triggering_event}} trigger_event tool Triggering event JSON

Skill Reference Variables

Variable Resolution Description
{{skill_d_tag}} Look up adopted skill by d-tag Layer 2 skill inclusion

Unknown variables resolve to empty values for portability.


Triggers

A skill with trigger tags can be fired automatically by a runtime when matching events occur.

Trigger Types

  • dm — Direct message received
  • cron — Scheduled time expression
  • nostr-subscription — Nostr event matches a filter
  • webhook — HTTP request received
  • chain — Another skill completed execution

Trigger Tags

Tag Required Description
trigger Yes Trigger type
filter Yes Type-specific filter

If a skill is in the adoption list, its triggers are active. There is no separate enabled flag — adoption IS enablement.

Execution Parameter Tags

These tags can appear at the top level of a skill event (defaults for any app) or on trigger-specific contexts (runtime overrides).

Tag Description
llm Model spec with fallback chain (see below)
max_tokens Max output tokens
temperature Sampling temperature
seed Optional deterministic seed

LLM Fallback Chain

The llm tag uses a CSS font-stack style fallback chain. The runtime tries each entry in order, falling back to the next if the previous is unavailable.

Format: provider/model, provider/model, ..., capability_keyword

["llm", "anthropic/claude-sonnet-4-20250514, openai/gpt-4o-mini, cheap"]

This means:

  1. Try anthropic/claude-sonnet-4-20250514 first
  2. If unavailable, try openai/gpt-4o-mini
  3. If unavailable, use whatever the runtime considers cheap

Each entry can be:

  • provider/model — specific provider and model (e.g., anthropic/claude-sonnet-4-20250514)
  • model — model name only, use the default provider (e.g., gpt-4o-mini)
  • Capability keyword — abstract tier the runtime resolves to its best available option

Capability keywords:

Keyword Meaning
cheap Lowest cost model available
fast Lowest latency model available
best Highest capability model available
default Use the agent/app default model

Examples:

["llm", "openai/gpt-4o-mini"]                              -- specific model, no fallback
["llm", "openai/gpt-4o-mini, cheap"]                       -- try gpt-4o-mini, fall back to cheapest
["llm", "anthropic/claude-opus-4-20250514, openai/gpt-4o, best"]  -- try opus, then gpt-4o, then best available
["llm", "fast"]                                             -- just use the fastest available
["llm", "default"]                                          -- use agent/app default

This is important for portability: a skill published with ["llm", "anthropic/claude-sonnet-4-20250514, cheap"] works on any runtime — if the runtime doesn't have Anthropic access, it falls back to its cheapest available model.

Execution Parameter Resolution

When a trigger fires:

  1. Start with agent/app defaults.
  2. Apply top-level execution tags from the skill event.
  3. Walk the llm fallback chain until a usable model is found.
  4. Apply trigger-specific overrides if present.
  5. Execute skill with those effective runtime settings.
  6. Restore defaults after the run.

Trigger Type Details

Each example below shows a complete skill event. Every ["tag", "value"] pair is a separate tag on the Nostr event.

dm

Fires when a direct message is received. filter is JSON with sender scope: {"from":"admin"}, {"from":"wot"}, or {"from":"any"}.

For DM triggers, the raw message content is always appended to the end of the context window.

{
  "kind": 31123,
  "content": "{{identity}}\n\nRespond helpfully to the admin.",
  "tags": [
    ["d", "chat"],
    ["description", "Chat with admin"],
    ["trigger", "dm"],
    ["filter", "{\"from\":\"admin\"}"],
    ["llm", "default"],
    ["requires_skill", "identity"]
  ]
}

cron

Fires on a schedule. filter is a standard 5-field cron expression: minute hour day-of-month month day-of-week.

{
  "kind": 31123,
  "content": "{{identity}}\n\nCheck the readme at the configured URL. If changed, post it and DM admin.",
  "tags": [
    ["d", "readme-monitor"],
    ["description", "Check readme for changes at noon"],
    ["trigger", "cron"],
    ["filter", "0 12 * * *"],
    ["llm", "openai/gpt-4o-mini, cheap"],
    ["max_tokens", "300"],
    ["requires_tool", "http_fetch"],
    ["requires_tool", "memory_read"],
    ["requires_tool", "memory_write"],
    ["requires_skill", "identity"]
  ]
}

nostr-subscription

Fires when a Nostr event matches a subscription filter. filter is a JSON-encoded Nostr subscription filter.

{
  "kind": 31123,
  "content": "{{identity}}\n\nWhen the triggering event mentions Bitcoin or Lightning, summarize and DM admin.",
  "tags": [
    ["d", "mention-monitor"],
    ["description", "Monitor mentions and summarize"],
    ["trigger", "nostr-subscription"],
    ["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
    ["llm", "openai/gpt-4o-mini, cheap"],
    ["temperature", "0"],
    ["requires_tool", "nostr_query"],
    ["requires_tool", "nostr_dm"],
    ["requires_skill", "identity"]
  ]
}

webhook

Fires when an HTTP request is received. filter can be {} (match all).

{
  "kind": 31123,
  "content": "{{identity}}\n\nProcess the webhook payload and take appropriate action.",
  "tags": [
    ["d", "webhook-handler"],
    ["description", "Process incoming webhook"],
    ["trigger", "webhook"],
    ["filter", "{}"]
  ]
}

chain

Fires when another skill completes execution. filter is the source skill's d tag.

{
  "kind": 31123,
  "content": "{{identity}}\n\nReview the output from the previous skill and DM admin a summary.",
  "tags": [
    ["d", "readme-reviewer"],
    ["description", "Review results from readme monitor"],
    ["trigger", "chain"],
    ["filter", "readme-monitor"],
    ["llm", "openai/gpt-4o-mini, cheap"],
    ["requires_skill", "identity"]
  ]
}

Requirements Tags

Skills declare what they need to run. Apps use these tags to determine which skills are compatible with their available capabilities.

Tag Description
requires_tool A tool that must be available for this skill to function
requires_skill An adopted skill that must be present for {{...}} resolution
optional_tool A tool that enhances the skill but is not required
["requires_tool", "http_fetch"],
["requires_tool", "memory_read"],
["requires_tool", "memory_write"],
["requires_tool", "nostr_post"],
["requires_skill", "identity"],
["optional_tool", "nostr_dm"]

How Apps Use Requirements

App starts up
  │
  ├─ Knows its available tools/capabilities
  │
  ├─ Fetches user's adopted skills from 10123
  │
  ├─ For each skill, checks requires_tool tags
  │    ├─ All required tools available? → skill is usable
  │    └─ Missing required tools?       → skill is disabled
  │
  └─ Presents only usable skills to the user

Tool names in requirements tags are capability names, not implementation names. http_fetch is a capability — a C binary implements it with libcurl, a browser implements it with fetch(), a mobile app implements it with its HTTP library. The capability is the same; the implementation varies.


Private Skill Encoding (31124)

Private skills use NIP-44 encryption on event content.

Rules for kind 31124:

  • Keepd tag exposed so the event stays addressable/replaceable.
  • Move non-d metadata into plaintext payload before encryption.
  • Encrypt full payload with NIP-44 and store ciphertext in eventcontent.
  • On receive: resolve byd, decryptcontent, then read content + private tags.

Private Skill Event (on relay)

{
  "kind": 31124,
  "content": "<nip44-ciphertext>",
  "tags": [
    ["d", "mention-monitor"]
  ]
}

Decrypted Private Payload (application-level JSON)

{
  "content": "{{identity}}\n\nWhen {{triggering_event}} includes Bitcoin or Lightning, summarize and DM admin.",
  "private_tags": [
    ["description", "Monitor mentions and DM summaries"],
    ["scope", "private"],
    ["trigger", "nostr-subscription"],
    ["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
    ["llm", "openai/gpt-4o-mini, cheap"],
    ["temperature", "0"],
    ["requires_tool", "nostr_query"],
    ["requires_tool", "nostr_dm"],
    ["requires_skill", "identity"]
  ]
}

Execution Flow

Trigger event occurs (DM, cron, subscription, webhook, chain)
  │
  ├─ Walk adoption list (10123)
  │    │
  │    ├─ For each skill whose trigger matches this event:
  │    │    ├─ Resolve template variables (tools + skill references)
  │    │    └─ Add to context (layer 1)
  │    │
  │    └─ Skills whose trigger does NOT match: skip
  │
  ├─ Append triggering event payload
  │    └─ For DM triggers: always append raw message content
  │
  ├─ Apply execution parameters (llm, temperature, max_tokens, tools)
  │
  └─ Send to LLM → multi-turn tool loop → response

Limits and Safety

Limit Default Description
Max concurrent triggers 16 Prevents resource exhaustion
Trigger cooldown 60s per skill Prevents rapid-fire execution
LLM action rate limit 10/min Prevents runaway LLM costs

Storage on Nostr

Data Storage
Skills Kind 31123/31124 events
Adopted skills Kind 10123 event
Trigger definitions + execution params Tags on skill events
Requirements declarations Tags on skill events

Portability

Skills are Nostr events. Any application that can read Nostr events and call a skill which will call an llm. Skills are not specific to Didactyl or any single runtime.

Use Cases Beyond Didactyl

  • Word processor — "Check spelling and grammar" button triggers a spellcheck skill
  • Browser extension — Highlight text, run a summarization skill
  • Mobile app — Voice input triggers a transcription skill
  • Browser-based agent — Same agent, different runtime, different available tools

Portability Guidelines

Guideline Rationale
Use {{message}} for user input Universal — every app has user input
Declare requirements via requires_tool / requires_skill tags Lets apps filter to compatible skills
Put default execution params as top-level tags Any app can read llm, temperature, etc.
Keep trigger tags as optional runtime hints Apps without trigger systems ignore them
Resolve unknown variables to empty Ensures graceful degradation
Prefer self-contained skills for maximum portability Skills with {{skill_d_tag}} references need the adoption ecosystem
Treat tool names as capabilities, not implementations http_fetch works in C, browser, mobile — same capability, different implementation

A skill should still be useful even when some variables, tools, or referenced skills are unavailable.