v0.0.72 - Refined wizard framing and menu hotkey display; diagnosed hardcoded Didactyl fallback causing name mismatch

This commit is contained in:
Your Name
2026-03-16 14:37:04 -04:00
parent 5857058a21
commit 0cc0667a80
43 changed files with 25348 additions and 18627 deletions

View File

@@ -178,7 +178,7 @@ Returns the context broken into labeled, individually-sized parts. Useful for un
| Name | Description |
|---|---|
| `system_prompt` | The agent soul / system prompt (first system message) |
| `system_prompt` | The agent base/default skill prompt (first system message) |
| `admin_identity` | Admin pubkey identification message |
| `admin_kind0` | Admin kind 0 profile metadata |
| `admin_relay_list` | Admin kind 10002 relay list |
@@ -513,8 +513,6 @@ The following endpoints are planned but not yet implemented:
| Method | Path | Description |
|---|---|---|
| GET | `/api/config` | Current runtime config with redacted secrets |
| GET | `/api/events/soul` | Fetch agent soul event |
| PUT | `/api/events/soul` | Update soul content |
| GET | `/api/events/skills` | List published skills |
| GET | `/api/events/skills/:d_tag` | Fetch skill by d_tag |
| PUT | `/api/events/skills/:d_tag` | Update skill |

View File

