v0.0.44 - Migrate config to JSONC with comment stripping parser and docs updates
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
/c-relay/
|
||||
/nips/
|
||||
/config.json
|
||||
/config.jsonc
|
||||
test_keys.txt
|
||||
|
||||
/mongoose/
|
||||
|
||||
22
README.md
22
README.md
@@ -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.43 — Unify self-authored event cache for skills and adoption list
|
||||
> Last release update: v0.0.44 — Migrate 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)
|
||||
|
||||
@@ -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
245
config.jsonc.example
Normal 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"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4863
context.log.md
4863
context.log.md
File diff suppressed because one or more lines are too long
@@ -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
367
docs/SKILLS.md
Normal 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
132
docs/TOOLS.md
Normal 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)
|
||||
@@ -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
|
||||
97
src/config.c
97
src/config.c
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
12
src/tools.c
12
src/tools.c
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user