v0.0.44 - Migrate config to JSONC with comment stripping parser and docs updates

This commit is contained in:
Your Name
2026-03-06 03:10:38 -04:00
parent 1bbae06722
commit 120664571f
15 changed files with 5730 additions and 712 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
/c-relay/
/nips/
/config.json
/config.jsonc
test_keys.txt
/mongoose/

View File

@@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.0.43
## Current Status — v0.0.44
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.43Unify self-authored event cache for skills and adoption list
> Last release update: v0.0.44Migrate config to JSONC with comment stripping parser and docs updates
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
@@ -80,7 +80,7 @@ Didactyl will support local inference, which is very privacy preserving. Remote
```bash
chmod +x ./didactyl_static_x86_64
./didactyl_static_x86_64 --config ./config.json
./didactyl_static_x86_64 --config ./config.jsonc
```
### Build from source (optional)
@@ -99,7 +99,7 @@ chmod +x ./didactyl_static_x86_64
### Configure
Edit [`config.json`](config.json):
Edit [`config.jsonc`](config.jsonc):
```json
{
@@ -176,13 +176,13 @@ Relays are sourced exclusively from startup kind `10002` `r` tags.
### Run
```bash
./didactyl_static_x86_64 --config ./config.json
./didactyl_static_x86_64 --config ./config.jsonc
```
Options:
```
./didactyl_static_x86_64 --config <path> # custom config file (default: ./config.json)
./didactyl_static_x86_64 --config <path> # custom config file (default: ./config.jsonc)
./didactyl_static_x86_64 --debug <0-5> # log verbosity (0 none, 3 info, 5 trace)
./didactyl_static_x86_64 --dump-schemas # print tool JSON schemas and exit
./didactyl_static_x86_64 --test-tool <name> <args_json> # run one tool directly and print JSON result
@@ -195,7 +195,7 @@ CLI debugger notes:
- Example:
```bash
./didactyl_static_x86_64 --config ./config.json --test-tool nostr_file_md_to_longform_post '{"file":"docs/TOOLS_AND_SKILLS.md","title":"TOOLS_AND_SKILLS"}'
./didactyl_static_x86_64 --config ./config.jsonc --test-tool nostr_file_md_to_longform_post '{"file":"docs/TOOLS_AND_SKILLS.md","title":"TOOLS_AND_SKILLS"}'
```
### Talk to it
@@ -287,7 +287,7 @@ Skills are shared across Nostr without any centralized registry or approval proc
## Startup
Didactyl startup behavior is configured in [`config.json`](config.json) under `startup_events`.
Didactyl startup behavior is configured in [`config.jsonc`](config.jsonc) under `startup_events`.
Also used at startup:
@@ -370,7 +370,7 @@ A localhost-only HTTP API on port `8484` (configurable) for agent inspection and
| `POST /api/prompt/run` | Run a full messages array with tools enabled |
| `POST /api/prompt/compare` | A/B compare two prompt variants |
| `GET /api/model` | Current LLM model config |
| `PUT /api/model` | Change model at runtime (persists to config.json) |
| `PUT /api/model` | Change model at runtime (persists to config.jsonc) |
| `GET /api/models` | List available models from provider |
Full reference: [`docs/API.md`](docs/API.md). Frontend brief: [`plans/admin_web_frontend.md`](plans/admin_web_frontend.md).
@@ -379,7 +379,7 @@ Full reference: [`docs/API.md`](docs/API.md). Frontend brief: [`plans/admin_web_
```
.
├── config.json # Agent/runtime config including startup_events + tools
├── config.jsonc # Agent/runtime config (JSONC with comments) including startup_events + tools
├── context.log # Appended outbound LLM context payloads
├── Makefile # Build system
├── build_static.sh # Preferred final build validation
@@ -433,7 +433,7 @@ All dependencies are statically linked into the binary at build time. No system
- [x] Adopted skills injected into LLM context automatically
- [x] Triggered skills — Nostr event filters that fire skill execution automatically
- [x] Localhost HTTP admin API — context inspection, prompt crafting, A/B comparison
- [x] Runtime model switching via `model_set` tool (persists to config.json)
- [x] Runtime model switching via `model_set` tool (persists to config.jsonc)
- [x] Soul-embedded prompt templates (`---template---`) — configurable context order, variable resolution, provider overrides
- [ ] Runtime skill loading from adopted `31123` events on relays
- [ ] Skill discovery CLI/tool (query WoT adoption lists)

View File

@@ -1,271 +0,0 @@
{
"keys": {
"nsec": "agent nsec",
"npub": "agent npub",
"npubHex": "agent hex pubkey",
"nsecHex": "agent hex secret key"
},
"admin": {
"pubkey": "admin pubkey"
},
"dm_protocol": "nip04",
"llm": {
"provider": "",
"api_key": "",
"model": "",
"base_url": "",
"max_tokens": 512,
"temperature": 0.7
},
"security": {
"verify_signatures": true,
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
"tiers": {
"admin": {
"tools_enabled": true
},
"wot": {
"enabled": true,
"tools_enabled": false
},
"stranger": {
"enabled": true
}
}
},
"admin_context": {
"enabled": true,
"subscribe_kinds": [
0,
3,
10002,
1
],
"kind_1_limit": 10
},
"api": {
"enabled": true,
"port": 8484,
"bind_address": "127.0.0.1"
},
"startup_events": [
{
"kind": 0,
"content_fields": {
"name": "Didactyl Agent",
"display_name": "Didactyl",
"about": "A sovereign AI agent on Nostr",
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
},
"tags": []
},
{
"kind": 10002,
"content": "",
"tags": [
[
"r",
"wss://relay.damus.io"
],
[
"r",
"wss://nos.lol"
],
[
"r",
"wss://relay.primal.net"
],
[
"r",
"ws://127.0.0.1:7777"
]
]
},
{
"kind": 10050,
"content": "",
"tags": [
[
"relay",
"wss://relay.damus.io"
],
[
"relay",
"wss://nos.lol"
]
]
},
{
"kind": 1,
"content": "Hello world from Didactyl startup",
"tags": [
[
"t",
"didactyl"
],
[
"t",
"startup"
]
]
},
{
"kind": 3,
"content": "",
"tags": []
},
{
"kind": 31120,
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n content: |\n ## Administrator Identity (source: config.admin.pubkey)\n\n This is your administrator! Admin pubkey (hex): {{admin_pubkey}}\n\n- section: admin_profile\n role: system\n content: |\n ## Administrator Kind 0 Profile (source: nostr kind 0)\n\n Administrator kind 0 profile content (JSON): {{admin_kind0_json}}\n provider:\n anthropic: |\n <admin_kind0_profile source=\"nostr_kind_0\">\n {{admin_kind0_json}}\n </admin_kind0_profile>\n\n- section: admin_relay_list\n role: system\n content: |\n ## Administrator Relay List (source: nostr kind 10002)\n\n Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}\n\n- section: startup_events\n role: system\n content: |\n ## Startup Events Memory (source: config.startup_events)\n\n Startup events memory (kinds/content/tags): {{startup_events_json}}\n\n- section: adopted_skills\n role: system\n content: |\n {{adopted_skills_content}}\n\n- section: agent_tasks\n role: system\n content: |\n {{tasks_content}}\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: admin_notes\n role: system\n content: |\n ## Administrator Recent Notes (source: nostr kind 1)\n\n {{admin_notes_content}}",
"tags": [
[
"d",
"soul"
],
[
"app",
"didactyl"
],
[
"scope",
"private"
]
]
},
{
"kind": 31123,
"content_fields": {
"name": "long_form_note",
"description": "How to publish a NIP-23 long-form article (kind 30023)",
"nip": "NIP-23",
"event_kind": 30023,
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
"required_tags": {
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
"title": "Human-readable article title.",
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
},
"optional_tags": {
"summary": "Short 1-2 sentence summary.",
"image": "URL for article preview image.",
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
},
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
"procedure": [
"Determine title and d tag from admin input or source material.",
"Draft markdown body content.",
"Set published_at as unix seconds string.",
"Add optional summary/image/t tags when known.",
"Publish with nostr_post kind 30023 including tags array."
]
},
"tags": [
[
"d",
"long_form_note"
],
[
"app",
"didactyl"
],
[
"scope",
"public"
],
[
"slug",
"long_form_note"
]
]
},
{
"kind": 31123,
"content_fields": {
"name": "post_readme_to_nostr",
"description": "Read README.md from the repo and publish it as a NIP-23 long-form note",
"uses_skill": "long_form_note",
"event_kind": 30023,
"procedure": [
"Read README.md using file_read with path README.md.",
"Set d tag exactly to readme.md.",
"Set title from the first markdown H1 heading in README.md.",
"Set summary from the opening paragraph of README.md.",
"Set image from project metadata when available (kind 0 picture/banner), otherwise omit image tag.",
"Generate 3-6 lowercase t tags from README section topics.",
"Set published_at to current unix timestamp as string.",
"Publish with nostr_post kind 30023, full README markdown in content, and full NIP-23 tag set."
],
"required_values": {
"d": "readme.md",
"summary_source": "opening paragraph"
}
},
"tags": [
[
"d",
"post_readme_to_nostr"
],
[
"app",
"didactyl"
],
[
"scope",
"public"
],
[
"slug",
"post_readme_to_nostr"
]
]
},
{
"kind": 31124,
"content_fields": {
"name": "admin_ops",
"description": "Private operational procedures"
},
"tags": [
[
"d",
"admin_ops"
],
[
"app",
"didactyl"
],
[
"scope",
"private"
],
[
"slug",
"admin_ops"
]
]
},
{
"kind": 10123,
"content": "",
"tags": [
[
"a",
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
],
[
"a",
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
],
[
"app",
"didactyl"
],
[
"scope",
"public"
]
]
}
]
}

245
config.jsonc.example Normal file
View File

@@ -0,0 +1,245 @@
{
// ─── Agent Identity Keys ───────────────────────────────────────────
// Your agent's Nostr keypair. Provide nsec (bech32) or hex format.
// The public key fields are optional — they are derived automatically.
"keys": {
"nsec": "agent nsec",
"npub": "agent npub",
"npubHex": "agent hex pubkey",
"nsecHex": "agent hex secret key"
},
// ─── Administrator ─────────────────────────────────────────────────
// The admin pubkey (npub or hex) controls who can issue privileged
// commands and use tools via DM.
"admin": {
"pubkey": "admin pubkey"
},
// ─── DM Protocol ──────────────────────────────────────────────────
// Which encrypted DM protocol to use: "nip04", "nip17", or "both"
"dm_protocol": "nip04",
// ─── LLM Provider ─────────────────────────────────────────────────
// Configure the language model backend. Any OpenAI-compatible API works.
"llm": {
"provider": "", // e.g. "openai", "anthropic", etc.
"api_key": "", // your API key
"model": "", // model identifier, e.g. "gpt-4o-mini"
"base_url": "", // API base URL, e.g. "https://api.openai.com/v1"
"max_tokens": 512, // max tokens per LLM response
"temperature": 0.7 // sampling temperature (0.0 2.0)
},
// ─── Security Tiers ───────────────────────────────────────────────
// Controls who can interact with the agent and what they can do.
"security": {
"verify_signatures": true, // verify Nostr event signatures
// Message sent to strangers outside the web of trust
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
"tiers": {
"admin": {
"tools_enabled": true // admin can always use tools
},
"wot": {
"enabled": true, // respond to web-of-trust contacts
"tools_enabled": false // WoT contacts cannot use tools by default
},
"stranger": {
"enabled": true // respond to strangers (with stranger_response)
}
}
},
// ─── Admin Context Subscriptions ──────────────────────────────────
// Subscribe to the admin's Nostr events to build context awareness.
"admin_context": {
"enabled": true,
"subscribe_kinds": [
0, // kind 0: profile metadata
3, // kind 3: contact list
10002, // kind 10002: relay list
1 // kind 1: text notes
],
"kind_1_limit": 10 // max recent kind-1 notes to track
},
// ─── HTTP Admin API ───────────────────────────────────────────────
// Local REST API for runtime inspection and control.
"api": {
"enabled": true,
"port": 8484,
"bind_address": "127.0.0.1" // bind to localhost only
},
// ─── Startup Events ───────────────────────────────────────────────
// Events published on boot. Includes profile, relay list, soul,
// skills, and adoption list.
"startup_events": [
// Kind 0: Agent profile metadata (NIP-01)
{
"kind": 0,
"content_fields": {
"name": "Didactyl Agent",
"display_name": "Didactyl",
"about": "A sovereign AI agent on Nostr",
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
},
"tags": []
},
// Kind 10002: Relay list (NIP-65)
// These relays are used for connecting and publishing events.
{
"kind": 10002,
"content": "",
"tags": [
["r", "wss://relay.damus.io"],
["r", "wss://nos.lol"],
["r", "wss://relay.primal.net"],
["r", "ws://127.0.0.1:7777"]
]
},
// Kind 10050: DM relay list (NIP-17)
// Relays used specifically for receiving encrypted DMs.
{
"kind": 10050,
"content": "",
"tags": [
["relay", "wss://relay.damus.io"],
["relay", "wss://nos.lol"]
]
},
// Kind 1: Startup announcement note
{
"kind": 1,
"content": "Hello world from Didactyl startup",
"tags": [
["t", "didactyl"],
["t", "startup"]
]
},
// Kind 3: Contact list (initially empty)
{
"kind": 3,
"content": "",
"tags": []
},
// Kind 31120: Soul event — the agent's personality and behavior rules.
// Contains the system prompt and template sections for LLM context.
// The ---template--- marker separates the system prompt from
// structured context sections that are injected at runtime.
{
"kind": 31120,
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Task Management\n- Maintain and use your internal task list as short-term working memory.\n- Break long or complex actions into clear tasks before executing them.\n- Update task status as you complete steps so your plan stays accurate.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n content: |\n ## Administrator Identity (source: config.admin.pubkey)\n\n This is your administrator! Admin pubkey (hex): {{admin_pubkey}}\n\n- section: admin_profile\n role: system\n content: |\n ## Administrator Kind 0 Profile (source: nostr kind 0)\n\n Administrator kind 0 profile content (JSON): {{admin_kind0_json}}\n provider:\n anthropic: |\n <admin_kind0_profile source=\"nostr_kind_0\">\n {{admin_kind0_json}}\n </admin_kind0_profile>\n\n- section: admin_contacts\n role: system\n content: |\n ## Administrator Contact List (source: nostr kind 3)\n\n Administrator kind 3 contact list pubkeys (JSON array): {{admin_kind3_json}}\n provider:\n anthropic: |\n <admin_contacts source=\"nostr_kind_3\">\n {{admin_kind3_json}}\n </admin_contacts>\n\n- section: admin_relays\n role: system\n content: |\n ## Administrator Relay List (source: nostr kind 10002)\n\n Administrator kind 10002 relay list (JSON): {{admin_kind10002_json}}\n provider:\n anthropic: |\n <admin_relays source=\"nostr_kind_10002\">\n {{admin_kind10002_json}}\n </admin_relays>\n\n- section: admin_notes\n role: system\n content: |\n ## Administrator Recent Notes (source: nostr kind 1)\n\n Administrator recent kind 1 notes (JSON array): {{admin_kind1_json}}\n provider:\n anthropic: |\n <admin_notes source=\"nostr_kind_1\">\n {{admin_kind1_json}}\n </admin_notes>\n\n- section: skills\n role: system\n content: |\n ## Adopted Skills\n\n {{skills_json}}\n\n- section: tasks\n role: system\n content: |\n ## Current Task List\n\n {{tasks_json}}\n\n- section: conversation\n role: conversation\n content: |\n {{conversation}}",
"tags": [
["d", "soul"],
["app", "didactyl"],
["scope", "private"]
]
},
// Kind 31123: Public skill — long_form_note
// Teaches the agent how to publish NIP-23 long-form articles.
{
"kind": 31123,
"content_fields": {
"name": "long_form_note",
"description": "How to publish a NIP-23 long-form article (kind 30023)",
"nip": "NIP-23",
"event_kind": 30023,
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
"required_tags": {
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
"title": "Human-readable article title.",
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
},
"optional_tags": {
"summary": "Short 1-2 sentence summary.",
"image": "URL for article preview image.",
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
},
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
"procedure": [
"Determine title and d tag from admin input or source material.",
"Draft markdown body content.",
"Set published_at as unix seconds string.",
"Add optional summary/image/t tags when known.",
"Publish with nostr_post kind 30023 including tags array."
]
},
"tags": [
["d", "long_form_note"],
["app", "didactyl"],
["scope", "public"],
["slug", "long_form_note"]
]
},
// Kind 31123: Public skill — post_readme_to_nostr
// Reads README.md and publishes it as a long-form note.
{
"kind": 31123,
"content_fields": {
"name": "post_readme_to_nostr",
"description": "Read README.md from the repo and publish it as a NIP-23 long-form note",
"uses_skill": "long_form_note",
"event_kind": 30023,
"procedure": [
"Read README.md using file_read with path README.md.",
"Set d tag exactly to readme.md.",
"Set title from the first markdown H1 heading in README.md.",
"Set summary from the opening paragraph of README.md.",
"Set image from project metadata when available (kind 0 picture/banner), otherwise omit image tag.",
"Generate 3-6 lowercase t tags from README section topics.",
"Set published_at to current unix timestamp as string.",
"Publish with nostr_post kind 30023, full README markdown in content, and full NIP-23 tag set."
],
"required_values": {
"d": "readme.md",
"summary_source": "opening paragraph"
}
},
"tags": [
["d", "post_readme_to_nostr"],
["app", "didactyl"],
["scope", "public"],
["slug", "post_readme_to_nostr"]
]
},
// Kind 31124: Private skill — admin_ops
// Private operational procedures (admin-only).
{
"kind": 31124,
"content_fields": {
"name": "admin_ops",
"description": "Private operational procedures"
},
"tags": [
["d", "admin_ops"],
["app", "didactyl"],
["scope", "private"],
["slug", "admin_ops"]
]
},
// Kind 10123: Skill adoption list
// References which public skills this agent has adopted.
{
"kind": 10123,
"content": "",
"tags": [
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"],
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"],
["app", "didactyl"],
["scope", "public"]
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@ All responses are JSON. CORS headers are included on every response for browser
## Configuration
Enable the API in `config.json`:
Enable the API in `config.jsonc`:
```json
{

367
docs/SKILLS.md Normal file
View File

@@ -0,0 +1,367 @@
# Didactyl — Skills
See also: [TOOLS.md](TOOLS.md)
## Overview
A skill is a **self-contained LLM execution unit** stored as a Nostr event. Each skill defines what the LLM sees (context template), which model runs it (LLM specification with fallbacks), what tools are available, and whether the agent's identity (soul) is included.
Skills are portable, shareable, and discoverable — they live on Nostr relays as standard events.
---
## 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 skill event's `content` field is a JSON object that defines the complete execution specification:
```json
{
"kind": 31123,
"content": {
"description": "Check spelling and grammar",
"context_mode": "full",
"llm": "openai/gpt-4o-mini, cheap",
"tools": false,
"max_tokens": 2000,
"temperature": 0.1,
"template": "system:\nYou are a spelling and grammar checker.\n\nRules:\n- Fix spelling errors\n- Fix grammar errors\n- Preserve original formatting\n- Ignore: API, JSON, HTTP, nostr, pubkey, npub, nsec, NIP\n- Canadian English preferred\n- Return ONLY the corrected text, no explanations\n\nuser:\n{{message}}"
},
"tags": [
["d", "spellcheck"],
["scope", "public"],
["description", "Spelling and grammar checker"]
]
}
```
### Content Fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `description` | string | — | Human-readable description |
| `context_mode` | string | `inject` | `inject`, `full`, or `override` |
| `llm` | string | `default` | LLM fallback chain |
| `tools` | bool/array | `true` | `false` = no tools, `true` = all tools, array = specific tool names |
| `template` | string | — | Context template (required for `full` mode) |
| `soul` | bool | `true` | Whether to include the agent's soul in context |
| `max_tokens` | int | — | Override max tokens for this skill |
| `temperature` | float | — | Override temperature for this skill |
---
## Context Modes
Skills control how the LLM context window is assembled.
### inject
The skill's instructions are appended to the agent's soul context. The soul is always present.
```
┌─────────────────────────────────────┐
│ Soul (agent identity + rules) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Skill instructions │ │
│ ├─────────────────────────────┤ │
│ │ Admin context, history, etc │ │
│ └─────────────────────────────┘ │
│ │
│ User message │
└─────────────────────────────────────┘
```
### full
The skill provides its own context template. The soul is **not included** unless the template explicitly references `{{soul}}`. This is for specialized, focused tasks that don't need the agent's identity.
```
┌─────────────────────────────────────┐
│ Skill-defined system prompt │
│ (from skill template) │
│ │
│ User message │
└─────────────────────────────────────┘
```
A `full` mode skill can still include the soul if desired:
```
system:
{{soul}}
You are now in translation mode. Translate the following to {{target_language}}.
Respond ONLY with the translation.
user:
{{message}}
```
### override
The skill replaces the soul's system prompt but keeps the standard context assembly structure (admin context, history, tools, etc.).
### Summary
| Mode | Soul | Template | Tools | Use Case |
|------|------|----------|-------|----------|
| `inject` | ✅ Always | Soul's template + skill instructions appended | Agent default | Behavioral rules, knowledge |
| `full` | ❌ Unless `{{soul}}` in template | Skill provides template | Skill specifies | Spellcheck, translation, focused analysis |
| `override` | Replaced by skill | Soul's template structure | Agent default | Different personality, same capabilities |
### Template Variables
A skill template can reference these context parts:
| Variable | Source |
|----------|--------|
| `{{soul}}` | The agent's identity/system context |
| `{{admin_profile}}` | Admin kind 0 profile |
| `{{admin_notes}}` | Admin recent notes |
| `{{admin_relays}}` | Admin relay list |
| `{{adopted_skills}}` | Other adopted skill instructions |
| `{{dm_history}}` | Recent DM conversation |
| `{{message}}` | Current user message |
| `{{triggering_event}}` | For triggered skills, the event JSON |
| `{{tools}}` | Available tool schemas |
---
## LLM Specification with Fallback Chains
Each skill specifies which LLM to use with CSS font-family-style fallbacks. The runtime tries each model in order; if one is unavailable (API error, rate limit, not configured), it falls back to the next.
### Format
```
model-spec := model-ref ["," model-ref]*
model-ref := provider "/" model-name
| category-alias
```
### Examples
| Skill | LLM Spec |
|-------|----------|
| Spelling checker | `openai/gpt-4o-mini, cheap` |
| Complex analysis | `anthropic/claude-sonnet-4-20250514, openai/gpt-4o, smart` |
| Translation | `openai/gpt-4o-mini, fast` |
| Default behavior | `default` |
### Category Aliases
Aliases map to models in the agent's config:
```json
{
"llm": {
"provider": "openai",
"model": "gpt-4o",
"aliases": {
"fast": "openai/gpt-4o-mini",
"cheap": "openai/gpt-4o-mini",
"smart": "anthropic/claude-sonnet-4-20250514"
}
}
}
```
If all specified models fail, the agent's default model is used as a last resort.
```mermaid
sequenceDiagram
participant Skill as Skill Spec
participant Resolver as LLM Resolver
participant API1 as Model 1
participant API2 as Model 2
participant Default as Default Model
Skill->>Resolver: "anthropic/claude-sonnet, openai/gpt-4o-mini, cheap"
Resolver->>API1: try anthropic/claude-sonnet
alt Available
API1-->>Resolver: ✅
else Unavailable
API1-->>Resolver: ❌
Resolver->>API2: try openai/gpt-4o-mini
alt Available
API2-->>Resolver: ✅
else Unavailable
Resolver->>Default: resolve "cheap" alias
end
end
```
---
## Triggered Skills
A triggered skill has a Nostr subscription filter attached. When matching events arrive, the skill executes automatically.
### Trigger Tags
| Tag | Required | Description |
|---|---|---|
| `trigger` | Yes | Trigger type: `nostr-subscription` |
| `filter` | Yes | JSON-encoded Nostr subscription filter |
| `action` | No | `template` or `llm` (default: `llm`) |
| `enabled` | No | Whether active (default: `true`) |
### Template Actions
Fast, deterministic, no LLM. The skill content is a template with placeholders from the triggering event:
```
DM admin: '{author_display_name} posted: {content_preview}'
```
Placeholders: `{event_id}`, `{pubkey}`, `{author_display_name}`, `{kind}`, `{content}`, `{content_preview}`, `{created_at}`, `{relay_url}`
Output prefixes: `DM admin: ...`, `DM <pubkey>: ...`, `POST: ...`, `LOG: ...`
### LLM-Mediated Actions
The skill content defines the execution context. The triggering event is available as `{{triggering_event}}`. The skill's LLM spec, context mode, and tool access all apply.
```json
{
"kind": 31124,
"content": {
"description": "Analyze mentions and notify admin",
"context_mode": "full",
"llm": "openai/gpt-4o-mini, cheap",
"tools": ["nostr_dm_send"],
"template": "system:\nYou monitor Nostr mentions. When the triggering event mentions Bitcoin or Lightning, summarize it and DM the admin. Otherwise, ignore silently.\n\nTriggering event:\n{{triggering_event}}"
},
"tags": [
["d", "mention-monitor"],
["trigger", "nostr-subscription"],
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
["action", "llm"],
["enabled", "true"]
]
}
```
### Trigger Lifecycle
```mermaid
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 + execute prefix]
CHECK_TYPE -->|llm| RESOLVE[Resolve LLM + assemble context + run]
end
PUBLISHED --> LOAD_SKILLS
```
---
## Execution Flow
```mermaid
sequenceDiagram
participant Input as Message/Trigger
participant Dispatch as Dispatcher
participant Skill as Skill Resolver
participant LLM_Res as LLM Resolver
participant Ctx as Context Assembler
participant LLM as LLM API
Input->>Dispatch: message or trigger event
Dispatch->>Skill: which skill handles this?
alt No specific skill
Skill-->>Dispatch: use default soul context
Dispatch->>LLM_Res: resolve "default" LLM
else Skill with context_mode=inject
Skill-->>Dispatch: soul + skill instructions
Dispatch->>LLM_Res: resolve skill.llm or "default"
else Skill with context_mode=full
Skill-->>Dispatch: skill template only
Dispatch->>LLM_Res: resolve skill.llm
end
LLM_Res->>LLM_Res: try models in fallback order
LLM_Res-->>Dispatch: resolved model
Dispatch->>Ctx: assemble context per mode
Ctx->>LLM: request with resolved model + context
LLM-->>Input: response
```
---
## The Soul and Skills
The **soul** is the agent's default identity and context template. Skills interact with it based on their context mode:
- **`inject`** — soul always present; skill instructions layered on top
- **`full`** — soul absent unless template includes `{{soul}}`
- **`override`** — skill replaces soul prompt, keeps standard context structure
A spelling checker runs with no soul — purely functional, minimal context, cheap model. A complex analysis skill includes the full soul and all context parts. The soul is the default, not a requirement.
---
## 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 |
| Template action rate limit | 60/min | Prevents DM spam |
---
## Storage on Nostr
| Data | Storage |
|---|---|
| Skills | Kind 31123/31124 events |
| Adopted skills | Kind 10123 event |
| Trigger definitions | Tags on skill events |
---
## Future Extensions
| Extension | Description |
|---|---|
| `cron` triggers | Time-based triggers |
| `webhook` triggers | HTTP webhook triggers |
| `chain` triggers | Output of one skill triggers another |
| Skill composition | Pipeline multiple skills |
| Agent-to-agent sharing | Discover and adopt skills across agents |
| Trigger marketplace | Popular triggers rise via adoption count |
---
## Related Documentation
- Tool architecture and complete tool catalog: [TOOLS.md](TOOLS.md)
- Combined index page: [TOOLS_AND_SKILLS.md](TOOLS_AND_SKILLS.md)

132
docs/TOOLS.md Normal file
View File

@@ -0,0 +1,132 @@
# Didactyl — Tools
See also: [SKILLS.md](SKILLS.md)
## Overview
Didactyl is a **Nostr-first sovereign AI agent** that receives commands via encrypted DMs, reasons with an LLM, and takes actions through **tools**.
This document describes the tools architecture: what tools are, how they are exposed to the model, how execution loops work, what tool categories exist, and how access is gated.
---
## What Tools Are
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
1. Admin sends a DM to didactyl
2. The agent builds an LLM request with the message, context, and a JSON schema of all available tools
3. The LLM decides whether to call a tool or respond directly
4. If a tool is called, didactyl executes it and feeds the result back to the LLM
5. The loop repeats until the LLM produces a final text response
6. The response is sent back as a DM
```mermaid
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
These tools manage skill and trigger lifecycle; skill semantics and trigger execution details are documented in [SKILLS.md](SKILLS.md).
| 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.jsonc` |
| `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
Tool access is 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 |
---
## Related Documentation
- Skill definitions, adoption, triggers, and autonomous activation: [SKILLS.md](SKILLS.md)
- Combined index page: [TOOLS_AND_SKILLS.md](TOOLS_AND_SKILLS.md)

View File

@@ -1,419 +0,0 @@
# 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
1. Admin sends a DM to didactyl
2. The agent builds an LLM request with the message, context, and a JSON schema of all available tools
3. The LLM decides whether to call a tool or respond directly
4. If a tool is called, didactyl executes it and feeds the result back to the LLM
5. The loop repeats until the LLM produces a final text response
6. The response is sent back as a DM
```mermaid
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:
```json
{
"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
```mermaid
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:
```json
{
"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 admin
- `DM <pubkey>: ...` — send a DM to a specific pubkey
- `POST: ...` — publish as a kind 1 note
- `LOG: ...` — 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
```mermaid
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:
1. **On startup**: Load all adopted skills, find triggered ones, create subscriptions
2. **On skill_create with trigger**: Immediately create a new subscription (no restart needed)
3. **On skill_remove with trigger**: Tear down the associated subscription
4. **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
```mermaid
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:
1. Resolves the admin's pubkey
2. Crafts a Nostr filter: `{"#p": ["<admin_pubkey>"], "kinds": [1]}`
3. Writes a skill with trigger tags via `skill_create`
4. The trigger manager picks it up and creates the subscription
5. 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

View File

@@ -12,6 +12,88 @@
#include "cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
/* ---------------------------------------------------------------------------
* jsonc_strip_comments strip JSONC comments from a buffer
*
* Returns a newly malloc'd string containing valid JSON (caller must free).
* The function is careful to skip over JSON string literals so that comment
* characters inside strings are preserved. Handles:
* - single-line comments // (to end of line)
* - block comments (slash-star ... star-slash, may span lines)
* - escaped quotes inside strings \"
* - trailing commas before ] or } are NOT removed (cJSON tolerates them)
*
* Returns NULL on allocation failure.
* ------------------------------------------------------------------------ */
char* jsonc_strip_comments(const char* src, size_t src_len) {
if (!src || src_len == 0) {
char* empty = (char*)malloc(1);
if (empty) empty[0] = '\0';
return empty;
}
char* out = (char*)malloc(src_len + 1);
if (!out) return NULL;
size_t o = 0;
size_t i = 0;
while (i < src_len) {
/* Inside a JSON string literal — copy verbatim until closing quote */
if (src[i] == '"') {
out[o++] = src[i++]; /* opening quote */
while (i < src_len) {
if (src[i] == '\\' && i + 1 < src_len) {
out[o++] = src[i++]; /* backslash */
out[o++] = src[i++]; /* escaped char */
} else if (src[i] == '"') {
out[o++] = src[i++]; /* closing quote */
break;
} else {
out[o++] = src[i++];
}
}
continue;
}
/* Single-line comment // */
if (src[i] == '/' && i + 1 < src_len && src[i + 1] == '/') {
/* skip to end of line */
i += 2;
while (i < src_len && src[i] != '\n') {
i++;
}
/* preserve the newline so line numbers stay meaningful */
if (i < src_len) {
out[o++] = src[i++]; /* the \n */
}
continue;
}
/* Block comment (slash-star ... star-slash) */
if (src[i] == '/' && i + 1 < src_len && src[i + 1] == '*') {
i += 2;
while (i + 1 < src_len && !(src[i] == '*' && src[i + 1] == '/')) {
/* preserve newlines inside block comments */
if (src[i] == '\n') {
out[o++] = '\n';
}
i++;
}
if (i + 1 < src_len) {
i += 2; /* skip closing */
}
continue;
}
/* Normal character — copy through */
out[o++] = src[i++];
}
out[o] = '\0';
return out;
}
static char g_config_last_error[512] = {0};
static void config_set_error(const char* fmt, ...) {
@@ -699,13 +781,22 @@ int config_load(const char* path, didactyl_config_t* config) {
config->api.port = 8484;
snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1");
char* json_buf = NULL;
size_t json_len = 0;
if (read_file_to_buffer(path, &json_buf, &json_len) != 0) {
char* raw_buf = NULL;
size_t raw_len = 0;
if (read_file_to_buffer(path, &raw_buf, &raw_len) != 0) {
config_set_error("failed to read config file '%s': %s", path, strerror(errno));
return -1;
}
/* Strip JSONC comments before parsing */
char* json_buf = jsonc_strip_comments(raw_buf, raw_len);
free(raw_buf);
if (!json_buf) {
config_set_error("failed to allocate memory for JSONC comment stripping");
return -1;
}
size_t json_len = strlen(json_buf);
cJSON* root = cJSON_ParseWithLength(json_buf, json_len);
free(json_buf);

View File

@@ -113,4 +113,7 @@ int config_load(const char* path, didactyl_config_t* config);
const char* config_last_error(void);
void config_free(didactyl_config_t* config);
/* Strip JSONC single-line and block comments, returning malloc'd pure JSON. */
char* jsonc_strip_comments(const char* src, size_t src_len);
#endif

View File

@@ -69,7 +69,7 @@ static int wait_for_connected_relays(int timeout_ms) {
}
int main(int argc, char** argv) {
const char* config_path = "./config.json";
const char* config_path = "./config.jsonc";
int debug_level = DEBUG_LEVEL_TRACE;
int dump_schemas = 0;
const char* test_tool_name = NULL;

View File

@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 43
#define DIDACTYL_VERSION "v0.0.43"
#define DIDACTYL_VERSION_PATCH 44
#define DIDACTYL_VERSION "v0.0.44"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"

View File

@@ -14,6 +14,7 @@
#include <sys/wait.h>
#include "cjson/cJSON.h"
#include "config.h"
#include "main.h"
#include "nostr_handler.h"
#include "trigger_manager.h"
@@ -1827,7 +1828,7 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON_AddStringToObject(t31, "type", "function");
cJSON_AddStringToObject(t31_fn, "name", "model_set");
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to config.json");
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to config.jsonc");
cJSON_AddStringToObject(t31_params, "type", "object");
cJSON_AddItemToObject(t31_params, "properties", t31_props);
@@ -4837,10 +4838,15 @@ static int persist_llm_config(tools_context_t* ctx, const llm_config_t* cfg) {
if (ctx->cfg->config_path[0] == '\0') return -1;
size_t src_len = 0;
char* src = read_entire_file(ctx->cfg->config_path, &src_len);
char* raw = read_entire_file(ctx->cfg->config_path, &src_len);
if (!raw) return -1;
/* Strip JSONC comments before parsing */
char* src = jsonc_strip_comments(raw, src_len);
free(raw);
if (!src) return -1;
cJSON* root = cJSON_ParseWithLength(src, src_len);
cJSON* root = cJSON_ParseWithLength(src, strlen(src));
free(src);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);

View File

@@ -1 +1 @@
{"tasks":[{"id":5,"text":"Tweet 3: Gets Smarter Over Time - \"Didactyl starts as an AI that explores and learns. Over time, the best workflows get locked in as reliable, fast processes. It evolves from experimental to hardened—from thinking to doing. An agent that improves itself. #nostr #agents\"","status":"pending","created_at":1772625247,"updated_at":1772625247}],"next_id":6}
{"tasks":[{"id":5,"text":"Tweet 3: Gets Smarter Over Time - \"Didactyl starts as an AI that explores and learns. Over time, the best workflows get locked in as reliable, fast processes. It evolves from experimental to hardened—from thinking to doing. An agent that improves itself. #nostr #agents\"","status":"pending","created_at":1772625247,"updated_at":1772625247},{"id":6,"text":"admin_info","status":"pending","created_at":1772751865,"updated_at":1772751865},{"id":7,"text":"REFERENCE: Nostr \"a\" tag format for replaceable events\n\nParameterized replaceable event:\n[\"a\", <kind integer>:<32-bytes lowercase hex of pubkey>:<d tag value>, <recommended relay URL (optional)>]\n\nNon-parameterized replaceable event:\n[\"a\", <kind integer>:<32-bytes lowercase hex of pubkey>:, <recommended relay URL (optional)>]\n\nUsed to refer to replaceable events (kinds 10000-19999 for non-parameterized, 30000-39999 for parameterized)","status":"pending","created_at":1772768115,"updated_at":1772768115}],"next_id":8}