@@ -4,32 +4,26 @@ See also: [SKILLS.md](SKILLS.md) · [TOOLS.md](TOOLS.md)
## What Is Context?
Every time Didactyl talks to an LLM, it sends a **context** — the complete package of information the model needs to reason and respond. Understanding context is fundamental to understanding how Didactyl works.
Every time Didactyl talks to an LLM, it sends a **context** — the complete package of information the model needs to reason and respond.
Context is not just "the prompt." It is everything the LLM receives in a single request:
Context is not just a prompt string; it is the full request payload:
1. **Messages**the conversation: system prompts, user messages, assistant responses, tool results
2. **Tool schemas** the JSON descriptions of every tool the LLM can call
3. **Model parameters** — temperature, max tokens, etc.
The LLM sees all of this together and produces its response based on the totality of what it was given.
1. **Messages**system/user/assistant/tool history
2. **Tool schemas** — JSON descriptions of callable tools
3. **Model parameters** model, temperature, max tokens, seed, etc.
---
## The OpenAI Chat Completion Format
## OpenAI-Compatible Chat Format
Didactyl uses the OpenAI-compatible chat completion API format, which is the industry standard. Every LLM request looks like this:
Didactyl uses OpenAI-compatible chat completions.
```json
{
"model": "claude-opus-4.6",
"messages": [
{"role": "system", "content": "You are Didactyl, a sovereign AI agent..."},
{"role": "system", "content": "Admin profile: ..."},
{"role": "user", "content": "Post a note about Bitcoin"},
{"role": "assistant", "content": null, "tool_calls": [...]},
{"role": "tool", "content": "{\"success\":true,...}", "tool_call_id": "..."},
{"role": "assistant", "content": "Done! I posted your note."}
{"role": "system", "content": "..."},
{"role": "user", "content": "..."}
],
"tools": [
{
@@ -37,7 +31,7 @@ Didactyl uses the OpenAI-compatible chat completion API format, which is the ind
"function": {
"name": "nostr_post",
"description": "Publish a Nostr event",
"parameters": { ... }
"parameters": {"type":"object"}
}
}
],
@@ -46,145 +40,122 @@ Didactyl uses the OpenAI-compatible chat completion API format, which is the ind
}
```
### Messages
Messages are an ordered array. Each message has a `role`:
### Message Roles
| Role | Purpose |
|------|---------|
| `system` | Instructions, identity, context data — the LLM reads these but the user didn't write them |
| `user` | What the human (or trigger event) said |
| `assistant` | What the LLM previously said or did (including tool calls) |
| `tool` | Results from tool executions, fed back to the LLM |
The LLM processes all messages in order and generates the next response.
### Tool Schemas
The `tools` array describes every tool the LLM can call. Each tool has a name, description, and JSON Schema for its parameters. The LLM uses these descriptions to decide when and how to call tools.
Tools are not part of the message history — they are a separate capability declaration sent alongside the messages.
### Model Parameters
Temperature, max tokens, and other settings control how the LLM generates its response. These can be set globally or overridden per-skill.
| `system` | Instructions and injected context |
| `user` | Input message or trigger payload |
| `assistant` | Model responses / tool call envelopes |
| `tool` | Tool execution results fed back to model |
---
## How Didactyl Builds Context
## Context Assembly Model
Didactyl assembles context dynamically for each request. The context varies depending on what triggered the request (DM, trigger event) and which skill is active.
Didactyl uses **skill composition by adoption order**.
### Context Assembly Flow
There are no context modes.
### 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).
```mermaid
flowchart TD
INPUT[Input: DM message or trigger event] --> MODE{Skill context_mode?}
MODE -->|inject| INJECT[Start with agent soul/personality]
INJECT --> ADD_ADMIN[Add admin identity + profile]
ADD_ADMIN --> ADD_SKILLS[Add adopted skill instructions]
ADD_SKILLS --> ADD_HISTORY[Add conversation history]
ADD_HISTORY --> ADD_SKILL_INST[Append this skill instructions]
ADD_SKILL_INST --> ADD_USER[Add user message or trigger event]
MODE -->|full| FULL[Start with skill template only]
FULL --> RESOLVE[Resolve template variables via tool calls]
RESOLVE --> ADD_USER2[Add user message or trigger event]
MODE -->|override| OVERRIDE[Replace soul with skill prompt]
OVERRIDE --> ADD_ADMIN2[Add admin identity + profile]
ADD_ADMIN2 --> ADD_SKILLS2[Add adopted skill instructions]
ADD_SKILLS2 --> ADD_HISTORY2[Add conversation history]
ADD_HISTORY2 --> ADD_USER3[Add user message or trigger event]
ADD_USER --> TOOLS[Attach tool schemas]
ADD_USER2 --> TOOLS
ADD_USER3 --> TOOLS
TOOLS --> LLM[Send to LLM]
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]
```
### Context Parts
### Why Order Matters
These are the building blocks that get assembled into the messages array:
- Earlier adopted skills usually establish broad behavior.
- Later adopted skills can refine or narrow behavior.
- If instructions conflict, prompt-order effects apply.
---
## Context Parts
| Part | Source | Description |
|------|--------|-------------|
| **Soul/Personality** | Kind 31120 event | The agent's identity, rules, and behavior instructions |
| **Admin Identity** | Config / kind 3 | Who the admin is, their pubkey |
| **Admin Profile** | Kind 0 event | Admin's display name, about, picture |
| **Admin Relay List** | Kind 10002 event | Admin's preferred relays |
| **Admin Notes** | Kind 1 events | Admin's recent public posts — gives the agent awareness of what the admin is doing |
| **Adopted Skills** | Kind 31123/31124 events | Instructions from other adopted skills |
| **Conversation History** | Kind 4/14 events | Recent DM exchanges between admin and agent |
| **Skill Instructions** | Active skill content | The specific skill being executed |
| **User Message** | DM or trigger event | The input that triggered this execution |
| **Tool Schemas** | Tool registry | JSON descriptions of available tools (sent separately from messages) |
### Template Variables Are Tool Calls
The soul's template system uses `tool:` directives to populate context sections. Each section can specify a tool to call, and the tool's output becomes that section's content:
```yaml
- section: admin_profile
role: system
tool: nostr_admin_profile
skip_if_empty: true
```
This calls `nostr_admin_profile`, gets the result, and inserts it as a system message. Template variables like `{{admin_profile}}` in skill templates work the same way — they resolve to tool calls. There is one unified resolution mechanism: `tools_execute()`.
This means new context sources are added by adding tools, not by modifying the template engine.
| 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 |
---
## Context Modes
## Template Variables Are Tool Calls
Skills control how much of the default context the LLM sees using the `context_mode` field:
Template variables resolve through tool execution.
| Mode | What the LLM sees | Use case |
|------|-------------------|----------|
| `inject` | Everything: soul + admin context + history + skill instructions | Default. Agent stays itself, gains new knowledge. |
| `full` | Only what the skill template provides | Focused tasks: spellcheck, translation. Minimal context = cheaper, faster. |
| `override` | Skill replaces soul, but admin context + history still included | Different personality, same capabilities. |
Example:
Most skills use `inject` (the default). `full` is for specialized tasks where you want to minimize token cost. `override` is rare.
- `{{admin_profile}}` resolves by running `nostr_admin_profile`
- `{{admin_notes}}` resolves by running `nostr_admin_notes`
Unknown variables should resolve to empty values for portability.
---
## Context for Triggered Skills
## Trigger Runtime Parameters
When a trigger fires (Nostr subscription match, webhook POST, cron timer, chain completion), the context is assembled the same way as for a DM — the only difference is the user message:
Execution controls are attached to trigger tags, not skill content:
| Input Source | User Message Content |
|---|---|
| Admin DM | The DM text |
| Nostr subscription trigger | The matching Nostr event as JSON |
| Webhook trigger | The HTTP POST payload as JSON |
| Cron trigger | A synthetic event with timestamp and cron expression |
| Chain trigger | A synthetic event with the source skill's d_tag and output |
- `llm`
- `max_tokens`
- `temperature`
- `seed`
- `tools`
The LLM receives the skill instructions as system context and the triggering event as the user message, then reasons about what to do and which tools to use.
Resolution order for a triggered run:
1. Start with agent defaults
2. Apply trigger tag overrides
3. Execute
4. Restore defaults
---
## Triggered vs Adopted Use
- **Adopted skill (`10123`)**: contributes context/instructions
- **Triggered skill**: contributes context and may supply execution overrides via tags
This separation keeps composition simple while allowing per-trigger runtime control.
---
## Token Budget
Context has a cost — every token sent to the LLM costs money and takes time. Didactyl manages this through:
Context cost is controlled by:
- **Context mode selection** — `full` mode skills send minimal context
- **Skip-if-empty sections** — template sections with no content are omitted
- **History limits** — conversation history is capped (default: 12 turns)
- **Skill content limits** — adopted skill instructions are truncated to prevent context overflow
- **Per-skill model selection** — cheap models for simple tasks, expensive models for complex ones
- Adoption-list ordering and skill count
- Conversation-history limits
- Skill/template truncation limits
- Per-trigger model/runtime parameter choices
The `/api/context/current` and `/api/context/parts` API endpoints let you inspect exactly what context would be sent right now, including character counts and token estimates.
Use runtime context inspection endpoints to see the exact payload before LLM calls.
---
## Related Documentation
- Skills — how to define LLM execution units: [SKILLS.md](SKILLS.md)
- Tools — what the LLM can do: [TOOLS.md](TOOLS.md)
- API — inspect context at runtime: [API.md](API.md)
- Skills spec: [SKILLS.md](SKILLS.md)
- Tool catalog: [TOOLS.md](TOOLS.md)
- API details: [API.md](API.md)

