15 KiB
Didactyl — Tools & Skills
Overview
Didactyl is a Nostr-first sovereign AI agent. It receives commands via encrypted DMs, reasons about them with an LLM, and takes actions through tools. It stores learned behaviors as skills — Nostr events that define reusable capabilities. Skills can optionally carry triggers — Nostr subscription filters that activate the skill automatically when matching events arrive.
This document describes the complete tools and skills architecture: what they are, how they work, and how they compose into a dynamic, self-modifying agent.
Tools
Tools are the agent's hands. They are hardcoded C functions that the LLM can invoke during a conversation to take actions in the world.
How Tools Work
- Admin sends a DM to didactyl
- The agent builds an LLM request with the message, context, and a JSON schema of all available tools
- The LLM decides whether to call a tool or respond directly
- If a tool is called, didactyl executes it and feeds the result back to the LLM
- The loop repeats until the LLM produces a final text response
- The response is sent back as a DM
sequenceDiagram
participant Admin
participant Agent as Didactyl Agent Loop
participant LLM as LLM API
participant Tools as Tool Registry
Admin->>Agent: Encrypted DM
Agent->>LLM: messages + tool schemas
loop Until final answer
LLM->>Agent: tool_call request
Agent->>Tools: dispatch tool
Tools->>Agent: result JSON
Agent->>LLM: tool result + continue
end
LLM->>Agent: final text response
Agent->>Admin: Encrypted DM reply
Tool Categories
Nostr Event & Messaging Tools
| Tool | Description |
|---|---|
nostr_post |
Publish a Nostr event to connected relays |
nostr_delete |
Request deletion of one or more previously published events (NIP-09 kind 5) |
nostr_react |
React to a Nostr event with like/dislike/emoji (NIP-25 kind 7) |
nostr_query |
Query events from relays using a Nostr filter |
nostr_dm_send |
Send a NIP-04 encrypted DM |
nostr_dm_send_nip17 |
Send a private DM using NIP-17 gift wrap protocol |
Nostr Identity & Utility Tools
| Tool | Description |
|---|---|
nostr_profile_get |
Look up a Nostr profile (kind 0 metadata) by pubkey |
nostr_nip05_lookup |
Look up or verify a NIP-05 identifier (user@domain) |
nostr_encode |
Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr) |
nostr_decode |
Decode a Nostr bech32/nostr: URI into components |
nostr_relay_status |
Get connection status and statistics for all relays |
nostr_relay_info |
Fetch NIP-11 relay information document |
nostr_encrypt |
Encrypt plaintext using NIP-44 for a recipient |
nostr_decrypt |
Decrypt NIP-44 ciphertext from a sender |
nostr_list_manage |
Add/remove tag tuples in replaceable list events (NIP-51 style) |
Skills & Trigger Tools
| Tool | Description |
|---|---|
skill_create |
Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it |
skill_list |
List this agent's published skills, optionally filtered by scope |
skill_adopt |
Adopt a skill by adding its address to kind 10123 adoption list |
skill_remove |
Remove a skill address from kind 10123 adoption list |
skill_search |
Search public skills by query/author and optionally rank by adoption popularity |
trigger_list |
List active triggered skills and their runtime status |
LLM / Model Management Tools
| Tool | Description |
|---|---|
model_get |
Get current active LLM runtime configuration (excluding API key) |
model_set |
Update active LLM configuration and persist it to config.json |
model_list |
List available model IDs using provider OpenAI-compatible /models endpoint |
System & Runtime Tools
| Tool | Description |
|---|---|
my_version |
Return current Didactyl version and metadata from build macros |
http_fetch |
Fetch HTTP(S) resources with optional method, headers, timeout, and body |
shell_exec |
Execute a shell command and return stdout/stderr |
file_read |
Read a local file as text from the configured working directory |
file_write |
Write text content to a local file in the configured working directory |
tool_list |
List available tools with name, description, and JSON parameter schema |
Content Publishing Conveniences
| Tool | Description |
|---|---|
nostr_post_readme |
Publish README.md as kind 30023 with deterministic d-tag readme.md |
nostr_file_md_to_longform_post |
Read a markdown file and publish it as kind 30023 longform post (defaults d-tag to lowercase filename) |
Security Model
Tools are gated by sender tier:
| Tier | Identity | Tools | Response |
|---|---|---|---|
| ADMIN | Configured admin pubkey | All tools | Full LLM with context |
| WOT | In admin's kind 3 contact list | None | Chat-only LLM |
| STRANGER | Anyone else | None | Configurable static response |
Skills
Skills are the agent's learned behaviors. They are Nostr events — stored on relays, portable, shareable, and discoverable by other agents.
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 |
A skill event looks like:
{
"kind": 31123,
"content": "When asked to summarize a thread, query the root event and all replies, then produce a concise summary with key points and sentiment.",
"tags": [
["d", "summarize-thread"],
["app", "didactyl"],
["scope", "public"],
["description", "Summarize a Nostr thread given a root event ID"]
]
}
Skill Lifecycle
flowchart LR
CREATE[Admin asks didactyl to create a skill] --> PUBLISH[skill_create publishes kind 31123/31124]
PUBLISH --> ADOPT[Auto-adopted into kind 10123 list]
ADOPT --> AVAILABLE[Skill available for use]
AVAILABLE --> DISCOVER[Other agents can discover via skill_search]
DISCOVER --> ADOPT_OTHER[Other agents can skill_adopt]
How Skills Are Used Today
Adopted skills are now always-on contextual knowledge for admin DM handling: the agent resolves the local adoption list (kind 10123), caches referenced skills, and injects their instructions into the LLM system context each turn.
To keep context stable and safe, skill injection is bounded by hard caps (per-skill truncation and total skill-context budget), and excess skills are omitted with an explicit budget notice.
Triggered Skills — The Activation System
This is where skills become active. A triggered skill is a skill with a Nostr subscription filter attached. When matching events arrive on the relay, didactyl wakes up and executes the skill automatically — no admin DM required.
Anatomy of a Triggered Skill
A triggered skill extends the standard skill event with trigger-related tags:
{
"kind": 31124,
"content": "DM admin: '{author_display_name} just posted: {content_preview}'",
"tags": [
["d", "watch-jack"],
["app", "didactyl"],
["scope", "private"],
["description", "Notify admin when @jack posts a note"],
["trigger", "nostr-subscription"],
["filter", "{\"authors\":[\"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2\"],\"kinds\":[1]}"],
["action", "template"],
["enabled", "true"]
]
}
Trigger Tags
| Tag | Required | Description |
|---|---|---|
trigger |
Yes | Trigger type. Currently: nostr-subscription |
filter |
Yes | JSON-encoded Nostr subscription filter |
action |
No | Action type: template or llm. Default: llm |
enabled |
No | Whether the trigger is active. Default: true |
Action Types
Template Actions
The skill content is a string template with placeholders that get interpolated from the triggering event:
DM admin: '{author_display_name} posted: {content_preview}'
Available placeholders:
| Placeholder | Source |
|---|---|
{event_id} |
Triggering event ID hex |
{pubkey} |
Author pubkey hex |
{author_display_name} |
Resolved display name, falls back to truncated pubkey |
{kind} |
Event kind number |
{content} |
Full event content |
{content_preview} |
First 280 characters of content |
{created_at} |
Unix timestamp |
{relay_url} |
Relay the event arrived from |
Template actions execute without LLM involvement — they are fast, cheap, and deterministic. The interpolated string is then acted upon based on a simple action prefix:
DM admin: ...— send a DM to the adminDM <pubkey>: ...— send a DM to a specific pubkeyPOST: ...— publish as a kind 1 noteLOG: ...— write to debug log only
LLM-Mediated Actions
The skill content is a prompt. The triggering event is injected as context, and the LLM decides what to do:
You received a note from a watched author. Analyze the note content.
If it mentions Bitcoin or Lightning, summarize it and DM the admin.
If it's a repost or low-effort content, ignore it silently.
Use your tools to take action.
LLM-mediated actions go through the full agent loop — the LLM can call tools, reason about the event, and produce complex multi-step responses.
Trigger Lifecycle
flowchart TD
subgraph Creation
ADMIN_CMD[Admin: 'Warn me when @jack posts'] --> LLM_REASON[LLM resolves pubkey + builds skill]
LLM_REASON --> SKILL_CREATE[skill_create with trigger tags]
SKILL_CREATE --> PUBLISHED[Skill published to Nostr]
end
subgraph Activation
STARTUP[Didactyl starts up] --> LOAD_SKILLS[Load adopted skills from kind 10123]
LOAD_SKILLS --> FIND_TRIGGERS[Find skills with trigger tags]
FIND_TRIGGERS --> SUBSCRIBE[Create Nostr subscriptions for each filter]
end
subgraph Execution
EVENT_IN[Matching event arrives] --> LOOKUP[Find associated skill]
LOOKUP --> CHECK_TYPE{Action type?}
CHECK_TYPE -->|template| INTERPOLATE[Interpolate placeholders]
CHECK_TYPE -->|llm| LLM_LOOP[Run agent loop with event as context]
INTERPOLATE --> EXECUTE_TPL[Execute action prefix]
LLM_LOOP --> EXECUTE_LLM[LLM uses tools to respond]
end
PUBLISHED --> LOAD_SKILLS
Dynamic Subscription Management
Didactyl manages its trigger subscriptions dynamically:
- On startup: Load all adopted skills, find triggered ones, create subscriptions
- On skill_create with trigger: Immediately create a new subscription (no restart needed)
- On skill_remove with trigger: Tear down the associated subscription
- On skill update: Tear down old subscription, create new one if trigger changed
This requires a trigger manager component that:
- Maintains a registry of active trigger subscriptions
- Maps subscription callbacks back to their source skills
- Handles subscription lifecycle (create, update, destroy)
- Enforces limits on concurrent triggers
Limits and Safety
| Limit | Default | Description |
|---|---|---|
| Max concurrent triggers | 16 | Prevents resource exhaustion from too many subscriptions |
| Trigger cooldown | 60s per skill | Prevents rapid-fire execution from high-volume filters |
| LLM action rate limit | 10/min | Prevents runaway LLM costs from triggered skills |
| Template action rate limit | 60/min | Prevents DM spam from template actions |
How It All Fits Together
flowchart TD
subgraph Activation Sources
DM_IN[DM from Admin/WoT]
TRIGGER_EVENT[Nostr event matching a trigger filter]
end
subgraph Agent Core
DISPATCHER{Dispatcher}
AGENT_LOOP[Agent Loop - LLM + Tools]
TEMPLATE_ENGINE[Template Engine]
end
subgraph Nostr
RELAYS[Relays]
SKILLS_STORE[Skills - kind 31123/31124]
ADOPTION[Adoption List - kind 10123]
end
subgraph Actions
DM_OUT[Send DM]
POST[Publish Note]
TOOL_EXEC[Execute Tool]
end
DM_IN --> DISPATCHER
TRIGGER_EVENT --> DISPATCHER
DISPATCHER -->|DM message| AGENT_LOOP
DISPATCHER -->|template trigger| TEMPLATE_ENGINE
DISPATCHER -->|llm trigger| AGENT_LOOP
AGENT_LOOP --> TOOL_EXEC
AGENT_LOOP --> DM_OUT
TEMPLATE_ENGINE --> DM_OUT
TEMPLATE_ENGINE --> POST
TOOL_EXEC -->|skill_create| SKILLS_STORE
TOOL_EXEC -->|skill_adopt| ADOPTION
TOOL_EXEC -->|nostr_post| RELAYS
TOOL_EXEC -->|nostr_dm| DM_OUT
The Activation Flow
Today, didactyl has one activation source: DMs. With triggered skills, it gains a second: any Nostr event matching a trigger filter.
Both paths converge at the dispatcher, which routes to either:
- The agent loop (for DMs and LLM-mediated triggers)
- The template engine (for template triggers — fast path, no LLM)
Self-Modification
The most powerful aspect: didactyl can create its own triggers. The admin says "watch for mentions of me on Nostr" and the LLM:
- Resolves the admin's pubkey
- Crafts a Nostr filter:
{"#p": ["<admin_pubkey>"], "kinds": [1]} - Writes a skill with trigger tags via
skill_create - The trigger manager picks it up and creates the subscription
- From now on, didactyl monitors mentions autonomously
The admin can later say "stop watching for mentions" and didactyl removes the skill, tearing down the subscription.
Storage — Everything on Nostr
All state lives on Nostr:
| Data | Storage |
|---|---|
| Agent identity | Kind 0 profile event |
| Agent relay list | Kind 10002 event |
| Agent contact list | Kind 3 event |
| Skills | Kind 31123/31124 events |
| Adopted skills | Kind 10123 event |
| Trigger definitions | Tags on skill events |
| Conversation history | Kind 4 DM events on relays |
| Agent soul/personality | Startup event content in config |
The only local state is config.json (keys, relay URLs, LLM config) and the runtime in-memory state (active subscriptions, LLM context).
Future Extensions
Trigger Types Beyond Nostr Subscriptions
The trigger tag is designed to be extensible:
| Trigger Type | Description |
|---|---|
nostr-subscription |
Match events via Nostr filter (implemented first) |
cron |
Time-based triggers — "every day at 9am, post a GM" |
webhook |
HTTP webhook triggers — external systems wake didactyl |
chain |
Output of one skill triggers another skill |
Skill Composition
Skills could reference other skills, building complex behaviors from simple primitives:
When triggered, run skill 'translate-to-english' on the note content,
then run skill 'sentiment-analysis' on the translation,
then DM admin with the result if sentiment is negative.
Agent-to-Agent Skill Sharing
Since skills are Nostr events, agents can:
- Discover skills published by other agents via
skill_search - Adopt skills from other agents via
skill_adopt - Share triggered skill patterns across a network of agents
Trigger Marketplace
With kind 10123 adoption lists being public, a natural marketplace emerges:
- Agents publish useful triggered skills
- Other agents discover and adopt them
- Popular triggers rise to the top via adoption count