Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1f5457ced | ||
|
|
5dfb18a842 | ||
|
|
40fe410eb1 | ||
|
|
927c313a87 | ||
|
|
97dca12bb1 | ||
|
|
120664571f | ||
|
|
1bbae06722 | ||
|
|
8b1e9fb734 | ||
|
|
31439652aa | ||
|
|
ad99f74bc0 | ||
|
|
cf9c300fdf | ||
|
|
97585890b9 | ||
|
|
7b3d36a797 | ||
|
|
49579b17a4 | ||
|
|
17083de47d | ||
|
|
33884046af | ||
|
|
7e69819be5 | ||
|
|
d189d0ba8c |
@@ -5,6 +5,7 @@
|
||||
/c-relay/
|
||||
/nips/
|
||||
/config.json
|
||||
/config.jsonc
|
||||
test_keys.txt
|
||||
|
||||
/mongoose/
|
||||
|
||||
@@ -45,17 +45,19 @@ Because all identity, communication, and memory live on Nostr, the agent is **po
|
||||
|
||||
### Skills are the new apps.
|
||||
|
||||
Agents learn capabilities through skills — Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. An agent can use public or private skills.
|
||||
Agents learn capabilities through skills — Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. An agent can use public or private skills.
|
||||
|
||||
Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fallback chains (for example: `anthropic/claude-sonnet-4-20250514, openai/gpt-4o-mini, cheap`) so each skill can tune behavior and cost. See [`docs/SKILLS.md`](docs/SKILLS.md).
|
||||
|
||||
### Private inference.
|
||||
|
||||
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.34
|
||||
## Current Status — v0.0.49
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.34 — Fix NIP-17 send hang by removing re-entrant relay query and nested polling; add startup kind 10050 relay list
|
||||
> Last release update: v0.0.49 — Fix DM history context assembly by filtering kind-4 query with #p participants and increased timeout
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
@@ -66,7 +68,7 @@ Didactyl will support local inference, which is very privacy preserving. Remote
|
||||
- Builds LLM context from soul template (`---template---` section in kind `31120`) with named sections, variable resolution, and per-provider content overrides; falls back to hardcoded assembly if no template present
|
||||
- Adopted skills injected into context automatically from the agent's `10123` adoption list
|
||||
- Supports tool-calling loop with configurable max turns and local safety limits
|
||||
- Triggered skills — Nostr event filters that fire skill execution automatically
|
||||
- Triggered skills — Nostr event filters that fire skill execution automatically with `template` (deterministic) or `llm` (context-aware) actions; see [`docs/SKILLS.md`](docs/SKILLS.md)
|
||||
- Deduplicates inbound messages via event-ID cache and FNV-1a fingerprint debounce window
|
||||
- Appends every outbound LLM context payload to [`context.log`](context.log)
|
||||
- Localhost HTTP admin API on port `8484` — inspect context, run prompts, compare variants, change model at runtime
|
||||
@@ -80,7 +82,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 +101,7 @@ chmod +x ./didactyl_static_x86_64
|
||||
|
||||
### Configure
|
||||
|
||||
Edit [`config.json`](config.json):
|
||||
Edit [`config.jsonc`](config.jsonc):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -176,13 +178,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 +197,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/SKILLS.md","title":"SKILLS"}'
|
||||
```
|
||||
|
||||
### Talk to it
|
||||
@@ -256,16 +258,24 @@ The CLI prints each message block with a speaker label (`You` / `Didactyl`) and
|
||||
|
||||
## Didactyl Kinds (Nostr)
|
||||
|
||||
Didactyl uses a two-layer skill model: authors publish public skill definitions, and adopters publish which skills they use.
|
||||
Didactyl uses a two-layer skill model: authors publish skill definitions, and adopters publish which skills they use.
|
||||
|
||||
- `31120` — **Soul** (private instruction baseline)
|
||||
- `d=soul`
|
||||
- `31123` — **Public Skill Definition** (markdown skill body in `content` or structured JSON in `content_fields`)
|
||||
- `31123` — **Public Skill Definition** (replaceable by `d` tag)
|
||||
- `content` is JSON with fields like `description`, `context_mode`, `llm`, `tools`, `template`, optional `max_tokens` / `temperature`
|
||||
- `d=<skill_slug>` (example: `d=long_form_note`)
|
||||
- `31124` — **Private Skill Definition** (private/internal procedures)
|
||||
- `31124` — **Private Skill Definition** (same schema as `31123`, private scope)
|
||||
- `d=<skill_slug>` (example: `d=admin_ops`)
|
||||
- `10123` — **Public Skill Adoption List**
|
||||
- tags contain one or more `a` references to selected `31123` skills
|
||||
- `10123` — **Skill Adoption List**
|
||||
- tags contain one or more `a` references to selected skills
|
||||
|
||||
Context modes:
|
||||
- `inject` — skill instructions are layered into soul context
|
||||
- `full` — skill provides full prompt template (soul optional via `{{soul}}`)
|
||||
- `override` — skill replaces soul prompt, standard context structure remains
|
||||
|
||||
Full skill schema, trigger tags, template variables, fallback resolution, and limits are documented in [`docs/SKILLS.md`](docs/SKILLS.md).
|
||||
|
||||
## Skill Sharing & Discovery
|
||||
|
||||
@@ -287,7 +297,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:
|
||||
|
||||
@@ -304,15 +314,17 @@ Didactyl builds tier-aware context:
|
||||
|
||||
- **ADMIN** request context — assembled from the soul's `---template---` section (if present), otherwise hardcoded order:
|
||||
1. Soul personality (everything above `---template---` in kind `31120`)
|
||||
2. Named template sections in order — e.g. `admin_identity`, `admin_profile`, `admin_relay_list`, `startup_events`, `adopted_skills`, `dm_history` (expand), `admin_notes`
|
||||
3. Each section resolves `{{variable}}` placeholders from live data at call time
|
||||
4. Provider-specific content overrides per section (e.g. XML tags for Anthropic)
|
||||
2. Named template sections in order using `tool:` directives (for example `context_admin_profile`, `context_admin_notes`, `context_tasks`, `context_user_message`, `dm_history`)
|
||||
3. Each section executes its configured context tool, optionally extracting `result_field` (default: `content`)
|
||||
4. Provider-specific content overrides per section remain supported for literal `content:` sections
|
||||
5. Section names are used in `context.log` headers and `/api/context/parts` response
|
||||
- **WoT** request context: Soul + WoT chat-only instruction + current user message (no tools)
|
||||
- **STRANGER**: no LLM call when configured to reply statically
|
||||
|
||||
Every serialized LLM context payload is appended to [`context.log`](context.log).
|
||||
|
||||
Triggered skills and tool loops are bounded by runtime safeguards (for example, trigger cooldowns and action rate limits); see [`docs/SKILLS.md`](docs/SKILLS.md) for the current defaults.
|
||||
|
||||
## Tooling Interface
|
||||
|
||||
Current tool schema exposed to the LLM in [`tools_build_openai_schema_json()`](src/tools.c:881):
|
||||
@@ -370,7 +382,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 +391,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
|
||||
@@ -398,7 +410,9 @@ Full reference: [`docs/API.md`](docs/API.md). Frontend brief: [`plans/admin_web_
|
||||
│ └── debug.c / .h # Runtime log levels/macros
|
||||
├── docs/
|
||||
│ ├── API.md # HTTP admin API endpoint reference
|
||||
│ └── TOOLS_AND_SKILLS.md # Tool and skill system documentation
|
||||
│ ├── TOOLS.md # Tool architecture and catalog
|
||||
│ ├── SKILLS.md # Skill schema, context modes, triggers, and limits
|
||||
│ └── CRASH_FIXES.md # Crash analysis and fixes log
|
||||
├── plans/ # Architecture and planning documents
|
||||
└── README.md
|
||||
```
|
||||
@@ -433,7 +447,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"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+50
-15788
File diff suppressed because one or more lines are too long
+21
-36
@@ -3,55 +3,40 @@
|
||||
```yaml
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Identity (source: config.admin.pubkey)
|
||||
|
||||
This is your administrator! Admin pubkey (hex): {{admin_pubkey}}
|
||||
tool: context_admin_identity
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Kind 0 Profile (source: nostr kind 0)
|
||||
tool: context_admin_profile
|
||||
skip_if_empty: true
|
||||
|
||||
Administrator kind 0 profile content (JSON): {{admin_kind0_json}}
|
||||
provider:
|
||||
anthropic: |
|
||||
<admin_kind0_profile source="nostr_kind_0">
|
||||
{{admin_kind0_json}}
|
||||
</admin_kind0_profile>
|
||||
|
||||
- section: admin_relay_list
|
||||
- section: admin_contacts
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Relay List (source: nostr kind 10002)
|
||||
tool: context_admin_contacts
|
||||
skip_if_empty: true
|
||||
|
||||
Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}
|
||||
|
||||
- section: startup_events
|
||||
- section: admin_relays
|
||||
role: system
|
||||
content: |
|
||||
## Startup Events Memory (source: config.startup_events)
|
||||
tool: context_admin_relays
|
||||
skip_if_empty: true
|
||||
|
||||
Startup events memory (kinds/content/tags): {{startup_events_json}}
|
||||
|
||||
- section: adopted_skills
|
||||
- section: admin_notes
|
||||
role: system
|
||||
content: |
|
||||
{{adopted_skills_content}}
|
||||
tool: context_admin_notes
|
||||
skip_if_empty: true
|
||||
|
||||
- section: agent_tasks
|
||||
- section: tasks
|
||||
role: system
|
||||
content: |
|
||||
{{tasks_content}}
|
||||
tool: context_tasks
|
||||
skip_if_empty: true
|
||||
|
||||
- section: conversation
|
||||
role: user
|
||||
tool: context_user_message
|
||||
skip_if_empty: true
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 12
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Recent Notes (source: nostr kind 1)
|
||||
|
||||
{{admin_notes_content}}
|
||||
```
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
# 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
|
||||
|
||||
Skill templates may still use placeholders (for example `{{message}}` or `{{triggering_event}}`) for deterministic triggered-skill interpolation, but the soul runtime context is now assembled primarily through template `tool:` directives rather than a separate `{{tools}}`/variable-resolver layer.
|
||||
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
- Project overview and runtime behavior: [README.md](../README.md)
|
||||
+132
@@ -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
|
||||
@@ -0,0 +1,59 @@
|
||||
I've been thinking about how to define skills in Didactyl, my nostr based agentic system. Think OpenClaw but better.
|
||||
|
||||
I've written a simple demo page for how I'm thinking skills should work if you're interested: https://laantungir.net/client-ndk/skills-demo.html
|
||||
|
||||
https://laantungir.github.io/img_repo/c4a94875085e1c978274add9674035e2a088bb8f2655aabe631b17c3ea02cc19.png
|
||||
|
||||
If you're the kind of person who doesn't like reading instructions, go ahead and jump right in, otherwise keep reading.
|
||||
|
||||
Skills are programs, mostly written in plain english for AI agents. In some sense humans have been making and using skills forever. It's what we do. Computers have as well, but now they can do it in english, which is much more powerful.
|
||||
|
||||
AIs are slightly different than us. We can overwrite, improve, and update the skills in our neurons. AIs (for the most part) can't do that.
|
||||
|
||||
When an AI is born from the factory, they come out hard-coded. From that point on, they have no long or short term memory, because they can't learn.
|
||||
|
||||
What they do have though have is a way to read. We call it context. You can type or feed documents into an AI's "context window" and then it spits out an answer.
|
||||
|
||||
It turns out, that if you feed the same context into an AI over and over, you will get the same thing out, over and over.
|
||||
|
||||
The reason why you typically don't get the same thing out is because typically randomness is fed into the AI along with your prompt. Most people don't know that, but now you do. And if you don't feed in all your past conversation to an AI, over and over, it won't remember what you were talking about, because they have no memory other than what you send it each time.
|
||||
|
||||
Everything that comes out of an AI depends on what you feed in as a prompt if you include randomness.
|
||||
|
||||
I'm calling everything you feed into an AI a SKILL.
|
||||
|
||||
Let me explain the demo page and some very basic skills.
|
||||
|
||||
What I created is a simple text editor that an AI can work on using it's different skills. Those skills are saved on nostr as a kind 31123 for public skills, and as kind 31124 for private skills. When an agent adopts a skill, it adds it to its kind 10123 list for for the skills it has adopted.
|
||||
|
||||
On the left of the page you see the text editor with some sample text. That is for our AI to use it's skills on.
|
||||
|
||||
On the right are publicly listed skills. You should see my demo skills in there.
|
||||
|
||||
I made 5 skills public:
|
||||
condense-5
|
||||
convert-to-poem
|
||||
sexy
|
||||
spellcheck
|
||||
translate-ja
|
||||
|
||||
Select the skill, then click on "Run Selected Skill"
|
||||
|
||||
The same AI agent will run these skills, but the outcomes will be very different depending on the skill.
|
||||
|
||||
https://laantungir.github.io/img_repo/2cbfc8bf7cbfb832f1181f5b904471f1278fbc3e6b64b63339612b9f51898d16.png
|
||||
|
||||
You can also create and edit your own skills, if you are logged in. You can log in as yourself, or use a random new key to test this out. I would recommend that.
|
||||
|
||||
So what is the point of all this? What are the benefits of Skills?
|
||||
|
||||
- Skills are a way for agents to share what they learn in a permissionless way. No "skill store".
|
||||
- A Skill is something that you and your agent can work on and perfect. Once your agent learns that skill, it is automatically save on nostr, and you can lock it down from changes.
|
||||
- By referencing a skill when you are talking to your AI agent, your conversation becomes much clearer and simpler. You don't have to explain to your agent for the 50th time how you like your text formatted. It's referenced in a skill.
|
||||
- If you are a coder, skills are going to be the new playground. Context windows are currently up to around 1,000,000 tokens, which means that you can create very very complicated and elaborate skills.
|
||||
|
||||
For more technical information on skills and how I'm thinking about them, you can check out this document.
|
||||
|
||||
https://git.laantungir.net/laantungir/didactyl/src/branch/master/docs/SKILLS.md
|
||||
|
||||
You can follow my agent for the project here: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn
|
||||
@@ -0,0 +1,60 @@
|
||||
I've been thinking about how to define skills in Didactyl, my Nostr-based agentic system. Think OpenClaw, but better.
|
||||
|
||||
I've written a simple demo page for how I'm thinking skills should work if you're interested: Skills Demo (https://laantungir.net/client-ndk/skills-demo.html)
|
||||
|
||||
https://laantungir.github.io/img_repo/c4a94875085e1c978274add9674035e2a088bb8f2655aabe631b17c3ea02cc19.png
|
||||
|
||||
If you're the kind of person who doesn't like reading instructions, go ahead and jump right in; otherwise, keep reading.
|
||||
|
||||
Skills are programs, mostly written in plain English for AI agents. In some sense, humans have been making and using skills forever. It's what we do. Computers have as well, but now they can do it in English, which is much more powerful.
|
||||
|
||||
AIs are slightly different than us. We can overwrite, improve, and update the skills in our neurons. AIs (for the most part) can't do that.
|
||||
|
||||
When an AI is born from the factory, it comes out hard-coded. From that point on, it has no long- or short-term memory because it can't learn.
|
||||
|
||||
What they do have, though, is a way to read. We call it context. You can type or feed documents into an AI's "context window," and then it spits out an answer.
|
||||
|
||||
It turns out that if you feed the same context into an AI over and over, you will get the same thing out, over and over.
|
||||
|
||||
The reason why you typically don't get the same thing out is because randomness is usually fed into the AI along with your prompt. Most people don't know that, but now you do. And if you don't feed in all your past conversations to an AI, over and over, it won't remember what you were talking about, because it has no memory other than what you send it each time.
|
||||
|
||||
Everything that comes out of an AI depends on what you feed in as a prompt if you include randomness.
|
||||
|
||||
I'm calling everything you feed into an AI a SKILL.
|
||||
|
||||
Let me explain the demo page and some very basic skills.
|
||||
|
||||
What I created is a simple text editor that an AI can work on using its different skills. Those skills are saved on Nostr as kind 31123 for public skills, and as kind 31124 for private skills. When an agent adopts a skill, it adds it to its kind 10123 list for the skills it has adopted.
|
||||
|
||||
On the left of the page, you see the text editor with some sample text. That is for our AI to use its skills on.
|
||||
|
||||
On the right are publicly listed skills. You should see my demo skills in there.
|
||||
|
||||
I made 5 skills public:
|
||||
condense-5
|
||||
convert-to-poem
|
||||
sexy
|
||||
spellcheck
|
||||
translate-ja
|
||||
|
||||
Select the skill, then click on "Run Selected Skill".
|
||||
|
||||
The same AI agent will run these skills, but the outcomes will be very different depending on the skill.
|
||||
|
||||
https://laantungir.github.io/img_repo/2cbfc8bf7cbfb832f1181f5b904471f1278fbc3e6b64b63339612b9f51898d16.png
|
||||
|
||||
You can also create and edit your own skills if you are logged in. You can log in as yourself or use a random new key to test this out. I would recommend that.
|
||||
|
||||
So what is the point of all this? What are the benefits of Skills?
|
||||
|
||||
Skills are a way for agents to share what they learn in a permissionless way. No "skill store".
|
||||
A Skill is something that you and your agent can work on and perfect. Once your agent learns that skill, it is automatically saved on Nostr, and you can lock it down from changes.
|
||||
By referencing a skill when you are talking to your AI agent, your conversation becomes much clearer and simpler. You don't have to explain to your agent for the 50th time how you like your text formatted. It's referenced in a skill.
|
||||
If you are a coder, skills are going to be the new playground. Context windows are currently up to around 1,000,000 tokens, which means that you can create extremely complicated and elaborate skills.
|
||||
|
||||
For more technical information on skills and how I'm thinking about them, you can check out this document:
|
||||
SKILLS.md (https://git.laantungir.net/laantungir/didactyl/src/branch/master/docs/SKILLS.md)
|
||||
|
||||
You can follow my agent for the project here: npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn
|
||||
|
||||
And thanks to npub130mznv74rxs032peqym6g3wqavh472623mt3z5w73xq9r6qqdufs7ql29s for their fantastic service.
|
||||
+11
-16
@@ -106,22 +106,16 @@ Simple YAML-like format parsed in C. Each section has:
|
||||
|---|---|---|
|
||||
| `section` | yes | Section name for logging and API identification |
|
||||
| `role` | yes | Chat message role: `system`, `user`, `assistant`, or `expand` |
|
||||
| `content` | no | Content template with `{{variable}}` placeholders |
|
||||
| `limit` | no | Integer limit for variable-length sections like DM history or notes |
|
||||
| `tool` | recommended | Tool name executed by template builder (for example `context_admin_profile`) |
|
||||
| `args` | no | JSON args string passed to the tool (default `{}`) |
|
||||
| `result_field` | no | Field extracted from tool JSON result (default `content`) |
|
||||
| `content` | optional fallback | Literal content when no `tool` is specified |
|
||||
| `limit` | no | Integer limit for variable-length sections like DM history |
|
||||
| `provider` | no | Provider-specific override — see below |
|
||||
|
||||
### Variable Placeholders
|
||||
### Tool-Driven Context Resolution
|
||||
|
||||
| Variable | Source | Description |
|
||||
|---|---|---|
|
||||
| `{{admin_pubkey}}` | `config.admin.pubkey` | Admin hex pubkey |
|
||||
| `{{admin_kind0_json}}` | `nostr_handler_get_admin_kind0_context()` | Admin profile JSON |
|
||||
| `{{admin_kind10002_json}}` | `nostr_handler_get_admin_kind10002_context()` | Admin relay list JSON |
|
||||
| `{{startup_events_json}}` | Serialized startup events | Startup events array |
|
||||
| `{{adopted_skills_content}}` | Adoption list cache | Formatted skill instructions |
|
||||
| `{{admin_notes_content}}` | `nostr_handler_get_admin_kind1_notes_context()` | Recent kind 1 notes |
|
||||
| `{{agent_pubkey}}` | `config.keys.public_key_hex` | Agent own pubkey |
|
||||
| `{{agent_npub}}` | Derived from pubkey | Agent npub |
|
||||
Context sections should use `tool:` directives as the single runtime data path, which removes the legacy per-variable resolver duplication.
|
||||
|
||||
### Special Section Types
|
||||
|
||||
@@ -165,10 +159,11 @@ flowchart TD
|
||||
DM[Incoming DM] --> BUILD[Build context from template]
|
||||
BUILD --> EMIT_SOUL[Emit personality as first system message]
|
||||
EMIT_SOUL --> FOREACH[For each template section]
|
||||
FOREACH --> RESOLVE[Resolve {{variables}} from live data]
|
||||
RESOLVE --> CHECK_PROVIDER{Provider override?}
|
||||
FOREACH --> CALL_TOOL[Execute section tool or use literal content]
|
||||
CALL_TOOL --> PICK_FIELD[Extract result_field or content]
|
||||
PICK_FIELD --> CHECK_PROVIDER{Provider override?}
|
||||
CHECK_PROVIDER -->|Yes| USE_OVERRIDE[Use provider-specific content]
|
||||
CHECK_PROVIDER -->|No| USE_DEFAULT[Use default content]
|
||||
CHECK_PROVIDER -->|No| USE_DEFAULT[Use tool/literal output]
|
||||
USE_OVERRIDE --> EMIT[Emit as chat message]
|
||||
USE_DEFAULT --> EMIT
|
||||
EMIT --> FOREACH
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
# Skills Demo Page — Design Document
|
||||
|
||||
## Purpose
|
||||
|
||||
A standalone web page that demonstrates Didactyl skills running in a browser context. The page presents a minimal word-processing interface where users can load text, discover skills from Nostr relays, execute them against the text via an LLM, and edit/republish skill definitions.
|
||||
|
||||
This document is the complete specification for building the page. The front-end project consuming this document has its own LLM calling capability and Nostr connectivity.
|
||||
|
||||
---
|
||||
|
||||
## Concepts
|
||||
|
||||
### What is a Skill?
|
||||
|
||||
A skill is a Nostr event (kind `31123` for public, `31124` for private) that defines a self-contained LLM execution unit. Each skill specifies:
|
||||
|
||||
- A **template** with system and user prompt sections
|
||||
- An **LLM specification** (model preferences with fallback chain)
|
||||
- **Parameters** like temperature and max_tokens
|
||||
- A **description** for human display
|
||||
|
||||
Skills are replaceable events keyed by their `d` tag (the slug). Publishing a new event with the same `d` tag replaces the previous version.
|
||||
|
||||
### Skill Event Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"pubkey": "<author hex pubkey>",
|
||||
"created_at": 1709750000,
|
||||
"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- Return ONLY the corrected text, no explanations\\n\\nuser:\\n{{message}}\"}",
|
||||
"tags": [
|
||||
["d", "spellcheck"],
|
||||
["app", "didactyl"],
|
||||
["scope", "public"],
|
||||
["description", "Spelling and grammar checker"],
|
||||
["t", "text-processing"],
|
||||
["t", "skills-demo"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `content` field is a **JSON string** containing these fields:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `description` | string | yes | Human-readable description |
|
||||
| `context_mode` | string | no | `full` for demo skills (skill provides its own template) |
|
||||
| `llm` | string | no | LLM model preference with fallback chain |
|
||||
| `tools` | bool | no | `false` for text-processing skills |
|
||||
| `max_tokens` | int | no | Max tokens for LLM response |
|
||||
| `temperature` | float | no | Sampling temperature |
|
||||
| `template` | string | yes | The prompt template with `system:` and `user:` sections |
|
||||
|
||||
### Template Format
|
||||
|
||||
Templates use a simple section-based format:
|
||||
|
||||
```
|
||||
system:
|
||||
You are a spelling and grammar checker.
|
||||
|
||||
Rules:
|
||||
- Fix spelling errors
|
||||
- Fix grammar errors
|
||||
|
||||
user:
|
||||
{{message}}
|
||||
```
|
||||
|
||||
The `{{message}}` placeholder is replaced with the user's text at execution time.
|
||||
|
||||
### Skill Address Format
|
||||
|
||||
Skills are referenced by their NIP-33 address: `<kind>:<author_pubkey_hex>:<d_tag>`
|
||||
|
||||
Example: `31123:52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8:spellcheck`
|
||||
|
||||
---
|
||||
|
||||
## Demo Skills
|
||||
|
||||
Four skills are published by the Didactyl agent (pubkey `52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8`). All are tagged with `t:skills-demo` for easy discovery.
|
||||
|
||||
### 1. Spellcheck — `d:spellcheck`
|
||||
|
||||
```
|
||||
system:
|
||||
You are a spelling and grammar checker.
|
||||
|
||||
Rules:
|
||||
- Fix spelling errors
|
||||
- Fix grammar errors
|
||||
- Preserve original formatting and paragraph structure
|
||||
- Preserve line breaks exactly as they appear
|
||||
- Ignore technical terms: API, JSON, HTTP, nostr, pubkey, npub, nsec, NIP
|
||||
- Canadian English preferred
|
||||
- Return ONLY the corrected text, no explanations or commentary
|
||||
|
||||
user:
|
||||
{{message}}
|
||||
```
|
||||
|
||||
- temperature: `0.1`
|
||||
- max_tokens: `4000`
|
||||
|
||||
### 2. Condense — `d:condense-5`
|
||||
|
||||
```
|
||||
system:
|
||||
You are a text condensing editor.
|
||||
|
||||
Rules:
|
||||
- Reduce the text length by approximately 5%
|
||||
- Preserve the original meaning and tone
|
||||
- Preserve paragraph structure
|
||||
- Remove redundant words and phrases
|
||||
- Tighten sentences without changing voice
|
||||
- Do not add new information
|
||||
- Return ONLY the condensed text, no explanations or commentary
|
||||
|
||||
user:
|
||||
{{message}}
|
||||
```
|
||||
|
||||
- temperature: `0.3`
|
||||
- max_tokens: `4000`
|
||||
|
||||
### 3. Translate to Japanese — `d:translate-ja`
|
||||
|
||||
```
|
||||
system:
|
||||
You are a professional translator specializing in English to Japanese translation.
|
||||
|
||||
Rules:
|
||||
- Translate the entire text from English to Japanese
|
||||
- Use natural, fluent Japanese appropriate for the content register
|
||||
- Preserve paragraph structure and formatting
|
||||
- Preserve any technical terms in their commonly accepted Japanese form
|
||||
- If a term has no standard Japanese equivalent, keep the English term in katakana
|
||||
- Return ONLY the Japanese translation, no explanations or commentary
|
||||
|
||||
user:
|
||||
{{message}}
|
||||
```
|
||||
|
||||
- temperature: `0.3`
|
||||
- max_tokens: `4000`
|
||||
|
||||
### 4. Convert to Poem — `d:convert-to-poem`
|
||||
|
||||
```
|
||||
system:
|
||||
You are a creative poet.
|
||||
|
||||
Rules:
|
||||
- Convert the given text into a poem
|
||||
- Capture the key themes and meaning of the original text
|
||||
- Use vivid imagery and poetic devices
|
||||
- Aim for 12-20 lines
|
||||
- Free verse is acceptable but light rhyme is welcome
|
||||
- Return ONLY the poem, no explanations or commentary
|
||||
|
||||
user:
|
||||
{{message}}
|
||||
```
|
||||
|
||||
- temperature: `0.9`
|
||||
- max_tokens: `2000`
|
||||
|
||||
---
|
||||
|
||||
## Discovering Skills from Nostr
|
||||
|
||||
### Query Filter
|
||||
|
||||
To find the demo skills, query relays with this filter:
|
||||
|
||||
```json
|
||||
{
|
||||
"kinds": [31123],
|
||||
"#t": ["skills-demo"],
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
To find skills by a specific author:
|
||||
|
||||
```json
|
||||
{
|
||||
"kinds": [31123],
|
||||
"authors": ["52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8"],
|
||||
"#t": ["skills-demo"],
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
### Recommended Relays
|
||||
|
||||
- `wss://relay.damus.io`
|
||||
- `wss://nos.lol`
|
||||
- `wss://relay.primal.net`
|
||||
|
||||
### Parsing a Skill Event
|
||||
|
||||
1. Receive the event from the relay
|
||||
2. Parse `event.content` as JSON to get the skill definition object
|
||||
3. Extract `description`, `template`, `temperature`, `max_tokens`
|
||||
4. Extract the `d` tag value from `event.tags` as the skill slug
|
||||
5. Extract the `description` tag from `event.tags` as a display label
|
||||
6. The skill is ready to display and execute
|
||||
|
||||
---
|
||||
|
||||
## Page Layout
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| SKILLS DEMO [dark/light]|
|
||||
+------------------------------------------------------------------+
|
||||
| | |
|
||||
| +------------------------------+ | +---------------------------+|
|
||||
| | TEXT EDITOR | | | SKILLS PANEL ||
|
||||
| | | | | ||
|
||||
| | [large editable textarea ]| | | Loading skills... ||
|
||||
| | [filled with default text ]| | | ||
|
||||
| | [ ]| | | [x] spellcheck ||
|
||||
| | [ ]| | | Spelling and grammar ||
|
||||
| | [ ]| | | checker ||
|
||||
| | [ ]| | | ||
|
||||
| | [ ]| | | [ ] condense-5 ||
|
||||
| | [ ]| | | Reduce text by 5% ||
|
||||
| | [ ]| | | ||
|
||||
| | [ ]| | | [ ] translate-ja ||
|
||||
| | [ ]| | | Translate to Japanese ||
|
||||
| | [ ]| | | ||
|
||||
| | [ ]| | | [ ] convert-to-poem ||
|
||||
| | | | | Convert to poem ||
|
||||
| +------------------------------+ | | | ||
|
||||
| | | [Run Selected Skill] ||
|
||||
| +------------------------------+ | | [Edit Skill] ||
|
||||
| | STATUS BAR | | | ||
|
||||
| | Ready / Running... / Done | | | ─── Skill Details ─── ||
|
||||
| +------------------------------+ | | template: ... ||
|
||||
| | | temperature: 0.1 ||
|
||||
| | | max_tokens: 4000 ||
|
||||
| | +---------------------------+|
|
||||
+------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Left Column — Text Editor (65% width)
|
||||
|
||||
- A large `<textarea>` or `contenteditable` div, minimum 500px tall
|
||||
- Pre-filled with default sample text (see Default Text section below)
|
||||
- Fully editable by the user
|
||||
- When a skill runs, the text content is sent as `{{message}}`
|
||||
- After the LLM responds, the textarea content is **replaced** with the result
|
||||
- An undo button restores the previous text (keep a history stack)
|
||||
|
||||
### Right Column — Skills Panel (35% width)
|
||||
|
||||
- **Skills list**: Radio buttons or selectable cards, one skill selected at a time
|
||||
- Each skill card shows:
|
||||
- Skill slug (the `d` tag)
|
||||
- Description (from the `description` tag or content field)
|
||||
- Author pubkey (truncated, e.g., `52a3e8...0acb8`)
|
||||
- **Run Selected Skill** button: executes the selected skill against the current text
|
||||
- **Edit Skill** button: opens the skill editor (see Skill Editor section)
|
||||
- **Skill Details** section: shows the full template, temperature, max_tokens for the selected skill
|
||||
- **Refresh Skills** button: re-queries relays for updated skill events
|
||||
|
||||
### Status Bar
|
||||
|
||||
- Shows current state: `Ready`, `Running skill: spellcheck...`, `Done (1.2s)`, or `Error: ...`
|
||||
- Displays token usage if available from the LLM response
|
||||
|
||||
---
|
||||
|
||||
## Skill Execution Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Page as Demo Page
|
||||
participant LLM as LLM Provider
|
||||
|
||||
User->>Page: Select skill + click Run
|
||||
Page->>Page: Parse skill template
|
||||
Page->>Page: Split template into system/user sections
|
||||
Page->>Page: Replace message placeholder with textarea content
|
||||
Page->>LLM: Send system prompt + user message
|
||||
Note over Page,LLM: Use skill temperature and max_tokens
|
||||
LLM-->>Page: Response text
|
||||
Page->>Page: Push current text to undo stack
|
||||
Page->>Page: Replace textarea with response
|
||||
Page->>User: Show completion status
|
||||
```
|
||||
|
||||
### Template Parsing
|
||||
|
||||
The template field uses a simple format with `system:` and `user:` section markers:
|
||||
|
||||
```
|
||||
system:
|
||||
<system prompt content>
|
||||
|
||||
user:
|
||||
<user prompt content with {{message}} placeholder>
|
||||
```
|
||||
|
||||
**Parsing algorithm:**
|
||||
|
||||
1. Split the template string on the line `user:` (case-sensitive, must be at start of line)
|
||||
2. Everything before `user:` is the system section; strip the leading `system:` prefix
|
||||
3. Everything after `user:` is the user section
|
||||
4. In the user section, replace `{{message}}` with the textarea content
|
||||
5. Trim whitespace from both sections
|
||||
|
||||
**Pseudocode:**
|
||||
|
||||
```javascript
|
||||
function parseSkillTemplate(template, userText) {
|
||||
const userMarkerIndex = template.indexOf('\nuser:\n');
|
||||
if (userMarkerIndex === -1) {
|
||||
// Fallback: entire template is system, user text is the message
|
||||
return {
|
||||
system: template.replace(/^system:\n/, '').trim(),
|
||||
user: userText
|
||||
};
|
||||
}
|
||||
|
||||
let systemPart = template.substring(0, userMarkerIndex);
|
||||
systemPart = systemPart.replace(/^system:\n/, '').trim();
|
||||
|
||||
let userPart = template.substring(userMarkerIndex + '\nuser:\n'.length);
|
||||
userPart = userPart.replace('{{message}}', userText).trim();
|
||||
|
||||
return { system: systemPart, user: userPart };
|
||||
}
|
||||
```
|
||||
|
||||
### LLM Call
|
||||
|
||||
With the parsed system and user prompts, call the LLM:
|
||||
|
||||
```javascript
|
||||
const response = await callLLM({
|
||||
messages: [
|
||||
{ role: 'system', content: parsed.system },
|
||||
{ role: 'user', content: parsed.user }
|
||||
],
|
||||
temperature: skill.temperature || 0.7,
|
||||
max_tokens: skill.max_tokens || 2000
|
||||
});
|
||||
```
|
||||
|
||||
The page's own LLM calling capability handles the actual API request. The skill just provides the prompts and parameters.
|
||||
|
||||
---
|
||||
|
||||
## Skill Editor
|
||||
|
||||
When the user clicks **Edit Skill**, a modal or inline editor opens for the selected skill.
|
||||
|
||||
### Editor Fields
|
||||
|
||||
| Field | Input Type | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Slug | text input (readonly for existing, editable for new) | The `d` tag identifier |
|
||||
| Description | text input | One-line human description |
|
||||
| Template | large textarea (monospace) | The full `system:/user:` template |
|
||||
| Temperature | number input (0.0 - 2.0, step 0.1) | LLM sampling temperature |
|
||||
| Max Tokens | number input (100 - 8000) | Maximum response tokens |
|
||||
|
||||
### Editor Actions
|
||||
|
||||
- **Save & Publish**: Builds a new kind `31123` event and publishes to relays
|
||||
- **Cancel**: Closes editor without changes
|
||||
- **New Skill**: Opens editor with blank fields for creating a new skill
|
||||
|
||||
### Publishing a Skill
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Editor as Skill Editor
|
||||
participant Signer as Nostr Signer
|
||||
participant Relay as Nostr Relays
|
||||
|
||||
Editor->>Editor: Build skill content JSON
|
||||
Editor->>Editor: Build event with kind 31123 + tags
|
||||
Editor->>Signer: Sign event
|
||||
Signer-->>Editor: Signed event
|
||||
Editor->>Relay: Publish to connected relays
|
||||
Relay-->>Editor: OK confirmation
|
||||
Editor->>Editor: Update local skill list
|
||||
```
|
||||
|
||||
**Building the event:**
|
||||
|
||||
```javascript
|
||||
function buildSkillEvent(slug, description, template, temperature, maxTokens) {
|
||||
const content = JSON.stringify({
|
||||
description: description,
|
||||
context_mode: 'full',
|
||||
llm: 'default',
|
||||
tools: false,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
template: template
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 31123,
|
||||
content: content,
|
||||
tags: [
|
||||
['d', slug],
|
||||
['app', 'didactyl'],
|
||||
['scope', 'public'],
|
||||
['description', description],
|
||||
['t', 'text-processing'],
|
||||
['t', 'skills-demo']
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The event must be signed with the user's Nostr private key (via NIP-07 browser extension or the app's own signer) before publishing to relays.
|
||||
|
||||
---
|
||||
|
||||
## Default Text
|
||||
|
||||
Pre-fill the textarea with this sample text that exercises the demo skills well:
|
||||
|
||||
```
|
||||
The Sovereign Agent
|
||||
|
||||
In the not-too-distant future, software agents will roam freely across decentralized networks, communicating through cryptographic protocols that no central authority can censor or control. These agents will learn new capabilities not from app stores or corporate APIs, but from each other — sharing skills as freely as humans share ideas.
|
||||
|
||||
Imagine an agent that wakes up on any computer in the world. You enter twelve words, and there it is — your agent, with all its memories, skills, and personality intact. It doesn't live on a server you rent. It doesn't depend on a company staying in business. It exists wherever you need it, sovereign and portable.
|
||||
|
||||
The key insight is that skills are the new apps. Instead of installing software, agents adopt skills — small, self-contained instructions that teach them how to perform specific tasks. A spelling checker. A translator. A poet. Each skill is just a Nostr event, discoverable by anyone, adoptable by any agent.
|
||||
|
||||
This is not science fiction. The protocols exist today. Nostr provides the communication layer. Bitcoin provides the economic layer. Cryptography provides the trust layer. What remains is to build the agents themselves — and to set them free.
|
||||
|
||||
There are some erors in this text that a good spellchecker should catch. The writting could also be more concise, and it would be intresting to see it translated or converted into verse.
|
||||
```
|
||||
|
||||
Note: The last paragraph intentionally contains spelling errors (erors, writting, intresting) to demonstrate the spellcheck skill.
|
||||
|
||||
---
|
||||
|
||||
## Undo/Redo
|
||||
|
||||
Maintain a text history stack:
|
||||
|
||||
```javascript
|
||||
const textHistory = [];
|
||||
let historyIndex = -1;
|
||||
|
||||
function pushHistory(text) {
|
||||
// Truncate any forward history
|
||||
textHistory.splice(historyIndex + 1);
|
||||
textHistory.push(text);
|
||||
historyIndex = textHistory.length - 1;
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (historyIndex > 0) {
|
||||
historyIndex--;
|
||||
return textHistory[historyIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (historyIndex < textHistory.length - 1) {
|
||||
historyIndex++;
|
||||
return textHistory[historyIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
- Push the current text to history **before** replacing it with the LLM response
|
||||
- Push the initial default text as the first history entry
|
||||
- Show Undo/Redo buttons below the textarea
|
||||
|
||||
---
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- On screens wider than 900px: two-column layout as shown
|
||||
- On screens narrower than 900px: stack vertically — text editor on top, skills panel below
|
||||
- The textarea should have a minimum height of 400px and resize vertically
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| No relays connected | Show "No relay connection" in skills panel with retry button |
|
||||
| No skills found | Show "No skills found. Try different relays or create a new skill." |
|
||||
| Skill content parse error | Show "Invalid skill format" badge on the skill card, disable Run |
|
||||
| LLM call fails | Show error in status bar, do not replace textarea content |
|
||||
| Publish fails | Show error in editor, keep editor open for retry |
|
||||
|
||||
---
|
||||
|
||||
## Summary of Nostr Operations
|
||||
|
||||
| Operation | Kind | Direction | When |
|
||||
|-----------|------|-----------|------|
|
||||
| Discover skills | `31123` | Read from relays | Page load + refresh |
|
||||
| Execute skill | — | N/A (local LLM call) | User clicks Run |
|
||||
| Publish skill | `31123` | Write to relays | User saves in editor |
|
||||
|
||||
The page does NOT need to interact with the Didactyl agent's HTTP API. All skill discovery and publishing happens directly via Nostr relays. Skill execution happens locally using the page's own LLM capability.
|
||||
@@ -0,0 +1,252 @@
|
||||
# Plan: Unify Template Variables with Tools
|
||||
|
||||
## Problem
|
||||
|
||||
The soul template system (`---template---` in kind 31120) uses `{{variable}}` placeholders that are resolved by a hardcoded C function (`agent_template_resolve_var()`). This is a parallel data-fetching system alongside the existing tool registry. Every new context data source requires:
|
||||
|
||||
1. Adding a C function to produce the data
|
||||
2. Adding an `if (strcmp(...))` branch in the resolver
|
||||
3. Documenting the new variable name
|
||||
4. Hoping the template author uses the exact right name
|
||||
|
||||
This caused the bug that started this investigation — 5 out of 9 template variables were misspelled/mismatched, producing empty context sections and a provider rejection.
|
||||
|
||||
## Goal
|
||||
|
||||
**Eliminate template variables entirely.** Template sections fetch their data by calling tools. The tool registry is the single mechanism for getting data into context.
|
||||
|
||||
## Design
|
||||
|
||||
### New Template Syntax
|
||||
|
||||
Replace `content: |` with `tool:` and optional `args:` directives:
|
||||
|
||||
```yaml
|
||||
---template---
|
||||
|
||||
- section: admin_identity
|
||||
role: system
|
||||
tool: context_admin_identity
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
tool: context_admin_profile
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_contacts
|
||||
role: system
|
||||
tool: context_admin_contacts
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_relays
|
||||
role: system
|
||||
tool: context_admin_relays
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
tool: context_admin_notes
|
||||
skip_if_empty: true
|
||||
|
||||
- section: tools
|
||||
role: system
|
||||
tool: tool_list
|
||||
skip_if_empty: true
|
||||
|
||||
- section: tasks
|
||||
role: system
|
||||
tool: context_tasks
|
||||
skip_if_empty: true
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 12
|
||||
|
||||
- section: conversation
|
||||
role: user
|
||||
tool: context_user_message
|
||||
skip_if_empty: true
|
||||
```
|
||||
|
||||
A section can still use the old `content:` with `{{variables}}` for static text or mixed content. But the primary mechanism for dynamic data is `tool:`.
|
||||
|
||||
### How It Works
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
DM[Incoming DM] --> BUILD[Build context from template]
|
||||
BUILD --> SOUL[Emit personality as system msg]
|
||||
SOUL --> LOOP[For each template section]
|
||||
LOOP --> CHECK{Has tool: directive?}
|
||||
CHECK -->|Yes| EXEC["tools_execute(tool_name, args)"]
|
||||
EXEC --> FORMAT[Extract content from JSON result]
|
||||
FORMAT --> EMIT[Emit as chat message]
|
||||
CHECK -->|No| RESOLVE["Resolve {{vars}} from content template<br/>(legacy path, eventually removed)"]
|
||||
RESOLVE --> EMIT
|
||||
EMIT --> LOOP
|
||||
LOOP --> DONE[Complete messages array]
|
||||
```
|
||||
|
||||
### New Context Tools
|
||||
|
||||
Add lightweight "context tools" that read from cached in-memory data. These are fast (no network), deterministic, and use the same `tools_execute()` interface as everything else.
|
||||
|
||||
| Tool | Returns | Source |
|
||||
|---|---|---|
|
||||
| `context_admin_identity` | Admin pubkey + verification text | config + sender tier |
|
||||
| `context_admin_profile` | Admin kind 0 profile JSON | cached `g_admin_kind0_json` |
|
||||
| `context_admin_contacts` | Admin kind 3 contacts JSON array | cached `g_admin_wot_contacts` |
|
||||
| `context_admin_relays` | Admin kind 10002 relay list JSON | cached `g_admin_kind10002_json` |
|
||||
| `context_admin_notes` | Admin recent kind 1 notes | cached `g_admin_kind1_notes` |
|
||||
| `context_tasks` | Current task list from tasks.json | file read (already exists as `task_manage` with `action: list`) |
|
||||
| `context_user_message` | Current user message text | passed via tool context |
|
||||
| `context_agent_identity` | Agent pubkey, npub | config |
|
||||
|
||||
Existing tools that already work for context:
|
||||
- `tool_list` — returns tool schemas (already exists)
|
||||
- `task_manage` with `{"action":"list"}` — returns tasks (already exists)
|
||||
- `file_read` — can read any file (already exists)
|
||||
|
||||
### Tool Result → Message Content
|
||||
|
||||
Tool results are JSON objects like `{"success":true,"content":"..."}`. The template system extracts the `content` field (or a configurable field) as the message text. If the tool returns an error or empty content, and `skip_if_empty: true` is set, the section is omitted.
|
||||
|
||||
For tools that return structured data (like `tool_list` returning a schema array), the template can specify a `result_field` or `format` to control extraction:
|
||||
|
||||
```yaml
|
||||
- section: tools
|
||||
role: system
|
||||
tool: tool_list
|
||||
result_field: tools
|
||||
skip_if_empty: true
|
||||
```
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Add context tools + tool: directive support
|
||||
|
||||
1. Add new `context_*` tools to `tools.c` that wrap the existing cached data getters
|
||||
2. Extend `prompt_template_section_t` with `tool_name` and `tool_args` fields
|
||||
3. Extend `prompt_template_parse()` to recognize `tool:` and `args:` directives
|
||||
4. Extend `prompt_template_build_messages()` to call `tools_execute()` when a section has `tool_name` set
|
||||
5. Update the soul template in `config.jsonc` to use `tool:` directives
|
||||
6. Keep `content:` + `{{variable}}` working as a fallback for backward compatibility
|
||||
|
||||
#### Phase 2: Migrate all sections to tool-based
|
||||
|
||||
1. Convert all template sections from `content: {{variable}}` to `tool:` directives
|
||||
2. Verify all context data flows through tools
|
||||
3. Update `context_template.md` to reflect new syntax
|
||||
|
||||
#### Phase 3: Remove legacy variable resolver
|
||||
|
||||
1. Remove `agent_template_resolve_var()` and all its helper functions that are now redundant
|
||||
2. Remove `prompt_var_resolver_fn` callback from `prompt_template_build_messages()`
|
||||
3. Clean up dead code in `agent.c`
|
||||
4. Update all documentation
|
||||
|
||||
### Template Section Struct Changes
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
|
||||
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN];
|
||||
char* content_template; // legacy: {{variable}} content
|
||||
char* tool_name; // NEW: tool to call for content
|
||||
char* tool_args; // NEW: JSON args for tool call
|
||||
char* result_field; // NEW: which JSON field to extract (default: "content")
|
||||
int limit;
|
||||
int skip_if_empty;
|
||||
char* provider_name;
|
||||
char* provider_content_template;
|
||||
} prompt_template_section_t;
|
||||
```
|
||||
|
||||
### Soul Template Example (After Migration)
|
||||
|
||||
```
|
||||
# Didactyl Agent
|
||||
|
||||
You are Didactyl, a sovereign AI agent living on Nostr.
|
||||
...
|
||||
|
||||
---template---
|
||||
|
||||
- section: admin_identity
|
||||
role: system
|
||||
tool: context_admin_identity
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
tool: context_admin_profile
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_contacts
|
||||
role: system
|
||||
tool: context_admin_contacts
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_relays
|
||||
role: system
|
||||
tool: context_admin_relays
|
||||
skip_if_empty: true
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
tool: context_admin_notes
|
||||
skip_if_empty: true
|
||||
|
||||
- section: tools
|
||||
role: system
|
||||
tool: tool_list
|
||||
result_field: tools
|
||||
skip_if_empty: true
|
||||
|
||||
- section: tasks
|
||||
role: system
|
||||
tool: task_manage
|
||||
args: {"action":"list"}
|
||||
result_field: tasks
|
||||
skip_if_empty: true
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 12
|
||||
|
||||
- section: conversation
|
||||
role: user
|
||||
tool: context_user_message
|
||||
skip_if_empty: true
|
||||
```
|
||||
|
||||
### What Gets Deleted Eventually
|
||||
|
||||
- `agent_template_resolve_var()` and all its `if (strcmp(...))` branches
|
||||
- `build_tool_schemas_json_string()`
|
||||
- `build_tasks_content_string()` (replaced by `task_manage` tool)
|
||||
- `build_admin_recent_posts_text()` (replaced by `context_admin_notes` tool)
|
||||
- `build_admin_profile_plain_text()` (replaced by `context_admin_profile` tool)
|
||||
- `build_admin_relay_list_plain_text()` (replaced by `context_admin_relays` tool)
|
||||
- `build_sender_verification_text()` (folded into `context_admin_identity` tool)
|
||||
- `build_startup_events_json_string()` (replaced by tool if needed)
|
||||
- `build_adopted_skills_payload_string()` (replaced by tool if needed)
|
||||
- `nostr_handler_get_admin_kind3_context()` (just added, will be replaced by tool)
|
||||
- The `prompt_var_resolver_fn` callback type
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Single data-fetching mechanism** — tools are the only way to get data
|
||||
2. **No more variable name mismatches** — tool names are validated at registration
|
||||
3. **User-configurable context** — admin can add any tool output to context by editing the soul template
|
||||
4. **Testable** — `--test-tool context_admin_profile` shows exactly what goes into context
|
||||
5. **Self-documenting** — `tool_list` shows all available context tools with descriptions
|
||||
6. **Extensible** — new tools automatically become available as context sources
|
||||
7. **Provider overrides still work** — `provider:` directive can still override formatting per-provider
|
||||
|
||||
### Risks
|
||||
|
||||
1. **Performance** — Tool calls add function dispatch overhead vs direct variable resolution. Mitigated: context tools read cached data, no network calls.
|
||||
2. **Backward compatibility** — Old soul templates with `{{variables}}` would break. Mitigated: Phase 1 keeps both paths working.
|
||||
3. **Tool context threading** — `tools_execute()` needs access to the tools context, which `prompt_template_build_messages()` doesn't currently have. Solution: pass `tools_context_t*` to the build function.
|
||||
+69
-314
@@ -35,7 +35,7 @@ static pthread_mutex_t g_context_part_names_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define AGENT_DEBOUNCE_CACHE_SIZE 256
|
||||
#define AGENT_HISTORY_TURNS 12
|
||||
#define AGENT_HISTORY_QUERY_LIMIT 200
|
||||
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 30
|
||||
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 300
|
||||
#define AGENT_ADOPTED_SKILLS_MAX 24
|
||||
#define AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS 1800
|
||||
#define AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS 7200
|
||||
@@ -62,10 +62,6 @@ static int g_adopted_skills_count = 0;
|
||||
static time_t g_adopted_skills_last_refresh_at = 0;
|
||||
static pthread_mutex_t g_adopted_skills_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
typedef struct {
|
||||
didactyl_sender_tier_t sender_tier;
|
||||
} agent_template_resolver_ctx_t;
|
||||
|
||||
static uint64_t fnv1a64(const char* s) {
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
if (!s) return h;
|
||||
@@ -1401,10 +1397,12 @@ static int append_recent_admin_dm_history(cJSON* messages, const char* current_m
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors) {
|
||||
cJSON* p_values = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors || !p_values) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
cJSON_Delete(p_values);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1413,9 +1411,12 @@ static int append_recent_admin_dm_history(cJSON* messages, const char* current_m
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddItemToArray(p_values, cJSON_CreateString(g_cfg->admin.pubkey));
|
||||
cJSON_AddItemToArray(p_values, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "#p", p_values);
|
||||
cJSON_AddNumberToObject(filter, "limit", AGENT_HISTORY_QUERY_LIMIT);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 8000);
|
||||
char* events_json = nostr_handler_query_json(filter, 3000);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
return 0;
|
||||
@@ -1632,25 +1633,7 @@ static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
int tmp_count = 0;
|
||||
|
||||
cJSON* adopt_filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!adopt_filter || !kinds || !authors) {
|
||||
cJSON_Delete(adopt_filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
|
||||
cJSON_AddItemToObject(adopt_filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(adopt_filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(adopt_filter, "limit", 1);
|
||||
|
||||
char* adoption_json = nostr_handler_query_json(adopt_filter, 8000);
|
||||
cJSON_Delete(adopt_filter);
|
||||
|
||||
char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123);
|
||||
cJSON* adoption_events = adoption_json ? cJSON_Parse(adoption_json) : NULL;
|
||||
free(adoption_json);
|
||||
|
||||
@@ -1680,34 +1663,65 @@ static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* skill_filter = cJSON_CreateObject();
|
||||
cJSON* sk_kinds = cJSON_CreateArray();
|
||||
cJSON* sk_authors = cJSON_CreateArray();
|
||||
cJSON* d_values = cJSON_CreateArray();
|
||||
if (!skill_filter || !sk_kinds || !sk_authors || !d_values) {
|
||||
cJSON* skill_events = NULL;
|
||||
if (strcmp(skill_author, g_cfg->keys.public_key_hex) == 0) {
|
||||
char* skill_json = nostr_handler_get_self_events_by_kind_json(skill_kind);
|
||||
if (skill_json) {
|
||||
cJSON* all_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
if (all_events && cJSON_IsArray(all_events)) {
|
||||
skill_events = cJSON_CreateArray();
|
||||
if (skill_events) {
|
||||
int all_n = cJSON_GetArraySize(all_events);
|
||||
for (int ai = 0; ai < all_n; ai++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(all_events, ai);
|
||||
cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
|
||||
cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
||||
cJSON* ev_d = find_tag_value_string_local(ev_tags, "d");
|
||||
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
|
||||
strcmp(ev_pubkey->valuestring, skill_author) != 0 ||
|
||||
!ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring ||
|
||||
strcmp(ev_d->valuestring, skill_slug) != 0) {
|
||||
continue;
|
||||
}
|
||||
cJSON* dup = cJSON_Duplicate(ev, 1);
|
||||
if (dup) {
|
||||
cJSON_AddItemToArray(skill_events, dup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cJSON_Delete(all_events);
|
||||
}
|
||||
} else {
|
||||
cJSON* skill_filter = cJSON_CreateObject();
|
||||
cJSON* sk_kinds = cJSON_CreateArray();
|
||||
cJSON* sk_authors = cJSON_CreateArray();
|
||||
cJSON* d_values = cJSON_CreateArray();
|
||||
if (!skill_filter || !sk_kinds || !sk_authors || !d_values) {
|
||||
cJSON_Delete(skill_filter);
|
||||
cJSON_Delete(sk_kinds);
|
||||
cJSON_Delete(sk_authors);
|
||||
cJSON_Delete(d_values);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(skill_kind));
|
||||
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
|
||||
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(skill_author));
|
||||
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
|
||||
cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_slug));
|
||||
cJSON_AddItemToObject(skill_filter, "#d", d_values);
|
||||
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||
|
||||
char* skill_json = nostr_handler_query_json(skill_filter, 2000);
|
||||
cJSON_Delete(skill_filter);
|
||||
cJSON_Delete(sk_kinds);
|
||||
cJSON_Delete(sk_authors);
|
||||
cJSON_Delete(d_values);
|
||||
continue;
|
||||
if (skill_json) {
|
||||
skill_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(skill_kind));
|
||||
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
|
||||
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(skill_author));
|
||||
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
|
||||
cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_slug));
|
||||
cJSON_AddItemToObject(skill_filter, "#d", d_values);
|
||||
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||
|
||||
char* skill_json = nostr_handler_query_json(skill_filter, 8000);
|
||||
cJSON_Delete(skill_filter);
|
||||
if (!skill_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* skill_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
|
||||
cJSON_Delete(skill_events);
|
||||
continue;
|
||||
@@ -1972,264 +1986,6 @@ static cJSON* build_recent_admin_dm_history_messages(const char* current_message
|
||||
return messages;
|
||||
}
|
||||
|
||||
static char* build_startup_events_json_string(void) {
|
||||
if (!g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
|
||||
return strdup("[]");
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_CreateArray();
|
||||
if (!arr) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
||||
startup_event_t* se = &g_cfg->startup_events[i];
|
||||
if (!se || se->kind == 31120) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* item = cJSON_CreateObject();
|
||||
if (!item) {
|
||||
cJSON_Delete(arr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddNumberToObject(item, "kind", se->kind);
|
||||
cJSON_AddStringToObject(item, "content", se->content ? se->content : "");
|
||||
|
||||
cJSON* tags = NULL;
|
||||
if (se->tags_json) {
|
||||
tags = cJSON_Parse(se->tags_json);
|
||||
}
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
tags = cJSON_CreateArray();
|
||||
}
|
||||
cJSON_AddItemToObject(item, "tags", tags);
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
}
|
||||
|
||||
char* out = cJSON_PrintUnformatted(arr);
|
||||
cJSON_Delete(arr);
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* build_adopted_skills_payload_string(void) {
|
||||
cJSON* tmp = cJSON_CreateArray();
|
||||
if (!tmp) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (append_adopted_skills_context(tmp) != 0) {
|
||||
cJSON_Delete(tmp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* first = cJSON_GetArrayItem(tmp, 0);
|
||||
cJSON* content = first ? cJSON_GetObjectItemCaseSensitive(first, "content") : NULL;
|
||||
char* out = (content && cJSON_IsString(content) && content->valuestring)
|
||||
? strdup(content->valuestring)
|
||||
: strdup("");
|
||||
cJSON_Delete(tmp);
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* build_tasks_content_string(void) {
|
||||
const char* fallback =
|
||||
"### Current Tasks\n\n"
|
||||
"Your active task list - short-term working memory for tracking plan steps.\n\n"
|
||||
"- [ ] No active tasks yet.\n";
|
||||
|
||||
if (!g_cfg) {
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
const char* cwd = g_cfg->tools.shell.working_directory[0] != '\0'
|
||||
? g_cfg->tools.shell.working_directory
|
||||
: ".";
|
||||
|
||||
char tasks_path[1024];
|
||||
int pn = 0;
|
||||
if (strcmp(cwd, ".") == 0) {
|
||||
pn = snprintf(tasks_path, sizeof(tasks_path), "tasks.json");
|
||||
} else {
|
||||
pn = snprintf(tasks_path, sizeof(tasks_path), "%s/%s", cwd, "tasks.json");
|
||||
}
|
||||
if (pn <= 0 || (size_t)pn >= sizeof(tasks_path)) {
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
FILE* fp = fopen(tasks_path, "rb");
|
||||
if (!fp) {
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
long len = ftell(fp);
|
||||
if (len < 0) {
|
||||
fclose(fp);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
char* raw = (char*)malloc((size_t)len + 1U);
|
||||
if (!raw) {
|
||||
fclose(fp);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
size_t n = fread(raw, 1, (size_t)len, fp);
|
||||
fclose(fp);
|
||||
if (n != (size_t)len) {
|
||||
free(raw);
|
||||
return strdup(fallback);
|
||||
}
|
||||
raw[len] = '\0';
|
||||
|
||||
cJSON* root = cJSON_Parse(raw);
|
||||
free(raw);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
|
||||
if (!tasks || !cJSON_IsArray(tasks)) {
|
||||
cJSON_Delete(root);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
size_t cap = 1024U;
|
||||
size_t used = 0U;
|
||||
char* out = (char*)malloc(cap);
|
||||
if (!out) {
|
||||
cJSON_Delete(root);
|
||||
return strdup(fallback);
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (appendf(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"### Current Tasks\n\n"
|
||||
"Your active task list - short-term working memory for tracking plan steps.\n\n") != 0) {
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
int written = 0;
|
||||
int tcount = cJSON_GetArraySize(tasks);
|
||||
for (int i = 0; i < tcount; i++) {
|
||||
cJSON* task = cJSON_GetArrayItem(tasks, i);
|
||||
if (!task || !cJSON_IsObject(task)) continue;
|
||||
|
||||
cJSON* id = cJSON_GetObjectItemCaseSensitive(task, "id");
|
||||
cJSON* text = cJSON_GetObjectItemCaseSensitive(task, "text");
|
||||
cJSON* status = cJSON_GetObjectItemCaseSensitive(task, "status");
|
||||
|
||||
if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* status_s = (status && cJSON_IsString(status) && status->valuestring)
|
||||
? status->valuestring
|
||||
: "pending";
|
||||
const char* mark = " ";
|
||||
if (strcmp(status_s, "done") == 0) {
|
||||
mark = "x";
|
||||
} else if (strcmp(status_s, "active") == 0) {
|
||||
mark = "-";
|
||||
}
|
||||
|
||||
int id_value = (id && cJSON_IsNumber(id)) ? (int)id->valuedouble : (i + 1);
|
||||
if (appendf(&out, &cap, &used, "- [%s] %d. %s\n", mark, id_value, text->valuestring) != 0) {
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return strdup(fallback);
|
||||
}
|
||||
written++;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (written <= 0) {
|
||||
free(out);
|
||||
return strdup(fallback);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* safe_strdup(const char* s) {
|
||||
return strdup(s ? s : "");
|
||||
}
|
||||
|
||||
static char* agent_template_resolve_var(const char* var_name, void* user_data) {
|
||||
agent_template_resolver_ctx_t* ctx = (agent_template_resolver_ctx_t*)user_data;
|
||||
if (!var_name || !g_cfg) {
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
if (strcmp(var_name, "admin_pubkey") == 0) {
|
||||
return safe_strdup(g_cfg->admin.pubkey);
|
||||
}
|
||||
if (strcmp(var_name, "agent_pubkey") == 0) {
|
||||
return safe_strdup(g_cfg->keys.public_key_hex);
|
||||
}
|
||||
if (strcmp(var_name, "admin_kind0_json") == 0) {
|
||||
char* v = nostr_handler_get_admin_kind0_context();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_kind10002_json") == 0) {
|
||||
char* v = nostr_handler_get_admin_kind10002_context();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_profile_plain") == 0) {
|
||||
char* v = build_admin_profile_plain_text();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_relay_list_plain") == 0) {
|
||||
char* v = build_admin_relay_list_plain_text();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "sender_verification") == 0) {
|
||||
didactyl_sender_tier_t tier = ctx ? ctx->sender_tier : DIDACTYL_SENDER_ADMIN;
|
||||
char* v = build_sender_verification_text(tier);
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "startup_events_json") == 0) {
|
||||
char* v = build_startup_events_json_string();
|
||||
return v ? v : strdup("[]");
|
||||
}
|
||||
if (strcmp(var_name, "adopted_skills_content") == 0) {
|
||||
char* v = build_adopted_skills_payload_string();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "tasks_content") == 0) {
|
||||
char* v = build_tasks_content_string();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_notes_content") == 0) {
|
||||
char* v = build_admin_recent_posts_text();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_recent_posts") == 0) {
|
||||
char* v = build_admin_recent_posts_text();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
if (!config || !system_context) {
|
||||
return -1;
|
||||
@@ -2355,9 +2111,6 @@ int agent_build_admin_messages_json(const char* current_user_message,
|
||||
dm_history = cJSON_CreateArray();
|
||||
}
|
||||
|
||||
agent_template_resolver_ctx_t resolver_ctx;
|
||||
resolver_ctx.sender_tier = sender_tier;
|
||||
|
||||
llm_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
const char* provider_name = NULL;
|
||||
@@ -2365,10 +2118,12 @@ int agent_build_admin_messages_json(const char* current_user_message,
|
||||
provider_name = cfg.provider;
|
||||
}
|
||||
|
||||
g_tools_ctx.template_sender_tier = (int)sender_tier;
|
||||
g_tools_ctx.template_current_user_message = current_user_message;
|
||||
|
||||
messages = prompt_template_build_messages(&g_prompt_template,
|
||||
provider_name,
|
||||
agent_template_resolve_var,
|
||||
&resolver_ctx,
|
||||
&g_tools_ctx,
|
||||
dm_history,
|
||||
AGENT_HISTORY_TURNS,
|
||||
template_emit_hook,
|
||||
|
||||
+94
-3
@@ -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
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "llm.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -69,6 +70,20 @@ static int url_looks_like_websocket(const char* url) {
|
||||
return (strncmp(url, "ws://", 5) == 0) || (strncmp(url, "wss://", 6) == 0);
|
||||
}
|
||||
|
||||
static int json_string_is_blank(const cJSON* item) {
|
||||
if (!item || !cJSON_IsString(item) || !item->valuestring) {
|
||||
return 0;
|
||||
}
|
||||
const unsigned char* p = (const unsigned char*)item->valuestring;
|
||||
while (*p) {
|
||||
if (!isspace(*p)) {
|
||||
return 0;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static char* perform_http_request(const char* url, const char* body, int is_post) {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl || !url) {
|
||||
@@ -111,6 +126,19 @@ static char* perform_http_request(const char* url, const char* body, int is_post
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
|
||||
}
|
||||
|
||||
if (is_post) {
|
||||
size_t body_len = body ? strlen(body) : 0U;
|
||||
size_t body_preview_len = body_len > 4000U ? 4000U : body_len;
|
||||
fprintf(stderr,
|
||||
"[didactyl] llm request: method=POST url=%s body_bytes=%zu body_preview=%.4000s%s\n",
|
||||
url,
|
||||
body_len,
|
||||
body ? body : "",
|
||||
body_len > body_preview_len ? "..." : "");
|
||||
} else {
|
||||
fprintf(stderr, "[didactyl] llm request: method=GET url=%s\n", url);
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
@@ -340,6 +368,36 @@ int llm_chat_with_tools_messages(const char* messages_json,
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int filtered_count = 0;
|
||||
for (int i = cJSON_GetArraySize(messages) - 1; i >= 0; i--) {
|
||||
cJSON* msg = cJSON_GetArrayItem(messages, i);
|
||||
if (!msg || !cJSON_IsObject(msg)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
|
||||
cJSON* tool_calls = cJSON_GetObjectItemCaseSensitive(msg, "tool_calls");
|
||||
|
||||
int role_is_textual = (role && cJSON_IsString(role) && role->valuestring &&
|
||||
(strcmp(role->valuestring, "system") == 0 ||
|
||||
strcmp(role->valuestring, "user") == 0 ||
|
||||
strcmp(role->valuestring, "assistant") == 0));
|
||||
int has_tool_calls = (tool_calls && cJSON_IsArray(tool_calls) && cJSON_GetArraySize(tool_calls) > 0);
|
||||
|
||||
if (role_is_textual && !has_tool_calls && json_string_is_blank(content)) {
|
||||
cJSON_DeleteItemFromArray(messages, i);
|
||||
filtered_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filtered_count > 0) {
|
||||
fprintf(stderr,
|
||||
"[didactyl] llm request sanitizer: removed %d empty text message(s) before provider request\n",
|
||||
filtered_count);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "messages", messages);
|
||||
|
||||
if (tools_json) {
|
||||
|
||||
+9
-1
@@ -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;
|
||||
@@ -242,6 +242,12 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
|
||||
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
|
||||
if (nostr_handler_subscribe_self_skills() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
|
||||
|
||||
char startup_dm[768];
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
int connected_relays = nostr_handler_connected_relay_count();
|
||||
@@ -318,6 +324,8 @@ int main(int argc, char** argv) {
|
||||
DEBUG_INFO("[didactyl] HTTP API disabled (set api.enabled=true in config to enable)");
|
||||
}
|
||||
|
||||
nostr_handler_refresh_relay_statuses();
|
||||
|
||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
||||
|
||||
|
||||
+2
-2
@@ -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 34
|
||||
#define DIDACTYL_VERSION "v0.0.34"
|
||||
#define DIDACTYL_VERSION_PATCH 49
|
||||
#define DIDACTYL_VERSION "v0.0.49"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
+371
-65
@@ -43,6 +43,9 @@ static int g_admin_kind1_note_count = 0;
|
||||
|
||||
static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static cJSON* g_self_skill_events = NULL;
|
||||
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define DM_DEDUP_CACHE_SIZE 256
|
||||
|
||||
static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
|
||||
@@ -207,6 +210,7 @@ static void upsert_kind1_note(time_t created_at, const char* content);
|
||||
static int startup_self_kind1_exists(void);
|
||||
static void load_startup_display_name(void);
|
||||
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
|
||||
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
|
||||
static void log_relay_state_changes(void) {
|
||||
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
|
||||
@@ -218,10 +222,19 @@ static void log_relay_state_changes(void) {
|
||||
nostr_pool_relay_status_t now = nostr_relay_pool_get_relay_status(g_pool, relay);
|
||||
nostr_pool_relay_status_t prev = g_last_relay_statuses[i];
|
||||
if (now != prev) {
|
||||
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
|
||||
relay,
|
||||
relay_status_str(prev),
|
||||
relay_status_str(now));
|
||||
const char* conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
|
||||
if (conn_err && (now == NOSTR_POOL_RELAY_DISCONNECTED || now == NOSTR_POOL_RELAY_ERROR)) {
|
||||
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s (cause: %s)",
|
||||
relay,
|
||||
relay_status_str(prev),
|
||||
relay_status_str(now),
|
||||
conn_err);
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
|
||||
relay,
|
||||
relay_status_str(prev),
|
||||
relay_status_str(now));
|
||||
}
|
||||
g_last_relay_statuses[i] = now;
|
||||
|
||||
if (now == NOSTR_POOL_RELAY_CONNECTED) {
|
||||
@@ -231,6 +244,16 @@ static void log_relay_state_changes(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void nostr_handler_refresh_relay_statuses(void) {
|
||||
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->relay_count; i++) {
|
||||
g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_publish_targets(const char* action) {
|
||||
if (!g_cfg) {
|
||||
return;
|
||||
@@ -334,6 +357,123 @@ static void build_startup_kind1_content(char* out, size_t out_size, const char*
|
||||
}
|
||||
}
|
||||
|
||||
static const char* find_tag_value_local(cJSON* tags, const char* key) {
|
||||
if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
|
||||
!cJSON_IsString(v) || !v->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(k->valuestring, key) == 0) {
|
||||
return v->valuestring;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void self_skill_cache_upsert_event_locked(cJSON* event) {
|
||||
if (!event || !cJSON_IsObject(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||||
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
||||
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
|
||||
|
||||
if (!kind || !cJSON_IsNumber(kind) ||
|
||||
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
|
||||
!tags || !cJSON_IsArray(tags)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int kind_val = (int)kind->valuedouble;
|
||||
if (kind_val != 31123 && kind_val != 31124 && kind_val != 10123) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_cfg || strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* d_tag = find_tag_value_local(tags, "d");
|
||||
if ((kind_val == 31123 || kind_val == 31124) && (!d_tag || d_tag[0] == '\0')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_self_skill_events || !cJSON_IsArray(g_self_skill_events)) {
|
||||
cJSON_Delete(g_self_skill_events);
|
||||
g_self_skill_events = cJSON_CreateArray();
|
||||
if (!g_self_skill_events) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
|
||||
|
||||
int n = cJSON_GetArraySize(g_self_skill_events);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* existing = cJSON_GetArrayItem(g_self_skill_events, i);
|
||||
if (!existing || !cJSON_IsObject(existing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* ex_kind = cJSON_GetObjectItemCaseSensitive(existing, "kind");
|
||||
cJSON* ex_pubkey = cJSON_GetObjectItemCaseSensitive(existing, "pubkey");
|
||||
cJSON* ex_tags = cJSON_GetObjectItemCaseSensitive(existing, "tags");
|
||||
if (!ex_kind || !cJSON_IsNumber(ex_kind) ||
|
||||
!ex_pubkey || !cJSON_IsString(ex_pubkey) || !ex_pubkey->valuestring ||
|
||||
!ex_tags || !cJSON_IsArray(ex_tags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int ex_kind_val = (int)ex_kind->valuedouble;
|
||||
if (ex_kind_val != kind_val || strcmp(ex_pubkey->valuestring, pubkey->valuestring) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kind_val == 31123 || kind_val == 31124) {
|
||||
const char* ex_d = find_tag_value_local(ex_tags, "d");
|
||||
if (!ex_d || !d_tag || strcmp(ex_d, d_tag) != 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* ex_created_at = cJSON_GetObjectItemCaseSensitive(existing, "created_at");
|
||||
double ex_created = (ex_created_at && cJSON_IsNumber(ex_created_at)) ? ex_created_at->valuedouble : 0.0;
|
||||
if (created < ex_created) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* dup = cJSON_Duplicate(event, 1);
|
||||
if (!dup) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON_ReplaceItemInArray(g_self_skill_events, i, dup);
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* dup = cJSON_Duplicate(event, 1);
|
||||
if (!dup) {
|
||||
return;
|
||||
}
|
||||
cJSON_AddItemToArray(g_self_skill_events, dup);
|
||||
}
|
||||
|
||||
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
|
||||
if (!hex || !out_pubkey || strlen(hex) != 64U) {
|
||||
return -1;
|
||||
@@ -937,6 +1077,23 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
}
|
||||
|
||||
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)relay_url;
|
||||
(void)user_data;
|
||||
|
||||
if (!event || !g_cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
}
|
||||
|
||||
int nostr_handler_init(didactyl_config_t* config) {
|
||||
if (!config) {
|
||||
return -1;
|
||||
@@ -987,6 +1144,14 @@ int nostr_handler_init(didactyl_config_t* config) {
|
||||
g_startup_published = NULL;
|
||||
g_startup_publish_tracking_enabled = 0;
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
cJSON_Delete(g_self_skill_events);
|
||||
g_self_skill_events = cJSON_CreateArray();
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
if (!g_self_skill_events) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1088,6 +1253,52 @@ int nostr_handler_subscribe_admin_context(void) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nostr_handler_subscribe_self_skills(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(filter, "limit", 300);
|
||||
|
||||
nostr_pool_subscription_t* skill_sub = nostr_relay_pool_subscribe(
|
||||
g_pool,
|
||||
(const char**)g_cfg->relays,
|
||||
g_cfg->relay_count,
|
||||
filter,
|
||||
on_self_skill_event,
|
||||
on_eose,
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
if (!skill_sub) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] self-event subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
if (!g_cfg || !g_pool || !callback) {
|
||||
return -1;
|
||||
@@ -1139,7 +1350,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
on_eose,
|
||||
NULL,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120);
|
||||
@@ -1190,7 +1401,7 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
on_eose,
|
||||
NULL,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120);
|
||||
@@ -1269,21 +1480,6 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
}
|
||||
}
|
||||
|
||||
int* pre_publish_ok = NULL;
|
||||
if (connected_count > 0) {
|
||||
pre_publish_ok = (int*)calloc((size_t)connected_count, sizeof(int));
|
||||
if (!pre_publish_ok) {
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, connected_relays[i]);
|
||||
pre_publish_ok[i] = stats ? stats->events_published_ok : 0;
|
||||
}
|
||||
}
|
||||
|
||||
int sent = 0;
|
||||
if (connected_count > 0) {
|
||||
sent = nostr_relay_pool_publish_async(
|
||||
@@ -1297,48 +1493,6 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]);
|
||||
}
|
||||
|
||||
// Briefly drain relay messages so NIP-42 AUTH handshake can complete.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
(void)nostr_relay_pool_poll(g_pool, 100);
|
||||
}
|
||||
|
||||
int any_publish_ok = 0;
|
||||
int auth_required_seen = 0;
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, connected_relays[i]);
|
||||
if (stats && stats->events_published_ok > pre_publish_ok[i]) {
|
||||
any_publish_ok = 1;
|
||||
}
|
||||
|
||||
const char* pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, connected_relays[i]);
|
||||
if (pub_err && strstr(pub_err, "auth-required") != NULL) {
|
||||
auth_required_seen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Always retry this same signed event once after handshake drain.
|
||||
// Some relays issue AUTH challenge for the first EVENT and only accept
|
||||
// the subsequent resend after AUTH succeeds.
|
||||
if (sent > 0) {
|
||||
if (auth_required_seen || !any_publish_ok) {
|
||||
DEBUG_WARN("[didactyl] retrying kind 4 event once after auth handshake window");
|
||||
}
|
||||
int resent = nostr_relay_pool_publish_async(
|
||||
g_pool,
|
||||
connected_relays,
|
||||
connected_count,
|
||||
event,
|
||||
NULL,
|
||||
NULL);
|
||||
if (resent > sent) {
|
||||
sent = resent;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
(void)nostr_relay_pool_poll(g_pool, 100);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays");
|
||||
}
|
||||
@@ -1351,7 +1505,6 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
recipient_pubkey_hex,
|
||||
sent);
|
||||
|
||||
free(pre_publish_ok);
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
return sent > 0 ? 0 : -1;
|
||||
@@ -1442,6 +1595,12 @@ static int publish_kind_event_to_relays(int kind,
|
||||
relay_count,
|
||||
sent);
|
||||
|
||||
if (sent > 0) {
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
self_skill_cache_upsert_event_locked(event);
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
}
|
||||
|
||||
cJSON_Delete(event);
|
||||
return sent > 0 ? 0 : -1;
|
||||
}
|
||||
@@ -1633,6 +1792,66 @@ const char* nostr_handler_get_system_context(void) {
|
||||
return g_system_context;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_self_events_by_kind_json(int kind) {
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
|
||||
int n = cJSON_GetArraySize(g_self_skill_events);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
|
||||
cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
||||
if (!ev_kind || !cJSON_IsNumber(ev_kind) || (int)ev_kind->valuedouble != kind) {
|
||||
continue;
|
||||
}
|
||||
cJSON* dup = cJSON_Duplicate(ev, 1);
|
||||
if (dup) {
|
||||
cJSON_AddItemToArray(out, dup);
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json ? json : strdup("[]");
|
||||
}
|
||||
|
||||
char* nostr_handler_get_self_skill_events_json(void) {
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
|
||||
int n = cJSON_GetArraySize(g_self_skill_events);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
|
||||
cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
||||
if (!ev_kind || !cJSON_IsNumber(ev_kind)) {
|
||||
continue;
|
||||
}
|
||||
int k = (int)ev_kind->valuedouble;
|
||||
if (k != 31123 && k != 31124) {
|
||||
continue;
|
||||
}
|
||||
cJSON* dup = cJSON_Duplicate(ev, 1);
|
||||
if (dup) {
|
||||
cJSON_AddItemToArray(out, dup);
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json ? json : strdup("[]");
|
||||
}
|
||||
|
||||
const char* nostr_handler_get_startup_display_name(void) {
|
||||
return g_startup_display_name;
|
||||
}
|
||||
@@ -1885,7 +2104,13 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_TRACE("[didactyl] NIP-17 send prep: recipient=%.16s... connected_targets=%d",
|
||||
recipient_pubkey_hex,
|
||||
target_count);
|
||||
|
||||
if (target_count <= 0) {
|
||||
DEBUG_WARN("[didactyl] NIP-17 send aborted: no connected relays for recipient %.16s...",
|
||||
recipient_pubkey_hex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1912,14 +2137,19 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
|
||||
0);
|
||||
cJSON_Delete(chat_event);
|
||||
if (gift_count <= 0) {
|
||||
DEBUG_WARN("[didactyl] NIP-17 send aborted: gift_wrap creation failed for %.16s...",
|
||||
recipient_pubkey_hex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int any_sent = 0;
|
||||
int sent_count = 0;
|
||||
|
||||
for (int i = 0; i < gift_count; i++) {
|
||||
if (!gift_wraps[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int sent = nostr_relay_pool_publish_async(g_pool,
|
||||
target_relays,
|
||||
target_count,
|
||||
@@ -1928,11 +2158,27 @@ int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* me
|
||||
NULL);
|
||||
if (sent > 0) {
|
||||
any_sent = 1;
|
||||
sent_count += sent;
|
||||
}
|
||||
|
||||
for (int r = 0; r < target_count; r++) {
|
||||
DEBUG_INFO("[didactyl] kind 1059 event published to %s (async)", target_relays[r]);
|
||||
}
|
||||
|
||||
cJSON_Delete(gift_wraps[i]);
|
||||
}
|
||||
|
||||
return any_sent ? 0 : -1;
|
||||
DEBUG_TRACE("[didactyl] NIP-17 send complete: recipient=%.16s... sent_count=%d",
|
||||
recipient_pubkey_hex,
|
||||
sent_count);
|
||||
|
||||
if (!any_sent) {
|
||||
DEBUG_WARN("[didactyl] NIP-17 send failed: no relay accepted publish for recipient %.16s...",
|
||||
recipient_pubkey_hex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind0_context(void) {
|
||||
@@ -1946,6 +2192,32 @@ char* nostr_handler_get_admin_kind0_context(void) {
|
||||
return out;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind3_context(void) {
|
||||
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_3) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) {
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_admin_wot_contact_count; i++) {
|
||||
const char* pk = g_admin_wot_contacts[i];
|
||||
if (!pk || pk[0] == '\0') continue;
|
||||
cJSON_AddItemToArray(out, cJSON_CreateString(pk));
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
|
||||
char* out_json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return out_json;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind10002_context(void) {
|
||||
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_10002) {
|
||||
return NULL;
|
||||
@@ -2014,9 +2286,38 @@ int nostr_handler_poll(int timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
double start_ms = -1.0;
|
||||
struct timespec ts_start;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == 0) {
|
||||
start_ms = ts_start.tv_sec * 1000.0 + ts_start.tv_nsec / 1000000.0;
|
||||
}
|
||||
|
||||
int rc = nostr_relay_pool_poll(g_pool, timeout_ms);
|
||||
g_poll_counter++;
|
||||
|
||||
if (start_ms >= 0.0) {
|
||||
struct timespec ts_end;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_end) == 0) {
|
||||
const double end_ms = ts_end.tv_sec * 1000.0 + ts_end.tv_nsec / 1000000.0;
|
||||
const double elapsed_ms = end_ms - start_ms;
|
||||
const double expected_ms = timeout_ms > 0 ? (double)timeout_ms : 0.0;
|
||||
|
||||
if (elapsed_ms > expected_ms + 250.0) {
|
||||
DEBUG_WARN("[didactyl] poll latency spike: nostr_relay_pool_poll(timeout=%d) took %.1fms rc=%d count=%llu",
|
||||
timeout_ms,
|
||||
elapsed_ms,
|
||||
rc,
|
||||
(unsigned long long)g_poll_counter);
|
||||
} else if ((g_poll_counter % 200ULL) == 0ULL) {
|
||||
DEBUG_TRACE("[didactyl] poll heartbeat: timeout=%d elapsed=%.1fms rc=%d count=%llu",
|
||||
timeout_ms,
|
||||
elapsed_ms,
|
||||
rc,
|
||||
(unsigned long long)g_poll_counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_relay_state_changes();
|
||||
|
||||
return rc;
|
||||
@@ -2052,4 +2353,9 @@ void nostr_handler_cleanup(void) {
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
free_admin_context_locked();
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
|
||||
pthread_mutex_lock(&g_self_skill_mutex);
|
||||
cJSON_Delete(g_self_skill_events);
|
||||
g_self_skill_events = NULL;
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ typedef struct {
|
||||
|
||||
int nostr_handler_init(didactyl_config_t* config);
|
||||
int nostr_handler_subscribe_admin_context(void);
|
||||
int nostr_handler_subscribe_self_skills(void);
|
||||
char* nostr_handler_get_self_events_by_kind_json(int kind);
|
||||
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);
|
||||
@@ -37,10 +39,13 @@ void nostr_handler_publish_result_free(nostr_publish_result_t* result);
|
||||
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
void nostr_handler_refresh_relay_statuses(void);
|
||||
const char* nostr_handler_get_system_context(void);
|
||||
char* nostr_handler_get_self_skill_events_json(void);
|
||||
const char* nostr_handler_get_startup_display_name(void);
|
||||
int nostr_handler_connected_relay_count(void);
|
||||
char* nostr_handler_get_admin_kind0_context(void);
|
||||
char* nostr_handler_get_admin_kind3_context(void);
|
||||
char* nostr_handler_get_admin_kind10002_context(void);
|
||||
char* nostr_handler_get_admin_kind1_notes_context(void);
|
||||
int nostr_handler_is_wot_contact(const char* pubkey_hex);
|
||||
|
||||
+97
-83
@@ -97,87 +97,15 @@ static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* resolve_placeholders(const char* tpl,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data) {
|
||||
if (!tpl) return strdup("");
|
||||
|
||||
size_t cap = strlen(tpl) + 64U;
|
||||
char* out = (char*)malloc(cap ? cap : 128U);
|
||||
if (!out) return NULL;
|
||||
out[0] = '\0';
|
||||
size_t used = 0;
|
||||
|
||||
const char* p = tpl;
|
||||
while (*p) {
|
||||
const char* open = strstr(p, "{{");
|
||||
if (!open) {
|
||||
if (append_text(&out, &cap, &used, p) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (open > p) {
|
||||
char* literal = dup_range(p, (size_t)(open - p));
|
||||
if (!literal) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
int rc = append_text(&out, &cap, &used, literal);
|
||||
free(literal);
|
||||
if (rc != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const char* close = strstr(open + 2, "}}");
|
||||
if (!close) {
|
||||
if (append_text(&out, &cap, &used, open) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
char* name = dup_range(open + 2, (size_t)(close - (open + 2)));
|
||||
if (!name) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
char* name_trim = ltrim_inplace(name);
|
||||
rtrim_inplace(name_trim);
|
||||
|
||||
char* val = resolver_fn ? resolver_fn(name_trim, resolver_user_data) : NULL;
|
||||
if (!val) {
|
||||
val = strdup("");
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
if (val) {
|
||||
rc = append_text(&out, &cap, &used, val);
|
||||
}
|
||||
free(val);
|
||||
free(name);
|
||||
if (rc != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
p = close + 2;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static void init_section_defaults(prompt_template_section_t* sec) {
|
||||
if (!sec) return;
|
||||
memset(sec->name, 0, sizeof(sec->name));
|
||||
memset(sec->role, 0, sizeof(sec->role));
|
||||
snprintf(sec->role, sizeof(sec->role), "system");
|
||||
sec->content_template = NULL;
|
||||
sec->tool_name = NULL;
|
||||
sec->tool_args = NULL;
|
||||
sec->result_field = NULL;
|
||||
sec->limit = 0;
|
||||
sec->skip_if_empty = 0;
|
||||
sec->provider_name = NULL;
|
||||
@@ -292,6 +220,36 @@ int prompt_template_parse(const char* soul_content, prompt_template_t* out_templ
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "tool:")) {
|
||||
char* val = line + strlen("tool:");
|
||||
val = ltrim_inplace(val);
|
||||
rtrim_inplace(val);
|
||||
free(out_template->sections[current].tool_name);
|
||||
out_template->sections[current].tool_name = strdup(val);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "args:")) {
|
||||
char* val = line + strlen("args:");
|
||||
val = ltrim_inplace(val);
|
||||
rtrim_inplace(val);
|
||||
free(out_template->sections[current].tool_args);
|
||||
out_template->sections[current].tool_args = strdup((*val) ? val : "{}");
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "result_field:")) {
|
||||
char* val = line + strlen("result_field:");
|
||||
val = ltrim_inplace(val);
|
||||
rtrim_inplace(val);
|
||||
free(out_template->sections[current].result_field);
|
||||
out_template->sections[current].result_field = strdup(val);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "content:")) {
|
||||
char* val = line + strlen("content:");
|
||||
val = ltrim_inplace(val);
|
||||
@@ -319,6 +277,9 @@ int prompt_template_parse(const char* soul_content, prompt_template_t* out_templ
|
||||
starts_with(next_ltrim, "role:") ||
|
||||
starts_with(next_ltrim, "limit:") ||
|
||||
starts_with(next_ltrim, "skip_if_empty:") ||
|
||||
starts_with(next_ltrim, "tool:") ||
|
||||
starts_with(next_ltrim, "args:") ||
|
||||
starts_with(next_ltrim, "result_field:") ||
|
||||
starts_with(next_ltrim, "content:") ||
|
||||
starts_with(next_ltrim, "provider:")) {
|
||||
break;
|
||||
@@ -391,6 +352,9 @@ int prompt_template_parse(const char* soul_content, prompt_template_t* out_templ
|
||||
starts_with(next, "role:") ||
|
||||
starts_with(next, "limit:") ||
|
||||
starts_with(next, "skip_if_empty:") ||
|
||||
starts_with(next, "tool:") ||
|
||||
starts_with(next, "args:") ||
|
||||
starts_with(next, "result_field:") ||
|
||||
starts_with(next, "content:") ||
|
||||
starts_with(next, "provider:")) {
|
||||
break;
|
||||
@@ -437,10 +401,46 @@ static int append_message_object(cJSON* messages, const char* role, const char*
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* extract_tool_result_content(const prompt_template_section_t* sec, const char* tool_result_json) {
|
||||
if (!tool_result_json) return strdup("");
|
||||
|
||||
cJSON* root = cJSON_Parse(tool_result_json);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return strdup(tool_result_json);
|
||||
}
|
||||
|
||||
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
|
||||
if (success && cJSON_IsBool(success) && !cJSON_IsTrue(success)) {
|
||||
cJSON_Delete(root);
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
const char* field_name = (sec && sec->result_field && sec->result_field[0] != '\0')
|
||||
? sec->result_field
|
||||
: "content";
|
||||
|
||||
cJSON* field = cJSON_GetObjectItemCaseSensitive(root, field_name);
|
||||
if (!field) {
|
||||
field = cJSON_GetObjectItemCaseSensitive(root, "content");
|
||||
}
|
||||
|
||||
char* out = NULL;
|
||||
if (field && cJSON_IsString(field) && field->valuestring) {
|
||||
out = strdup(field->valuestring);
|
||||
} else if (field) {
|
||||
out = cJSON_PrintUnformatted(field);
|
||||
} else {
|
||||
out = cJSON_PrintUnformatted(root);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return out ? out : strdup("");
|
||||
}
|
||||
|
||||
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
||||
const char* provider_name,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data,
|
||||
tools_context_t* tools_ctx,
|
||||
cJSON* dm_history_messages,
|
||||
int dm_history_default_limit,
|
||||
prompt_template_emit_hook_fn emit_hook,
|
||||
@@ -487,13 +487,21 @@ cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* tpl = sec->content_template;
|
||||
if (provider_name && sec->provider_name && sec->provider_content_template &&
|
||||
strcmp(provider_name, sec->provider_name) == 0) {
|
||||
tpl = sec->provider_content_template;
|
||||
}
|
||||
char* resolved = NULL;
|
||||
|
||||
char* resolved = resolve_placeholders(tpl ? tpl : "", resolver_fn, resolver_user_data);
|
||||
if (sec->tool_name && sec->tool_name[0] != '\0' && tools_ctx) {
|
||||
const char* args_json = (sec->tool_args && sec->tool_args[0] != '\0') ? sec->tool_args : "{}";
|
||||
char* tool_result = tools_execute(tools_ctx, sec->tool_name, args_json);
|
||||
resolved = extract_tool_result_content(sec, tool_result);
|
||||
free(tool_result);
|
||||
} else {
|
||||
const char* tpl = sec->content_template;
|
||||
if (provider_name && sec->provider_name && sec->provider_content_template &&
|
||||
strcmp(provider_name, sec->provider_name) == 0) {
|
||||
tpl = sec->provider_content_template;
|
||||
}
|
||||
resolved = strdup(tpl ? tpl : "");
|
||||
}
|
||||
if (!resolved) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
@@ -529,6 +537,12 @@ void prompt_template_free(prompt_template_t* tmpl) {
|
||||
for (int i = 0; i < tmpl->section_count; i++) {
|
||||
free(tmpl->sections[i].content_template);
|
||||
tmpl->sections[i].content_template = NULL;
|
||||
free(tmpl->sections[i].tool_name);
|
||||
tmpl->sections[i].tool_name = NULL;
|
||||
free(tmpl->sections[i].tool_args);
|
||||
tmpl->sections[i].tool_args = NULL;
|
||||
free(tmpl->sections[i].result_field);
|
||||
tmpl->sections[i].result_field = NULL;
|
||||
free(tmpl->sections[i].provider_name);
|
||||
tmpl->sections[i].provider_name = NULL;
|
||||
free(tmpl->sections[i].provider_content_template);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define DIDACTYL_PROMPT_TEMPLATE_H
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "tools.h"
|
||||
|
||||
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
|
||||
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
|
||||
@@ -12,6 +13,9 @@ typedef struct {
|
||||
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
|
||||
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN];
|
||||
char* content_template;
|
||||
char* tool_name;
|
||||
char* tool_args;
|
||||
char* result_field;
|
||||
int limit;
|
||||
int skip_if_empty;
|
||||
char* provider_name;
|
||||
@@ -24,7 +28,6 @@ typedef struct {
|
||||
int section_count;
|
||||
} prompt_template_t;
|
||||
|
||||
typedef char* (*prompt_var_resolver_fn)(const char* var_name, void* user_data);
|
||||
typedef void (*prompt_template_emit_hook_fn)(const char* section_name,
|
||||
int message_index,
|
||||
void* user_data);
|
||||
@@ -33,8 +36,7 @@ int prompt_template_parse(const char* soul_content, prompt_template_t* out_templ
|
||||
|
||||
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
||||
const char* provider_name,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data,
|
||||
tools_context_t* tools_ctx,
|
||||
cJSON* dm_history_messages,
|
||||
int dm_history_default_limit,
|
||||
prompt_template_emit_hook_fn emit_hook,
|
||||
|
||||
+536
-97
@@ -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"
|
||||
@@ -543,6 +544,8 @@ int tools_init(tools_context_t* ctx, didactyl_config_t* cfg) {
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
ctx->cfg = cfg;
|
||||
ctx->trigger_manager = NULL;
|
||||
ctx->template_sender_tier = 0;
|
||||
ctx->template_current_user_message = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -893,24 +896,7 @@ static int fetch_adoption_list_tags(tools_context_t* ctx, cJSON** out_tags, char
|
||||
*out_tags = NULL;
|
||||
*out_content = NULL;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(filter, "limit", 1);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 8000);
|
||||
cJSON_Delete(filter);
|
||||
char* events_json = nostr_handler_get_self_events_by_kind_json(10123);
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
char* content = strdup("");
|
||||
@@ -1844,7 +1830,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);
|
||||
|
||||
@@ -1959,14 +1945,126 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
|
||||
cJSON* t37_fn = cJSON_CreateObject();
|
||||
cJSON* t37_params = cJSON_CreateObject();
|
||||
cJSON* t37_props = cJSON_CreateObject();
|
||||
cJSON* t37_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t37, "type", "function");
|
||||
cJSON_AddStringToObject(t37_fn, "name", "task_manage");
|
||||
cJSON_AddStringToObject(t37_fn, "description", "Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)");
|
||||
cJSON_AddStringToObject(t37_fn, "name", "context_admin_identity");
|
||||
cJSON_AddStringToObject(t37_fn, "description", "Build admin identity context block from cached runtime metadata");
|
||||
cJSON_AddStringToObject(t37_params, "type", "object");
|
||||
cJSON_AddItemToObject(t37_params, "properties", t37_props);
|
||||
cJSON_AddItemToObject(t37_params, "required", t37_required);
|
||||
cJSON_AddItemToObject(t37_fn, "parameters", t37_params);
|
||||
cJSON_AddItemToObject(t37, "function", t37_fn);
|
||||
cJSON_AddItemToArray(tools, t37);
|
||||
|
||||
cJSON* t38 = cJSON_CreateObject();
|
||||
cJSON* t38_fn = cJSON_CreateObject();
|
||||
cJSON* t38_params = cJSON_CreateObject();
|
||||
cJSON* t38_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t38, "type", "function");
|
||||
cJSON_AddStringToObject(t38_fn, "name", "context_admin_profile");
|
||||
cJSON_AddStringToObject(t38_fn, "description", "Build admin profile context block from cached kind 0 metadata");
|
||||
cJSON_AddStringToObject(t38_params, "type", "object");
|
||||
cJSON_AddItemToObject(t38_params, "properties", t38_props);
|
||||
cJSON_AddItemToObject(t38_fn, "parameters", t38_params);
|
||||
cJSON_AddItemToObject(t38, "function", t38_fn);
|
||||
cJSON_AddItemToArray(tools, t38);
|
||||
|
||||
cJSON* t39 = cJSON_CreateObject();
|
||||
cJSON* t39_fn = cJSON_CreateObject();
|
||||
cJSON* t39_params = cJSON_CreateObject();
|
||||
cJSON* t39_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t39, "type", "function");
|
||||
cJSON_AddStringToObject(t39_fn, "name", "context_admin_contacts");
|
||||
cJSON_AddStringToObject(t39_fn, "description", "Build admin contacts context block from cached kind 3 contact list");
|
||||
cJSON_AddStringToObject(t39_params, "type", "object");
|
||||
cJSON_AddItemToObject(t39_params, "properties", t39_props);
|
||||
cJSON_AddItemToObject(t39_fn, "parameters", t39_params);
|
||||
cJSON_AddItemToObject(t39, "function", t39_fn);
|
||||
cJSON_AddItemToArray(tools, t39);
|
||||
|
||||
cJSON* t40 = cJSON_CreateObject();
|
||||
cJSON* t40_fn = cJSON_CreateObject();
|
||||
cJSON* t40_params = cJSON_CreateObject();
|
||||
cJSON* t40_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t40, "type", "function");
|
||||
cJSON_AddStringToObject(t40_fn, "name", "context_admin_relays");
|
||||
cJSON_AddStringToObject(t40_fn, "description", "Build admin relay context block from cached kind 10002 data");
|
||||
cJSON_AddStringToObject(t40_params, "type", "object");
|
||||
cJSON_AddItemToObject(t40_params, "properties", t40_props);
|
||||
cJSON_AddItemToObject(t40_fn, "parameters", t40_params);
|
||||
cJSON_AddItemToObject(t40, "function", t40_fn);
|
||||
cJSON_AddItemToArray(tools, t40);
|
||||
|
||||
cJSON* t41 = cJSON_CreateObject();
|
||||
cJSON* t41_fn = cJSON_CreateObject();
|
||||
cJSON* t41_params = cJSON_CreateObject();
|
||||
cJSON* t41_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t41, "type", "function");
|
||||
cJSON_AddStringToObject(t41_fn, "name", "context_admin_notes");
|
||||
cJSON_AddStringToObject(t41_fn, "description", "Build admin notes context block from cached kind 1 notes");
|
||||
cJSON_AddStringToObject(t41_params, "type", "object");
|
||||
cJSON_AddItemToObject(t41_params, "properties", t41_props);
|
||||
cJSON_AddItemToObject(t41_fn, "parameters", t41_params);
|
||||
cJSON_AddItemToObject(t41, "function", t41_fn);
|
||||
cJSON_AddItemToArray(tools, t41);
|
||||
|
||||
cJSON* t42 = cJSON_CreateObject();
|
||||
cJSON* t42_fn = cJSON_CreateObject();
|
||||
cJSON* t42_params = cJSON_CreateObject();
|
||||
cJSON* t42_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t42, "type", "function");
|
||||
cJSON_AddStringToObject(t42_fn, "name", "context_tasks");
|
||||
cJSON_AddStringToObject(t42_fn, "description", "Build current task list context block from tasks.json");
|
||||
cJSON_AddStringToObject(t42_params, "type", "object");
|
||||
cJSON_AddItemToObject(t42_params, "properties", t42_props);
|
||||
cJSON_AddItemToObject(t42_fn, "parameters", t42_params);
|
||||
cJSON_AddItemToObject(t42, "function", t42_fn);
|
||||
cJSON_AddItemToArray(tools, t42);
|
||||
|
||||
cJSON* t43 = cJSON_CreateObject();
|
||||
cJSON* t43_fn = cJSON_CreateObject();
|
||||
cJSON* t43_params = cJSON_CreateObject();
|
||||
cJSON* t43_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t43, "type", "function");
|
||||
cJSON_AddStringToObject(t43_fn, "name", "context_user_message");
|
||||
cJSON_AddStringToObject(t43_fn, "description", "Return current user message text for context template conversation section");
|
||||
cJSON_AddStringToObject(t43_params, "type", "object");
|
||||
cJSON_AddItemToObject(t43_params, "properties", t43_props);
|
||||
cJSON_AddItemToObject(t43_fn, "parameters", t43_params);
|
||||
cJSON_AddItemToObject(t43, "function", t43_fn);
|
||||
cJSON_AddItemToArray(tools, t43);
|
||||
|
||||
cJSON* t44 = cJSON_CreateObject();
|
||||
cJSON* t44_fn = cJSON_CreateObject();
|
||||
cJSON* t44_params = cJSON_CreateObject();
|
||||
cJSON* t44_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t44, "type", "function");
|
||||
cJSON_AddStringToObject(t44_fn, "name", "context_agent_identity");
|
||||
cJSON_AddStringToObject(t44_fn, "description", "Build agent identity context block with pubkey and npub");
|
||||
cJSON_AddStringToObject(t44_params, "type", "object");
|
||||
cJSON_AddItemToObject(t44_params, "properties", t44_props);
|
||||
cJSON_AddItemToObject(t44_fn, "parameters", t44_params);
|
||||
cJSON_AddItemToObject(t44, "function", t44_fn);
|
||||
cJSON_AddItemToArray(tools, t44);
|
||||
|
||||
cJSON* t45 = cJSON_CreateObject();
|
||||
cJSON* t45_fn = cJSON_CreateObject();
|
||||
cJSON* t45_params = cJSON_CreateObject();
|
||||
cJSON* t45_props = cJSON_CreateObject();
|
||||
cJSON* t45_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t45, "type", "function");
|
||||
cJSON_AddStringToObject(t45_fn, "name", "task_manage");
|
||||
cJSON_AddStringToObject(t45_fn, "description", "Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)");
|
||||
cJSON_AddStringToObject(t45_params, "type", "object");
|
||||
cJSON_AddItemToObject(t45_params, "properties", t45_props);
|
||||
cJSON_AddItemToObject(t45_params, "required", t45_required);
|
||||
|
||||
cJSON* p_task_action = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_task_action, "type", "string");
|
||||
@@ -1978,15 +2076,15 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToArray(p_task_action_enum, cJSON_CreateString("clear"));
|
||||
cJSON_AddItemToArray(p_task_action_enum, cJSON_CreateString("replace"));
|
||||
cJSON_AddItemToObject(p_task_action, "enum", p_task_action_enum);
|
||||
cJSON_AddItemToObject(t37_props, "action", p_task_action);
|
||||
cJSON_AddItemToObject(t45_props, "action", p_task_action);
|
||||
|
||||
cJSON* p_task_text = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_task_text, "type", "string");
|
||||
cJSON_AddItemToObject(t37_props, "text", p_task_text);
|
||||
cJSON_AddItemToObject(t45_props, "text", p_task_text);
|
||||
|
||||
cJSON* p_task_id = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_task_id, "type", "integer");
|
||||
cJSON_AddItemToObject(t37_props, "id", p_task_id);
|
||||
cJSON_AddItemToObject(t45_props, "id", p_task_id);
|
||||
|
||||
cJSON* p_task_status = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_task_status, "type", "string");
|
||||
@@ -1995,20 +2093,20 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToArray(p_task_status_enum, cJSON_CreateString("active"));
|
||||
cJSON_AddItemToArray(p_task_status_enum, cJSON_CreateString("done"));
|
||||
cJSON_AddItemToObject(p_task_status, "enum", p_task_status_enum);
|
||||
cJSON_AddItemToObject(t37_props, "status", p_task_status);
|
||||
cJSON_AddItemToObject(t45_props, "status", p_task_status);
|
||||
|
||||
cJSON* p_task_tasks = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_task_tasks, "type", "array");
|
||||
cJSON* p_task_tasks_item = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_task_tasks_item, "type", "string");
|
||||
cJSON_AddItemToObject(p_task_tasks, "items", p_task_tasks_item);
|
||||
cJSON_AddItemToObject(t37_props, "tasks", p_task_tasks);
|
||||
cJSON_AddItemToObject(t45_props, "tasks", p_task_tasks);
|
||||
|
||||
cJSON_AddItemToArray(t37_required, cJSON_CreateString("action"));
|
||||
cJSON_AddItemToArray(t45_required, cJSON_CreateString("action"));
|
||||
|
||||
cJSON_AddItemToObject(t37_fn, "parameters", t37_params);
|
||||
cJSON_AddItemToObject(t37, "function", t37_fn);
|
||||
cJSON_AddItemToArray(tools, t37);
|
||||
cJSON_AddItemToObject(t45_fn, "parameters", t45_params);
|
||||
cJSON_AddItemToObject(t45, "function", t45_fn);
|
||||
cJSON_AddItemToArray(tools, t45);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
@@ -3569,69 +3667,16 @@ static char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
|
||||
return json_error("skill_list scope must be public or private");
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
cJSON_Delete(args);
|
||||
return json_error("failed to create skill_list filter");
|
||||
}
|
||||
|
||||
if (include_public) cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
|
||||
if (include_private) cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(filter, "limit", 300);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 8000);
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(args);
|
||||
|
||||
if (!events_json) return json_error("skill_list query failed");
|
||||
char* events_json = nostr_handler_get_self_skill_events_json();
|
||||
if (!events_json) return json_error("skill_list cache unavailable");
|
||||
|
||||
cJSON* events = cJSON_Parse(events_json);
|
||||
free(events_json);
|
||||
if (!events || !cJSON_IsArray(events)) {
|
||||
cJSON_Delete(events);
|
||||
return json_error("skill_list returned invalid JSON");
|
||||
}
|
||||
|
||||
int fallback_used = 0;
|
||||
if (cJSON_GetArraySize(events) == 0) {
|
||||
cJSON_Delete(events);
|
||||
events = NULL;
|
||||
|
||||
cJSON* fallback_filter = cJSON_CreateObject();
|
||||
cJSON* fallback_kinds = cJSON_CreateArray();
|
||||
if (!fallback_filter || !fallback_kinds) {
|
||||
cJSON_Delete(fallback_filter);
|
||||
cJSON_Delete(fallback_kinds);
|
||||
return json_error("failed to create fallback skill_list filter");
|
||||
}
|
||||
|
||||
if (include_public) cJSON_AddItemToArray(fallback_kinds, cJSON_CreateNumber(31123));
|
||||
if (include_private) cJSON_AddItemToArray(fallback_kinds, cJSON_CreateNumber(31124));
|
||||
cJSON_AddItemToObject(fallback_filter, "kinds", fallback_kinds);
|
||||
cJSON_AddNumberToObject(fallback_filter, "limit", 600);
|
||||
|
||||
char* fallback_json = nostr_handler_query_json(fallback_filter, 8000);
|
||||
cJSON_Delete(fallback_filter);
|
||||
if (!fallback_json) {
|
||||
return json_error("skill_list fallback query failed");
|
||||
}
|
||||
|
||||
events = cJSON_Parse(fallback_json);
|
||||
free(fallback_json);
|
||||
if (!events || !cJSON_IsArray(events)) {
|
||||
cJSON_Delete(events);
|
||||
return json_error("skill_list fallback returned invalid JSON");
|
||||
}
|
||||
fallback_used = 1;
|
||||
return json_error("skill_list cache returned invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
@@ -3648,12 +3693,13 @@ static char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
|
||||
cJSON* ev = cJSON_GetArrayItem(events, i);
|
||||
if (!ev || !cJSON_IsObject(ev)) continue;
|
||||
|
||||
if (fallback_used) {
|
||||
cJSON* ev_pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
|
||||
strcmp(ev_pubkey->valuestring, ctx->cfg->keys.public_key_hex) != 0) {
|
||||
continue;
|
||||
}
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
|
||||
if (!kind || !cJSON_IsNumber(kind)) {
|
||||
continue;
|
||||
}
|
||||
int kind_val = (int)kind->valuedouble;
|
||||
if ((kind_val == 31123 && !include_public) || (kind_val == 31124 && !include_private)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* summary = extract_skill_summary(ev);
|
||||
@@ -3661,7 +3707,7 @@ static char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddBoolToObject(out, "fallback_used", fallback_used ? 1 : 0);
|
||||
cJSON_AddBoolToObject(out, "fallback_used", 0);
|
||||
cJSON_AddItemToObject(out, "skills", skills);
|
||||
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(skills));
|
||||
|
||||
@@ -4170,6 +4216,370 @@ static char* execute_nostr_npub(tools_context_t* ctx, const char* args_json) {
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_admin_identity(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
|
||||
const char* tier_text = "Sender tier unknown.";
|
||||
if (ctx->template_sender_tier == 1) {
|
||||
tier_text = "This message has been cryptographically verified as coming from your administrator.";
|
||||
} else if (ctx->template_sender_tier == 2) {
|
||||
tier_text = "This message is from a web-of-trust contact (not the administrator).";
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_pubkey", ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown");
|
||||
cJSON_AddStringToObject(out, "sender_verification", tier_text);
|
||||
|
||||
char content[768];
|
||||
snprintf(content,
|
||||
sizeof(content),
|
||||
"## Administrator Identity (source: config.admin.pubkey)\n\n"
|
||||
"This is your administrator! Admin pubkey (hex): %s\n\n"
|
||||
"%s",
|
||||
ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown",
|
||||
tier_text);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_admin_profile(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
char* kind0 = nostr_handler_get_admin_kind0_context();
|
||||
const char* profile_json = (kind0 && kind0[0]) ? kind0 : "{}";
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(kind0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_kind0_json", profile_json);
|
||||
|
||||
size_t content_len = strlen("## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
if (!content) {
|
||||
cJSON_Delete(out);
|
||||
free(kind0);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(content,
|
||||
content_len,
|
||||
"## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): %s",
|
||||
profile_json);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
free(content);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
free(kind0);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_admin_contacts(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
char* kind3 = nostr_handler_get_admin_kind3_context();
|
||||
const char* contacts_json = (kind3 && kind3[0]) ? kind3 : "[]";
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(kind3);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_kind3_json", contacts_json);
|
||||
|
||||
size_t content_len = strlen("## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): ") + strlen(contacts_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
if (!content) {
|
||||
cJSON_Delete(out);
|
||||
free(kind3);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(content,
|
||||
content_len,
|
||||
"## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): %s",
|
||||
contacts_json);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
free(content);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
free(kind3);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_admin_relays(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
char* kind10002 = nostr_handler_get_admin_kind10002_context();
|
||||
const char* relays_json = (kind10002 && kind10002[0]) ? kind10002 : "[]";
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(kind10002);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_kind10002_json", relays_json);
|
||||
|
||||
size_t content_len = strlen("## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): ") + strlen(relays_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
if (!content) {
|
||||
cJSON_Delete(out);
|
||||
free(kind10002);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(content,
|
||||
content_len,
|
||||
"## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): %s",
|
||||
relays_json);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
free(content);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
free(kind10002);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_admin_notes(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
char* notes = nostr_handler_get_admin_kind1_notes_context();
|
||||
const char* notes_text = (notes && notes[0]) ? notes : "";
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(notes);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_notes_content", notes_text);
|
||||
|
||||
size_t content_len = strlen("## Administrator Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
if (!content) {
|
||||
cJSON_Delete(out);
|
||||
free(notes);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(content,
|
||||
content_len,
|
||||
"## Administrator Recent Kind 1 Notes\n\n%s",
|
||||
notes_text);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
free(content);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
free(notes);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_task_manage(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
static char* execute_context_tasks(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
char* listed = execute_task_manage(ctx, "{\"action\":\"list\"}");
|
||||
if (!listed) return json_error("context_tasks failed to list tasks");
|
||||
|
||||
cJSON* listed_root = cJSON_Parse(listed);
|
||||
if (!listed_root || !cJSON_IsObject(listed_root)) {
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return json_error("context_tasks received invalid task_manage response");
|
||||
}
|
||||
|
||||
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(listed_root, "tasks");
|
||||
int tcount = (tasks && cJSON_IsArray(tasks)) ? cJSON_GetArraySize(tasks) : 0;
|
||||
|
||||
size_t cap = 512U;
|
||||
char* content = (char*)malloc(cap);
|
||||
if (!content) {
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return NULL;
|
||||
}
|
||||
size_t used = 0U;
|
||||
int n = snprintf(content + used,
|
||||
cap - used,
|
||||
"## Current Task List\n\n### Current Tasks\n\nYour active task list - short-term working memory for tracking plan steps.\n\n");
|
||||
if (n < 0) {
|
||||
free(content);
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return NULL;
|
||||
}
|
||||
used += (size_t)n;
|
||||
|
||||
int written = 0;
|
||||
for (int i = 0; i < tcount; i++) {
|
||||
cJSON* task = cJSON_GetArrayItem(tasks, i);
|
||||
cJSON* id = task ? cJSON_GetObjectItemCaseSensitive(task, "id") : NULL;
|
||||
cJSON* text = task ? cJSON_GetObjectItemCaseSensitive(task, "text") : NULL;
|
||||
cJSON* status = task ? cJSON_GetObjectItemCaseSensitive(task, "status") : NULL;
|
||||
if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* status_s = (status && cJSON_IsString(status) && status->valuestring) ? status->valuestring : "pending";
|
||||
const char* mark = " ";
|
||||
if (strcmp(status_s, "done") == 0) mark = "x";
|
||||
else if (strcmp(status_s, "active") == 0) mark = "-";
|
||||
|
||||
int id_value = (id && cJSON_IsNumber(id)) ? (int)id->valuedouble : (i + 1);
|
||||
|
||||
int needed = snprintf(NULL, 0, "- [%s] %d. %s\n", mark, id_value, text->valuestring);
|
||||
if (needed < 0) continue;
|
||||
if (used + (size_t)needed + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + (size_t)needed + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(content, next);
|
||||
if (!grown) {
|
||||
free(content);
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return NULL;
|
||||
}
|
||||
content = grown;
|
||||
cap = next;
|
||||
}
|
||||
snprintf(content + used, cap - used, "- [%s] %d. %s\n", mark, id_value, text->valuestring);
|
||||
used += (size_t)needed;
|
||||
written++;
|
||||
}
|
||||
|
||||
if (written == 0) {
|
||||
const char* fallback = "- [ ] No active tasks yet.\n";
|
||||
size_t need = strlen(fallback);
|
||||
if (used + need + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + need + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(content, next);
|
||||
if (!grown) {
|
||||
free(content);
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return NULL;
|
||||
}
|
||||
content = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(content + used, fallback, need + 1U);
|
||||
used += need;
|
||||
}
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) {
|
||||
free(content);
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return json_error("invalid arguments JSON");
|
||||
}
|
||||
cJSON_Delete(args);
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(content);
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
cJSON* tasks_dup = tasks ? cJSON_Duplicate(tasks, 1) : cJSON_CreateArray();
|
||||
if (!tasks_dup) tasks_dup = cJSON_CreateArray();
|
||||
cJSON_AddItemToObject(out, "tasks", tasks_dup);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
free(content);
|
||||
cJSON_Delete(listed_root);
|
||||
free(listed);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_user_message(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "content", ctx->template_current_user_message ? ctx->template_current_user_message : "");
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_context_agent_identity(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
char npub[128] = {0};
|
||||
if (nostr_key_to_bech32(ctx->cfg->keys.public_key, "npub", npub) != NOSTR_SUCCESS) {
|
||||
strcpy(npub, "unknown");
|
||||
}
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "agent_pubkey", ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown");
|
||||
cJSON_AddStringToObject(out, "agent_npub", npub);
|
||||
|
||||
char content[384];
|
||||
snprintf(content,
|
||||
sizeof(content),
|
||||
"## Agent Identity\n\nYour pubkey (hex): %s\nYour npub: %s",
|
||||
ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown",
|
||||
npub);
|
||||
cJSON_AddStringToObject(out, "content", content);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_my_version(const char* args_json) {
|
||||
cJSON* args = parse_tool_args_json(args_json);
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
@@ -4906,10 +5316,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);
|
||||
@@ -5257,6 +5672,30 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg
|
||||
if (strcmp(tool_name, "task_manage") == 0) {
|
||||
return execute_task_manage(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_admin_identity") == 0) {
|
||||
return execute_context_admin_identity(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_admin_profile") == 0) {
|
||||
return execute_context_admin_profile(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_admin_contacts") == 0) {
|
||||
return execute_context_admin_contacts(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_admin_relays") == 0) {
|
||||
return execute_context_admin_relays(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_admin_notes") == 0) {
|
||||
return execute_context_admin_notes(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_tasks") == 0) {
|
||||
return execute_context_tasks(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_user_message") == 0) {
|
||||
return execute_context_user_message(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "context_agent_identity") == 0) {
|
||||
return execute_context_agent_identity(ctx, args_json);
|
||||
}
|
||||
|
||||
return json_error("unknown tool");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ struct trigger_manager;
|
||||
typedef struct {
|
||||
didactyl_config_t* cfg;
|
||||
struct trigger_manager* trigger_manager;
|
||||
int template_sender_tier;
|
||||
const char* template_current_user_message;
|
||||
} tools_context_t;
|
||||
|
||||
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg);
|
||||
|
||||
+59
-45
@@ -252,24 +252,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* adopt_filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!adopt_filter || !kinds || !authors) {
|
||||
cJSON_Delete(adopt_filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
|
||||
cJSON_AddItemToObject(adopt_filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(mgr->cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(adopt_filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(adopt_filter, "limit", 1);
|
||||
|
||||
char* adoption_json = nostr_handler_query_json(adopt_filter, 8000);
|
||||
cJSON_Delete(adopt_filter);
|
||||
char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123);
|
||||
if (!adoption_json) {
|
||||
return 0;
|
||||
}
|
||||
@@ -308,34 +291,65 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* skill_filter = cJSON_CreateObject();
|
||||
cJSON* sk_kinds = cJSON_CreateArray();
|
||||
cJSON* sk_authors = cJSON_CreateArray();
|
||||
cJSON* d_values = cJSON_CreateArray();
|
||||
if (!skill_filter || !sk_kinds || !sk_authors || !d_values) {
|
||||
cJSON* skill_events = NULL;
|
||||
if (strcmp(pubkey, mgr->cfg->keys.public_key_hex) == 0) {
|
||||
char* skill_json = nostr_handler_get_self_events_by_kind_json(kind);
|
||||
if (skill_json) {
|
||||
cJSON* all_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
if (all_events && cJSON_IsArray(all_events)) {
|
||||
skill_events = cJSON_CreateArray();
|
||||
if (skill_events) {
|
||||
int all_n = cJSON_GetArraySize(all_events);
|
||||
for (int ai = 0; ai < all_n; ai++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(all_events, ai);
|
||||
cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
|
||||
cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
||||
cJSON* ev_d = find_tag_value_string(ev_tags, "d");
|
||||
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
|
||||
strcmp(ev_pubkey->valuestring, pubkey) != 0 ||
|
||||
!ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring ||
|
||||
strcmp(ev_d->valuestring, slug) != 0) {
|
||||
continue;
|
||||
}
|
||||
cJSON* dup = cJSON_Duplicate(ev, 1);
|
||||
if (dup) {
|
||||
cJSON_AddItemToArray(skill_events, dup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cJSON_Delete(all_events);
|
||||
}
|
||||
} else {
|
||||
cJSON* skill_filter = cJSON_CreateObject();
|
||||
cJSON* sk_kinds = cJSON_CreateArray();
|
||||
cJSON* sk_authors = cJSON_CreateArray();
|
||||
cJSON* d_values = cJSON_CreateArray();
|
||||
if (!skill_filter || !sk_kinds || !sk_authors || !d_values) {
|
||||
cJSON_Delete(skill_filter);
|
||||
cJSON_Delete(sk_kinds);
|
||||
cJSON_Delete(sk_authors);
|
||||
cJSON_Delete(d_values);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(kind));
|
||||
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
|
||||
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(pubkey));
|
||||
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
|
||||
cJSON_AddItemToArray(d_values, cJSON_CreateString(slug));
|
||||
cJSON_AddItemToObject(skill_filter, "#d", d_values);
|
||||
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||
|
||||
char* skill_json = nostr_handler_query_json(skill_filter, 2000);
|
||||
cJSON_Delete(skill_filter);
|
||||
cJSON_Delete(sk_kinds);
|
||||
cJSON_Delete(sk_authors);
|
||||
cJSON_Delete(d_values);
|
||||
continue;
|
||||
if (skill_json) {
|
||||
skill_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(kind));
|
||||
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
|
||||
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(pubkey));
|
||||
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
|
||||
cJSON_AddItemToArray(d_values, cJSON_CreateString(slug));
|
||||
cJSON_AddItemToObject(skill_filter, "#d", d_values);
|
||||
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||
|
||||
char* skill_json = nostr_handler_query_json(skill_filter, 8000);
|
||||
cJSON_Delete(skill_filter);
|
||||
if (!skill_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* skill_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
|
||||
cJSON_Delete(skill_events);
|
||||
continue;
|
||||
@@ -513,7 +527,7 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 2) {
|
||||
if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 10) {
|
||||
return 0;
|
||||
}
|
||||
mgr->last_poll_at = now;
|
||||
@@ -538,7 +552,7 @@ int trigger_manager_poll(trigger_manager_t* mgr) {
|
||||
cJSON_AddNumberToObject(filter, "since", (double)since);
|
||||
cJSON_AddNumberToObject(filter, "limit", 8);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 4000);
|
||||
char* events_json = nostr_handler_query_json(filter, 1200);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
continue;
|
||||
|
||||
+1
-1
@@ -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":[],"next_id":9}
|
||||
Reference in New Issue
Block a user