136
docs/GENESIS.md Normal file
View File

@@ -0,0 +1,136 @@
# Didactyl — Genesis Bootstrapping
See also: [CONTEXT.md](CONTEXT.md) · [SKILLS.md](SKILLS.md) · [README.md](../README.md)
## Purpose
`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.
After bootstrap, the long-term direction is **nsec-only startup** with state recovered from Nostr events.
---
## File Format
`genesis.jsonc` is JSONC (JSON + comments).
Minimum practical sections:
- `key.nsec` (or runtime `--nsec` / `DIDACTYL_NSEC`)
- `admin.pubkey`
- `llm`
- `startup_events` (must include kind `10002` relay tags)
Typical optional sections:
- `dm_protocol`
- `tools`
- `security`
- `admin_context`
- `api`
- `default_skill` (reserved for default-skill publishing workflows)
---
## First-Run Flow
On first run, the agent:
1. Loads `genesis.jsonc`.
2. Derives keys (from `key.nsec` or runtime nsec override).
3. Connects to relay set from startup kind `10002` tags.
4. Publishes/reconciles startup events.
5. Initializes runtime services (DM subscriptions, triggers, API if enabled).
First-run detection is based on querying own kind `10002` relay-list availability.
---
## Subsequent-Run Flow
On subsequent runs, the agent can start with nsec supplied via:
- CLI: `--nsec <nsec_or_hex>`
- Environment: `DIDACTYL_NSEC`
Optional runtime API overrides:
- `--api-port <port>`
- `--api-bind <address>`
Subsequent-run bootstrap-event republishing is skipped when prior kind `10002` state is found.
---
## Interactive Setup (Zero-Argument Startup)
When Didactyl is run with **no arguments**:
```bash
./didactyl
```
it enters an interactive setup wizard instead of immediately trying to load `./genesis.jsonc`.
Wizard entry choices:
- **New agent** — generate/provide identity, configure admin + LLM + relays
- **Existing agent** — provide nsec and recover relay/admin/LLM config from Nostr
- **Load genesis** — load a specified genesis file path
Menu UX conventions:
- Single-letter hotkeys (case-insensitive)
- First-letter menu mnemonics (e.g., `N` for New, `E` for Existing)
- `q` / `x` exits or backs out of menus
Security behavior:
- nsec entry is masked in terminal
- writing genesis with nsec is explicit and warned
- recommended export mode is genesis without nsec plus runtime `--nsec`/`DIDACTYL_NSEC`
---
## Encrypted Config Events
Didactyl exposes config persistence tools for encrypted self-config on Nostr:
- `config_store` — publish encrypted kind `30078` config by `d_tag`
- `config_recall` — query+decrypt kind `30078` config by `d_tag`
Recommended tags:
- `d=llm_config`
- `d=agent_config`
These are encrypted to self with NIP-44.
---
## Relay Bootstrap Strategy
`startup_events` must include kind `10002` relay tags (`["r", "wss://..."]`).
That relay list is used as the initial network attachment for querying existing state and publishing startup events.
---
## Migration Notes (Legacy -> Genesis)
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.
---
## Security Notes
- Keep nsec secret.
- Prefer environment or secure credential injection for production nsec handling.
- Avoid publishing plaintext sensitive config; use encrypted `config_store` for long-term state.

View File

@@ -4,11 +4,13 @@ See also: [CONTEXT.md](CONTEXT.md) · [TOOLS.md](TOOLS.md)
## Overview
A skill is a **set of instructions for the LLM** stored as a Nostr event. Skills teach the agent how to accomplish tasks — the LLM reads the instructions, reasons about them, and uses tools to take action.
A skill is a **set of instructions for the LLM** stored as a Nostr event.
Think of it like a woodshop: a **skill** is knowing how to carve — the technique, the judgment, the decision-making. A **tool** is the chisel. The skill never directly uses the chisel without the craftsperson (the LLM) in the loop. If you want a hardcoded program that runs without reasoning, that's a tool or an external program — not a skill.
Skills teach the agent how to accomplish tasks — the LLM reads the instructions, reasons about them, and uses tools to take action.
Skills are portable, shareable, and discoverable — they live on Nostr relays as standard events. Every skill execution involves the LLM.
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.
---
@@ -24,20 +26,14 @@ Skills are portable, shareable, and discoverable — they live on Nostr relays a
## Skill Content
The skill event's `content` field is a JSON object that defines the complete execution specification:
Skill `content` is JSON and should focus on **instructions**, not transport/runtime controls.
```json
{
"kind": 31123,
"content": {
"description": "Check spelling and grammar",
"context_mode": "full",
"llm": "openai/gpt-4o-mini, cheap",
"tools": false,
"max_tokens": 2000,
"temperature": 0.1,
"seed": 42,
"template": "system:\nYou are a spelling and grammar checker.\n\nRules:\n- Fix spelling errors\n- Fix grammar errors\n- Preserve original formatting\n- Ignore: API, JSON, HTTP, nostr, pubkey, npub, nsec, NIP\n- Canadian English preferred\n- Return ONLY the corrected text, no explanations\n\nuser:\n{{message}}"
"template": "system:\nYou are a spelling and grammar checker.\n\nRules:\n- Fix spelling errors\n- Fix grammar errors\n- Preserve original formatting\n- Return ONLY the corrected text, no explanations\n\nuser:\n{{message}}"
},
"tags": [
["d", "spellcheck"],
@@ -52,31 +48,176 @@ The skill event's `content` field is a JSON object that defines the complete exe
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `description` | string | — | Human-readable description |
| `context_mode` | string | `inject` | `inject`, `full`, or `override` |
| `llm` | string | `default` | LLM fallback chain |
| `tools` | bool/array | `true` | `false` = no tools, `true` = all tools, array = specific tool names |
| `template` | string | — | Context template (required for `full` mode) |
| `soul` | bool | `true` | Whether to include the agent's soul in context |
| `max_tokens` | int | — | Override max tokens for this skill |
| `temperature` | float | — | Override temperature for this skill |
| `seed` | int | — | Optional deterministic seed (effective only on providers/engines that support seeded sampling) |
| `template` | string | — | Skill instructions/template text (recommended) |
| `base` | bool | `false` | Optional hint that this skill is intended as base/default behavior |
### Determinism Controls
> Execution parameters (`llm`, `max_tokens`, `temperature`, `seed`, `tools`) are defined on **trigger tags**, not in content.
For reproducible outputs, set `temperature` low (typically `0`) and also set `seed` when the selected model backend supports it. If a provider ignores seeds, `temperature` still applies but deterministic replay is not guaranteed.
---
## Composition Model (No Context Modes)
Skills do **not** use `context_mode`.
Context is assembled from kind `10123` adoption list order:
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.
The adoption list itself is the context definition.
- One adopted skill = single-skill behavior.
- Multiple adopted skills = layered behavior in explicit order.
- Reordering `10123` changes precedence naturally.
### Ordering Convention
- 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.
### Template Variables Are Tool Calls
Template variables are tool calls.
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.
---
## Triggered Skills
A triggered skill has a trigger source attached.
Didactyl trigger types:
- `nostr-subscription`
- `webhook`
- `cron`
- `chain`
- `dm`
### 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 |
### 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.
---
## Trigger Types
### `nostr-subscription`
`filter` is a JSON-encoded Nostr subscription filter.
```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"]
```
### `cron`
`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"]
```
### `chain`
`filter` is the source skill `d` tag to chain from.
```json
["trigger", "chain"],
["filter", "source-skill-d-tag"],
["llm", "default"],
["enabled", "true"]
```
### `dm`
`filter` is JSON with sender scope:
- `{"from":"admin"}`
- `{"from":"wot"}`
- `{"from":"any"}`
```json
["trigger", "dm"],
["filter", "{\"from\":\"admin\"}"],
["llm", "default"],
["tools", "true"],
["enabled", "true"]
```
---
## Private Skill Encoding (`31124`)
Private skills use NIP-44 encryption on the event `content` field.
Private skills use NIP-44 encryption on event `content`.
Rules for kind `31124`:
- Keep `d` tag exposed so the event remains addressable/replaceable.
- Move all other skill metadata (including non-`d` tags) into the plaintext payload before encryption.
- Encrypt the full payload with NIP-44 and store the ciphertext in event `content`.
- On receive: resolve by `d`, decrypt `content`, then read both content variables and tag values from decrypted payload.
- 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.
### Private Skill Event (on relay)
@@ -96,241 +237,21 @@ Rules for kind `31124`:
{
"content": {
"description": "Monitor mentions and DM summaries",
"context_mode": "inject",
"llm": "openai/gpt-4o-mini, fast",
"tools": ["nostr_query", "nostr_dm"],
"temperature": 0,
"seed": 42,
"template": "When {{triggering_event}} includes Bitcoin or Lightning, summarize and DM admin."
},
"private_tags": [
["scope", "private"],
["trigger", "nostr-subscription"],
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
["llm", "openai/gpt-4o-mini, fast"],
["temperature", "0"],
["seed", "42"],
["tools", "nostr_query,nostr_dm"],
["enabled", "true"]
]
}
```
This mirrors the DM-style encrypted payload pattern (historically associated with NIP-04), but private skills should use NIP-44 ciphertext encoding.
## Context Modes
Skills control how the LLM context window is assembled using the `context_mode` field:
| Mode | Description | Use Case |
|------|-------------|----------|
| `inject` | Skill instructions appended to the agent's default context | Behavioral rules, knowledge additions |
| `full` | Skill provides its own complete context template | Spellcheck, translation, focused analysis |
| `override` | Skill replaces the default system prompt, keeps standard context structure | Different personality, same capabilities |
Default is `inject`. See the agent's soul/personality documentation for details on how context assembly works.
### Template Variables Are Tool Calls
Template variables are tool calls. When the template engine encounters a variable like `{{admin_profile}}`, it runs the corresponding tool and inserts the result into the context that gets sent to the LLM.
This is the unified resolution model: the soul prompt template uses `tool:` section directives, and skill templates use `{{tool_name}}` inline syntax, but both resolve through `tools_execute()`. There is no separate variable-resolver layer — every variable is backed by a tool.
| Variable | Tool Called | Description |
|----------|-----------|-------------|
| `{{soul}}` | *(built-in)* | The agent's identity/system context |
| `{{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` | For triggered skills, the event JSON |
This means new data sources are added by adding tools, not by extending the template engine. If you can call it as a tool, you can use it as a template variable.
---
## LLM Specification with Fallback Chains
Each skill specifies which LLM to use with CSS font-family-style fallbacks. The runtime tries each model in order; if one is unavailable (API error, rate limit, not configured), it falls back to the next.
### Format
```
model-spec := model-ref ["," model-ref]*
model-ref := provider "/" model-name
| category-alias
```
### Examples
| Skill | LLM Spec |
|-------|----------|
| Spelling checker | `openai/gpt-4o-mini, cheap` |
| Complex analysis | `anthropic/claude-sonnet-4-20250514, openai/gpt-4o, smart` |
| Translation | `openai/gpt-4o-mini, fast` |
| Default behavior | `default` |
### Category Aliases
Aliases map to models in the agent's config:
```json
{
"llm": {
"provider": "openai",
"model": "gpt-4o",
"aliases": {
"fast": "openai/gpt-4o-mini",
"cheap": "openai/gpt-4o-mini",
"smart": "anthropic/claude-sonnet-4-20250514"
}
}
}
```
If all specified models fail, the agent's default model is used as a last resort.
```mermaid
sequenceDiagram
participant Skill as Skill Spec
participant Resolver as LLM Resolver
participant API1 as Model 1
participant API2 as Model 2
participant Default as Default Model
Skill->>Resolver: "anthropic/claude-sonnet, openai/gpt-4o-mini, cheap"
Resolver->>API1: try anthropic/claude-sonnet
alt Available
API1-->>Resolver: ✅
else Unavailable
API1-->>Resolver: ❌
Resolver->>API2: try openai/gpt-4o-mini
alt Available
API2-->>Resolver: ✅
else Unavailable
Resolver->>Default: resolve "cheap" alias
end
end
```
---
## Triggered Skills
A triggered skill has a trigger source attached. Didactyl supports `nostr-subscription`, `webhook`, `cron`, and `chain` trigger types.
### Trigger Tags
| Tag | Required | Description |
|---|---|---|
| `trigger` | Yes | Trigger type: `nostr-subscription`, `webhook`, `cron`, or `chain` |
| `filter` | Yes | Type-specific filter — see Trigger Types below |
| `enabled` | No | Whether active (default: `true`) |
> **Note:** The `action` tag is deprecated. All triggered skills are LLM-mediated. If you need a fast hardcoded action without LLM reasoning, use a tool or an external program — not a skill. Skills are instructions for the LLM; the LLM is always in the loop.
### Triggered Skill Execution
When a trigger fires, the skill content is sent to the LLM as instructions along with the triggering event JSON. The LLM reads both, reasons about what to do, and uses tools to take action.
This is the same execution model as DM-invoked skills — the only difference is the input source. A DM-invoked skill receives a user message; a triggered skill receives a triggering event.
```json
{
"kind": 31123,
"content": "You monitor Nostr mentions. When the triggering event mentions Bitcoin or Lightning, summarize it and DM the admin. Otherwise, ignore silently.",
"tags": [
["d", "mention-monitor"],
["trigger", "nostr-subscription"],
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"],
["enabled", "true"]
]
}
```
A cron-triggered skill:
```json
{
"kind": 31123,
"content": "DM the admin a short heartbeat status message with the current time.",
"tags": [
["d", "heartbeat"],
["trigger", "cron"],
["filter", "*/5 * * * *"],
["enabled", "true"]
]
}
```
### Trigger Types
#### `nostr-subscription`
`filter` is a JSON-encoded Nostr subscription filter.
```json
["trigger", "nostr-subscription"],
["filter", "{\"#p\":[\"<admin_pubkey>\"],\"kinds\":[1]}"]
```
#### `webhook`
`filter` is required for schema compatibility and can be a simple placeholder like `{}`; webhook firing happens via HTTP.
```json
["trigger", "webhook"],
["filter", "{}"]
```
#### `cron`
`filter` is a standard 5-field cron expression: `minute hour day-of-month month day-of-week`.
```json
["trigger", "cron"],
["filter", "0 * * * *"]
```
#### `chain`
`filter` is the source skill `d` tag to chain from.
```json
["trigger", "chain"],
["filter", "source-skill-d-tag"]
```
### Trigger Lifecycle
```mermaid
flowchart TD
subgraph Creation
ADMIN_CMD[Admin: 'Warn me when @jack posts'] --> LLM_REASON[LLM resolves pubkey + builds skill]
LLM_REASON --> SKILL_CREATE[skill_create with trigger tags]
SKILL_CREATE --> PUBLISHED[Skill published to Nostr]
end
subgraph Activation
STARTUP[Didactyl starts up] --> LOAD_SKILLS[Load adopted skills from kind 10123]
LOAD_SKILLS --> FIND_TRIGGERS[Find skills with trigger tags]
FIND_TRIGGERS --> REGISTER[Register trigger by type]
REGISTER --> NOSTR_SUB[nostr-subscription: create Nostr subscriptions]
REGISTER --> CRON_REG[cron: keep expression for poll loop]
REGISTER --> WEBHOOK_REG[webhook: route via /api/trigger/:d_tag]
REGISTER --> CHAIN_REG[chain: wait for source skill completion]
end
subgraph Execution
EVENT_IN[Matching Nostr event] --> LOOKUP[Find associated skill]
WEBHOOK_IN[POST /api/trigger/:d_tag] --> LOOKUP
CRON_TICK[cron poll match] --> LOOKUP
LOOKUP --> RESOLVE[Resolve LLM + assemble context + run with tools]
RESOLVE --> CHAIN_CHECK[Check chain triggers]
CHAIN_CHECK --> CHAIN_FIRE[Fire matching chain triggers]
end
PUBLISHED --> LOAD_SKILLS
```
---
## Execution Flow
@@ -339,30 +260,18 @@ flowchart TD
sequenceDiagram
participant Input as Message/Trigger
participant Dispatch as Dispatcher
participant Skill as Skill Resolver
participant LLM_Res as LLM Resolver
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->>Skill: which skill handles this?
alt No specific skill
Skill-->>Dispatch: use default soul context
Dispatch->>LLM_Res: resolve "default" LLM
else Skill with context_mode=inject
Skill-->>Dispatch: soul + skill instructions
Dispatch->>LLM_Res: resolve skill.llm or "default"
else Skill with context_mode=full
Skill-->>Dispatch: skill template only
Dispatch->>LLM_Res: resolve skill.llm
end
LLM_Res->>LLM_Res: try models in fallback order
LLM_Res-->>Dispatch: resolved model
Dispatch->>Ctx: assemble context per mode
Ctx->>LLM: request with resolved model + context
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
```
@@ -384,21 +293,25 @@ sequenceDiagram
|---|---|
| Skills | Kind 31123/31124 events |
| Adopted skills | Kind 10123 event |
| Trigger definitions | Tags on skill events |
| Trigger definitions + execution params | Tags on skill events |
---
## Future Extensions
## Portability Guidelines
| Extension | Description |
|---|---|
| Skill composition | Pipeline multiple skills |
| Agent-to-agent sharing | Discover and adopt skills across agents |
| Trigger marketplace | Popular triggers rise via adoption count |
To keep skills reusable across agents/clients:
- 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"]`).
A skill should still be useful even when some variables/tools are unavailable.
---
## Related Documentation
- Tool architecture and complete tool catalog: [TOOLS.md](TOOLS.md)
- Project overview and runtime behavior: [README.md](../README.md)
- Context assembly model: [CONTEXT.md](CONTEXT.md)
- Project overview/runtime behavior: [README.md](../README.md)