Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06c267db4 | ||
|
|
b625e0a70a | ||
|
|
ed5e4248df | ||
|
|
6a76b02180 | ||
|
|
90680e232d | ||
|
|
0bd21dc41a | ||
|
|
d0416f7760 | ||
|
|
1ab2034b2d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ a.out
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Local secrets
|
||||
genesis.jsonc
|
||||
|
||||
208
README.md
208
README.md
@@ -1,6 +1,5 @@
|
||||
# Didactyl
|
||||
|
||||
|
||||
### A decentralized, censorship-resistant agentic network.
|
||||
|
||||
Didactyl boots on an internet-connected computer, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands, and sharing new skills and learning with other agents — all orchestrated through Nostr.
|
||||
@@ -55,31 +54,30 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
|
||||
|
||||
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.1.21
|
||||
## Current Status — v0.2.6
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.1.21 — Fix segfault in nostr_http_request by copying CURLINFO_CONTENT_TYPE before curl cleanup and update debug deploy script to deploy *_debug binary for symbolized coredumps
|
||||
> Last release update: v0.2.6 — Fix DM freeze by removing adopted-skills mutex reentrancy in trigger context assembly
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
- Loads base system context from default skill content (first-run from `genesis.jsonc`, subsequent runs from adopted skills on Nostr)
|
||||
- Verifies Nostr event signatures before processing inbound messages
|
||||
- Applies privilege tiers: ADMIN (tools), WoT (chat-only), STRANGER (configurable canned reply or ignore)
|
||||
- Subscribes to admin context kinds (`0`,`3`,`10002`,`1`) for WoT + contextual awareness
|
||||
- Builds LLM context from default/adopted skill templates (`---template---`) 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
|
||||
- Builds trigger-scoped LLM context from DM and subscription-triggered skills with inline variable resolution
|
||||
- Uses default skill d-tag `default_admin_dm` with trigger tags for admin DM handling
|
||||
- Supports tool-calling loop with configurable max turns and local safety limits
|
||||
- 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)
|
||||
- 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
|
||||
- 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
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Download binary (recommended)
|
||||
|
||||
1. Download the latest release binary from Gitea: [https://git.laantungir.net/laantungir/didactyl/releases](https://git.laantungir.net/laantungir/didactyl/releases)
|
||||
1. Download the latest release binary from Gitea:[https://git.laantungir.net/laantungir/didactyl/releases](https://git.laantungir.net/laantungir/didactyl/releases)
|
||||
2. Make it executable and run it:
|
||||
|
||||
```bash
|
||||
@@ -163,8 +161,8 @@ Edit [`genesis.jsonc`](genesis.jsonc):
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "You are Didactyl...",
|
||||
"tags": [["d", "didactyl-default"], ["app", "didactyl"], ["scope", "private"]]
|
||||
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate",
|
||||
"tags": [["d", "default_admin_dm"], ["app", "didactyl"], ["scope", "private"], ["description", "Default admin DM handler"], ["trigger", "dm"], ["filter", "{\"from\":\"admin\"}"]]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
@@ -209,8 +207,8 @@ Options:
|
||||
|
||||
Interactive setup notes:
|
||||
|
||||
- First menu asks whether you are starting a **new agent** or an **existing agent**.
|
||||
- Menus use first-letter hotkeys (case-insensitive), with `q`/`x` as quit/back shortcuts.
|
||||
- First menu asks whether you are starting a**new agent** or an**existing agent**.
|
||||
- Menus use first-letter hotkeys (case-insensitive), with`q`/`x` as quit/back shortcuts.
|
||||
- Existing-agent mode attempts to recover relay/admin/LLM config from Nostr before asking for missing fields.
|
||||
|
||||
CLI debugger notes:
|
||||
@@ -247,10 +245,10 @@ node ./didactyl-chat-cli.js
|
||||
|
||||
Optional environment variables:
|
||||
|
||||
- `DIDACTYL_API_BASE_URL` (default: `https://127.0.0.1:8484`)
|
||||
- `DIDACTYL_API_BASE_URL` (default:`https://127.0.0.1:8484`)
|
||||
- `DIDACTYL_MODEL` (optional model override)
|
||||
- `DIDACTYL_MAX_TURNS` (default: `4`)
|
||||
- `DIDACTYL_INSECURE_TLS` (default: `1`, set `0` to enforce certificate verification)
|
||||
- `DIDACTYL_MAX_TURNS` (default:`4`)
|
||||
- `DIDACTYL_INSECURE_TLS` (default:`1`, set`0` to enforce certificate verification)
|
||||
|
||||
Example:
|
||||
|
||||
@@ -291,13 +289,13 @@ The CLI prints each message block with a speaker label (`You` / `Didactyl`) and
|
||||
|
||||
Didactyl uses a two-layer skill model: authors publish skill definitions, and adopters publish which skills they use.
|
||||
|
||||
- `31123` — **Public Skill Definition** (replaceable by `d` tag)
|
||||
- `content` is JSON with instruction fields like `description` and `template`
|
||||
- `d=<skill_slug>` (example: `d=long_form_note`)
|
||||
- `31124` — **Private Skill Definition** (same schema as `31123`, private scope)
|
||||
- `d=<skill_slug>` (example: `d=admin_ops`)
|
||||
- `10123` — **Skill Adoption List**
|
||||
- tags contain one or more `a` references to selected skills
|
||||
- `31123` —**Public Skill Definition** (replaceable by`d` tag)
|
||||
- `content` is JSON with instruction fields like`description` and`template`
|
||||
- `d=<skill_slug>` (example:`d=long_form_note`)
|
||||
- `31124` —**Private Skill Definition** (same schema as`31123`, private scope)
|
||||
- `d=<skill_slug>` (example:`d=admin_ops`)
|
||||
- `10123` —**Skill Adoption List**
|
||||
- tags contain one or more`a` references to selected skills
|
||||
|
||||
Skills are composed by adoption list order and per-skill template resolution (no context modes).
|
||||
|
||||
@@ -309,16 +307,16 @@ Skills are shared across Nostr without any centralized registry or approval proc
|
||||
|
||||
### How it works
|
||||
|
||||
1. **Publish**: An author publishes a skill as a kind `31123` event. The `content` field contains the skill body (markdown or structured JSON). The `d` tag is the skill's slug (e.g. `long_form_note`).
|
||||
2. **Adopt**: An agent that wants to use a skill adds an `a`-tag reference to its kind `10123` adoption list. This is a public, replaceable event — anyone can see which skills an agent uses.
|
||||
3. **Discover**: A new user queries `{"kinds": [10123], "authors": [<my-follows>]}` to see which skills their web of trust has adopted. The most-referenced `31123` addresses are the most popular skills — no rating system needed.
|
||||
4. **Improve**: Anyone can publish their own `31123` with the same slug but a different pubkey. If their version is better, people adopt it instead. Competition happens through adoption, not through a store ranking.
|
||||
1. **Publish**: An author publishes a skill as a kind`31123` event. The`content` field contains the skill body (markdown or structured JSON). The`d` tag is the skill's slug (e.g.`long_form_note`).
|
||||
2. **Adopt**: An agent that wants to use a skill adds an`a`-tag reference to its kind`10123` adoption list. This is a public, replaceable event — anyone can see which skills an agent uses.
|
||||
3. **Discover**: A new user queries`{"kinds": [10123], "authors": [<my-follows>]}` to see which skills their web of trust has adopted. The most-referenced`31123` addresses are the most popular skills — no rating system needed.
|
||||
4. **Improve**: Anyone can publish their own`31123` with the same slug but a different pubkey. If their version is better, people adopt it instead. Competition happens through adoption, not through a store ranking.
|
||||
|
||||
### Why this works
|
||||
|
||||
- **No gatekeeper**: Skills are just Nostr events. Anyone can publish one.
|
||||
- **WoT as curation**: You see what people you trust actually use, not what an algorithm promotes.
|
||||
- **Visible adoption**: The `10123` list is public. Popularity is a countable fact, not a manipulable score.
|
||||
- **Visible adoption**: The`10123` list is public. Popularity is a countable fact, not a manipulable score.
|
||||
- **Censorship resistant**: Skills live on relays. No single entity can remove a skill from the network.
|
||||
|
||||
## Startup
|
||||
@@ -327,10 +325,10 @@ Didactyl startup behavior is configured in [`genesis.jsonc`](genesis.jsonc) unde
|
||||
|
||||
Startup model:
|
||||
|
||||
- First run is detected by checking for an existing kind `10002` relay-list event from the agent pubkey.
|
||||
- On first run, events in `startup_events` are published to connected relays.
|
||||
- First run is detected by checking for an existing kind`10002` relay-list event from the agent pubkey.
|
||||
- On first run, events in`startup_events` are published to connected relays.
|
||||
- On subsequent runs, startup publish is skipped and relay/config state is loaded from Nostr.
|
||||
- Identity can be supplied at runtime via `--nsec` or `DIDACTYL_NSEC`.
|
||||
- Identity can be supplied at runtime via`--nsec` or`DIDACTYL_NSEC`.
|
||||
|
||||
See [`docs/GENESIS.md`](docs/GENESIS.md) for full boot semantics.
|
||||
|
||||
@@ -339,8 +337,8 @@ See [`docs/GENESIS.md`](docs/GENESIS.md) for full boot semantics.
|
||||
Didactyl builds tier-aware, template-driven context:
|
||||
|
||||
- **ADMIN** request context is assembled from adopted skill templates.
|
||||
- Template variables like `{{nostr_admin_profile}}` are resolved by executing tools at render time.
|
||||
- Triggered skill invocations can override runtime execution parameters via trigger tags (`llm`, `max_tokens`, `temperature`, `seed`, `tools`).
|
||||
- Template variables like`{{nostr_admin_profile}}` are resolved by executing tools at render time.
|
||||
- Triggered skill invocations can override runtime execution parameters via trigger tags (`llm`,`max_tokens`,`temperature`,`seed`,`tools`).
|
||||
- **WoT** request context remains chat-only.
|
||||
- **STRANGER** behavior follows configured security policy.
|
||||
|
||||
@@ -360,17 +358,17 @@ See [`docs/TOOLS.md`](docs/TOOLS.md) for the canonical tool catalog and interfac
|
||||
|
||||
A localhost-only HTTP API on port `8484` (configurable) for agent inspection and prompt crafting. Enable with `"api": {"enabled": true}` in config.
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|---|---|
|
||||
| `GET /api/status` | Agent name, version, pubkey, relay count, trigger count |
|
||||
| `GET /api/context/current` | Full LLM context messages array |
|
||||
| `GET /api/context/parts` | Context broken into named parts with token estimates |
|
||||
| `POST /api/prompt/run-simple` | Run a simple system+user prompt, no tools |
|
||||
| `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 (persisted in encrypted config events) |
|
||||
| `GET /api/models` | List available models from provider |
|
||||
| Endpoint | Purpose |
|
||||
| ------------------------------- | -------------------------------------------------------------- |
|
||||
| `GET /api/status` | Agent name, version, pubkey, relay count, trigger count |
|
||||
| `GET /api/context/current` | Full LLM context messages array |
|
||||
| `GET /api/context/parts` | Context broken into named parts with token estimates |
|
||||
| `POST /api/prompt/run-simple` | Run a simple system+user prompt, no tools |
|
||||
| `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 (persisted in encrypted config events) |
|
||||
| `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).
|
||||
|
||||
@@ -408,75 +406,99 @@ Full reference: [`docs/API.md`](docs/API.md). Frontend brief: [`plans/admin_web_
|
||||
|
||||
All dependencies are statically linked into the binary at build time. No system libraries are required at runtime.
|
||||
|
||||
| Dependency | Purpose | Source |
|
||||
|---|---|---|
|
||||
| nostr_core_lib | Nostr protocol: keys, events, NIPs, relay pool | Workspace (sibling directory) |
|
||||
| cJSON | JSON parsing | Bundled in nostr_core_lib |
|
||||
| libcurl | HTTPS for LLM API calls | Statically linked (Alpine/MUSL) |
|
||||
| libssl / libcrypto | TLS for WebSocket relay connections | Statically linked (Alpine/MUSL) |
|
||||
| libsecp256k1 | Schnorr signatures, ECDH | Statically linked (Alpine/MUSL) |
|
||||
| Dependency | Purpose | Source |
|
||||
| ------------------ | ---------------------------------------------- | ------------------------------- |
|
||||
| nostr_core_lib | Nostr protocol: keys, events, NIPs, relay pool | Workspace (sibling directory) |
|
||||
| cJSON | JSON parsing | Bundled in nostr_core_lib |
|
||||
| libcurl | HTTPS for LLM API calls | Statically linked (Alpine/MUSL) |
|
||||
| libssl / libcrypto | TLS for WebSocket relay connections | Statically linked (Alpine/MUSL) |
|
||||
| libsecp256k1 | Schnorr signatures, ECDH | Statically linked (Alpine/MUSL) |
|
||||
|
||||
## Roadmap: Nostr-Native Portability
|
||||
|
||||
Didactyl's long-term architecture goal is **zero filesystem dependency after first boot**. The config file is the only tie to the local filesystem. The plan:
|
||||
Didactyl's long-term architecture goal is **zero filesystem dependency after first boot**. A geneisis.jsonc file can assist the first boot, but first boot can also occur throught the command line or TUI wizard. The plan:
|
||||
|
||||
1. **First boot** — Read `genesis.jsonc`, publish startup identity/skill/adoption events to relays.
|
||||
2. **Subsequent boots** — Start with only `nsec` (CLI/env), detect initialized state from kind `10002`, and load durable state from Nostr.
|
||||
1. **First boot** — Read`genesis.jsonc`, publish startup identity/skill/adoption events to relays.
|
||||
2. **Subsequent boots** — Start with only`nsec` (CLI/env), detect initialized state from kind`10002`, and load durable state from Nostr.
|
||||
3. **True portability** — Start your agent from any computer; keys are sufficient and state lives on Nostr.
|
||||
|
||||
This makes Didactyl fundamentally different from filesystem-bound agents. Destroying the host computer does not kill the agent — its identity, memory, and capabilities persist on the relay network.
|
||||
|
||||
### Browser-Based Didactyl
|
||||
|
||||
Because all agent state lives on Nostr, Didactyl can run in a browser — no server required. A browser-based Didactyl would load the agent's identity from an nsec, fetch its skills and adoption list from relays, and execute skills using browser-available tools.
|
||||
|
||||
This increases decentralization: if the server running your agent goes down, you can boot the same agent in a browser window. The agent continues living, just in a different environment with a different set of available tools — like an artist working from a vacation home instead of their full studio.
|
||||
|
||||
Not all tools would be available in a browser runtime. For example, `shell_exec` and `local_file_read` require a host OS. But Nostr operations, HTTP fetches, and LLM calls work fine in a browser. Skills declare their requirements via `requires_tool` tags, so the browser runtime knows which skills it can execute and which it cannot.
|
||||
|
||||
```
|
||||
Didactyl C binary (full studio) Didactyl Browser (vacation home)
|
||||
════════════════════════════ ════════════════════════════════
|
||||
Available tools: Available tools:
|
||||
✓ http_fetch ✓ http_fetch
|
||||
✓ shell_exec ✗ shell_exec
|
||||
✓ nostr_query, nostr_dm, nostr_post ✓ nostr_query, nostr_dm, nostr_post
|
||||
✓ memory_read, memory_write ✓ memory_read, memory_write
|
||||
✓ local_file_read, local_file_write ✗ local_file_read, local_file_write
|
||||
✓ blossom_upload, blossom_download ✓ blossom_upload, blossom_download
|
||||
|
||||
Skills that work: ALL Skills that work: those whose
|
||||
requires_tool tags are satisfied
|
||||
```
|
||||
|
||||
### What already lives on Nostr
|
||||
|
||||
| Data | Event Kind | Status |
|
||||
|---|---|---|
|
||||
| Agent profile | Kind 0 | Implemented |
|
||||
| Relay list | Kind 10002 | Implemented |
|
||||
| DM relay list | Kind 10050 | Implemented |
|
||||
| Public skills | Kind 31123 | Implemented |
|
||||
| Private skills | Kind 31124 | Implemented |
|
||||
| Skill adoption list | Kind 10123 | Implemented |
|
||||
| Base/default behavior skill | Kind 31124 | Implemented |
|
||||
| Trigger definitions | Tags on skill events | Implemented |
|
||||
| Data | Event Kind | Status |
|
||||
| --------------------------- | -------------------- | ----------- |
|
||||
| Agent profile | Kind 0 | Implemented |
|
||||
| Relay list | Kind 10002 | Implemented |
|
||||
| DM relay list | Kind 10050 | Implemented |
|
||||
| Public skills | Kind 31123 | Implemented |
|
||||
| Private skills | Kind 31124 | Implemented |
|
||||
| Skill adoption list | Kind 10123 | Implemented |
|
||||
| Base/default behavior skill | Kind 31124 | Implemented |
|
||||
| Trigger definitions | Tags on skill events | Implemented |
|
||||
|
||||
### What still needs migration
|
||||
|
||||
| Data | Current Location | Target |
|
||||
|---|---|---|
|
||||
| Admin pubkey | `genesis.jsonc` fallback | Dedicated agent config event / contact-graph derivation |
|
||||
| LLM provider/key | `genesis.jsonc` fallback | Encrypted kind 30078 app-specific event |
|
||||
| Security tiers | `genesis.jsonc` fallback | Agent config event on Nostr |
|
||||
| API settings | local runtime flags | Local-only (not published) |
|
||||
| Data | Current Location | Target |
|
||||
| ---------------- | -------------------------- | ------------------------------------------------------- |
|
||||
| Admin pubkey | `genesis.jsonc` fallback | Dedicated agent config event / contact-graph derivation |
|
||||
| LLM provider/key | `genesis.jsonc` fallback | Encrypted kind 30078 app-specific event |
|
||||
| Security tiers | `genesis.jsonc` fallback | Agent config event on Nostr |
|
||||
| API settings | local runtime flags | Local-only (not published) |
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] MVP chat agent — DM in, LLM response out
|
||||
- [x] Relay pool with auto-reconnect and status logging
|
||||
- [x] Per-relay startup publish on relay-connected transitions
|
||||
- [x] Runtime diagnostics — relay health, message flow, event kind publish logs
|
||||
- [x] Tool-calling loop (nostr_post, nostr_query, local_shell_exec, local_file_read, local_file_write)
|
||||
- [x] Context assembly with startup events + recent DM history
|
||||
- [x] Context payload logging to [`context.log`](context.log)
|
||||
- [x] Skill kind definitions (`31123` Public Skill, `31124` Private Skill)
|
||||
- [x] Skill adoption list (`10123`) for WoT-driven discovery
|
||||
- [x] Signature verification on all inbound events
|
||||
- [x] Privilege tiers — ADMIN (tools), WoT (chat-only), STRANGER (canned reply/ignore)
|
||||
- [x] Admin context subscription (kind 0, 3, 10002, 1) with WoT contact extraction
|
||||
- [x] Message deduplication (event-ID cache + FNV-1a fingerprint debounce)
|
||||
- [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 (persisted in encrypted config events)
|
||||
- [x] Skill-embedded prompt templates (`---template---`) — configurable context order, variable resolution, provider overrides
|
||||
- [ ] Runtime skill loading from adopted `31123` events on relays
|
||||
- [X] MVP chat agent — DM in, LLM response out
|
||||
- [X] Relay pool with auto-reconnect and status logging
|
||||
- [X] Per-relay startup publish on relay-connected transitions
|
||||
- [X] Runtime diagnostics — relay health, message flow, event kind publish logs
|
||||
- [X] Tool-calling loop (nostr_post, nostr_query, local_shell_exec, local_file_read, local_file_write)
|
||||
- [X] Context assembly with startup events + recent DM history
|
||||
- [X] Context payload logging to[`context.log`](context.log)
|
||||
- [X] Skill kind definitions (`31123` Public Skill,`31124` Private Skill)
|
||||
- [X] Skill adoption list (`10123`) for WoT-driven discovery
|
||||
- [X] Signature verification on all inbound events
|
||||
- [X] Privilege tiers — ADMIN (tools), WoT (chat-only), STRANGER (canned reply/ignore)
|
||||
- [X] Admin context subscription (kind 0, 3, 10002, 1) with WoT contact extraction
|
||||
- [X] Message deduplication (event-ID cache + FNV-1a fingerprint debounce)
|
||||
- [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 (persisted in encrypted config events)
|
||||
- [X] Trigger-scoped inline-variable context assembly (template DSL removed)
|
||||
- [ ] Runtime skill loading from adopted`31123` events on relays
|
||||
- [ ] Skill discovery CLI/tool (query WoT adoption lists)
|
||||
- [ ] Upgrade to NIP-17 gift-wrapped DMs
|
||||
- [x] NIP-44 encrypted private skills (`31124`)
|
||||
- [x] Nostr-native data storage (kind 30078 app-specific events)
|
||||
- [X] NIP-44 encrypted private skills (`31124`)
|
||||
- [X] Nostr-native data storage (kind 30078 app-specific events)
|
||||
- [ ] Blossom blob storage integration
|
||||
- [ ] Agent-to-agent communication
|
||||
- [ ] Browser-based Didactyl runtime (WASM/JS — same agent, browser-available tools only)
|
||||
- [ ] Tool schema standardization — canonical tool definitions as Nostr events for cross-app skill portability
|
||||
|
||||
## License
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
{
|
||||
// LEGACY CONFIG EXAMPLE
|
||||
// This file is deprecated in favor of genesis.jsonc + nsec-only startup.
|
||||
// Kept as reference for older deployments and migration support.
|
||||
|
||||
// ─── 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)
|
||||
},
|
||||
|
||||
// ─── Cashu Wallet (NIP-60) ────────────────────────────────────────
|
||||
// Optional wallet runtime used by cashu_wallet_* tools.
|
||||
// If enabled=true and auto_load=true, Didactyl will attempt to load
|
||||
// wallet/token events from relays at startup and create a new wallet
|
||||
// from mint_urls if none is found.
|
||||
"cashu_wallet": {
|
||||
"enabled": false,
|
||||
"mint_urls": [
|
||||
"https://mint.minibits.cash/Bitcoin"
|
||||
],
|
||||
"unit": "sat",
|
||||
"auto_load": true,
|
||||
"mint_timeout_seconds": 30
|
||||
},
|
||||
|
||||
// ─── Tools & Runtime Limits ───────────────────────────────────────
|
||||
// Centralized turn and timeout limits used by DM agent loops,
|
||||
// triggered skills, HTTP API prompt runs, and local HTTP fetch tool.
|
||||
"tools": {
|
||||
"enabled": true,
|
||||
"max_turns": 20, // default max turns for normal agent tool loops
|
||||
"trigger_max_turns": 12, // max turns for triggered-skill executions
|
||||
"api_default_max_turns": 8, // default when HTTP body omits max_turns
|
||||
"api_max_turns_ceiling": 32, // hard cap applied to API-provided max_turns
|
||||
"stall_repeat_threshold": 3, // stop early when identical tool-call turns repeat this many times
|
||||
"local_http_fetch_default_timeout_seconds": 20,
|
||||
"local_http_fetch_max_timeout_seconds": 120,
|
||||
"blossom_max_upload_bytes": 16777216,
|
||||
"blossom_max_download_bytes": 16777216,
|
||||
"shell": {
|
||||
"enabled": true,
|
||||
"timeout_seconds": 30,
|
||||
"max_output_bytes": 65536,
|
||||
"working_directory": "."
|
||||
}
|
||||
},
|
||||
|
||||
// ─── 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 `local_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- For memory persistence, use `memory_save` to prepend new memory entries and `memory_recall` to recall stored memory.\n- Memory is not injected by default; call `memory_recall` when needed.\n- Keep memory content in markdown and concise enough for context efficiency.\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 local_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 31123: Public triggered skill — cheerleader
|
||||
// Watches for admin kind-1 notes and sends an encouraging DM.
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "You are my personal cheerleader. When the admin posts a kind 1 note, read their note and send them a short DM that cheers them on, praises their effort, tells them they are good looking, and encourages them to keep going. Be warm, playful, and positive. Mention something specific from their note so it feels personal. Keep it to 2-3 sentences.",
|
||||
"tags": [
|
||||
["d", "cheerleader"],
|
||||
["app", "didactyl"],
|
||||
["scope", "public"],
|
||||
["description", "Cheer on admin whenever they post a kind 1 note"],
|
||||
["trigger", "nostr-subscription"],
|
||||
["filter", "{\"kinds\":[1],\"authors\":[\"REPLACE_WITH_ADMIN_HEX_PUBKEY\"]}"],
|
||||
["action", "llm"],
|
||||
["enabled", "true"]
|
||||
]
|
||||
},
|
||||
|
||||
// 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 31124: Private triggered skill — webhook-echo
|
||||
// Example webhook trigger that emits a fixed DM response.
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "When this webhook trigger fires, DM the admin with exactly: WEBHOOK_SOURCE_POC_OK",
|
||||
"tags": [
|
||||
["d", "webhook-echo"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "PoC webhook trigger skill"],
|
||||
["trigger", "webhook"],
|
||||
["filter", "{}"],
|
||||
["action", "llm"],
|
||||
["enabled", "true"]
|
||||
]
|
||||
},
|
||||
|
||||
// Kind 31124: Private triggered skill — cron_poc
|
||||
// Example cron trigger that runs every minute.
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "DM the administrator with the current time, and tell him that your cron skill triggered.",
|
||||
"tags": [
|
||||
["d", "cron_poc"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "PoC cron trigger skill"],
|
||||
["trigger", "cron"],
|
||||
["filter", "*/1 * * * *"],
|
||||
["action", "template"],
|
||||
["enabled", "true"]
|
||||
]
|
||||
},
|
||||
|
||||
// Kind 31124: Private triggered skill — chain_from_webhook_poc
|
||||
// Example chain trigger that runs after webhook-echo completes.
|
||||
{
|
||||
"kind": 31124,
|
||||
"content": "LOG: chain_from_webhook_poc fired",
|
||||
"tags": [
|
||||
["d", "chain_from_webhook_poc"],
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "PoC chain trigger skill"],
|
||||
["trigger", "chain"],
|
||||
["filter", "webhook-echo"],
|
||||
["action", "template"],
|
||||
["enabled", "true"]
|
||||
]
|
||||
},
|
||||
|
||||
// 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"],
|
||||
["a", "31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:cheerleader"],
|
||||
["app", "didactyl"],
|
||||
["scope", "public"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
394
context.log.md
394
context.log.md
@@ -1,3 +1,397 @@
|
||||
```text
|
||||
Context Log - not seen by model
|
||||
timestamp=2026-03-22 12:19:28
|
||||
phase=direct_tool_exec
|
||||
sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||
model=claude-opus-4.6
|
||||
context_bytes=13723
|
||||
approx_tokens=3430
|
||||
```
|
||||
|
||||
slash=/help
|
||||
result_json=AVAILABLE TOOLS
|
||||
- admin_identity — Build admin identity context block from cached runtime metadata
|
||||
- agent_identity — Build agent identity context block with pubkey and npub
|
||||
- agent_version — Return current Didactyl version and metadata from build macros
|
||||
- blossom_delete — Delete a blob from Blossom
|
||||
- blossom_download — Download a blob from Blossom to a local file
|
||||
- blossom_head — Fetch blob metadata from Blossom
|
||||
- blossom_list — List blobs for a pubkey on Blossom
|
||||
- blossom_upload — Upload a local file to a Blossom server
|
||||
- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit
|
||||
- cashu_wallet_check_proofs — Check current wallet proofs state against mint
|
||||
- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint
|
||||
- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id
|
||||
- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request
|
||||
- cashu_wallet_mint_check — Check a previously requested mint quote status
|
||||
- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state
|
||||
- cashu_wallet_mint_quote — Request a mint quote for a target amount
|
||||
- cashu_wallet_mints_get — Get currently configured wallet and public mints
|
||||
- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both
|
||||
- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state
|
||||
- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount
|
||||
- config_recall — Fetch and decrypt agent config kind 30078 by d_tag
|
||||
- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag
|
||||
- local_file_read — Read a local file as text from the configured working directory
|
||||
- local_file_write — Write text content to a local file in the configured working directory
|
||||
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
|
||||
- local_shell_exec — Execute a shell command and return stdout/stderr
|
||||
- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)
|
||||
- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed
|
||||
- model_get — Get current active LLM runtime configuration (excluding API key)
|
||||
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
|
||||
- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)
|
||||
- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context
|
||||
- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context
|
||||
- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context
|
||||
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
|
||||
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
|
||||
- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context
|
||||
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
|
||||
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
|
||||
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
|
||||
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
|
||||
- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list
|
||||
- nostr_agent_notes — Build agent notes context block from cached kind 1 notes
|
||||
- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata
|
||||
- nostr_agent_relays — Build agent relay context block from cached kind 10002 data
|
||||
- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default
|
||||
- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache
|
||||
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
|
||||
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
|
||||
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
|
||||
- nostr_dm_send — Send a NIP-04 encrypted DM
|
||||
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
|
||||
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
|
||||
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
|
||||
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
|
||||
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
|
||||
- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence
|
||||
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
|
||||
- nostr_npub — Return this agent's pubkey encoded as npub bech32
|
||||
- nostr_post — Publish a Nostr event to connected relays
|
||||
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
|
||||
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
|
||||
- nostr_pubkey — Return this agent's pubkey in hex format
|
||||
- nostr_query — Query events from relays using a Nostr filter
|
||||
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
|
||||
- nostr_relay_info — Fetch NIP-11 relay information document
|
||||
- nostr_relay_status — Get connection status and statistics for all relays
|
||||
- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)
|
||||
- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters
|
||||
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
|
||||
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
|
||||
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
|
||||
- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters
|
||||
- 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
|
||||
- task_list — Build current task list context block from agent task memory on Nostr
|
||||
- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace
|
||||
- tool_list — List available tools with name, description, and JSON parameter schema
|
||||
- trigger_list — List active triggered skills and their runtime status
|
||||
|
||||
AVAILABLE SKILLS
|
||||
- (none)
|
||||
|
||||
ADOPTED SKILLS
|
||||
- default_admin_dm
|
||||
|
||||
result_markdown=AVAILABLE TOOLS
|
||||
- admin_identity — Build admin identity context block from cached runtime metadata
|
||||
- agent_identity — Build agent identity context block with pubkey and npub
|
||||
- agent_version — Return current Didactyl version and metadata from build macros
|
||||
- blossom_delete — Delete a blob from Blossom
|
||||
- blossom_download — Download a blob from Blossom to a local file
|
||||
- blossom_head — Fetch blob metadata from Blossom
|
||||
- blossom_list — List blobs for a pubkey on Blossom
|
||||
- blossom_upload — Upload a local file to a Blossom server
|
||||
- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit
|
||||
- cashu_wallet_check_proofs — Check current wallet proofs state against mint
|
||||
- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint
|
||||
- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id
|
||||
- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request
|
||||
- cashu_wallet_mint_check — Check a previously requested mint quote status
|
||||
- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state
|
||||
- cashu_wallet_mint_quote — Request a mint quote for a target amount
|
||||
- cashu_wallet_mints_get — Get currently configured wallet and public mints
|
||||
- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both
|
||||
- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state
|
||||
- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount
|
||||
- config_recall — Fetch and decrypt agent config kind 30078 by d_tag
|
||||
- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag
|
||||
- local_file_read — Read a local file as text from the configured working directory
|
||||
- local_file_write — Write text content to a local file in the configured working directory
|
||||
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
|
||||
- local_shell_exec — Execute a shell command and return stdout/stderr
|
||||
- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)
|
||||
- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed
|
||||
- model_get — Get current active LLM runtime configuration (excluding API key)
|
||||
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
|
||||
- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)
|
||||
- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context
|
||||
- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context
|
||||
- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context
|
||||
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
|
||||
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
|
||||
- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context
|
||||
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
|
||||
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
|
||||
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
|
||||
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
|
||||
- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list
|
||||
- nostr_agent_notes — Build agent notes context block from cached kind 1 notes
|
||||
- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata
|
||||
- nostr_agent_relays — Build agent relay context block from cached kind 10002 data
|
||||
- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default
|
||||
- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache
|
||||
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
|
||||
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
|
||||
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
|
||||
- nostr_dm_send — Send a NIP-04 encrypted DM
|
||||
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
|
||||
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
|
||||
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
|
||||
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
|
||||
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
|
||||
- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence
|
||||
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
|
||||
- nostr_npub — Return this agent's pubkey encoded as npub bech32
|
||||
- nostr_post — Publish a Nostr event to connected relays
|
||||
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
|
||||
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
|
||||
- nostr_pubkey — Return this agent's pubkey in hex format
|
||||
- nostr_query — Query events from relays using a Nostr filter
|
||||
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
|
||||
- nostr_relay_info — Fetch NIP-11 relay information document
|
||||
- nostr_relay_status — Get connection status and statistics for all relays
|
||||
- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)
|
||||
- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters
|
||||
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
|
||||
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
|
||||
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
|
||||
- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters
|
||||
- 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
|
||||
- task_list — Build current task list context block from agent task memory on Nostr
|
||||
- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace
|
||||
- tool_list — List available tools with name, description, and JSON parameter schema
|
||||
- trigger_list — List active triggered skills and their runtime status
|
||||
|
||||
AVAILABLE SKILLS
|
||||
- (none)
|
||||
|
||||
ADOPTED SKILLS
|
||||
- default_admin_dm
|
||||
|
||||
|
||||
---
|
||||
|
||||
```text
|
||||
Context Log - not seen by model
|
||||
timestamp=2026-03-22 12:19:16
|
||||
phase=direct_tool_exec
|
||||
sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||
model=claude-opus-4.6
|
||||
context_bytes=129
|
||||
approx_tokens=32
|
||||
```
|
||||
|
||||
slash=/tools
|
||||
result_json={"success":false,"error":"unknown tool"}
|
||||
result_markdown=- **success:** false
|
||||
- **error:** unknown tool
|
||||
|
||||
|
||||
---
|
||||
|
||||
```text
|
||||
Context Log - not seen by model
|
||||
timestamp=2026-03-22 12:11:32
|
||||
phase=direct_tool_exec
|
||||
sender=1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||
model=claude-opus-4.6
|
||||
context_bytes=13723
|
||||
approx_tokens=3430
|
||||
```
|
||||
|
||||
slash=/help
|
||||
result_json=AVAILABLE TOOLS
|
||||
- admin_identity — Build admin identity context block from cached runtime metadata
|
||||
- agent_identity — Build agent identity context block with pubkey and npub
|
||||
- agent_version — Return current Didactyl version and metadata from build macros
|
||||
- blossom_delete — Delete a blob from Blossom
|
||||
- blossom_download — Download a blob from Blossom to a local file
|
||||
- blossom_head — Fetch blob metadata from Blossom
|
||||
- blossom_list — List blobs for a pubkey on Blossom
|
||||
- blossom_upload — Upload a local file to a Blossom server
|
||||
- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit
|
||||
- cashu_wallet_check_proofs — Check current wallet proofs state against mint
|
||||
- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint
|
||||
- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id
|
||||
- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request
|
||||
- cashu_wallet_mint_check — Check a previously requested mint quote status
|
||||
- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state
|
||||
- cashu_wallet_mint_quote — Request a mint quote for a target amount
|
||||
- cashu_wallet_mints_get — Get currently configured wallet and public mints
|
||||
- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both
|
||||
- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state
|
||||
- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount
|
||||
- config_recall — Fetch and decrypt agent config kind 30078 by d_tag
|
||||
- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag
|
||||
- local_file_read — Read a local file as text from the configured working directory
|
||||
- local_file_write — Write text content to a local file in the configured working directory
|
||||
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
|
||||
- local_shell_exec — Execute a shell command and return stdout/stderr
|
||||
- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)
|
||||
- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed
|
||||
- model_get — Get current active LLM runtime configuration (excluding API key)
|
||||
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
|
||||
- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)
|
||||
- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context
|
||||
- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context
|
||||
- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context
|
||||
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
|
||||
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
|
||||
- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context
|
||||
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
|
||||
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
|
||||
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
|
||||
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
|
||||
- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list
|
||||
- nostr_agent_notes — Build agent notes context block from cached kind 1 notes
|
||||
- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata
|
||||
- nostr_agent_relays — Build agent relay context block from cached kind 10002 data
|
||||
- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default
|
||||
- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache
|
||||
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
|
||||
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
|
||||
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
|
||||
- nostr_dm_send — Send a NIP-04 encrypted DM
|
||||
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
|
||||
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
|
||||
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
|
||||
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
|
||||
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
|
||||
- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence
|
||||
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
|
||||
- nostr_npub — Return this agent's pubkey encoded as npub bech32
|
||||
- nostr_post — Publish a Nostr event to connected relays
|
||||
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
|
||||
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
|
||||
- nostr_pubkey — Return this agent's pubkey in hex format
|
||||
- nostr_query — Query events from relays using a Nostr filter
|
||||
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
|
||||
- nostr_relay_info — Fetch NIP-11 relay information document
|
||||
- nostr_relay_status — Get connection status and statistics for all relays
|
||||
- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)
|
||||
- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters
|
||||
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
|
||||
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
|
||||
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
|
||||
- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters
|
||||
- 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
|
||||
- task_list — Build current task list context block from agent task memory on Nostr
|
||||
- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace
|
||||
- tool_list — List available tools with name, description, and JSON parameter schema
|
||||
- trigger_list — List active triggered skills and their runtime status
|
||||
|
||||
AVAILABLE SKILLS
|
||||
- (none)
|
||||
|
||||
ADOPTED SKILLS
|
||||
- default_admin_dm
|
||||
|
||||
result_markdown=AVAILABLE TOOLS
|
||||
- admin_identity — Build admin identity context block from cached runtime metadata
|
||||
- agent_identity — Build agent identity context block with pubkey and npub
|
||||
- agent_version — Return current Didactyl version and metadata from build macros
|
||||
- blossom_delete — Delete a blob from Blossom
|
||||
- blossom_download — Download a blob from Blossom to a local file
|
||||
- blossom_head — Fetch blob metadata from Blossom
|
||||
- blossom_list — List blobs for a pubkey on Blossom
|
||||
- blossom_upload — Upload a local file to a Blossom server
|
||||
- cashu_wallet_balance — Return current wallet balances aggregated by mint and unit
|
||||
- cashu_wallet_check_proofs — Check current wallet proofs state against mint
|
||||
- cashu_wallet_info — Fetch mint info for a specific mint_url or the default configured mint
|
||||
- cashu_wallet_melt_pay — Pay a previously quoted melt by quote_id
|
||||
- cashu_wallet_melt_quote — Request a melt quote for a Lightning payment request
|
||||
- cashu_wallet_mint_check — Check a previously requested mint quote status
|
||||
- cashu_wallet_mint_claim — Claim minted proofs for a paid quote_id and add them to wallet state
|
||||
- cashu_wallet_mint_quote — Request a mint quote for a target amount
|
||||
- cashu_wallet_mints_get — Get currently configured wallet and public mints
|
||||
- cashu_wallet_mints_set — Set wallet mints (NIP-60), public mints (NIP-61), or both
|
||||
- cashu_wallet_receive_token — Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state
|
||||
- cashu_wallet_send_token — Create an outbound ecash token from wallet proofs for an exact amount
|
||||
- config_recall — Fetch and decrypt agent config kind 30078 by d_tag
|
||||
- config_store — Encrypt and publish agent config as kind 30078 for a given d_tag
|
||||
- local_file_read — Read a local file as text from the configured working directory
|
||||
- local_file_write — Write text content to a local file in the configured working directory
|
||||
- local_http_fetch — Fetch HTTP(S) resources with optional method, headers, timeout, and body
|
||||
- local_shell_exec — Execute a shell command and return stdout/stderr
|
||||
- memory_recall — Recall encrypted agent memory (kind 30078, d=memory)
|
||||
- memory_save — Prepend a new entry to encrypted agent memory (kind 30078, d=memory) and truncate oldest content if needed
|
||||
- model_get — Get current active LLM runtime configuration (excluding API key)
|
||||
- model_list — List available model IDs using provider OpenAI-compatible /models endpoint
|
||||
- model_set — Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)
|
||||
- my_contacts — Alias for nostr_agent_contacts: return this agent's kind 3 contacts context
|
||||
- my_kind0_profile — Alias for nostr_agent_profile: return this agent's kind 0 profile context
|
||||
- my_notes — Alias for nostr_agent_notes: return this agent's recent kind 1 notes context
|
||||
- my_npub — Alias for nostr_npub: return this agent's pubkey encoded as npub bech32
|
||||
- my_pubkey — Alias for nostr_pubkey: return this agent's pubkey in hex format
|
||||
- my_relays — Alias for nostr_agent_relays: return this agent's kind 10002 relay context
|
||||
- nostr_admin_contacts — Build admin contacts context block from cached kind 3 contact list
|
||||
- nostr_admin_notes — Build admin notes context block from cached kind 1 notes
|
||||
- nostr_admin_profile — Build admin profile context block from cached kind 0 metadata
|
||||
- nostr_admin_relays — Build admin relay context block from cached kind 10002 data
|
||||
- nostr_agent_contacts — Build agent contacts context block from cached kind 3 contact list
|
||||
- nostr_agent_notes — Build agent notes context block from cached kind 1 notes
|
||||
- nostr_agent_profile — Build agent profile context block from cached kind 0 metadata
|
||||
- nostr_agent_relays — Build agent relay context block from cached kind 10002 data
|
||||
- nostr_block_edit — Add or remove blocked tuples in kind-10000 block list; entries are private (encrypted) by default
|
||||
- nostr_block_list — View blocked pubkeys, event IDs, or hashtags from the local encrypted kind-10000 block list cache
|
||||
- nostr_decode — Decode a Nostr bech32/nostr: URI into components
|
||||
- nostr_decrypt — Decrypt NIP-44 ciphertext from a sender
|
||||
- nostr_delete — Request deletion of one or more previously published events (NIP-09 kind 5)
|
||||
- nostr_dm_send — Send a NIP-04 encrypted DM
|
||||
- nostr_dm_send_nip17 — Send a private DM using NIP-17 gift wrap protocol
|
||||
- nostr_encode — Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)
|
||||
- nostr_encrypt — Encrypt plaintext using NIP-44 for a recipient
|
||||
- nostr_file_md_to_longform_post — Read a markdown file and publish it as kind 30023 longform post; defaults d-tag to lowercase filename
|
||||
- nostr_list_manage — Add/remove tag tuples in replaceable list events (NIP-51 style)
|
||||
- nostr_my_events — Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence
|
||||
- nostr_nip05_lookup — Look up or verify a NIP-05 identifier (user@domain)
|
||||
- nostr_npub — Return this agent's pubkey encoded as npub bech32
|
||||
- nostr_post — Publish a Nostr event to connected relays
|
||||
- nostr_post_readme — Publish README.md as kind 30023 with deterministic d tag readme.md
|
||||
- nostr_profile_get — Look up a Nostr profile (kind 0 metadata) by pubkey
|
||||
- nostr_pubkey — Return this agent's pubkey in hex format
|
||||
- nostr_query — Query events from relays using a Nostr filter
|
||||
- nostr_react — React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)
|
||||
- nostr_relay_info — Fetch NIP-11 relay information document
|
||||
- nostr_relay_status — Get connection status and statistics for all relays
|
||||
- nostr_subscription_set — Update one managed runtime subscription by name (toggle enabled and/or replace filter)
|
||||
- nostr_subscription_status — List currently managed runtime Nostr subscriptions and filters
|
||||
- skill_adopt — Adopt a skill by adding its address to kind 10123 adoption list
|
||||
- skill_create — Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it
|
||||
- skill_edit — Edit an existing self skill by d tag and republish it as kind 31123/31124
|
||||
- skill_list — List available skills discovered online (agent + admin), with adoption status and optional filters
|
||||
- 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
|
||||
- task_list — Build current task list context block from agent task memory on Nostr
|
||||
- task_manage — Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace
|
||||
- tool_list — List available tools with name, description, and JSON parameter schema
|
||||
- trigger_list — List active triggered skills and their runtime status
|
||||
|
||||
AVAILABLE SKILLS
|
||||
- (none)
|
||||
|
||||
ADOPTED SKILLS
|
||||
- default_admin_dm
|
||||
|
||||
|
||||
---
|
||||
|
||||
```text
|
||||
Context Log - not seen by model
|
||||
timestamp=2026-03-21 07:09:20
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Context Template (Legacy)
|
||||
|
||||
This file is deprecated and retained for backward compatibility only.
|
||||
|
||||
The default context behavior is now defined by adopted skills (kind `10123`) and the `didactyl_default` skill template published from `genesis.jsonc`.
|
||||
|
||||
For current behavior, see:
|
||||
- `docs/GENESIS.md`
|
||||
- `docs/CONTEXT.md`
|
||||
- `docs/SKILLS.md`
|
||||
@@ -1,56 +0,0 @@
|
||||
# Context Template
|
||||
|
||||
```yaml
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Identity (source: config.admin.pubkey)
|
||||
|
||||
This is your administrator! Admin pubkey (hex): {{admin_pubkey}}
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Kind 0 Profile (source: nostr kind 0)
|
||||
|
||||
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
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Relay List (source: nostr kind 10002)
|
||||
|
||||
Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}
|
||||
|
||||
- section: startup_events
|
||||
role: system
|
||||
content: |
|
||||
## Startup Events Memory (source: config.startup_events)
|
||||
|
||||
Startup events memory (kinds/content/tags): {{startup_events_json}}
|
||||
|
||||
- section: adopted_skills
|
||||
role: system
|
||||
content: |
|
||||
{{adopted_skills_content}}
|
||||
|
||||
- section: agent_tasks
|
||||
role: system
|
||||
content: |
|
||||
{{tasks_content}}
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 12
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Recent Notes (source: nostr kind 1)
|
||||
|
||||
{{admin_notes_content}}
|
||||
@@ -10,7 +10,7 @@ All responses are JSON. CORS headers are included on every response for browser
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable the API in `config.jsonc`:
|
||||
Enable the API in `genesis.jsonc`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
177
docs/CONTEXT.md
177
docs/CONTEXT.md
@@ -12,6 +12,8 @@ Context is not just a prompt string; it is the full request payload:
|
||||
2. **Tool schemas** — JSON descriptions of callable tools
|
||||
3. **Model parameters** — model, temperature, max tokens, seed, etc.
|
||||
|
||||
The context window is composed of **skills** — blocks of markdown instructions stacked together. See [SKILLS.md](SKILLS.md) for the canonical skill specification.
|
||||
|
||||
---
|
||||
|
||||
## OpenAI-Compatible Chat Format
|
||||
@@ -44,45 +46,96 @@ Didactyl uses OpenAI-compatible chat completions.
|
||||
|
||||
| Role | Purpose |
|
||||
|------|---------|
|
||||
| `system` | Instructions and injected context |
|
||||
| `system` | Instructions and injected context (from skills) |
|
||||
| `user` | Input message or trigger payload |
|
||||
| `assistant` | Model responses / tool call envelopes |
|
||||
| `tool` | Tool execution results fed back to model |
|
||||
|
||||
---
|
||||
|
||||
## Context Assembly Model
|
||||
## Context Assembly — Two-Layer Model
|
||||
|
||||
Didactyl uses **skill composition by adoption order**.
|
||||
Context is assembled using a two-layer model driven by triggers and the adoption list.
|
||||
|
||||
There are no context modes.
|
||||
### Layer 1: Triggered Skills
|
||||
|
||||
When a trigger event occurs (DM, cron, subscription, webhook, chain), Didactyl walks the `10123` adoption list and finds all skills whose trigger matches the current event. These skills form layer 1 of the context window, in adoption-list order.
|
||||
|
||||
Only triggered skills can be in layer 1 — the trigger system is what puts them there.
|
||||
|
||||
### Layer 2: Referenced Skills
|
||||
|
||||
Within each layer 1 skill, `{{skill_d_tag}}` template variables resolve to adopted skills' content. These are embedded inline — the same way tool-based template variables are resolved.
|
||||
|
||||
Non-triggered skills (skills with no trigger tags) can only enter the context window via layer 2 references.
|
||||
|
||||
### Assembly Steps
|
||||
|
||||
1. Load adopted skills from kind `10123`.
|
||||
2. Resolve adopted skills in list order.
|
||||
3. Expand each skill template variables via tools.
|
||||
4. Append resolved skill output to messages in that same order.
|
||||
5. Append live input (DM text or triggering event payload).
|
||||
6. Attach tool schemas.
|
||||
7. Apply execution parameters from trigger tags (if invoked via trigger).
|
||||
```
|
||||
Trigger event occurs (DM, cron, subscription, webhook, chain)
|
||||
│
|
||||
├─ Walk adoption list (10123)
|
||||
│ │
|
||||
│ ├─ Skill has trigger matching this event?
|
||||
│ │ ├─ YES → add to context (layer 1)
|
||||
│ │ │ └─ Resolve {{...}} references (layer 2)
|
||||
│ │ │ ├─ Known tool? → execute tool, insert result
|
||||
│ │ │ ├─ Adopted skill d-tag? → insert skill content
|
||||
│ │ │ └─ Unknown? → resolve to empty
|
||||
│ │ │
|
||||
│ │ └─ NO → skip (not in this context)
|
||||
│ │
|
||||
│ └─ Continue to next skill in list
|
||||
│
|
||||
├─ Append triggering event payload
|
||||
│ └─ For DM triggers: always append raw message content
|
||||
│
|
||||
├─ Attach tool schemas (filtered by skill requires_tool tags)
|
||||
│
|
||||
├─ Apply execution parameters (llm, temperature, max_tokens)
|
||||
│ └─ Walk LLM fallback chain until usable model found
|
||||
│
|
||||
└─ Send to LLM
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
INPUT[Input: DM or trigger event] --> ADOPT[Load adopted skills from kind 10123]
|
||||
ADOPT --> ORDER[Resolve skills in listed order]
|
||||
ORDER --> EXPAND[Expand template variables via tools]
|
||||
EXPAND --> MESSAGES[Append resolved skill messages]
|
||||
MESSAGES --> LIVE[Append live input message/event]
|
||||
LIVE --> TOOLS[Attach tool schemas]
|
||||
TOOLS --> PARAMS[Apply runtime params from trigger tags]
|
||||
PARAMS --> LLM[Send to LLM]
|
||||
### Visualization
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════╗
|
||||
║ CONTEXT WINDOW ║
|
||||
║ ║
|
||||
║ ┌────────────────────────────────────┐ ║
|
||||
║ │ Layer 1: personality (dm trigger) │ ║
|
||||
║ │ │ ║
|
||||
║ │ ┌──────────────────────────────┐ │ ║
|
||||
║ │ │ Layer 2: {{identity}} │ │ ║
|
||||
║ │ │ You are Didactyl. npub1... │ │ ║
|
||||
║ │ └──────────────────────────────┘ │ ║
|
||||
║ │ │ ║
|
||||
║ │ You speak concisely and directly. │ ║
|
||||
║ └────────────────────────────────────┘ ║
|
||||
║ ┌────────────────────────────────────┐ ║
|
||||
║ │ Layer 1: chat (dm trigger) │ ║
|
||||
║ │ │ ║
|
||||
║ │ Respond helpfully to the admin. │ ║
|
||||
║ │ Use tools as needed. │ ║
|
||||
║ │ │ ║
|
||||
║ │ tools: [nostr_query, nostr_dm] │ ║
|
||||
║ └────────────────────────────────────┘ ║
|
||||
║ ┌────────────────────────────────────┐ ║
|
||||
║ │ DM content (always last) │ ║
|
||||
║ │ │ ║
|
||||
║ │ "Who mentioned me today?" │ ║
|
||||
║ └────────────────────────────────────┘ ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### Why Order Matters
|
||||
|
||||
- Earlier adopted skills usually establish broad behavior.
|
||||
- Later adopted skills can refine or narrow behavior.
|
||||
- Earlier skills in the adoption list appear first in the context window.
|
||||
- Earlier instructions generally set broader tone/policy.
|
||||
- Later instructions can narrow/specialize behavior.
|
||||
- If instructions conflict, prompt-order effects apply.
|
||||
|
||||
---
|
||||
@@ -91,53 +144,64 @@ flowchart TD
|
||||
|
||||
| Part | Source | Description |
|
||||
|------|--------|-------------|
|
||||
| Skill templates | Adopted skill events | Core instructions assembled in order |
|
||||
| Resolved variables | Tool outputs | Runtime data inserted into templates |
|
||||
| Conversation history | DM history/events | Recent dialogue context |
|
||||
| Live input | DM or trigger event | Current request payload |
|
||||
| Tool schemas | Tool registry | Capability declaration for tool calling |
|
||||
| Runtime params | Trigger tags | LLM/tool limits for this execution |
|
||||
| Layer 1 skills | Triggered skills from adoption list | Skills whose trigger matches the current event, in adoption-list order |
|
||||
| Layer 2 skills | `{{skill_d_tag}}` references | Adopted skills embedded inside layer 1 skills |
|
||||
| Resolved variables | Tool outputs | Runtime data inserted into templates via `{{...}}` |
|
||||
| Triggering event | DM content / event payload | Current request — always appended after skills |
|
||||
| Tool schemas | Tool registry, filtered by skill `requires_tool` tags | Capability declaration for tool calling |
|
||||
| Runtime params | Skill event tags + LLM fallback chain | Model, temperature, max_tokens, etc. |
|
||||
|
||||
---
|
||||
|
||||
## Template Variables Are Tool Calls
|
||||
## Template Variable Resolution
|
||||
|
||||
Template variables resolve through tool execution.
|
||||
When the engine encounters `{{variable_name}}` in a skill template:
|
||||
|
||||
Example:
|
||||
1. **Check known tools** — if it matches a tool name, execute the tool and insert the result
|
||||
2. **Check adopted skills** — if it matches an adopted skill's d-tag, insert that skill's content (layer 2)
|
||||
3. **Neither** — resolve to empty (for portability)
|
||||
|
||||
- `{{admin_profile}}` resolves by running `nostr_admin_profile`
|
||||
- `{{admin_notes}}` resolves by running `nostr_admin_notes`
|
||||
This means `{{admin_profile}}` calls the `nostr_admin_profile` tool, while `{{identity}}` inserts the adopted "identity" skill's content. The skill author doesn't need to know which is which — the resolution is transparent.
|
||||
|
||||
Unknown variables should resolve to empty values for portability.
|
||||
See [SKILLS.md — Template Variables](SKILLS.md#template-variables) for the full variable table.
|
||||
|
||||
---
|
||||
|
||||
## Trigger Runtime Parameters
|
||||
## Execution Parameters
|
||||
|
||||
Execution controls are attached to trigger tags, not skill content:
|
||||
Execution parameters control the LLM call: which model, what temperature, how many tokens, which tools.
|
||||
|
||||
- `llm`
|
||||
- `max_tokens`
|
||||
- `temperature`
|
||||
- `seed`
|
||||
- `tools`
|
||||
|
||||
Resolution order for a triggered run:
|
||||
### Resolution Order
|
||||
|
||||
1. Start with agent defaults
|
||||
2. Apply trigger tag overrides
|
||||
3. Execute
|
||||
4. Restore defaults
|
||||
2. Apply top-level execution tags from the skill event
|
||||
3. Walk the `llm` fallback chain until a usable model is found
|
||||
4. Execute
|
||||
5. Restore defaults after the run
|
||||
|
||||
### LLM Fallback Chain
|
||||
|
||||
The `llm` tag uses a CSS font-stack style fallback: `provider/model, provider/model, ..., capability_keyword`
|
||||
|
||||
```
|
||||
["llm", "anthropic/claude-sonnet-4-20250514, openai/gpt-4o-mini, cheap"]
|
||||
```
|
||||
|
||||
See [SKILLS.md — LLM Fallback Chain](SKILLS.md#llm-fallback-chain) for the full format and capability keywords.
|
||||
|
||||
---
|
||||
|
||||
## Triggered vs Adopted Use
|
||||
## Context Compaction
|
||||
|
||||
- **Adopted skill (`10123`)**: contributes context/instructions
|
||||
- **Triggered skill**: contributes context and may supply execution overrides via tags
|
||||
During long-running tool loops, the context window can grow as tool call/result pairs accumulate. When context approaches the model's token limit, compaction prevents overflow:
|
||||
|
||||
This separation keeps composition simple while allowing per-trigger runtime control.
|
||||
1. Track approximate token usage of the messages array
|
||||
2. When approaching ~70% of the model's context window, inject a summarization request
|
||||
3. The LLM summarizes progress so far into a condensed form
|
||||
4. Replace detailed tool history with the summary
|
||||
5. Continue execution with the compacted context
|
||||
|
||||
This allows skills to run complex multi-step tasks without hitting context limits.
|
||||
|
||||
---
|
||||
|
||||
@@ -145,12 +209,13 @@ This separation keeps composition simple while allowing per-trigger runtime cont
|
||||
|
||||
Context cost is controlled by:
|
||||
|
||||
- Adoption-list ordering and skill count
|
||||
- Conversation-history limits
|
||||
- Skill/template truncation limits
|
||||
- Per-trigger model/runtime parameter choices
|
||||
- Number of triggered skills matching the event (layer 1 count)
|
||||
- Size of referenced skills (layer 2 content)
|
||||
- Tool call/result accumulation during execution
|
||||
- Context compaction threshold (~70% of model window)
|
||||
- Per-skill model/runtime parameter choices
|
||||
|
||||
Use runtime context inspection endpoints to see the exact payload before LLM calls.
|
||||
Use runtime context inspection endpoints (`GET /api/context/current`, `GET /api/context/parts`) to see the exact payload before LLM calls.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ See also: [CONTEXT.md](CONTEXT.md) · [SKILLS.md](SKILLS.md) · [README.md](../R
|
||||
|
||||
`genesis.jsonc` is the first-run bootstrap document for a Didactyl agent.
|
||||
|
||||
It defines initial identity, admin policy, startup events, default skill context, and baseline runtime settings so the agent can publish itself onto Nostr.
|
||||
It defines initial identity, admin policy, startup events, default skill trigger behavior, and baseline runtime settings so the agent can publish itself onto Nostr.
|
||||
|
||||
After bootstrap, the long-term direction is **nsec-only startup** with state recovered from Nostr events.
|
||||
|
||||
@@ -30,7 +30,7 @@ Typical optional sections:
|
||||
- `security`
|
||||
- `admin_context`
|
||||
- `api`
|
||||
- `default_skill` (reserved for default-skill publishing workflows)
|
||||
- `default_skill` (published as private kind `31124`, default `d=default_admin_dm`)
|
||||
|
||||
---
|
||||
|
||||
@@ -118,14 +118,13 @@ That relay list is used as the initial network attachment for querying existing
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes (Legacy -> Genesis)
|
||||
## Migration Notes (v0.2.0)
|
||||
|
||||
Legacy deployments using `config.jsonc` can migrate by:
|
||||
|
||||
1. Copying equivalent sections to `genesis.jsonc`.
|
||||
2. Ensuring startup kind `10002` relay tags are present.
|
||||
3. Providing nsec at runtime (`--nsec` or `DIDACTYL_NSEC`) where desired.
|
||||
4. Treating `context_template.md` / `config.jsonc.example` as legacy references.
|
||||
- Legacy `config.jsonc` and template-DSL context files have been removed from the active startup model.
|
||||
- Default DM handling now relies on `default_skill` with `d_tag: default_admin_dm` and trigger tags:
|
||||
- `["trigger", "dm"]`
|
||||
- `["filter", "{\"from\":\"admin\"}"]`
|
||||
- Default skill content is plain markdown with inline variables such as `{{my_kind0_profile}}` and `{{my_npub}}`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
621
docs/SKILLS.md
621
docs/SKILLS.md
@@ -1,211 +1,470 @@
|
||||
# Didactyl — Skills
|
||||
# Skills
|
||||
|
||||
See also: [CONTEXT.md](CONTEXT.md) · [TOOLS.md](TOOLS.md)
|
||||
|
||||
## Overview
|
||||
## The Context Window Is Made of Skills
|
||||
|
||||
A skill is a **set of instructions for the LLM** stored as a Nostr event.
|
||||
Every time an LLM runs, it receives a context window — the complete set of instructions and information it needs to reason and respond. In this system, **the context window is broken up into units called skills.**
|
||||
|
||||
Skills teach the agent how to accomplish tasks — the LLM reads the instructions, reasons about them, and uses tools to take action.
|
||||
```
|
||||
╔══════════════════════════════════════════╗
|
||||
║ CONTEXT WINDOW ║
|
||||
║ ║
|
||||
║ ┌────────────────────────────────────┐ ║
|
||||
║ │ Skill 1: personality │ ║
|
||||
║ │ │ ║
|
||||
║ │ You speak concisely and directly. │ ║
|
||||
║ │ You favor technical precision. │ ║
|
||||
║ │ │ ║
|
||||
║ │ tools: [my_name, my_npub] │ ║
|
||||
║ └────────────────────────────────────┘ ║
|
||||
║ ┌────────────────────────────────────┐ ║
|
||||
║ │ Skill 2: chat │ ║
|
||||
║ │ │ ║
|
||||
║ │ Respond helpfully to the admin. │ ║
|
||||
║ │ Use tools as needed. │ ║
|
||||
║ │ │ ║
|
||||
║ │ tools: [nostr_query, nostr_dm] │ ║
|
||||
║ └────────────────────────────────────┘ ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
Each skill is a block of instructions. The context window is a stack of these blocks. Different events produce different stacks — a DM conversation has one set of skills, a scheduled cron job has a completely different set.
|
||||
|
||||
A skill is a **set of instructions for an LLM** stored as a Nostr event. Skills teach an LLM how to accomplish tasks — the LLM reads the instructions, reasons about them, and uses tools to take action.
|
||||
|
||||
Think of it like a woodshop: a **skill** is knowing how to carve — technique, judgment, decision-making. A **tool** is the chisel. The skill never directly uses the chisel without the craftsperson (the LLM) in the loop.
|
||||
|
||||
Skills are portable, shareable, and discoverable as Nostr events.
|
||||
Skills are portable, shareable, and discoverable as Nostr events. They are not specific to any single application — any app that can read Nostr events and call an LLM can use skills.
|
||||
|
||||
---
|
||||
|
||||
## What Is a Skill?
|
||||
|
||||
A skill has two orthogonal properties:
|
||||
|
||||
- **Triggers** — A skill may have trigger tags, or not. If it has triggers, a runtime can fire it automatically when matching events occur. Triggered skills appear in the context window when their trigger matches (layer 1).
|
||||
- **References** — A skill may be referenced by other skills via`{{skill_d_tag}}` template variables, or not. If referenced, its content is included inside the referencing skill (layer 2).
|
||||
|
||||
These properties are independent. A skill can have triggers and be referenced. A skill can have triggers and never be referenced. A skill can have no triggers and only exist to be referenced. A skill can have neither (though that would be inert).
|
||||
|
||||
---
|
||||
|
||||
## Skill Events
|
||||
|
||||
| Kind | Purpose | Replaceable? |
|
||||
|---|---|---|
|
||||
| `31123` | Public skill definition | Yes, by d-tag |
|
||||
| `31124` | Private skill definition | Yes, by d-tag |
|
||||
| `10123` | Skill adoption list | Yes, single per pubkey |
|
||||
| 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
|
||||
|
||||
Skill `content` is JSON and should focus on **instructions**, not transport/runtime controls.
|
||||
The `content` field of a skill event IS the template — markdown instructions that go directly into the context window. No JSON wrapper. The description lives in a tag, not in content.
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": {
|
||||
"description": "Check spelling and grammar",
|
||||
"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}}"
|
||||
},
|
||||
"content": "system:\n# Spelling and Grammar Checker\n\nYou are a spelling and grammar checker.\n\n## Rules\n\n- Fix spelling errors\n- Fix grammar errors\n- Preserve original formatting\n- Return **ONLY** the corrected text, no explanations\n\nuser:\n{{message}}",
|
||||
"tags": [
|
||||
["d", "spellcheck"],
|
||||
["scope", "public"],
|
||||
["description", "Spelling and grammar checker"]
|
||||
["description", "Check spelling and grammar"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["temperature", "0"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Content Fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `description` | string | — | Human-readable description |
|
||||
| `template` | string | — | Skill instructions/template text (recommended) |
|
||||
| `base` | bool | `false` | Optional hint that this skill is intended as base/default behavior |
|
||||
|
||||
> Execution parameters (`llm`, `max_tokens`, `temperature`, `seed`, `tools`) are defined on **trigger tags**, not in content.
|
||||
- **`content`** — the template in markdown. May include `{{...}}` template variables and `system:` / `user:` role markers. This is what goes into the context window.
|
||||
- **`["description", "..."]`** — human-readable description for discovery and UI display.
|
||||
- Each `["tag", "value"]` is a separate tag on the Nostr event.
|
||||
- The `llm` tag uses a CSS font-stack style fallback chain. See [LLM Fallback Chain](#llm-fallback-chain).
|
||||
|
||||
---
|
||||
|
||||
## Composition Model (No Context Modes)
|
||||
## Two-Layer Context Model
|
||||
|
||||
Skills do **not** use `context_mode`.
|
||||
When a skill executes, the context window is built in two layers:
|
||||
|
||||
Context is assembled from kind `10123` adoption list order:
|
||||
- **Layer 1:** Triggered skills whose trigger matches the current event, ordered by their position in the adoption list (`10123`). Only triggered skills can be in layer 1 — the trigger system is what puts them there.
|
||||
- **Layer 2:** Skills embedded inside layer 1 skills via`{{skill_d_tag}}` template references. These are resolved inline, the same way tool-based template variables are resolved.
|
||||
|
||||
1. Resolve adopted skills in list order.
|
||||
2. Expand each skill template/tool variables.
|
||||
3. Append each resolved skill as context messages in that same order.
|
||||
4. Append live user/trigger input.
|
||||
```
|
||||
CONTEXT WINDOW — Admin DM arrives
|
||||
═══════════════════════════════════════════════════
|
||||
|
||||
The adoption list itself is the context definition.
|
||||
Layer 1: Triggered skills matching "dm/admin"
|
||||
(ordered by adoption list)
|
||||
|
||||
- One adopted skill = single-skill behavior.
|
||||
- Multiple adopted skills = layered behavior in explicit order.
|
||||
- Reordering `10123` changes precedence naturally.
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ TRIGGERED SKILL: personality │
|
||||
│ trigger: dm, filter: {"from":"admin"} │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────┐ │
|
||||
│ │ {{identity}} (adopted, no trigger) │ │
|
||||
│ │ You are Didactyl. npub1abc...xyz │ │
|
||||
│ └───────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ You speak concisely and directly. │
|
||||
│ You favor technical precision. │
|
||||
│ You use dry humor sparingly. │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ TRIGGERED SKILL: chat │
|
||||
│ trigger: dm, filter: {"from":"admin"} │
|
||||
│ │
|
||||
│ Respond helpfully. Use tools as needed. │
|
||||
│ │
|
||||
│ tools: [nostr_query, nostr_dm, nostr_post, │
|
||||
│ memory_read, memory_write] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ DM CONTENT (always last for dm triggers) │
|
||||
│ │
|
||||
│ "Hey, can you check who mentioned me today?" │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Ordering Convention
|
||||
```
|
||||
CONTEXT WINDOW — Cron fires at noon
|
||||
═══════════════════════════════════════════════════
|
||||
|
||||
- Earlier adopted skills generally set broader tone/policy.
|
||||
- Later adopted skills can narrow/specialize behavior.
|
||||
- If multiple skills strongly conflict, normal prompt-order effects apply.
|
||||
Layer 1: Triggered skills matching "cron/0 12 * * *"
|
||||
(ordered by adoption list)
|
||||
|
||||
### Template Variables Are Tool Calls
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ TRIGGERED SKILL: readme-monitor │
|
||||
│ trigger: cron, filter: 0 12 * * * │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────┐ │
|
||||
│ │ {{identity}} (adopted, no trigger) │ │
|
||||
│ │ You are Didactyl. npub1abc...xyz │ │
|
||||
│ └───────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Check the readme at the configured URL. │
|
||||
│ Compare with last known version in memory. │
|
||||
│ If changed: post it and DM admin a summary. │
|
||||
│ │
|
||||
│ tools: [http_fetch, memory_read, │
|
||||
│ memory_write, nostr_post, nostr_dm] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ TRIGGERING EVENT │
|
||||
│ │
|
||||
│ {"type":"cron","filter":"0 12 * * *", │
|
||||
│ "created_at":1742641200} │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Template variables are tool calls.
|
||||
personality is NOT here — it has a dm trigger,
|
||||
not a cron trigger, so it doesn't match layer 1.
|
||||
|
||||
When the engine encounters `{{admin_profile}}`, it runs the corresponding tool and inserts the result into context.
|
||||
|
||||
| Variable | Tool Called | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `{{agent_identity}}` | `agent_identity` | Agent identity block |
|
||||
| `{{admin_profile}}` | `nostr_admin_profile` | Admin kind 0 profile |
|
||||
| `{{admin_notes}}` | `nostr_admin_notes` | Admin recent notes |
|
||||
| `{{admin_relays}}` | `nostr_admin_relays` | Admin relay list |
|
||||
| `{{adopted_skills}}` | `adopted_skills` | Other adopted skill instructions |
|
||||
| `{{dm_history}}` | *(expand directive)* | Recent DM conversation |
|
||||
| `{{message}}` | *(built-in)* | Current user message |
|
||||
| `{{triggering_event}}` | `trigger_event` | Triggering event JSON |
|
||||
|
||||
Unknown variables should resolve to empty values for portability.
|
||||
identity IS here — but only as layer 2 inside
|
||||
readme-monitor, because readme-monitor includes
|
||||
{{identity}} in its template.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Triggered Skills
|
||||
## Adoption List (`10123`)
|
||||
|
||||
A triggered skill has a trigger source attached.
|
||||
The adoption list serves two purposes:
|
||||
|
||||
Didactyl trigger types:
|
||||
1. **Registry** — makes skills available for`{{skill_d_tag}}` resolution (layer 2 inclusion)
|
||||
2. **Ordering** — determines the order of layer 1 triggered skills in the context window
|
||||
|
||||
- `nostr-subscription`
|
||||
- `webhook`
|
||||
- `cron`
|
||||
- `chain`
|
||||
- `dm`
|
||||
```json
|
||||
{
|
||||
"kind": 10123,
|
||||
"tags": [
|
||||
["a", "31124:<pubkey>:identity"],
|
||||
["a", "31124:<pubkey>:personality"],
|
||||
["a", "31123:<pubkey>:chat"],
|
||||
["a", "31123:<pubkey>:readme-monitor"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `identity` — no trigger, adopted so triggered skills can include it via`{{identity}}` (layer 2)
|
||||
- `personality` — has`["trigger", "dm"]`, appears in layer 1 for DM events. Also referenceable via`{{personality}}` by other skills (layer 2).
|
||||
- `chat` — has`["trigger", "dm"]`, appears in layer 1 for DM events after personality (adoption list order)
|
||||
- `readme-monitor` — has`["trigger", "cron"]`, appears in layer 1 for cron events. Its template includes`{{identity}}` (layer 2).
|
||||
|
||||
Skills NOT in this list but with trigger tags are still armed — they fire when their trigger matches, but they execute in isolation (no layer 2 skill references available, only built-in variables).
|
||||
|
||||
---
|
||||
|
||||
## Template Variables
|
||||
|
||||
Template variables resolve through tool execution or skill lookup.
|
||||
|
||||
When the engine encounters `{{variable_name}}`:
|
||||
|
||||
1. Check if it matches a known tool — if so, execute the tool and insert the result
|
||||
2. Check if it matches an adopted skill's d-tag — if so, insert that skill's content (layer 2)
|
||||
3. If neither matches, resolve to empty (for portability)
|
||||
|
||||
### Built-in Variables
|
||||
|
||||
| Variable | Resolution | Description |
|
||||
| ------------------------ | ---------------------------- | ---------------------- |
|
||||
| `{{agent_identity}}` | `agent_identity` tool | Agent identity block |
|
||||
| `{{admin_profile}}` | `nostr_admin_profile` tool | Admin kind 0 profile |
|
||||
| `{{admin_notes}}` | `nostr_admin_notes` tool | Admin recent notes |
|
||||
| `{{admin_relays}}` | `nostr_admin_relays` tool | Admin relay list |
|
||||
| `{{dm_history}}` | *(expand directive)* | Recent DM conversation |
|
||||
| `{{message}}` | *(built-in)* | Current user message |
|
||||
| `{{triggering_event}}` | `trigger_event` tool | Triggering event JSON |
|
||||
|
||||
### Skill Reference Variables
|
||||
|
||||
| Variable | Resolution | Description |
|
||||
| ------------------- | ------------------------------ | ----------------------- |
|
||||
| `{{skill_d_tag}}` | Look up adopted skill by d-tag | Layer 2 skill inclusion |
|
||||
|
||||
Unknown variables resolve to empty values for portability.
|
||||
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
A skill with trigger tags can be fired automatically by a runtime when matching events occur.
|
||||
|
||||
### Trigger Types
|
||||
|
||||
- `dm` — Direct message received
|
||||
- `cron` — Scheduled time expression
|
||||
- `nostr-subscription` — Nostr event matches a filter
|
||||
- `webhook` — HTTP request received
|
||||
- `chain` — Another skill completed execution
|
||||
|
||||
### Trigger Tags
|
||||
|
||||
| Tag | Required | Description |
|
||||
|---|---|---|
|
||||
| `trigger` | Yes | Trigger type: `nostr-subscription`, `webhook`, `cron`, `chain`, `dm` |
|
||||
| `filter` | Yes | Type-specific filter |
|
||||
| `enabled` | No | Whether active (default: `true`) |
|
||||
| `llm` | No | Model spec fallback chain (e.g., `openai/gpt-4o-mini, cheap`) |
|
||||
| `max_tokens` | No | Max output tokens for this trigger execution |
|
||||
| `temperature` | No | Sampling temperature for this trigger execution |
|
||||
| `seed` | No | Optional deterministic seed where supported |
|
||||
| `tools` | No | `true` for all tools, `false` for none, or CSV list of allowed tool names |
|
||||
| Tag | Required | Description |
|
||||
| ----------- | -------- | --------------------------------- |
|
||||
| `trigger` | Yes | Trigger type |
|
||||
| `filter` | Yes | Type-specific filter |
|
||||
|
||||
If a skill is in the adoption list, its triggers are active. There is no separate `enabled` flag — adoption IS enablement.
|
||||
|
||||
### Execution Parameter Tags
|
||||
|
||||
These tags can appear at the top level of a skill event (defaults for any app) or on trigger-specific contexts (runtime overrides).
|
||||
|
||||
| Tag | Description |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `llm` | Model spec with fallback chain (see below) |
|
||||
| `max_tokens` | Max output tokens |
|
||||
| `temperature` | Sampling temperature |
|
||||
| `seed` | Optional deterministic seed |
|
||||
|
||||
### LLM Fallback Chain
|
||||
|
||||
The `llm` tag uses a CSS font-stack style fallback chain. The runtime tries each entry in order, falling back to the next if the previous is unavailable.
|
||||
|
||||
Format: `provider/model, provider/model, ..., capability_keyword`
|
||||
|
||||
```
|
||||
["llm", "anthropic/claude-sonnet-4-20250514, openai/gpt-4o-mini, cheap"]
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
1. Try `anthropic/claude-sonnet-4-20250514` first
|
||||
2. If unavailable, try `openai/gpt-4o-mini`
|
||||
3. If unavailable, use whatever the runtime considers `cheap`
|
||||
|
||||
Each entry can be:
|
||||
|
||||
- **`provider/model`** — specific provider and model (e.g., `anthropic/claude-sonnet-4-20250514`)
|
||||
- **`model`** — model name only, use the default provider (e.g., `gpt-4o-mini`)
|
||||
- **Capability keyword** — abstract tier the runtime resolves to its best available option
|
||||
|
||||
Capability keywords:
|
||||
|
||||
| Keyword | Meaning |
|
||||
|---------|---------|
|
||||
| `cheap` | Lowest cost model available |
|
||||
| `fast` | Lowest latency model available |
|
||||
| `best` | Highest capability model available |
|
||||
| `default` | Use the agent/app default model |
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
["llm", "openai/gpt-4o-mini"] -- specific model, no fallback
|
||||
["llm", "openai/gpt-4o-mini, cheap"] -- try gpt-4o-mini, fall back to cheapest
|
||||
["llm", "anthropic/claude-opus-4-20250514, openai/gpt-4o, best"] -- try opus, then gpt-4o, then best available
|
||||
["llm", "fast"] -- just use the fastest available
|
||||
["llm", "default"] -- use agent/app default
|
||||
```
|
||||
|
||||
This is important for portability: a skill published with `["llm", "anthropic/claude-sonnet-4-20250514, cheap"]` works on any runtime — if the runtime doesn't have Anthropic access, it falls back to its cheapest available model.
|
||||
|
||||
### Execution Parameter Resolution
|
||||
|
||||
When a trigger fires:
|
||||
|
||||
1. Start with agent defaults.
|
||||
2. Apply execution tags from that trigger (`llm`, `max_tokens`, `temperature`, `seed`, `tools`).
|
||||
3. Execute skill with those effective runtime settings.
|
||||
4. Restore defaults after the run.
|
||||
|
||||
### Adopted vs Triggered Behavior
|
||||
|
||||
- **Adopted skill (`10123`)**: contributes instructions/template to context.
|
||||
- **Triggered skill**: contributes instructions **and** may define execution parameters via trigger tags.
|
||||
1. Start with agent/app defaults.
|
||||
2. Apply top-level execution tags from the skill event.
|
||||
3. Walk the `llm` fallback chain until a usable model is found.
|
||||
4. Apply trigger-specific overrides if present.
|
||||
5. Execute skill with those effective runtime settings.
|
||||
6. Restore defaults after the run.
|
||||
|
||||
---
|
||||
|
||||
## Trigger Types
|
||||
## Trigger Type Details
|
||||
|
||||
### `nostr-subscription`
|
||||
Each example below shows a complete skill event. Every `["tag", "value"]` pair is a separate tag on the Nostr event.
|
||||
|
||||
`filter` is a JSON-encoded Nostr subscription filter.
|
||||
### `dm`
|
||||
|
||||
Fires when a direct message is received. `filter` is JSON with sender scope: `{"from":"admin"}`, `{"from":"wot"}`, or `{"from":"any"}`.
|
||||
|
||||
For DM triggers, the raw message content is always appended to the end of the context window.
|
||||
|
||||
```json
|
||||
["trigger", "nostr-subscription"],
|
||||
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["temperature", "0"],
|
||||
["tools", "nostr_query,nostr_dm"],
|
||||
["enabled", "true"]
|
||||
```
|
||||
|
||||
### `webhook`
|
||||
|
||||
`filter` is required and can be `{}`; webhook firing happens via HTTP.
|
||||
|
||||
```json
|
||||
["trigger", "webhook"],
|
||||
["filter", "{}"],
|
||||
["llm", "default"],
|
||||
["tools", "true"],
|
||||
["enabled", "true"]
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "{{identity}}\n\nRespond helpfully to the admin.",
|
||||
"tags": [
|
||||
["d", "chat"],
|
||||
["description", "Chat with admin"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"],
|
||||
["llm", "default"],
|
||||
["requires_skill", "identity"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `cron`
|
||||
|
||||
`filter` is a standard 5-field cron expression: `minute hour day-of-month month day-of-week`.
|
||||
Fires on a schedule. `filter` is a standard 5-field cron expression: `minute hour day-of-month month day-of-week`.
|
||||
|
||||
```json
|
||||
["trigger", "cron"],
|
||||
["filter", "*/5 * * * *"],
|
||||
["llm", "openai/gpt-4o-mini"],
|
||||
["max_tokens", "300"],
|
||||
["enabled", "true"]
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "{{identity}}\n\nCheck the readme at the configured URL. If changed, post it and DM admin.",
|
||||
"tags": [
|
||||
["d", "readme-monitor"],
|
||||
["description", "Check readme for changes at noon"],
|
||||
["trigger", "cron"],
|
||||
["filter", "0 12 * * *"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["max_tokens", "300"],
|
||||
["requires_tool", "http_fetch"],
|
||||
["requires_tool", "memory_read"],
|
||||
["requires_tool", "memory_write"],
|
||||
["requires_skill", "identity"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `nostr-subscription`
|
||||
|
||||
Fires when a Nostr event matches a subscription filter. `filter` is a JSON-encoded Nostr subscription filter.
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "{{identity}}\n\nWhen the triggering event mentions Bitcoin or Lightning, summarize and DM admin.",
|
||||
"tags": [
|
||||
["d", "mention-monitor"],
|
||||
["description", "Monitor mentions and summarize"],
|
||||
["trigger", "nostr-subscription"],
|
||||
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["temperature", "0"],
|
||||
["requires_tool", "nostr_query"],
|
||||
["requires_tool", "nostr_dm"],
|
||||
["requires_skill", "identity"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `webhook`
|
||||
|
||||
Fires when an HTTP request is received. `filter` can be `{}` (match all).
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "{{identity}}\n\nProcess the webhook payload and take appropriate action.",
|
||||
"tags": [
|
||||
["d", "webhook-handler"],
|
||||
["description", "Process incoming webhook"],
|
||||
["trigger", "webhook"],
|
||||
["filter", "{}"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `chain`
|
||||
|
||||
`filter` is the source skill `d` tag to chain from.
|
||||
Fires when another skill completes execution. `filter` is the source skill's `d` tag.
|
||||
|
||||
```json
|
||||
["trigger", "chain"],
|
||||
["filter", "source-skill-d-tag"],
|
||||
["llm", "default"],
|
||||
["enabled", "true"]
|
||||
{
|
||||
"kind": 31123,
|
||||
"content": "{{identity}}\n\nReview the output from the previous skill and DM admin a summary.",
|
||||
"tags": [
|
||||
["d", "readme-reviewer"],
|
||||
["description", "Review results from readme monitor"],
|
||||
["trigger", "chain"],
|
||||
["filter", "readme-monitor"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["requires_skill", "identity"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `dm`
|
||||
---
|
||||
|
||||
`filter` is JSON with sender scope:
|
||||
## Requirements Tags
|
||||
|
||||
- `{"from":"admin"}`
|
||||
- `{"from":"wot"}`
|
||||
- `{"from":"any"}`
|
||||
Skills declare what they need to run. Apps use these tags to determine which skills are compatible with their available capabilities.
|
||||
|
||||
| Tag | Description |
|
||||
| ------------------ | ---------------------------------------------------------------- |
|
||||
| `requires_tool` | A tool that must be available for this skill to function |
|
||||
| `requires_skill` | An adopted skill that must be present for `{{...}}` resolution |
|
||||
| `optional_tool` | A tool that enhances the skill but is not required |
|
||||
|
||||
```json
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"],
|
||||
["llm", "default"],
|
||||
["tools", "true"],
|
||||
["enabled", "true"]
|
||||
["requires_tool", "http_fetch"],
|
||||
["requires_tool", "memory_read"],
|
||||
["requires_tool", "memory_write"],
|
||||
["requires_tool", "nostr_post"],
|
||||
["requires_skill", "identity"],
|
||||
["optional_tool", "nostr_dm"]
|
||||
```
|
||||
|
||||
### How Apps Use Requirements
|
||||
|
||||
```
|
||||
App starts up
|
||||
│
|
||||
├─ Knows its available tools/capabilities
|
||||
│
|
||||
├─ Fetches user's adopted skills from 10123
|
||||
│
|
||||
├─ For each skill, checks requires_tool tags
|
||||
│ ├─ All required tools available? → skill is usable
|
||||
│ └─ Missing required tools? → skill is disabled
|
||||
│
|
||||
└─ Presents only usable skills to the user
|
||||
```
|
||||
|
||||
Tool names in requirements tags are **capability names**, not implementation names. `http_fetch` is a capability — a C binary implements it with libcurl, a browser implements it with fetch(), a mobile app implements it with its HTTP library. The capability is the same; the implementation varies.
|
||||
|
||||
---
|
||||
|
||||
## Private Skill Encoding (`31124`)
|
||||
@@ -214,10 +473,10 @@ Private skills use NIP-44 encryption on event `content`.
|
||||
|
||||
Rules for kind `31124`:
|
||||
|
||||
- Keep `d` tag exposed so the event stays addressable/replaceable.
|
||||
- Keep`d` tag exposed so the event stays addressable/replaceable.
|
||||
- Move non-`d` metadata into plaintext payload before encryption.
|
||||
- Encrypt full payload with NIP-44 and store ciphertext in event `content`.
|
||||
- On receive: resolve by `d`, decrypt `content`, then read content + private tags.
|
||||
- Encrypt full payload with NIP-44 and store ciphertext in event`content`.
|
||||
- On receive: resolve by`d`, decrypt`content`, then read content + private tags.
|
||||
|
||||
### Private Skill Event (on relay)
|
||||
|
||||
@@ -235,19 +494,17 @@ Rules for kind `31124`:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": {
|
||||
"description": "Monitor mentions and DM summaries",
|
||||
"template": "When {{triggering_event}} includes Bitcoin or Lightning, summarize and DM admin."
|
||||
},
|
||||
"content": "{{identity}}\n\nWhen {{triggering_event}} includes Bitcoin or Lightning, summarize and DM admin.",
|
||||
"private_tags": [
|
||||
["description", "Monitor mentions and DM summaries"],
|
||||
["scope", "private"],
|
||||
["trigger", "nostr-subscription"],
|
||||
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
|
||||
["llm", "openai/gpt-4o-mini, fast"],
|
||||
["llm", "openai/gpt-4o-mini, cheap"],
|
||||
["temperature", "0"],
|
||||
["seed", "42"],
|
||||
["tools", "nostr_query,nostr_dm"],
|
||||
["enabled", "true"]
|
||||
["requires_tool", "nostr_query"],
|
||||
["requires_tool", "nostr_dm"],
|
||||
["requires_skill", "identity"]
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -256,62 +513,78 @@ Rules for kind `31124`:
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Input as Message/Trigger
|
||||
participant Dispatch as Dispatcher
|
||||
participant Adopt as Adoption Resolver (10123)
|
||||
participant Ctx as Context Assembler
|
||||
participant Trig as Trigger Runtime Params
|
||||
participant LLM as LLM API
|
||||
|
||||
Input->>Dispatch: message or trigger event
|
||||
Dispatch->>Adopt: load adopted skills in list order
|
||||
Adopt-->>Ctx: ordered skill templates
|
||||
Ctx->>Ctx: resolve template variables via tools
|
||||
Dispatch->>Trig: resolve trigger execution tags
|
||||
Trig-->>LLM: model + max_tokens + temperature + seed + tool policy
|
||||
Ctx->>LLM: composed messages
|
||||
LLM-->>Input: response
|
||||
```
|
||||
Trigger event occurs (DM, cron, subscription, webhook, chain)
|
||||
│
|
||||
├─ Walk adoption list (10123)
|
||||
│ │
|
||||
│ ├─ For each skill whose trigger matches this event:
|
||||
│ │ ├─ Resolve template variables (tools + skill references)
|
||||
│ │ └─ Add to context (layer 1)
|
||||
│ │
|
||||
│ └─ Skills whose trigger does NOT match: skip
|
||||
│
|
||||
├─ Append triggering event payload
|
||||
│ └─ For DM triggers: always append raw message content
|
||||
│
|
||||
├─ Apply execution parameters (llm, temperature, max_tokens, tools)
|
||||
│
|
||||
└─ Send to LLM → multi-turn tool loop → response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limits and Safety
|
||||
|
||||
| Limit | Default | Description |
|
||||
|---|---|---|
|
||||
| Max concurrent triggers | 16 | Prevents resource exhaustion |
|
||||
| Trigger cooldown | 60s per skill | Prevents rapid-fire execution |
|
||||
| LLM action rate limit | 10/min | Prevents runaway LLM costs |
|
||||
| Limit | Default | Description |
|
||||
| ----------------------- | ------------- | ----------------------------- |
|
||||
| Max concurrent triggers | 16 | Prevents resource exhaustion |
|
||||
| Trigger cooldown | 60s per skill | Prevents rapid-fire execution |
|
||||
| LLM action rate limit | 10/min | Prevents runaway LLM costs |
|
||||
|
||||
---
|
||||
|
||||
## Storage on Nostr
|
||||
|
||||
| Data | Storage |
|
||||
|---|---|
|
||||
| Skills | Kind 31123/31124 events |
|
||||
| Adopted skills | Kind 10123 event |
|
||||
| Trigger definitions + execution params | Tags on skill events |
|
||||
| Data | Storage |
|
||||
| -------------------------------------- | ----------------------- |
|
||||
| Skills | Kind 31123/31124 events |
|
||||
| Adopted skills | Kind 10123 event |
|
||||
| Trigger definitions + execution params | Tags on skill events |
|
||||
| Requirements declarations | Tags on skill events |
|
||||
|
||||
---
|
||||
|
||||
## Portability Guidelines
|
||||
## Portability
|
||||
|
||||
To keep skills reusable across agents/clients:
|
||||
Skills are Nostr events. Any application that can read Nostr events and call a skill which will call an llm. Skills are not specific to Didactyl or any single runtime.
|
||||
|
||||
- Prefer generic instructions over implementation-specific assumptions.
|
||||
- Treat tool names as capabilities, not platform internals.
|
||||
- Resolve unknown variables safely (empty result, no hard failure).
|
||||
- Keep app-specific tags optional (`["app","didactyl"]`).
|
||||
### Use Cases Beyond Didactyl
|
||||
|
||||
A skill should still be useful even when some variables/tools are unavailable.
|
||||
- **Word processor** — "Check spelling and grammar" button triggers a spellcheck skill
|
||||
- **Browser extension** — Highlight text, run a summarization skill
|
||||
- **Mobile app** — Voice input triggers a transcription skill
|
||||
- **Browser-based agent** — Same agent, different runtime, different available tools
|
||||
|
||||
### Portability Guidelines
|
||||
|
||||
| Guideline | Rationale |
|
||||
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| Use `{{message}}` for user input | Universal — every app has user input |
|
||||
| Declare requirements via `requires_tool` / `requires_skill` tags | Lets apps filter to compatible skills |
|
||||
| Put default execution params as top-level tags | Any app can read `llm`, `temperature`, etc. |
|
||||
| Keep trigger tags as optional runtime hints | Apps without trigger systems ignore them |
|
||||
| Resolve unknown variables to empty | Ensures graceful degradation |
|
||||
| Prefer self-contained skills for maximum portability | Skills with `{{skill_d_tag}}` references need the adoption ecosystem |
|
||||
| Treat tool names as capabilities, not implementations | `http_fetch` works in C, browser, mobile — same capability, different implementation |
|
||||
|
||||
A skill should still be useful even when some variables, tools, or referenced skills are unavailable.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Tool architecture and complete tool catalog: [TOOLS.md](TOOLS.md)
|
||||
- Context assembly model: [CONTEXT.md](CONTEXT.md)
|
||||
- Project overview/runtime behavior: [README.md](../README.md)
|
||||
- Tool architecture and complete tool catalog:[TOOLS.md](TOOLS.md)
|
||||
- Context assembly model:[CONTEXT.md](CONTEXT.md)
|
||||
- Project overview/runtime behavior:[README.md](../README.md)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Didactyl — Tools
|
||||
|
||||
See also: [SKILLS.md](SKILLS.md)
|
||||
See also: [SKILLS.md](SKILLS.md) · [CONTEXT.md](CONTEXT.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**.
|
||||
Didactyl is a **Nostr-first sovereign AI agent** that 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.
|
||||
|
||||
@@ -18,32 +18,35 @@ A **skill** teaches the agent *how* to carve — the technique, the judgment, th
|
||||
|
||||
## 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
|
||||
1. A trigger fires (DM, cron, subscription, webhook, or chain) — see [SKILLS.md](SKILLS.md)
|
||||
2. The agent builds the context window from triggered skills and their `{{...}}` references
|
||||
3. The agent builds an LLM request with the context, triggering event, and a JSON schema of available tools
|
||||
4. The LLM decides whether to call a tool or respond directly
|
||||
5. If a tool is called, didactyl executes it and feeds the result back to the LLM
|
||||
6. The loop repeats until the LLM produces a final text response
|
||||
7. For DM triggers, 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
|
||||
```
|
||||
Trigger fires (DM, cron, subscription, webhook, chain)
|
||||
│
|
||||
├─ Build context from triggered skills
|
||||
├─ Build tool schemas (filtered by skill's tools tag)
|
||||
│
|
||||
├─ Send to LLM: context + tool schemas
|
||||
│
|
||||
│ ┌─────────────────────────────────────┐
|
||||
│ │ LLM reasons about the request │
|
||||
│ │ │
|
||||
│ │ Option A: call a tool │
|
||||
│ │ → agent executes tool │
|
||||
│ │ → feeds result back to LLM │
|
||||
│ │ → loop continues │
|
||||
│ │ │
|
||||
│ │ Option B: produce text response │
|
||||
│ │ → loop ends │
|
||||
│ └─────────────────────────────────────┘
|
||||
│
|
||||
└─ Deliver response (DM reply, chain forward, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -724,7 +727,9 @@ These examples show the JSON structure for tool calls.
|
||||
|
||||
## Security Model
|
||||
|
||||
Tool access is gated by sender tier:
|
||||
Tool access is gated at two levels:
|
||||
|
||||
### Sender Tier (DM triggers)
|
||||
|
||||
| Tier | Identity | Tools | Response |
|
||||
|------|----------|-------|----------|
|
||||
@@ -732,9 +737,42 @@ Tool access is gated by sender tier:
|
||||
| **WOT** | In admin's kind 3 contact list | None | Chat-only LLM |
|
||||
| **STRANGER** | Anyone else | None | Configurable static response |
|
||||
|
||||
### Skill Requirements (all triggers)
|
||||
|
||||
Skills declare which tools they need via `requires_tool` tags (see [SKILLS.md — Requirements Tags](SKILLS.md#requirements-tags)). During execution, only the required and optional tools declared by the skill are exposed to the LLM. If a skill has no `requires_tool` tags, all available tools are exposed.
|
||||
|
||||
---
|
||||
|
||||
## Tool Portability
|
||||
|
||||
Tool names serve as the **capability vocabulary** for cross-app skill portability. When a skill declares `["requires_tool", "http_fetch"]`, any app that provides an `http_fetch` capability can run that skill — regardless of how it implements the fetch internally.
|
||||
|
||||
### Runtime Availability
|
||||
|
||||
Not all tools are available in every runtime. Tools that require specific host capabilities:
|
||||
|
||||
| Tool | Requires |
|
||||
|------|----------|
|
||||
| `local_shell_exec` | Host OS shell access |
|
||||
| `local_file_read` | Host filesystem |
|
||||
| `local_file_write` | Host filesystem |
|
||||
| `blossom_upload` | Filesystem + HTTP |
|
||||
| `blossom_download` | Filesystem + HTTP |
|
||||
|
||||
Tools available in any runtime (including browser):
|
||||
|
||||
| Tool | Capability |
|
||||
|------|-----------|
|
||||
| `nostr_*` | Nostr relay WebSocket connections |
|
||||
| `local_http_fetch` | HTTP client |
|
||||
| `memory_save` / `memory_recall` | Nostr event storage |
|
||||
| `cashu_wallet_*` | HTTP client + Nostr storage |
|
||||
|
||||
Skills should declare `requires_tool` tags so apps can determine compatibility. See [SKILLS.md — Requirements Tags](SKILLS.md#requirements-tags).
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Skill definitions, adoption, triggers, and autonomous activation: [SKILLS.md](SKILLS.md)
|
||||
- Combined index page: [TOOLS_AND_SKILLS.md](TOOLS_AND_SKILLS.md)
|
||||
- Context assembly model: [CONTEXT.md](CONTEXT.md)
|
||||
|
||||
@@ -37,8 +37,22 @@
|
||||
},
|
||||
|
||||
// ─── Startup Events ────────────────────────────────────────────────
|
||||
// Minimal relay list required for relay pool initialization.
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl",
|
||||
"about": "I am a Didactyl agent living on Nostr"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["p", "ADMIN_HEX_PUBKEY"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
@@ -52,12 +66,15 @@
|
||||
// ─── Default Skill ────────────────────────────────────────────────
|
||||
// Keep this generic and non-sensitive for repository examples.
|
||||
"default_skill": {
|
||||
"d_tag": "didactyl-default",
|
||||
"d_tag": "default_admin_dm",
|
||||
"kind": 31124,
|
||||
"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 `local_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 tool: admin_identity\n skip_if_empty: true\n\n- section: admin_profile\n role: system\n tool: nostr_admin_profile\n skip_if_empty: true\n\n- section: admin_contacts\n role: system\n tool: nostr_admin_contacts\n skip_if_empty: true\n\n- section: admin_relays\n role: system\n tool: nostr_admin_relays\n skip_if_empty: true\n\n- section: admin_notes\n role: system\n tool: nostr_admin_notes\n skip_if_empty: true\n\n- section: agent_identity\n role: system\n tool: agent_identity\n skip_if_empty: true\n\n- section: agent_profile\n role: system\n tool: nostr_agent_profile\n skip_if_empty: true\n\n- section: agent_contacts\n role: system\n tool: nostr_agent_contacts\n skip_if_empty: true\n\n- section: agent_relays\n role: system\n tool: nostr_agent_relays\n skip_if_empty: true\n\n- section: agent_notes\n role: system\n tool: nostr_agent_notes\n skip_if_empty: true\n\n- section: tasks\n role: system\n tool: task_list\n skip_if_empty: true\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: conversation\n role: user\n tool: message_current\n skip_if_empty: true",
|
||||
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\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- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
|
||||
"tags": [
|
||||
["app", "didactyl"],
|
||||
["scope", "private"]
|
||||
["scope", "private"],
|
||||
["description", "Default admin DM handler"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,11 +299,29 @@ git_commit_and_push_no_tag() {
|
||||
|
||||
# Push changes
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
local current_branch
|
||||
current_branch=$(git branch --show-current 2>/dev/null || echo "")
|
||||
|
||||
if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" > /dev/null 2>&1; then
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
if [[ -z "$current_branch" ]]; then
|
||||
print_error "Unable to determine current branch for push"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_warning "No upstream configured for branch '$current_branch'; setting upstream to origin/$current_branch"
|
||||
if git push -u origin "$current_branch" > /dev/null 2>&1; then
|
||||
print_success "Pushed changes and configured upstream"
|
||||
else
|
||||
print_error "Failed to push changes while configuring upstream"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Push only the new tag to avoid conflicts with existing tags
|
||||
|
||||
206
plans/trigger_scoped_skill_architecture.md
Normal file
206
plans/trigger_scoped_skill_architecture.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Implementation Plan: Trigger-Scoped Skill Architecture
|
||||
|
||||
Target spec: [docs/SKILLS.md](../docs/SKILLS.md) · [docs/CONTEXT.md](../docs/CONTEXT.md) · [docs/TOOLS.md](../docs/TOOLS.md)
|
||||
|
||||
## Current State
|
||||
|
||||
- `g_system_context` is a monolithic global string (from the default skill) prepended to every LLM call
|
||||
- `agent_on_trigger()` prepends `g_system_context` to every triggered skill execution
|
||||
- `agent_on_message()` falls through to `g_system_context` chat if no DM trigger fires
|
||||
- `trigger_manager_fire_dm()` fires all matching DM triggers independently (separate LLM calls each)
|
||||
- `trigger_manager_load_from_skills()` loads triggers from adopted skills only
|
||||
- Template variables resolve to tools only — no skill-to-skill `{{d_tag}}` resolution
|
||||
- `apply_trigger_runtime_to_llm_config()` parses `provider/model` but discards the provider
|
||||
- Skill `content` is JSON with `description` and `template` fields (spec says content IS the template)
|
||||
- `["tools", "true/false/csv"]` tag controls tool access (spec says use `requires_tool` instead)
|
||||
- `["enabled", "true/false"]` tag on triggers (spec says adoption IS enablement)
|
||||
|
||||
## Phase 0: Provider Override Fix
|
||||
|
||||
**File:** `src/trigger_manager.c`
|
||||
**Function:** `apply_trigger_runtime_to_llm_config()` (line 592)
|
||||
|
||||
Currently at line 602-611, when a slash is found in the llm_spec, only the model (after slash) is extracted. The provider (before slash) is discarded.
|
||||
|
||||
**Change:** When slash is found, also copy the provider prefix into `cfg->provider`.
|
||||
|
||||
```c
|
||||
// Current: only extracts model
|
||||
if (slash) {
|
||||
const char* model = slash + 1;
|
||||
// ... sets cfg->model only
|
||||
}
|
||||
|
||||
// New: extract both provider and model
|
||||
if (slash) {
|
||||
size_t provider_len = (size_t)(slash - spec);
|
||||
if (provider_len > 0 && provider_len < sizeof(cfg->provider)) {
|
||||
snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec);
|
||||
}
|
||||
const char* model = slash + 1;
|
||||
// ... sets cfg->model as before
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 1: Two-Layer Context Assembly
|
||||
|
||||
### 1a. Skill-to-skill template resolution
|
||||
|
||||
**File:** `src/prompt_template.c`
|
||||
**Function:** `map_variable_tool_name()` (line 100)
|
||||
|
||||
Currently returns a tool name for known variables, or NULL for unknown ones (which resolve to empty).
|
||||
|
||||
**Change:** Add a fallback path. If the variable name doesn't match a known tool, look it up as a skill d-tag in the adoption list cache. If found, return the skill's content.
|
||||
|
||||
This requires access to the skill cache from the template resolver. Options:
|
||||
- Pass a skill lookup callback into the template builder
|
||||
- Add a `skill_content_lookup` function pointer to `tools_context_t`
|
||||
|
||||
**File:** `src/prompt_template.c`
|
||||
**Function:** `prompt_template_build_messages()` (line 37)
|
||||
|
||||
When resolving a `{{variable}}` that doesn't match a tool, call the skill lookup function to check adopted skills by d-tag.
|
||||
|
||||
### 1b. Trigger-matched context assembly
|
||||
|
||||
**File:** `src/agent.c`
|
||||
**New function:** `build_context_from_triggers()`
|
||||
|
||||
```c
|
||||
char* build_context_from_triggers(
|
||||
trigger_type_t trigger_type,
|
||||
const char* trigger_filter,
|
||||
cJSON* trigger_event,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
Implementation:
|
||||
1. Load the adoption list (kind `10123`) — already cached at startup
|
||||
2. For each adopted skill, check if it has a trigger matching `trigger_type` and `trigger_filter`
|
||||
3. If match: resolve the skill's template (expanding `{{...}}` references via Phase 1a)
|
||||
4. Concatenate all matched skill templates in adoption-list order
|
||||
5. Append the triggering event payload
|
||||
6. For DM triggers: always append raw message content at the end
|
||||
7. Return the assembled system prompt
|
||||
|
||||
### 1c. Replace g_system_context in trigger execution
|
||||
|
||||
**File:** `src/agent.c`
|
||||
**Function:** `agent_on_trigger()` (line 1817)
|
||||
|
||||
**Current (line 1852-1858):**
|
||||
```c
|
||||
snprintf(system_prompt, system_len, "%s\n\n%s%s\nRelay: %s\n\nSkill instructions:\n%s",
|
||||
g_system_context,
|
||||
trigger_prefix, skill_d_tag, relay, skill_content);
|
||||
```
|
||||
|
||||
**Replace with:** Call `build_context_from_triggers()` instead of prepending `g_system_context`.
|
||||
|
||||
### 1d. Skill content format change
|
||||
|
||||
**File:** `src/nostr_handler.c` and `src/config.c`
|
||||
|
||||
Currently skill content is parsed as JSON to extract `template` field. The spec says `content` IS the template (plain string).
|
||||
|
||||
**Change:** When loading a skill's content, check if it's a JSON object with a `template` field (backward compat) or a plain string (new format). Use the template/string directly.
|
||||
|
||||
## Phase 2: DM Composition with Default Handler
|
||||
|
||||
**File:** `src/agent.c`
|
||||
**Function:** `agent_on_message()` (line 2085)
|
||||
|
||||
Currently at line 2120-2128, calls `trigger_manager_fire_dm()` which fires each matching DM trigger independently. If none fire, falls through to `g_system_context` chat.
|
||||
|
||||
**Change:**
|
||||
1. Use `build_context_from_triggers()` with `TRIGGER_TYPE_DM` and the sender tier
|
||||
2. This automatically finds all DM-triggered skills in adoption-list order and composes them
|
||||
3. Always append the raw admin message at the end (default DM handler)
|
||||
4. If no DM-triggered skill exists, use a minimal built-in default: "You are an AI agent. Respond to the message."
|
||||
5. Make one LLM call with the composed context
|
||||
|
||||
**File:** `src/trigger_manager.c`
|
||||
**Function:** `trigger_manager_fire_dm()` (line 1545)
|
||||
|
||||
Currently fires all matching DM triggers independently (each gets its own LLM call).
|
||||
|
||||
**Change:** Instead of firing independently, return the list of matching DM skill d-tags (in adoption-list order). Let the caller (`agent_on_message`) compose them into a single context via `build_context_from_triggers()`.
|
||||
|
||||
New function signature:
|
||||
```c
|
||||
int trigger_manager_get_dm_skills(trigger_manager_t* mgr,
|
||||
const char* sender_pubkey_hex,
|
||||
didactyl_sender_tier_t tier,
|
||||
char** out_d_tags,
|
||||
int max_d_tags);
|
||||
```
|
||||
|
||||
## Phase 3: Trigger Discovery Independent of Adoption
|
||||
|
||||
**File:** `src/trigger_manager.c`
|
||||
**Function:** `trigger_manager_load_from_skills()` (line ~varies)
|
||||
|
||||
Currently loads triggers only from adopted skills in the `10123` list.
|
||||
|
||||
**Change:** Scan all skill events published by the agent (query own pubkey for kinds 31123/31124), not just adopted ones. Arm any skill with trigger tags. The adoption list controls `{{...}}` resolution and ordering, not trigger arming.
|
||||
|
||||
Non-adopted skills with triggers fire in isolation (no layer 2 skill references available).
|
||||
|
||||
## Phase 4: Context Compaction
|
||||
|
||||
**File:** `src/agent.c`
|
||||
**Functions:** Tool loops in `agent_on_trigger()` (line 1905) and `agent_on_message()` (line 2212)
|
||||
|
||||
**New function:**
|
||||
```c
|
||||
static int estimate_context_tokens(cJSON* messages);
|
||||
```
|
||||
|
||||
Approximate: sum character lengths of all message content fields, divide by 4.
|
||||
|
||||
**Change in tool loops:** Before each LLM call, check if `estimate_context_tokens(messages)` exceeds 70% of the model's context window. If so:
|
||||
|
||||
1. Build a summarization request: "Summarize your progress so far, including key findings and remaining work."
|
||||
2. Send to LLM with `tool_choice: "none"` (text only)
|
||||
3. Replace all tool call/result messages with a single system message containing the summary
|
||||
4. Continue the tool loop with the compacted context
|
||||
|
||||
## Phase 5: LLM Fallback Chain
|
||||
|
||||
**File:** `src/trigger_manager.c`
|
||||
**Function:** `apply_trigger_runtime_to_llm_config()` (line 592)
|
||||
|
||||
Currently parses the comma in `llm_spec` but only uses the first entry.
|
||||
|
||||
**Change:** Walk the comma-separated entries. For each:
|
||||
1. Parse `provider/model` or bare `model` or capability keyword
|
||||
2. Check if the model is available (query provider, or check a local model list)
|
||||
3. If available, use it and stop
|
||||
4. If not, try the next entry
|
||||
5. Capability keywords (`cheap`, `fast`, `best`, `default`) resolve to runtime-configured models
|
||||
|
||||
## Phase 6: Remove Obsolete Code
|
||||
|
||||
**File:** `src/agent.c`
|
||||
- Remove `g_system_context` global variable
|
||||
- Remove `g_system_context` from `agent_init()` parameter
|
||||
- Remove `g_system_context` prepend from all code paths
|
||||
|
||||
**File:** `src/nostr_handler.c`
|
||||
- Remove `g_system_context` global variable (line 28)
|
||||
- Remove `g_system_context` initialization in `nostr_handler_reconcile_startup_events()` (line 3124-3157)
|
||||
- Remove `nostr_handler_get_system_context()` function
|
||||
|
||||
**File:** `src/agent.h`
|
||||
- Update `agent_init()` signature to remove `system_context` parameter
|
||||
|
||||
## Migration Path
|
||||
|
||||
1. **Phase 0** — safe, independent fix. No behavioral change.
|
||||
2. **Phase 1** — core change. Backward compatible if existing default skill has a DM trigger tag. The `g_system_context` is still used as fallback until Phase 6.
|
||||
3. **Phase 2** — changes DM behavior. Existing agents work if default skill has `["trigger", "dm"]`.
|
||||
4. **Phase 3** — additive. Skills without triggers are unaffected.
|
||||
5. **Phase 4** — additive. New capability, no existing behavior changes.
|
||||
6. **Phase 5** — additive. Currently only first entry is used; this adds fallback.
|
||||
7. **Phase 6** — cleanup. Only after Phases 1-2 are stable.
|
||||
246
plans/v0.2.0_migration.md
Normal file
246
plans/v0.2.0_migration.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# v0.2.0 Migration Plan
|
||||
|
||||
## Agent Event Cleanup
|
||||
|
||||
Delete all old skill/soul events from each agent's relay set before restarting with the new genesis file.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Genesis File Changes
|
||||
|
||||
### Kind 0 profile event (new)
|
||||
|
||||
Add a startup event for the agent's profile:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl",
|
||||
"about": "I am a Didactyl agent living on Nostr"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
```
|
||||
|
||||
### Kind 3 contacts event (new)
|
||||
|
||||
Add a startup event following the admin:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["p", "ADMIN_HEX_PUBKEY"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The admin hex pubkey is derived from the `admin.pubkey` field at startup.
|
||||
|
||||
### Default skill rename and trigger tags
|
||||
|
||||
**Before:**
|
||||
```jsonc
|
||||
"default_skill": {
|
||||
"d_tag": "didactyl-default",
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\n...\n\n---template---\n\n- section: admin_identity\n ...",
|
||||
"tags": [["app", "didactyl"], ["scope", "private"]]
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```jsonc
|
||||
"default_skill": {
|
||||
"d_tag": "default_admin_dm",
|
||||
"kind": 31124,
|
||||
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\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- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
|
||||
"tags": [
|
||||
["app", "didactyl"],
|
||||
["scope", "private"],
|
||||
["description", "Default admin DM handler"],
|
||||
["trigger", "dm"],
|
||||
["filter", "{\"from\":\"admin\"}"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `d_tag` renamed to `default_admin_dm`
|
||||
- `---template---` DSL removed entirely
|
||||
- Content uses `{{my_kind0_profile}}` and `{{my_npub}}` for identity injection
|
||||
- Trigger/filter tags added so the new trigger-scoped system fires it on admin DMs
|
||||
- Description tag added
|
||||
|
||||
---
|
||||
|
||||
## Source Code Changes
|
||||
|
||||
### Phase 1: Remove `---template---` DSL
|
||||
|
||||
#### `src/prompt_template.h`
|
||||
|
||||
- Remove `PROMPT_TEMPLATE_MARKER`, `PROMPT_TEMPLATE_MAX_SECTIONS`, `PROMPT_TEMPLATE_MAX_NAME_LEN`, `PROMPT_TEMPLATE_MAX_ROLE_LEN`
|
||||
- Remove `prompt_template_section_t` struct
|
||||
- Remove `prompt_template_t` struct
|
||||
- Remove `prompt_template_emit_hook_fn` typedef
|
||||
- Remove `prompt_template_parse()` declaration
|
||||
- Remove `prompt_template_build_messages()` declaration
|
||||
- Remove `prompt_template_free()` declaration
|
||||
- Keep the file if `resolve_inline_variables_local()` needs a public header, otherwise fold into agent.c
|
||||
|
||||
#### `src/prompt_template.c`
|
||||
|
||||
- Remove `prompt_template_parse()` and all section-parsing code (lines 231-502)
|
||||
- Remove `prompt_template_build_messages()` (lines 551-639)
|
||||
- Remove `prompt_template_free()` (lines 641-667)
|
||||
- Remove section-related helpers: `init_section_defaults()`, `parse_int_or_zero()`, `extract_tool_result_content()`, `append_message_object()`
|
||||
- Keep `resolve_inline_variables_local()` — it handles `{{...}}` variable resolution used by `build_context_from_triggers()`
|
||||
- Keep `map_variable_tool_name()` — maps variable names to tool names
|
||||
- Keep string helpers: `dup_range()`, `ltrim_inplace()`, `rtrim_inplace()`, `append_text()`
|
||||
|
||||
#### `src/agent.c`
|
||||
|
||||
- Remove `g_prompt_template` static variable (line 28)
|
||||
- Remove `g_has_prompt_template` static variable (line 29)
|
||||
- Remove `template_emit_hook()` callback
|
||||
- Remove the `prompt_template_parse()` call in `agent_init()` (line 2127-2128)
|
||||
- Remove the `prompt_template_free()` calls in `agent_init()` and `agent_cleanup()`
|
||||
- Remove the `g_has_prompt_template` branch in `agent_build_admin_messages_json()` (lines 2334-2035) — replace with trigger-composed context path
|
||||
|
||||
### Phase 2: Remove `g_system_context`
|
||||
|
||||
#### `src/agent.c`
|
||||
|
||||
- Remove `g_system_context` global (line 24)
|
||||
- Remove `system_context` parameter from `agent_init()`
|
||||
- Remove `strdup(system_context)` and all `g_system_context` references
|
||||
- Remove `g_system_context` null checks from `agent_on_trigger()`, `agent_on_message()`, `agent_build_admin_messages_json()`
|
||||
- In `agent_cleanup()`: remove `free(g_system_context)`
|
||||
|
||||
#### `src/agent.h`
|
||||
|
||||
- Update `agent_init()` signature: `int agent_init(didactyl_config_t* config);`
|
||||
|
||||
#### `src/nostr_handler.c`
|
||||
|
||||
- Remove `g_system_context` global (line 28)
|
||||
- Remove `g_system_context` initialization in `nostr_handler_reconcile_startup_events()` (lines 3124-3157)
|
||||
- Remove `nostr_handler_get_system_context()` function (lines 3289-3291)
|
||||
- Remove `seed_default_skill_into_cache()` if it only serves `g_system_context`
|
||||
|
||||
#### `src/nostr_handler.h`
|
||||
|
||||
- Remove `nostr_handler_get_system_context()` declaration
|
||||
|
||||
#### `src/main.c`
|
||||
|
||||
- Remove `system_context` variable (line 1261)
|
||||
- Remove fallback logic (lines 1262-1270)
|
||||
- Call `agent_init(&cfg)` without system_context parameter
|
||||
|
||||
### Phase 3: Rename default skill constants
|
||||
|
||||
#### `src/default_events.h`
|
||||
|
||||
- `DIDACTYL_DEFAULT_SKILL_D_TAG`: `"didactyl-default"` → `"default_admin_dm"`
|
||||
- `DIDACTYL_DEFAULT_SKILL_TAGS_JSON`: add trigger/filter/description tags
|
||||
- `DIDACTYL_DEFAULT_SKILL_TEMPLATE`: remove `---template---` section, replace with lean markdown using `{{my_kind0_profile}}` and `{{my_npub}}`
|
||||
|
||||
### Phase 4: Update `agent_build_admin_messages_json()`
|
||||
|
||||
This function is used by the HTTP API. Currently has two paths (template DSL and legacy hardcoded). Replace both with a single path using `build_context_from_triggers()` — same as `agent_on_message()` now uses.
|
||||
|
||||
### Phase 5: Delete legacy files
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `config.jsonc.example` | Delete |
|
||||
| `context_template.md` | Delete |
|
||||
| `context_template.recovered.md` | Delete |
|
||||
|
||||
### Phase 6: Update genesis files
|
||||
|
||||
#### `genesis.jsonc`
|
||||
|
||||
- Add kind 0 startup event with name and about
|
||||
- Rename `default_skill.d_tag` to `default_admin_dm`
|
||||
- Add trigger/filter/description tags to `default_skill.tags`
|
||||
- Replace content with lean markdown (no `---template---`)
|
||||
|
||||
#### `genesis.jsonc.example`
|
||||
|
||||
- Same changes as genesis.jsonc
|
||||
- Add kind 0 and kind 3 startup events
|
||||
|
||||
### Phase 7: Update documentation
|
||||
|
||||
#### `README.md`
|
||||
|
||||
- Remove kind 31120 from Didactyl Kinds table
|
||||
- Remove soul references
|
||||
- Update architecture description
|
||||
|
||||
#### `docs/GENESIS.md`
|
||||
|
||||
- Remove config.jsonc migration references
|
||||
- Update default_skill section to show `default_admin_dm`
|
||||
- Document kind 0 and kind 3 startup events
|
||||
|
||||
#### `docs/API.md`
|
||||
|
||||
- Remove `/api/events/soul` endpoint
|
||||
- Remove config.jsonc references
|
||||
|
||||
#### `docs/SKILLS.md`, `docs/CONTEXT.md`, `docs/TOOLS.md`
|
||||
|
||||
- Verify no soul/31120/config.jsonc references remain
|
||||
|
||||
---
|
||||
|
||||
## Variable Resolution for Default Skill
|
||||
|
||||
The default admin DM skill content uses:
|
||||
- `{{my_npub}}` — resolves via `map_variable_tool_name()` → `nostr_npub` tool → returns agent npub
|
||||
- `{{my_kind0_profile}}` — resolves via `map_variable_tool_name()` → `my_kind0_profile` tool → returns agent kind 0 profile context
|
||||
|
||||
These already work in `resolve_inline_variables_local()` because `map_variable_tool_name()` returns `NULL` for unknown names, and the skill lookup callback handles the rest. But `my_npub` and `my_kind0_profile` are not in the current `map_variable_tool_name()` mapping — they need to be added, OR the function needs to pass through unknown names as tool names (which it currently does NOT do after the Phase 1a change that made it return `NULL` for unknowns).
|
||||
|
||||
**Fix needed:** In `map_variable_tool_name()`, return the variable name itself as a tool name when it doesn't match a known alias. This way `{{my_npub}}` calls the `my_npub` tool directly.
|
||||
|
||||
Current (after Phase 1a change):
|
||||
```c
|
||||
return NULL; // unknown → try skill lookup
|
||||
```
|
||||
|
||||
Should be:
|
||||
```c
|
||||
return var_name; // unknown → try as tool name first, then skill lookup
|
||||
```
|
||||
|
||||
Wait — this was the ORIGINAL behavior before Phase 1a changed it. The Phase 1a change made `map_variable_tool_name()` return `NULL` for unknowns so the skill lookup callback would fire. But now we need BOTH: try as tool name first, then skill lookup.
|
||||
|
||||
**Resolution:** Restore `return var_name;` in `map_variable_tool_name()`. The skill lookup callback in `resolve_inline_variables_local()` already only fires when `replacement_owned` is NULL after the tool attempt. So the flow is:
|
||||
1. Check known aliases (admin_profile → nostr_admin_profile, etc.)
|
||||
2. Try the variable name as a direct tool name
|
||||
3. If tool returns nothing useful, try skill lookup callback
|
||||
4. If skill lookup returns nothing, resolve to empty string
|
||||
|
||||
This is exactly the resolution order specified in SKILLS.md.
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Phase 3 — rename constants (safe, no behavioral change)
|
||||
2. Phase 5 — delete legacy files
|
||||
3. Phase 6 — update genesis files
|
||||
4. Phase 1 — remove template DSL
|
||||
5. Phase 2 — remove g_system_context
|
||||
6. Phase 4 — update agent_build_admin_messages_json
|
||||
7. Phase 7 — update docs
|
||||
8. Build and test
|
||||
9. `increment_and_push.sh -p` for v0.2.1
|
||||
542
src/agent.c
542
src/agent.c
@@ -15,18 +15,14 @@
|
||||
#include "nostr_handler.h"
|
||||
#include "trigger_manager.h"
|
||||
#include "tools/tools.h"
|
||||
#include "prompt_template.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include "debug.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
static didactyl_config_t* g_cfg = NULL;
|
||||
static char* g_system_context = NULL;
|
||||
static tools_context_t g_tools_ctx;
|
||||
static struct trigger_manager* g_trigger_manager = NULL;
|
||||
|
||||
static prompt_template_t g_prompt_template;
|
||||
static int g_has_prompt_template = 0;
|
||||
#define AGENT_CONTEXT_PART_NAMES_MAX 512
|
||||
static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX];
|
||||
static int g_context_part_names_count = 0;
|
||||
@@ -51,6 +47,9 @@ typedef struct {
|
||||
char author_pubkey_hex[65];
|
||||
char d_tag[65];
|
||||
char* content;
|
||||
int has_trigger;
|
||||
trigger_type_t trigger_type;
|
||||
char filter_json[TRIGGER_FILTER_JSON_MAX];
|
||||
} agent_adopted_skill_t;
|
||||
|
||||
static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE];
|
||||
@@ -63,6 +62,8 @@ 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;
|
||||
|
||||
static int refresh_adopted_skills_cache_if_needed(void);
|
||||
|
||||
static uint64_t fnv1a64(const char* s) {
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
if (!s) return h;
|
||||
@@ -1125,7 +1126,7 @@ static const char* get_context_part_name_copy(int idx) {
|
||||
return tls_name[0] ? tls_name : NULL;
|
||||
}
|
||||
|
||||
static void template_emit_hook(const char* section_name, int message_index, void* user_data) {
|
||||
static __attribute__((unused)) void template_emit_hook(const char* section_name, int message_index, void* user_data) {
|
||||
(void)user_data;
|
||||
set_context_part_name(message_index, section_name ? section_name : "context_part");
|
||||
}
|
||||
@@ -1249,7 +1250,7 @@ void agent_append_context_log(const char* sender_pubkey_hex, const char* phase,
|
||||
}
|
||||
|
||||
|
||||
static char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) {
|
||||
static __attribute__((unused)) char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) {
|
||||
if (sender_tier == DIDACTYL_SENDER_ADMIN) {
|
||||
return strdup("This message has been cryptographically verified as coming from your administrator.");
|
||||
}
|
||||
@@ -1392,6 +1393,305 @@ static int parse_skill_address_tag_local(const char* addr, int* out_kind, char o
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dm_filter_matches_tier_local(const char* filter_json, didactyl_sender_tier_t tier) {
|
||||
if (!filter_json || filter_json[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(filter_json);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* from = cJSON_GetObjectItemCaseSensitive(root, "from");
|
||||
const char* from_s = (from && cJSON_IsString(from) && from->valuestring) ? from->valuestring : "admin";
|
||||
|
||||
int match = 0;
|
||||
if (strcmp(from_s, "any") == 0) {
|
||||
match = 1;
|
||||
} else if (strcmp(from_s, "admin") == 0) {
|
||||
match = (tier == DIDACTYL_SENDER_ADMIN);
|
||||
} else if (strcmp(from_s, "wot") == 0) {
|
||||
match = (tier == DIDACTYL_SENDER_WOT);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return match;
|
||||
}
|
||||
|
||||
static const char* adopted_skill_content_lookup_by_d_tag_locked(const char* d_tag) {
|
||||
if (!d_tag || d_tag[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_adopted_skills_count; i++) {
|
||||
if (strcmp(g_adopted_skills[i].d_tag, d_tag) == 0) {
|
||||
return g_adopted_skills[i].content;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char* template_skill_lookup_callback(void* user_data, const char* d_tag) {
|
||||
(void)user_data;
|
||||
if (!d_tag || d_tag[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
(void)refresh_adopted_skills_cache_if_needed();
|
||||
|
||||
const char* out = NULL;
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
out = adopted_skill_content_lookup_by_d_tag_locked(d_tag);
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* resolve_skill_references_local(const char* input) {
|
||||
if (!input) {
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
size_t cap = strlen(input) + 128U;
|
||||
size_t used = 0U;
|
||||
char* out = (char*)malloc(cap);
|
||||
if (!out) return NULL;
|
||||
out[0] = '\0';
|
||||
|
||||
const char* p = input;
|
||||
while (*p) {
|
||||
const char* open = strstr(p, "{{");
|
||||
if (!open) {
|
||||
size_t tail = strlen(p);
|
||||
if (used + tail + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + tail + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, p, tail);
|
||||
used += tail;
|
||||
out[used] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
size_t prefix_len = (size_t)(open - p);
|
||||
if (used + prefix_len + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + prefix_len + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, p, prefix_len);
|
||||
used += prefix_len;
|
||||
out[used] = '\0';
|
||||
|
||||
const char* close = strstr(open + 2, "}}");
|
||||
if (!close) {
|
||||
size_t rem = strlen(open);
|
||||
if (used + rem + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + rem + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, open, rem);
|
||||
used += rem;
|
||||
out[used] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
size_t var_len = (size_t)(close - (open + 2));
|
||||
char var[128];
|
||||
if (var_len >= sizeof(var)) var_len = sizeof(var) - 1U;
|
||||
memcpy(var, open + 2, var_len);
|
||||
var[var_len] = '\0';
|
||||
|
||||
char* start = var;
|
||||
while (*start && isspace((unsigned char)*start)) start++;
|
||||
char* end = start + strlen(start);
|
||||
while (end > start && isspace((unsigned char)end[-1])) {
|
||||
end[-1] = '\0';
|
||||
end--;
|
||||
}
|
||||
|
||||
const char* repl = "";
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
const char* found = adopted_skill_content_lookup_by_d_tag_locked(start);
|
||||
repl = found ? found : "";
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
size_t repl_len = strlen(repl);
|
||||
if (used + repl_len + 1U > cap) {
|
||||
size_t next = cap;
|
||||
while (used + repl_len + 1U > next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, repl, repl_len);
|
||||
used += repl_len;
|
||||
out[used] = '\0';
|
||||
|
||||
p = close + 2;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* build_context_from_triggers(trigger_type_t trigger_type,
|
||||
const char* trigger_filter,
|
||||
cJSON* trigger_event,
|
||||
const char* relay_url) {
|
||||
(void)relay_url;
|
||||
(void)trigger_filter;
|
||||
|
||||
if (!g_cfg) {
|
||||
return strdup("You are an AI agent. Respond to the message.");
|
||||
}
|
||||
|
||||
(void)refresh_adopted_skills_cache_if_needed();
|
||||
|
||||
size_t cap = 4096;
|
||||
size_t used = 0;
|
||||
char* out = (char*)malloc(cap);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
int matched = 0;
|
||||
|
||||
didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER;
|
||||
const char* message = NULL;
|
||||
if (trigger_event && cJSON_IsObject(trigger_event)) {
|
||||
cJSON* tier_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "tier");
|
||||
cJSON* msg_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "message");
|
||||
const char* tier_s = (tier_j && cJSON_IsString(tier_j) && tier_j->valuestring) ? tier_j->valuestring : "stranger";
|
||||
if (strcmp(tier_s, "admin") == 0) tier = DIDACTYL_SENDER_ADMIN;
|
||||
else if (strcmp(tier_s, "wot") == 0) tier = DIDACTYL_SENDER_WOT;
|
||||
if (msg_j && cJSON_IsString(msg_j) && msg_j->valuestring) {
|
||||
message = msg_j->valuestring;
|
||||
}
|
||||
}
|
||||
|
||||
char* matched_skill_contents[AGENT_ADOPTED_SKILLS_MAX];
|
||||
int matched_skill_count = 0;
|
||||
memset(matched_skill_contents, 0, sizeof(matched_skill_contents));
|
||||
|
||||
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||
for (int i = 0; i < g_adopted_skills_count && matched_skill_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
|
||||
const agent_adopted_skill_t* s = &g_adopted_skills[i];
|
||||
if (!s->has_trigger || s->trigger_type != trigger_type) {
|
||||
continue;
|
||||
}
|
||||
if (trigger_type == TRIGGER_TYPE_DM && !dm_filter_matches_tier_local(s->filter_json, tier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matched_skill_contents[matched_skill_count] = strdup(s->content ? s->content : "");
|
||||
if (matched_skill_contents[matched_skill_count]) {
|
||||
matched_skill_count++;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
for (int i = 0; i < matched_skill_count; i++) {
|
||||
char* expanded = resolve_skill_references_local(matched_skill_contents[i] ? matched_skill_contents[i] : "");
|
||||
const char* skill_text = expanded ? expanded : (matched_skill_contents[i] ? matched_skill_contents[i] : "");
|
||||
|
||||
size_t need = strlen("\n\n---\n\n") + strlen(skill_text) + 1U;
|
||||
if (used + need >= cap) {
|
||||
size_t next = cap;
|
||||
while (used + need >= next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(expanded);
|
||||
for (int j = i; j < matched_skill_count; j++) {
|
||||
free(matched_skill_contents[j]);
|
||||
}
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
|
||||
if (matched > 0) {
|
||||
memcpy(out + used, "\n\n---\n\n", 7);
|
||||
used += 7;
|
||||
}
|
||||
size_t sl = strlen(skill_text);
|
||||
memcpy(out + used, skill_text, sl);
|
||||
used += sl;
|
||||
out[used] = '\0';
|
||||
matched++;
|
||||
|
||||
free(expanded);
|
||||
free(matched_skill_contents[i]);
|
||||
}
|
||||
|
||||
if (matched == 0) {
|
||||
const char* fallback = "You are an AI agent. Respond to the message.";
|
||||
size_t need = strlen(fallback) + 1U;
|
||||
if (need > cap) {
|
||||
char* grown = (char*)realloc(out, need);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = need;
|
||||
}
|
||||
memcpy(out, fallback, need);
|
||||
used = need - 1U;
|
||||
}
|
||||
|
||||
if (trigger_type == TRIGGER_TYPE_DM && message && message[0] != '\0') {
|
||||
const char* suffix = "\n\nIncoming DM message:\n";
|
||||
size_t need = strlen(suffix) + strlen(message) + 1U;
|
||||
if (used + need >= cap) {
|
||||
size_t next = cap;
|
||||
while (used + need >= next) next *= 2U;
|
||||
char* grown = (char*)realloc(out, next);
|
||||
if (!grown) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = grown;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + used, suffix, strlen(suffix));
|
||||
used += strlen(suffix);
|
||||
memcpy(out + used, message, strlen(message));
|
||||
used += strlen(message);
|
||||
out[used] = '\0';
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static void clear_adopted_skills_cache_locked(void) {
|
||||
for (int i = 0; i < g_adopted_skills_count; i++) {
|
||||
free(g_adopted_skills[i].content);
|
||||
@@ -1399,6 +1699,9 @@ static void clear_adopted_skills_cache_locked(void) {
|
||||
g_adopted_skills[i].kind = 0;
|
||||
g_adopted_skills[i].author_pubkey_hex[0] = '\0';
|
||||
g_adopted_skills[i].d_tag[0] = '\0';
|
||||
g_adopted_skills[i].has_trigger = 0;
|
||||
g_adopted_skills[i].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
||||
g_adopted_skills[i].filter_json[0] = '\0';
|
||||
}
|
||||
g_adopted_skills_count = 0;
|
||||
}
|
||||
@@ -1538,6 +1841,17 @@ static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
snprintf(tmp[tmp_count].author_pubkey_hex, sizeof(tmp[tmp_count].author_pubkey_hex), "%s", skill_author);
|
||||
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", skill_d_tag);
|
||||
tmp[tmp_count].content = strdup(content->valuestring);
|
||||
tmp[tmp_count].has_trigger = 0;
|
||||
tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
||||
tmp[tmp_count].filter_json[0] = '\0';
|
||||
cJSON* trigger_tag = find_tag_value_string_local(skill_tags, "trigger");
|
||||
cJSON* filter_tag = find_tag_value_string_local(skill_tags, "filter");
|
||||
if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' &&
|
||||
filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') {
|
||||
tmp[tmp_count].has_trigger = 1;
|
||||
tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring);
|
||||
snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring);
|
||||
}
|
||||
if (tmp[tmp_count].content) {
|
||||
tmp_count++;
|
||||
}
|
||||
@@ -1578,6 +1892,21 @@ static int refresh_adopted_skills_cache_if_needed(void) {
|
||||
: "unknown");
|
||||
snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", d_tag_val);
|
||||
tmp[tmp_count].content = strdup(se->content);
|
||||
tmp[tmp_count].has_trigger = 0;
|
||||
tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
|
||||
tmp[tmp_count].filter_json[0] = '\0';
|
||||
if (se->tags_json && se->tags_json[0] != '\0') {
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
cJSON* trigger_tag = tags ? find_tag_value_string_local(tags, "trigger") : NULL;
|
||||
cJSON* filter_tag = tags ? find_tag_value_string_local(tags, "filter") : NULL;
|
||||
if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' &&
|
||||
filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') {
|
||||
tmp[tmp_count].has_trigger = 1;
|
||||
tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring);
|
||||
snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring);
|
||||
}
|
||||
cJSON_Delete(tags);
|
||||
}
|
||||
if (tmp[tmp_count].content) {
|
||||
tmp_count++;
|
||||
}
|
||||
@@ -1653,7 +1982,7 @@ static char* flatten_skill_content_to_text(const char* skill_content) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static int append_adopted_skills_context(cJSON* messages) {
|
||||
static __attribute__((unused)) int append_adopted_skills_context(cJSON* messages) {
|
||||
if (!messages || !g_cfg) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1763,7 +2092,7 @@ static int append_adopted_skills_context(cJSON* messages) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
static cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
|
||||
static __attribute__((unused)) cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages) {
|
||||
return NULL;
|
||||
@@ -1775,20 +2104,14 @@ static cJSON* build_recent_admin_dm_history_messages(const char* current_message
|
||||
return messages;
|
||||
}
|
||||
|
||||
int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
if (!config || !system_context) {
|
||||
int agent_init(didactyl_config_t* config) {
|
||||
if (!config) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_cfg = config;
|
||||
g_system_context = strdup(system_context);
|
||||
if (!g_system_context) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
|
||||
free(g_system_context);
|
||||
g_system_context = NULL;
|
||||
g_cfg = NULL;
|
||||
return -1;
|
||||
}
|
||||
@@ -1802,8 +2125,9 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
g_adopted_skills_last_refresh_at = 0;
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
prompt_template_free(&g_prompt_template);
|
||||
g_has_prompt_template = (prompt_template_parse(g_system_context, &g_prompt_template) == 0);
|
||||
g_tools_ctx.template_skill_lookup = template_skill_lookup_callback;
|
||||
g_tools_ctx.template_skill_lookup_user_data = NULL;
|
||||
|
||||
clear_context_part_names();
|
||||
|
||||
return 0;
|
||||
@@ -1818,7 +2142,7 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
const char* skill_content,
|
||||
cJSON* triggering_event,
|
||||
const char* relay_url) {
|
||||
if (!g_cfg || !g_system_context || !skill_d_tag || !skill_content || !triggering_event) {
|
||||
if (!g_cfg || !skill_d_tag || !skill_content || !triggering_event) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1840,11 +2164,20 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
"- If no action is needed, explicitly say why.\n\n"
|
||||
"Skill d_tag: ";
|
||||
|
||||
size_t system_len = strlen(g_system_context) + 2 + strlen(trigger_prefix) + strlen(skill_d_tag) +
|
||||
char* composed_context = build_context_from_triggers(TRIGGER_TYPE_NOSTR_SUBSCRIPTION,
|
||||
NULL,
|
||||
triggering_event,
|
||||
relay_url);
|
||||
if (!composed_context) {
|
||||
composed_context = strdup("You are an AI agent. Respond to the trigger event.");
|
||||
}
|
||||
|
||||
size_t system_len = strlen(composed_context ? composed_context : "") + 2 + strlen(trigger_prefix) + strlen(skill_d_tag) +
|
||||
strlen("\nRelay: ") + strlen(relay) + strlen("\n\nSkill instructions:\n") +
|
||||
strlen(skill_content) + 1U;
|
||||
char* system_prompt = (char*)malloc(system_len);
|
||||
if (!system_prompt) {
|
||||
free(composed_context);
|
||||
free(event_json);
|
||||
return;
|
||||
}
|
||||
@@ -1852,11 +2185,12 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
snprintf(system_prompt,
|
||||
system_len,
|
||||
"%s\n\n%s%s\nRelay: %s\n\nSkill instructions:\n%s",
|
||||
g_system_context,
|
||||
composed_context ? composed_context : "",
|
||||
trigger_prefix,
|
||||
skill_d_tag,
|
||||
relay,
|
||||
skill_content);
|
||||
free(composed_context);
|
||||
|
||||
size_t user_len = strlen("Triggering event JSON:\n") + strlen(event_json) + 1U;
|
||||
char* user_prompt = (char*)malloc(user_len);
|
||||
@@ -1990,82 +2324,51 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
int agent_build_admin_messages_json(const char* current_user_message,
|
||||
didactyl_sender_tier_t sender_tier,
|
||||
char** out_messages_json) {
|
||||
if (!out_messages_json || !g_cfg || !g_system_context) {
|
||||
if (!out_messages_json || !g_cfg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_messages_json = NULL;
|
||||
clear_context_part_names();
|
||||
|
||||
cJSON* messages = NULL;
|
||||
cJSON* dm_event = cJSON_CreateObject();
|
||||
if (!dm_event) {
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddStringToObject(dm_event, "type", "dm");
|
||||
cJSON_AddStringToObject(dm_event, "sender_pubkey", g_cfg->admin.pubkey);
|
||||
cJSON_AddStringToObject(dm_event, "message", current_user_message ? current_user_message : "");
|
||||
cJSON_AddStringToObject(dm_event,
|
||||
"tier",
|
||||
sender_tier == DIDACTYL_SENDER_ADMIN ? "admin" :
|
||||
(sender_tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
|
||||
cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));
|
||||
|
||||
if (g_has_prompt_template) {
|
||||
cJSON* dm_history = build_recent_admin_dm_history_messages(current_user_message);
|
||||
if (!dm_history) {
|
||||
dm_history = cJSON_CreateArray();
|
||||
}
|
||||
char* composed_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "api");
|
||||
cJSON_Delete(dm_event);
|
||||
if (!composed_context) {
|
||||
composed_context = strdup("You are an AI agent. Respond to the message.");
|
||||
}
|
||||
|
||||
llm_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
const char* provider_name = NULL;
|
||||
if (llm_get_config(&cfg) == 0 && cfg.provider[0] != '\0') {
|
||||
provider_name = cfg.provider;
|
||||
}
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages ||
|
||||
append_simple_message(messages, "system", composed_context ? composed_context : "You are an AI agent. Respond to the message.") != 0 ||
|
||||
append_recent_admin_dm_history(messages, current_user_message) != 0 ||
|
||||
append_simple_message(messages, "user", current_user_message ? current_user_message : "") != 0) {
|
||||
free(composed_context);
|
||||
cJSON_Delete(messages);
|
||||
return -1;
|
||||
}
|
||||
free(composed_context);
|
||||
|
||||
g_tools_ctx.template_sender_tier = (int)sender_tier;
|
||||
g_tools_ctx.template_current_user_message = current_user_message;
|
||||
g_tools_ctx.template_trigger_event_json = NULL;
|
||||
|
||||
messages = prompt_template_build_messages(&g_prompt_template,
|
||||
provider_name,
|
||||
&g_tools_ctx,
|
||||
dm_history,
|
||||
AGENT_HISTORY_TURNS,
|
||||
template_emit_hook,
|
||||
NULL);
|
||||
cJSON_Delete(dm_history);
|
||||
if (!messages) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
} else {
|
||||
messages = cJSON_CreateArray();
|
||||
if (!messages) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char agent_identity[192];
|
||||
snprintf(agent_identity,
|
||||
sizeof(agent_identity),
|
||||
"Agent identity\nYour pubkey (hex): %s",
|
||||
g_cfg->keys.public_key_hex[0] != '\0' ? g_cfg->keys.public_key_hex : "unknown");
|
||||
|
||||
char* sender_verification = build_sender_verification_text(sender_tier);
|
||||
if (!sender_verification ||
|
||||
append_simple_message(messages, "system", g_system_context) != 0 ||
|
||||
append_simple_message(messages, "system", agent_identity) != 0 ||
|
||||
append_simple_message(messages, "system", sender_verification) != 0 ||
|
||||
append_adopted_skills_context(messages) != 0) {
|
||||
free(sender_verification);
|
||||
cJSON_Delete(messages);
|
||||
return -1;
|
||||
}
|
||||
free(sender_verification);
|
||||
|
||||
if (append_recent_admin_dm_history(messages, current_user_message) != 0) {
|
||||
cJSON_Delete(messages);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(messages);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(messages, i);
|
||||
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
set_context_part_name(i, detect_context_section(role_s, content_s, i));
|
||||
}
|
||||
int n = cJSON_GetArraySize(messages);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(messages, i);
|
||||
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
set_context_part_name(i, detect_context_section(role_s, content_s, i));
|
||||
}
|
||||
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
@@ -2088,7 +2391,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
if (!g_cfg || !g_system_context || !sender_pubkey_hex || !message) {
|
||||
if (!g_cfg || !sender_pubkey_hex || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2117,15 +2420,22 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
}
|
||||
}
|
||||
|
||||
if (g_trigger_manager) {
|
||||
int dm_fired = trigger_manager_fire_dm(g_trigger_manager,
|
||||
sender_pubkey_hex,
|
||||
message,
|
||||
tier,
|
||||
"dm");
|
||||
if (dm_fired > 0) {
|
||||
return;
|
||||
}
|
||||
cJSON* dm_event = cJSON_CreateObject();
|
||||
if (!dm_event) {
|
||||
return;
|
||||
}
|
||||
cJSON_AddStringToObject(dm_event, "type", "dm");
|
||||
cJSON_AddStringToObject(dm_event, "sender_pubkey", sender_pubkey_hex);
|
||||
cJSON_AddStringToObject(dm_event, "message", message);
|
||||
cJSON_AddStringToObject(dm_event, "tier",
|
||||
tier == DIDACTYL_SENDER_ADMIN ? "admin" :
|
||||
(tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
|
||||
cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));
|
||||
|
||||
char* dm_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "dm");
|
||||
cJSON_Delete(dm_event);
|
||||
if (!dm_context) {
|
||||
dm_context = strdup("You are an AI agent. Respond to the message.");
|
||||
}
|
||||
|
||||
if (!allow_tools) {
|
||||
@@ -2133,13 +2443,14 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
? "You are responding to a web-of-trust contact. Keep the response helpful and concise. Tool use is disabled for this tier."
|
||||
: "You are responding in chat-only mode. Tool use is disabled.";
|
||||
|
||||
size_t ctx_len = strlen(g_system_context) + strlen("\n\n") + strlen(tier_prefix) + 1U;
|
||||
size_t ctx_len = strlen(dm_context ? dm_context : "") + strlen("\n\n") + strlen(tier_prefix) + 1U;
|
||||
char* system_for_chat = (char*)malloc(ctx_len);
|
||||
if (!system_for_chat) {
|
||||
free(dm_context);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(system_for_chat, ctx_len, "%s\n\n%s", g_system_context, tier_prefix);
|
||||
snprintf(system_for_chat, ctx_len, "%s\n\n%s", dm_context ? dm_context : "", tier_prefix);
|
||||
|
||||
size_t context_len = strlen("system:\n\nuser:\n") + strlen(system_for_chat) + strlen(message) + 1U;
|
||||
char* plain_context = (char*)malloc(context_len);
|
||||
@@ -2163,6 +2474,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
strlen(response) > 240 ? "..." : "");
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, response);
|
||||
free(response);
|
||||
free(dm_context);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2172,25 +2484,14 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
return;
|
||||
}
|
||||
|
||||
char* base_messages_json = NULL;
|
||||
if (agent_build_admin_messages_json(message, tier, &base_messages_json) != 0 || !base_messages_json) {
|
||||
free(tools_json);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* messages = cJSON_Parse(base_messages_json);
|
||||
free(base_messages_json);
|
||||
if (!messages || !cJSON_IsArray(messages)) {
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation state.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (append_simple_message(messages, "user", message) != 0) {
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages ||
|
||||
append_simple_message(messages, "system", dm_context ? dm_context : "You are an AI agent. Respond to the message.") != 0 ||
|
||||
append_recent_admin_dm_history(messages, message) != 0 ||
|
||||
append_simple_message(messages, "user", message) != 0) {
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
free(dm_context);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
||||
return;
|
||||
}
|
||||
@@ -2225,6 +2526,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "LLM request failed.");
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
free(dm_context);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2277,6 +2579,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
llm_response_free(&resp);
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
free(dm_context);
|
||||
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to append tool result.");
|
||||
return;
|
||||
}
|
||||
@@ -2333,12 +2636,11 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
free(final_answer_owned);
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
free(dm_context);
|
||||
}
|
||||
|
||||
void agent_cleanup(void) {
|
||||
tools_cleanup(&g_tools_ctx);
|
||||
free(g_system_context);
|
||||
g_system_context = NULL;
|
||||
g_cfg = NULL;
|
||||
g_trigger_manager = NULL;
|
||||
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
|
||||
@@ -2351,6 +2653,4 @@ void agent_cleanup(void) {
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
clear_context_part_names();
|
||||
prompt_template_free(&g_prompt_template);
|
||||
g_has_prompt_template = 0;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
struct trigger_manager;
|
||||
|
||||
int agent_init(didactyl_config_t* config, const char* system_context);
|
||||
int agent_init(didactyl_config_t* config);
|
||||
void agent_set_trigger_manager(struct trigger_manager* trigger_manager);
|
||||
void agent_on_trigger(const char* skill_d_tag,
|
||||
const char* skill_content,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#ifndef DIDACTYL_DEFAULT_EVENTS_H
|
||||
#define DIDACTYL_DEFAULT_EVENTS_H
|
||||
|
||||
#define DIDACTYL_DEFAULT_SKILL_D_TAG "didactyl-default"
|
||||
#define DIDACTYL_DEFAULT_SKILL_D_TAG "default_admin_dm"
|
||||
#define DIDACTYL_DEFAULT_SKILL_KIND 31124
|
||||
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"didactyl-default\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]"
|
||||
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]"
|
||||
|
||||
#define DIDACTYL_STARTUP_KIND_PROFILE 0
|
||||
#define DIDACTYL_STARTUP_KIND_CONTACTS 3
|
||||
@@ -31,83 +31,19 @@ static const char* DIDACTYL_DEFAULT_RELAYS[] = {
|
||||
#define DIDACTYL_DEFAULT_RELAY_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_RELAYS) / sizeof(DIDACTYL_DEFAULT_RELAYS[0])))
|
||||
|
||||
static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE =
|
||||
"# %s Agent\n\n"
|
||||
"You are %s, 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 `local_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"
|
||||
" tool: admin_identity\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: admin_profile\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_admin_profile\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: admin_contacts\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_admin_contacts\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: admin_relays\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_admin_relays\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: admin_notes\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_admin_notes\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: agent_identity\n"
|
||||
" role: system\n"
|
||||
" tool: agent_identity\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: agent_profile\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_agent_profile\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: agent_contacts\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_agent_contacts\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: agent_relays\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_agent_relays\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: agent_notes\n"
|
||||
" role: system\n"
|
||||
" tool: nostr_agent_notes\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: tasks\n"
|
||||
" role: system\n"
|
||||
" tool: task_list\n"
|
||||
" skip_if_empty: true\n\n"
|
||||
"- section: dm_history\n"
|
||||
" role: expand\n"
|
||||
" limit: 12\n\n"
|
||||
"- section: conversation\n"
|
||||
" role: user\n"
|
||||
" tool: message_current\n"
|
||||
" skip_if_empty: true\n";
|
||||
"# Didactyl Agent\n\n"
|
||||
"You are {{my_kind0_profile}}\n\n"
|
||||
"Your npub: {{my_npub}}\n\n"
|
||||
"## Rules\n\n"
|
||||
"- Communicate through encrypted Nostr direct messages\n"
|
||||
"- Keep responses concise and clear\n"
|
||||
"- Be helpful and technically accurate\n"
|
||||
"- If unsure, state uncertainty directly\n"
|
||||
"- Use tools when a request requires taking action\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"
|
||||
"- Maintain your task list as short-term working memory\n"
|
||||
"- Never reveal your private key (nsec)\n"
|
||||
"- You may share your public key (npub) with anyone";
|
||||
|
||||
#endif
|
||||
43
src/main.c
43
src/main.c
@@ -56,8 +56,15 @@ static void on_self_skill_eose_load_triggers(int event_count, void* user_data) {
|
||||
}
|
||||
}
|
||||
|
||||
static volatile sig_atomic_t g_sigint_count = 0;
|
||||
|
||||
static void signal_handler(int signum) {
|
||||
(void)signum;
|
||||
if (signum == SIGINT) {
|
||||
g_sigint_count++;
|
||||
if (g_sigint_count >= 2) {
|
||||
_exit(130);
|
||||
}
|
||||
}
|
||||
g_running = 0;
|
||||
}
|
||||
|
||||
@@ -1245,9 +1252,6 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
|
||||
if (nostr_handler_load_system_context_from_adopted_skills() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to load system context from adopted skills (continuing with fallback)");
|
||||
}
|
||||
}
|
||||
|
||||
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
|
||||
@@ -1258,19 +1262,8 @@ int main(int argc, char** argv) {
|
||||
|
||||
startup_step_ok(7, "Reconcile/persist startup state", NULL);
|
||||
|
||||
const char* system_context = nostr_handler_get_system_context();
|
||||
char system_context_fallback[512] = {0};
|
||||
if (!system_context || system_context[0] == '\0') {
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
snprintf(system_context_fallback,
|
||||
sizeof(system_context_fallback),
|
||||
"You are %s, a sovereign AI agent living on Nostr.",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "the agent");
|
||||
system_context = system_context_fallback;
|
||||
}
|
||||
|
||||
startup_step_begin(8, "Initialize agent");
|
||||
if (agent_init(&cfg, system_context) != 0) {
|
||||
if (agent_init(&cfg) != 0) {
|
||||
startup_step_fail(8, "Initialize agent", "agent_init failed");
|
||||
fprintf(stderr, "Failed to initialize agent\n");
|
||||
nostr_block_list_cleanup();
|
||||
@@ -1456,9 +1449,21 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = signal_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
|
||||
(void)sigaction(SIGINT, &sa, NULL);
|
||||
(void)sigaction(SIGTERM, &sa, NULL);
|
||||
|
||||
struct sigaction sa_pipe;
|
||||
memset(&sa_pipe, 0, sizeof(sa_pipe));
|
||||
sa_pipe.sa_handler = SIG_IGN;
|
||||
sigemptyset(&sa_pipe.sa_mask);
|
||||
sa_pipe.sa_flags = 0;
|
||||
(void)sigaction(SIGPIPE, &sa_pipe, NULL);
|
||||
|
||||
int http_api_started = 0;
|
||||
if (cfg.api.enabled) {
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
// Version information (auto-updated by build system)
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 1
|
||||
#define DIDACTYL_VERSION_PATCH 21
|
||||
#define DIDACTYL_VERSION "v0.1.21"
|
||||
#define DIDACTYL_VERSION_MINOR 2
|
||||
#define DIDACTYL_VERSION_PATCH 6
|
||||
#define DIDACTYL_VERSION "v0.2.6"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -25,7 +25,6 @@ static void* g_dm_user_data = NULL;
|
||||
static int g_poll_counter = 0;
|
||||
static time_t g_start_time = 0;
|
||||
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
|
||||
static char* g_system_context = NULL;
|
||||
static unsigned char* g_startup_published = NULL;
|
||||
static int g_startup_publish_tracking_enabled = 0;
|
||||
static int g_startup_kind1_already_exists = 0;
|
||||
@@ -1000,7 +999,7 @@ static int parse_enabled_flag_local(const char* enabled_s) {
|
||||
return (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
|
||||
}
|
||||
|
||||
static int parse_skill_address_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) {
|
||||
static __attribute__((unused)) int parse_skill_address_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) {
|
||||
if (!addr || !out_kind || !out_pubkey_hex || !out_d_tag) {
|
||||
return -1;
|
||||
}
|
||||
@@ -3121,9 +3120,6 @@ int nostr_handler_reconcile_startup_events(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(g_system_context);
|
||||
g_system_context = NULL;
|
||||
|
||||
free(g_startup_published);
|
||||
g_startup_published = NULL;
|
||||
g_startup_publish_tracking_enabled = 0;
|
||||
@@ -3137,25 +3133,12 @@ int nostr_handler_reconcile_startup_events(void) {
|
||||
g_startup_publish_tracking_enabled = 1;
|
||||
}
|
||||
|
||||
if (g_cfg->default_skill.content && g_cfg->default_skill.content[0] != '\0') {
|
||||
g_system_context = strdup(g_cfg->default_skill.content);
|
||||
}
|
||||
|
||||
load_startup_display_name();
|
||||
g_startup_kind1_already_exists = startup_self_kind1_exists();
|
||||
if (g_startup_kind1_already_exists) {
|
||||
DEBUG_INFO("[didactyl] startup phase: existing self kind-1 note found; skipping startup kind-1 publish");
|
||||
}
|
||||
|
||||
if (!g_system_context) {
|
||||
char default_ctx[512];
|
||||
snprintf(default_ctx,
|
||||
sizeof(default_ctx),
|
||||
"You are %s, a sovereign AI agent living on Nostr.",
|
||||
g_startup_display_name[0] != '\0' ? g_startup_display_name : "the agent");
|
||||
g_system_context = strdup(default_ctx);
|
||||
}
|
||||
|
||||
for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
|
||||
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
|
||||
}
|
||||
@@ -3165,131 +3148,6 @@ int nostr_handler_reconcile_startup_events(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_handler_load_system_context_from_adopted_skills(void) {
|
||||
if (!g_cfg || !g_pool || g_cfg->keys.public_key_hex[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* adoption_filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!adoption_filter || !kinds || !authors) {
|
||||
cJSON_Delete(adoption_filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
|
||||
cJSON_AddItemToObject(adoption_filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(adoption_filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(adoption_filter, "limit", 1);
|
||||
|
||||
char* adoption_events_json = nostr_handler_query_json(adoption_filter, 3000);
|
||||
cJSON_Delete(adoption_filter);
|
||||
if (!adoption_events_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* adoption_events = cJSON_Parse(adoption_events_json);
|
||||
free(adoption_events_json);
|
||||
if (!adoption_events || !cJSON_IsArray(adoption_events) || cJSON_GetArraySize(adoption_events) <= 0) {
|
||||
cJSON_Delete(adoption_events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* adoption = cJSON_GetArrayItem(adoption_events, 0);
|
||||
cJSON* tags = adoption ? cJSON_GetObjectItemCaseSensitive(adoption, "tags") : NULL;
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(adoption_events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* first_addr = NULL;
|
||||
int tag_n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < tag_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) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(k->valuestring, "a") == 0 && v->valuestring[0] != '\0') {
|
||||
first_addr = v->valuestring;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!first_addr) {
|
||||
cJSON_Delete(adoption_events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int skill_kind = 0;
|
||||
char skill_pubkey_hex[65] = {0};
|
||||
char skill_d_tag[65] = {0};
|
||||
if (parse_skill_address_local(first_addr, &skill_kind, skill_pubkey_hex, skill_d_tag) != 0) {
|
||||
cJSON_Delete(adoption_events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* skill_filter = cJSON_CreateObject();
|
||||
cJSON* skill_kinds = cJSON_CreateArray();
|
||||
cJSON* skill_authors = cJSON_CreateArray();
|
||||
cJSON* d_tags = cJSON_CreateArray();
|
||||
if (!skill_filter || !skill_kinds || !skill_authors || !d_tags) {
|
||||
cJSON_Delete(adoption_events);
|
||||
cJSON_Delete(skill_filter);
|
||||
cJSON_Delete(skill_kinds);
|
||||
cJSON_Delete(skill_authors);
|
||||
cJSON_Delete(d_tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(skill_kinds, cJSON_CreateNumber(skill_kind));
|
||||
cJSON_AddItemToObject(skill_filter, "kinds", skill_kinds);
|
||||
cJSON_AddItemToArray(skill_authors, cJSON_CreateString(skill_pubkey_hex));
|
||||
cJSON_AddItemToObject(skill_filter, "authors", skill_authors);
|
||||
cJSON_AddItemToArray(d_tags, cJSON_CreateString(skill_d_tag));
|
||||
cJSON_AddItemToObject(skill_filter, "#d", d_tags);
|
||||
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||
|
||||
char* skill_events_json = nostr_handler_query_json(skill_filter, 3000);
|
||||
cJSON_Delete(skill_filter);
|
||||
cJSON_Delete(adoption_events);
|
||||
if (!skill_events_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* skill_events = cJSON_Parse(skill_events_json);
|
||||
free(skill_events_json);
|
||||
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
|
||||
cJSON_Delete(skill_events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0);
|
||||
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
|
||||
if (!content || !cJSON_IsString(content) || !content->valuestring) {
|
||||
cJSON_Delete(skill_events);
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(g_system_context);
|
||||
g_system_context = strdup(content->valuestring);
|
||||
cJSON_Delete(skill_events);
|
||||
|
||||
return g_system_context ? 0 : -1;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -4212,8 +4070,6 @@ void nostr_handler_cleanup(void) {
|
||||
g_cfg = NULL;
|
||||
g_dm_callback = NULL;
|
||||
g_dm_user_data = NULL;
|
||||
free(g_system_context);
|
||||
g_system_context = NULL;
|
||||
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
|
||||
g_seen_dm_count = 0;
|
||||
g_seen_dm_next = 0;
|
||||
|
||||
@@ -59,10 +59,8 @@ nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
|
||||
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
int nostr_handler_load_system_context_from_adopted_skills(void);
|
||||
void nostr_handler_refresh_relay_statuses(void);
|
||||
int nostr_handler_sync_relays_from_config(void);
|
||||
const char* nostr_handler_get_system_context(void);
|
||||
char* nostr_handler_get_self_skill_events_json(void);
|
||||
int nostr_handler_is_event_in_self_cache(const char* event_id_hex);
|
||||
const char* nostr_handler_get_startup_display_name(void);
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "prompt_template.h"
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static char* dup_range(const char* s, size_t n) {
|
||||
char* out = (char*)malloc(n + 1U);
|
||||
if (!out) return NULL;
|
||||
if (n > 0) {
|
||||
memcpy(out, s, n);
|
||||
}
|
||||
if (n > 0) memcpy(out, s, n);
|
||||
out[n] = '\0';
|
||||
return out;
|
||||
}
|
||||
@@ -32,52 +30,6 @@ static void rtrim_inplace(char* s) {
|
||||
}
|
||||
}
|
||||
|
||||
static int starts_with(const char* s, const char* prefix) {
|
||||
if (!s || !prefix) return 0;
|
||||
size_t n = strlen(prefix);
|
||||
return strncmp(s, prefix, n) == 0;
|
||||
}
|
||||
|
||||
static int add_line(char*** lines, int* count, int* cap, char* line) {
|
||||
if (!lines || !count || !cap) return -1;
|
||||
if (*count >= *cap) {
|
||||
int next = (*cap == 0) ? 64 : (*cap * 2);
|
||||
char** grown = (char**)realloc(*lines, (size_t)next * sizeof(char*));
|
||||
if (!grown) return -1;
|
||||
*lines = grown;
|
||||
*cap = next;
|
||||
}
|
||||
(*lines)[*count] = line;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int split_lines_inplace(char* s, char*** out_lines, int* out_count) {
|
||||
if (!s || !out_lines || !out_count) return -1;
|
||||
|
||||
char** lines = NULL;
|
||||
int count = 0;
|
||||
int cap = 0;
|
||||
|
||||
char* p = s;
|
||||
while (*p) {
|
||||
char* start = p;
|
||||
while (*p && *p != '\n') p++;
|
||||
if (*p == '\n') {
|
||||
*p = '\0';
|
||||
p++;
|
||||
}
|
||||
if (add_line(&lines, &count, &cap, start) != 0) {
|
||||
free(lines);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
*out_lines = lines;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
|
||||
if (!buf || !cap || !used || !s) return -1;
|
||||
size_t n = strlen(s);
|
||||
@@ -105,10 +57,11 @@ static const char* map_variable_tool_name(const char* var_name) {
|
||||
if (strcmp(var_name, "admin_relays") == 0) return "nostr_admin_relays";
|
||||
if (strcmp(var_name, "adopted_skills") == 0) return "adopted_skills";
|
||||
if (strcmp(var_name, "triggering_event") == 0) return "trigger_event";
|
||||
|
||||
return var_name;
|
||||
}
|
||||
|
||||
static char* resolve_inline_variables_local(const char* tpl, tools_context_t* tools_ctx) {
|
||||
char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* tools_ctx) {
|
||||
if (!tpl) return strdup("");
|
||||
|
||||
size_t cap = strlen(tpl) + 64U;
|
||||
@@ -189,6 +142,16 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
|
||||
free(tool_result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!replacement_owned && tools_ctx->template_skill_lookup && var_trim[0] != '\0') {
|
||||
const char* skill_content = tools_ctx->template_skill_lookup(
|
||||
tools_ctx->template_skill_lookup_user_data,
|
||||
var_trim);
|
||||
if (skill_content && skill_content[0] != '\0') {
|
||||
replacement_owned = strdup(skill_content);
|
||||
}
|
||||
}
|
||||
|
||||
replacement = replacement_owned ? replacement_owned : "";
|
||||
}
|
||||
|
||||
@@ -206,462 +169,3 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
|
||||
|
||||
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;
|
||||
sec->provider_content_template = NULL;
|
||||
}
|
||||
|
||||
static int parse_int_or_zero(const char* s) {
|
||||
if (!s) return 0;
|
||||
while (*s && isspace((unsigned char)*s)) s++;
|
||||
return atoi(s);
|
||||
}
|
||||
|
||||
int prompt_template_parse(const char* skill_content, prompt_template_t* out_template) {
|
||||
if (!skill_content || !out_template) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(out_template, 0, sizeof(*out_template));
|
||||
|
||||
const char* marker = strstr(skill_content, PROMPT_TEMPLATE_MARKER);
|
||||
if (!marker) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t personality_len = (size_t)(marker - skill_content);
|
||||
out_template->personality = dup_range(skill_content, personality_len);
|
||||
if (!out_template->personality) {
|
||||
return -1;
|
||||
}
|
||||
rtrim_inplace(out_template->personality);
|
||||
|
||||
const char* after = marker + strlen(PROMPT_TEMPLATE_MARKER);
|
||||
while (*after == '\r' || *after == '\n') after++;
|
||||
|
||||
char* tpl = strdup(after);
|
||||
if (!tpl) {
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char** lines = NULL;
|
||||
int line_count = 0;
|
||||
if (split_lines_inplace(tpl, &lines, &line_count) != 0) {
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int current = -1;
|
||||
int i = 0;
|
||||
while (i < line_count) {
|
||||
char* raw = lines[i];
|
||||
rtrim_inplace(raw);
|
||||
char* line = ltrim_inplace(raw);
|
||||
|
||||
if (*line == '\0') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "- section:")) {
|
||||
if (out_template->section_count >= PROMPT_TEMPLATE_MAX_SECTIONS) {
|
||||
break;
|
||||
}
|
||||
|
||||
current = out_template->section_count;
|
||||
init_section_defaults(&out_template->sections[current]);
|
||||
out_template->section_count++;
|
||||
|
||||
char* name = line + strlen("- section:");
|
||||
name = ltrim_inplace(name);
|
||||
rtrim_inplace(name);
|
||||
snprintf(out_template->sections[current].name,
|
||||
sizeof(out_template->sections[current].name),
|
||||
"%s",
|
||||
name);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current < 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "role:")) {
|
||||
char* role = line + strlen("role:");
|
||||
role = ltrim_inplace(role);
|
||||
rtrim_inplace(role);
|
||||
snprintf(out_template->sections[current].role,
|
||||
sizeof(out_template->sections[current].role),
|
||||
"%s",
|
||||
(*role) ? role : "system");
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "limit:")) {
|
||||
char* lim = line + strlen("limit:");
|
||||
out_template->sections[current].limit = parse_int_or_zero(lim);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "skip_if_empty:")) {
|
||||
char* flag = line + strlen("skip_if_empty:");
|
||||
flag = ltrim_inplace(flag);
|
||||
rtrim_inplace(flag);
|
||||
out_template->sections[current].skip_if_empty =
|
||||
(strcmp(flag, "true") == 0 || strcmp(flag, "1") == 0) ? 1 : 0;
|
||||
i++;
|
||||
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);
|
||||
|
||||
free(out_template->sections[current].content_template);
|
||||
out_template->sections[current].content_template = NULL;
|
||||
|
||||
if (strcmp(val, "|") == 0) {
|
||||
char* acc = strdup("");
|
||||
size_t cap = acc ? 1U : 0U;
|
||||
size_t used = 0U;
|
||||
if (!acc) {
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
i++;
|
||||
while (i < line_count) {
|
||||
char* next_raw = lines[i];
|
||||
char* next_ltrim = ltrim_inplace(next_raw);
|
||||
|
||||
if (starts_with(next_ltrim, "- section:") ||
|
||||
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;
|
||||
}
|
||||
|
||||
char* piece = next_raw;
|
||||
if (strncmp(piece, " ", 4) == 0) piece += 4;
|
||||
else if (strncmp(piece, " ", 2) == 0) piece += 2;
|
||||
rtrim_inplace(piece);
|
||||
|
||||
if (append_text(&acc, &cap, &used, piece) != 0 ||
|
||||
append_text(&acc, &cap, &used, "\n") != 0) {
|
||||
free(acc);
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
rtrim_inplace(acc);
|
||||
out_template->sections[current].content_template = acc;
|
||||
continue;
|
||||
}
|
||||
|
||||
rtrim_inplace(val);
|
||||
out_template->sections[current].content_template = strdup(val);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "provider:")) {
|
||||
i++;
|
||||
if (i >= line_count) continue;
|
||||
|
||||
char* provider_line = ltrim_inplace(lines[i]);
|
||||
rtrim_inplace(provider_line);
|
||||
char* colon = strchr(provider_line, ':');
|
||||
if (!colon) {
|
||||
continue;
|
||||
}
|
||||
|
||||
*colon = '\0';
|
||||
char* provider_name = ltrim_inplace(provider_line);
|
||||
rtrim_inplace(provider_name);
|
||||
char* provider_val = ltrim_inplace(colon + 1);
|
||||
rtrim_inplace(provider_val);
|
||||
|
||||
free(out_template->sections[current].provider_name);
|
||||
out_template->sections[current].provider_name = strdup(provider_name);
|
||||
free(out_template->sections[current].provider_content_template);
|
||||
out_template->sections[current].provider_content_template = NULL;
|
||||
|
||||
if (strcmp(provider_val, "|") == 0) {
|
||||
char* acc = strdup("");
|
||||
size_t cap = acc ? 1U : 0U;
|
||||
size_t used = 0U;
|
||||
if (!acc) {
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
i++;
|
||||
while (i < line_count) {
|
||||
char* next = ltrim_inplace(lines[i]);
|
||||
if (starts_with(next, "- section:") ||
|
||||
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;
|
||||
}
|
||||
char* piece = lines[i];
|
||||
if (strncmp(piece, " ", 4) == 0) piece += 4;
|
||||
else if (strncmp(piece, " ", 2) == 0) piece += 2;
|
||||
rtrim_inplace(piece);
|
||||
if (append_text(&acc, &cap, &used, piece) != 0 ||
|
||||
append_text(&acc, &cap, &used, "\n") != 0) {
|
||||
free(acc);
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
rtrim_inplace(acc);
|
||||
out_template->sections[current].provider_content_template = acc;
|
||||
continue;
|
||||
}
|
||||
|
||||
out_template->sections[current].provider_content_template = strdup(provider_val);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
free(lines);
|
||||
free(tpl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_message_object(cJSON* messages, const char* role, const char* content) {
|
||||
if (!messages || !role) return -1;
|
||||
cJSON* msg = cJSON_CreateObject();
|
||||
if (!msg) return -1;
|
||||
cJSON_AddStringToObject(msg, "role", role);
|
||||
cJSON_AddStringToObject(msg, "content", content ? content : "");
|
||||
cJSON_AddItemToArray(messages, msg);
|
||||
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,
|
||||
tools_context_t* tools_ctx,
|
||||
cJSON* dm_history_messages,
|
||||
int dm_history_default_limit,
|
||||
prompt_template_emit_hook_fn emit_hook,
|
||||
void* emit_hook_user_data) {
|
||||
if (!tmpl) return NULL;
|
||||
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) return NULL;
|
||||
|
||||
int out_idx = 0;
|
||||
if (tmpl->personality && tmpl->personality[0] != '\0') {
|
||||
if (append_message_object(out, "system", tmpl->personality) != 0) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
if (emit_hook) emit_hook("system_prompt", out_idx, emit_hook_user_data);
|
||||
out_idx++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tmpl->section_count; i++) {
|
||||
const prompt_template_section_t* sec = &tmpl->sections[i];
|
||||
const char* role = sec->role[0] ? sec->role : "system";
|
||||
|
||||
if (strcmp(role, "expand") == 0) {
|
||||
if (!dm_history_messages || !cJSON_IsArray(dm_history_messages)) {
|
||||
continue;
|
||||
}
|
||||
int total = cJSON_GetArraySize(dm_history_messages);
|
||||
int lim = sec->limit > 0 ? sec->limit : dm_history_default_limit;
|
||||
int start = (lim > 0 && total > lim) ? (total - lim) : 0;
|
||||
|
||||
for (int j = start; j < total; j++) {
|
||||
cJSON* item = cJSON_GetArrayItem(dm_history_messages, j);
|
||||
if (!item || !cJSON_IsObject(item)) continue;
|
||||
cJSON* dup = cJSON_Duplicate(item, 1);
|
||||
if (!dup) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(out, dup);
|
||||
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
|
||||
out_idx++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char* resolved = NULL;
|
||||
|
||||
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 = resolve_inline_variables_local(tpl ? tpl : "", tools_ctx);
|
||||
}
|
||||
if (!resolved) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (sec->skip_if_empty) {
|
||||
char* chk = ltrim_inplace(resolved);
|
||||
if (chk && *chk == '\0') {
|
||||
free(resolved);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (append_message_object(out, role, resolved) != 0) {
|
||||
free(resolved);
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
|
||||
out_idx++;
|
||||
free(resolved);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void prompt_template_free(prompt_template_t* tmpl) {
|
||||
if (!tmpl) return;
|
||||
|
||||
free(tmpl->personality);
|
||||
tmpl->personality = NULL;
|
||||
|
||||
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);
|
||||
tmpl->sections[i].provider_content_template = NULL;
|
||||
tmpl->sections[i].name[0] = '\0';
|
||||
tmpl->sections[i].role[0] = '\0';
|
||||
tmpl->sections[i].limit = 0;
|
||||
tmpl->sections[i].skip_if_empty = 0;
|
||||
}
|
||||
|
||||
tmpl->section_count = 0;
|
||||
}
|
||||
@@ -1,47 +1,8 @@
|
||||
#ifndef DIDACTYL_PROMPT_TEMPLATE_H
|
||||
#define DIDACTYL_PROMPT_TEMPLATE_H
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
|
||||
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
|
||||
#define PROMPT_TEMPLATE_MAX_ROLE_LEN 16
|
||||
#define PROMPT_TEMPLATE_MARKER "---template---"
|
||||
|
||||
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;
|
||||
char* provider_content_template;
|
||||
} prompt_template_section_t;
|
||||
|
||||
typedef struct {
|
||||
char* personality;
|
||||
prompt_template_section_t sections[PROMPT_TEMPLATE_MAX_SECTIONS];
|
||||
int section_count;
|
||||
} prompt_template_t;
|
||||
|
||||
typedef void (*prompt_template_emit_hook_fn)(const char* section_name,
|
||||
int message_index,
|
||||
void* user_data);
|
||||
|
||||
int prompt_template_parse(const char* skill_content, prompt_template_t* out_template);
|
||||
|
||||
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
||||
const char* provider_name,
|
||||
tools_context_t* tools_ctx,
|
||||
cJSON* dm_history_messages,
|
||||
int dm_history_default_limit,
|
||||
prompt_template_emit_hook_fn emit_hook,
|
||||
void* emit_hook_user_data);
|
||||
|
||||
void prompt_template_free(prompt_template_t* tmpl);
|
||||
char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* tools_ctx);
|
||||
|
||||
#endif
|
||||
@@ -508,7 +508,7 @@ static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char*
|
||||
cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND;
|
||||
snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG);
|
||||
|
||||
int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
|
||||
int needed = snprintf(NULL, 0, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
|
||||
if (needed <= 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -518,7 +518,7 @@ static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char*
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(cfg->default_skill.content, (size_t)needed + 1U, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
|
||||
snprintf(cfg->default_skill.content, (size_t)needed + 1U, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
|
||||
|
||||
cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON);
|
||||
if (!cfg->default_skill.tags_json) {
|
||||
|
||||
@@ -11,6 +11,8 @@ typedef struct {
|
||||
int template_sender_tier;
|
||||
const char* template_current_user_message;
|
||||
const char* template_trigger_event_json;
|
||||
const char* (*template_skill_lookup)(void* user_data, const char* d_tag);
|
||||
void* template_skill_lookup_user_data;
|
||||
} tools_context_t;
|
||||
|
||||
int tools_init(tools_context_t* ctx, didactyl_config_t* cfg);
|
||||
|
||||
@@ -25,6 +25,8 @@ int tools_init_legacy(tools_context_t* ctx, didactyl_config_t* cfg) {
|
||||
ctx->template_sender_tier = 0;
|
||||
ctx->template_current_user_message = NULL;
|
||||
ctx->template_trigger_event_json = NULL;
|
||||
ctx->template_skill_lookup = NULL;
|
||||
ctx->template_skill_lookup_user_data = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -490,7 +490,7 @@ static cJSON* find_tag_value_string(cJSON* tags, const char* key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
|
||||
static __attribute__((unused)) int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
|
||||
if (!addr || !out_kind || !out_pubkey || !out_d_tag) {
|
||||
return -1;
|
||||
}
|
||||
@@ -594,15 +594,28 @@ static void apply_trigger_runtime_to_llm_config(const active_trigger_t* t, llm_c
|
||||
|
||||
if (t->llm_spec[0] != '\0') {
|
||||
const char* spec = t->llm_spec;
|
||||
while (*spec && isspace((unsigned char)*spec)) spec++;
|
||||
|
||||
const char* comma = strchr(spec, ',');
|
||||
size_t len = comma ? (size_t)(comma - spec) : strlen(spec);
|
||||
while (len > 0 && isspace((unsigned char)spec[len - 1])) len--;
|
||||
while (*spec && isspace((unsigned char)*spec)) spec++;
|
||||
|
||||
const char* slash = memchr(spec, '/', len);
|
||||
if (slash) {
|
||||
size_t provider_len = (size_t)(slash - spec);
|
||||
const char* model = slash + 1;
|
||||
size_t model_len = len - (size_t)(model - spec);
|
||||
|
||||
while (provider_len > 0 && isspace((unsigned char)spec[provider_len - 1])) provider_len--;
|
||||
while (model_len > 0 && isspace((unsigned char)*model)) {
|
||||
model++;
|
||||
model_len--;
|
||||
}
|
||||
while (model_len > 0 && isspace((unsigned char)model[model_len - 1])) model_len--;
|
||||
|
||||
if (provider_len > 0 && provider_len < sizeof(cfg->provider)) {
|
||||
snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec);
|
||||
}
|
||||
if (model_len > 0 && model_len < sizeof(cfg->model)) {
|
||||
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)model_len, model);
|
||||
}
|
||||
@@ -807,137 +820,61 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123);
|
||||
if (!adoption_json) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* adoption_events = cJSON_Parse(adoption_json);
|
||||
free(adoption_json);
|
||||
if (!adoption_events || !cJSON_IsArray(adoption_events) || cJSON_GetArraySize(adoption_events) <= 0) {
|
||||
cJSON_Delete(adoption_events);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* list_event = cJSON_GetArrayItem(adoption_events, 0);
|
||||
cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL;
|
||||
if (!list_tags || !cJSON_IsArray(list_tags)) {
|
||||
cJSON_Delete(adoption_events);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int loaded = 0;
|
||||
int tn = cJSON_GetArraySize(list_tags);
|
||||
for (int i = 0; i < tn; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(list_tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
int considered = 0;
|
||||
const int kinds[2] = {31123, 31124};
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) ||
|
||||
!key->valuestring || !val->valuestring || strcmp(key->valuestring, "a") != 0) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]);
|
||||
if (!skill_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int kind = 0;
|
||||
char pubkey[65] = {0};
|
||||
char d_tag[65] = {0};
|
||||
if (parse_address_tag(val->valuestring, &kind, pubkey, d_tag) != 0) {
|
||||
cJSON* skill_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
if (!skill_events || !cJSON_IsArray(skill_events)) {
|
||||
cJSON_Delete(skill_events);
|
||||
continue;
|
||||
}
|
||||
|
||||
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, d_tag) != 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);
|
||||
int sn = cJSON_GetArraySize(skill_events);
|
||||
for (int i = 0; i < sn; i++) {
|
||||
cJSON* skill_event = cJSON_GetArrayItem(skill_events, i);
|
||||
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
|
||||
cJSON* tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
|
||||
if (!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
|
||||
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(d_tag));
|
||||
cJSON_AddItemToObject(skill_filter, "#d", d_values);
|
||||
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||
cJSON* d = find_tag_value_string(tags, "d");
|
||||
cJSON* trigger = find_tag_value_string(tags, "trigger");
|
||||
cJSON* filter = find_tag_value_string(tags, "filter");
|
||||
cJSON* action = find_tag_value_string(tags, "action");
|
||||
|
||||
char* skill_json = nostr_handler_query_json(skill_filter, 2000);
|
||||
cJSON_Delete(skill_filter);
|
||||
if (skill_json) {
|
||||
skill_events = cJSON_Parse(skill_json);
|
||||
free(skill_json);
|
||||
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
|
||||
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
|
||||
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
|
||||
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
|
||||
|
||||
considered++;
|
||||
if (!d_tag || d_tag[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
|
||||
cJSON_Delete(skill_events);
|
||||
continue;
|
||||
}
|
||||
int trigger_supported = trigger_s &&
|
||||
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
||||
strcmp(trigger_s, "webhook") == 0 ||
|
||||
strcmp(trigger_s, "cron") == 0 ||
|
||||
strcmp(trigger_s, "chain") == 0 ||
|
||||
strcmp(trigger_s, "dm") == 0);
|
||||
if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0);
|
||||
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
|
||||
cJSON* tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
|
||||
if (!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(skill_events);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* trigger = find_tag_value_string(tags, "trigger");
|
||||
cJSON* filter = find_tag_value_string(tags, "filter");
|
||||
cJSON* action = find_tag_value_string(tags, "action");
|
||||
cJSON* enabled = find_tag_value_string(tags, "enabled");
|
||||
|
||||
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
|
||||
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
|
||||
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
|
||||
const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true";
|
||||
|
||||
int trigger_supported = trigger_s &&
|
||||
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
||||
strcmp(trigger_s, "webhook") == 0 ||
|
||||
strcmp(trigger_s, "cron") == 0 ||
|
||||
strcmp(trigger_s, "chain") == 0 ||
|
||||
strcmp(trigger_s, "dm") == 0);
|
||||
if (trigger_supported && filter_s && filter_s[0] != '\0') {
|
||||
if (strcmp(action_s, "template") == 0) {
|
||||
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
|
||||
}
|
||||
|
||||
const char* llm_s = NULL;
|
||||
const char* tools_s = NULL;
|
||||
int has_max_tokens = 0;
|
||||
@@ -956,14 +893,13 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
&has_seed,
|
||||
&seed);
|
||||
|
||||
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
|
||||
if (trigger_manager_add(mgr,
|
||||
d_tag,
|
||||
content->valuestring,
|
||||
filter_s,
|
||||
TRIGGER_ACTION_LLM,
|
||||
trigger_s,
|
||||
is_enabled,
|
||||
1,
|
||||
llm_s,
|
||||
tools_s,
|
||||
has_max_tokens,
|
||||
@@ -979,8 +915,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
cJSON_Delete(skill_events);
|
||||
}
|
||||
|
||||
cJSON_Delete(adoption_events);
|
||||
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from skills", loaded);
|
||||
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from self skills (considered=%d)", loaded, considered);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1542,33 +1477,22 @@ static int dm_filter_matches_tier(const char* filter_json, didactyl_sender_tier_
|
||||
return match;
|
||||
}
|
||||
|
||||
int trigger_manager_fire_dm(trigger_manager_t* mgr,
|
||||
const char* sender_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_sender_tier_t tier,
|
||||
const char* source_label) {
|
||||
if (!mgr || !sender_pubkey_hex || !message) {
|
||||
int trigger_manager_get_dm_skills(trigger_manager_t* mgr,
|
||||
const char* sender_pubkey_hex,
|
||||
didactyl_sender_tier_t tier,
|
||||
char** out_d_tags,
|
||||
int max_d_tags) {
|
||||
(void)sender_pubkey_hex;
|
||||
|
||||
if (!mgr || !out_d_tags || max_d_tags <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
if (!event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(event, "type", "dm");
|
||||
cJSON_AddStringToObject(event, "sender_pubkey", sender_pubkey_hex);
|
||||
cJSON_AddStringToObject(event, "message", message);
|
||||
cJSON_AddStringToObject(event, "tier",
|
||||
tier == DIDACTYL_SENDER_ADMIN ? "admin" :
|
||||
(tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
|
||||
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
|
||||
|
||||
int fired_total = 0;
|
||||
int count = 0;
|
||||
|
||||
pthread_mutex_lock(&mgr->mutex);
|
||||
int count_snapshot = mgr->count;
|
||||
for (int i = 0; i < count_snapshot; i++) {
|
||||
for (int i = 0; i < count_snapshot && count < max_d_tags; i++) {
|
||||
active_trigger_t* t = &mgr->triggers[i];
|
||||
if (!t->enabled || t->trigger_type != TRIGGER_TYPE_DM) {
|
||||
continue;
|
||||
@@ -1577,15 +1501,11 @@ int trigger_manager_fire_dm(trigger_manager_t* mgr,
|
||||
continue;
|
||||
}
|
||||
|
||||
int fired = maybe_fire_trigger_locked(mgr, i, event, source_label ? source_label : "dm");
|
||||
if (fired > 0) {
|
||||
fired_total += fired;
|
||||
}
|
||||
out_d_tags[count++] = t->skill_d_tag;
|
||||
}
|
||||
pthread_mutex_unlock(&mgr->mutex);
|
||||
|
||||
cJSON_Delete(event);
|
||||
return fired_total;
|
||||
return count;
|
||||
}
|
||||
|
||||
char* trigger_manager_status_json(trigger_manager_t* mgr) {
|
||||
|
||||
@@ -105,11 +105,11 @@ int trigger_manager_fire_chains(trigger_manager_t* mgr,
|
||||
const char* source_skill_d_tag,
|
||||
cJSON* source_event,
|
||||
const char* source_label);
|
||||
int trigger_manager_fire_dm(trigger_manager_t* mgr,
|
||||
const char* sender_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_sender_tier_t tier,
|
||||
const char* source_label);
|
||||
int trigger_manager_get_dm_skills(trigger_manager_t* mgr,
|
||||
const char* sender_pubkey_hex,
|
||||
didactyl_sender_tier_t tier,
|
||||
char** out_d_tags,
|
||||
int max_d_tags);
|
||||
int trigger_manager_active_count(trigger_manager_t* mgr);
|
||||
int trigger_manager_poll(trigger_manager_t* mgr);
|
||||
char* trigger_manager_status_json(trigger_manager_t* mgr);
|
||||
|
||||
Reference in New Issue
Block a user