v0.0.5 - Release the first binary
This commit is contained in:
@@ -10,48 +10,53 @@ Didactyl boots on any internet-connected machine, connects to Nostr relays, list
|
||||
|
||||
Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (no single entity can erase its memory).
|
||||
|
||||
**Skills are the new apps.** Agents learn capabilities through skills — public Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. If someone publishes a useful skill, your agent can find it through your web of trust and start using it. Popularity is measured by adoption, not by a rating algorithm. The best skills spread because agents actually use them.
|
||||
|
||||
## Current Status
|
||||
|
||||
**MVP — Working chat agent with relay connectivity and LLM integration.**
|
||||
**Active build — relay-aware autonomous agent with tool-use and Nostr-native startup memory.**
|
||||
|
||||
- Connects to configured Nostr relays with auto-reconnect
|
||||
- Publishes agent profile (kind 0 metadata)
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
- Uses kind `31120` startup content as live Soul at boot
|
||||
- Listens for NIP-04 encrypted DMs from authorized admin
|
||||
- Forwards messages to an OpenAI-compatible LLM API
|
||||
- Sends LLM responses back as encrypted DMs
|
||||
- Runtime logging: relay status, connection health, message flow
|
||||
|
||||
**Next: Agentic tool-use system** — see [plans/didactyl_agentic.md](plans/didactyl_agentic.md).
|
||||
- Builds LLM context from system prompt + startup events + last 12 DM turns
|
||||
- Supports tool-calling loop with configurable max turns and local safety limits
|
||||
- Appends every outbound LLM context payload to [`context.log`](context.log)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- GCC with C99 support
|
||||
- libcurl, libssl, libcrypto, libsecp256k1
|
||||
- Docker (for static binary build)
|
||||
- An OpenAI-compatible LLM API key (OpenAI, PPQ, Ollama, etc.)
|
||||
- A Nostr keypair (nsec)
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
make deps # builds nostr_core_lib
|
||||
make # builds didactyl
|
||||
./build_static.sh # builds a fully static MUSL binary via Docker
|
||||
```
|
||||
|
||||
### Configure
|
||||
|
||||
Edit `config.json`:
|
||||
Edit [`config.json`](config.json):
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr"
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://...",
|
||||
"banner": "https://...",
|
||||
"nip05": ""
|
||||
},
|
||||
"keys": {
|
||||
"nsec": "nsec1..."
|
||||
"nsec": "nsec1...",
|
||||
"npub": "npub1...",
|
||||
"npubHex": "<optional helper>",
|
||||
"nsecHex": "<optional helper>"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub1... or hex pubkey"
|
||||
@@ -61,17 +66,44 @@ Edit `config.json`:
|
||||
"wss://nos.lol"
|
||||
],
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"provider": "openai|ppq|...",
|
||||
"api_key": "sk-...",
|
||||
"model": "gpt-4o-mini",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"enabled": true,
|
||||
"max_turns": 8,
|
||||
"shell": {
|
||||
"enabled": true,
|
||||
"timeout_seconds": 30,
|
||||
"max_output_bytes": 65536,
|
||||
"working_directory": "."
|
||||
}
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "You are Didactyl...",
|
||||
"tags": [["d", "soul"], ["app", "didactyl"], ["scope", "private"]]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {"name": "long_form_note", "description": "..."},
|
||||
"tags": [["d", "long_form_note"], ["app", "didactyl"], ["scope", "public"], ["slug", "long_form_note"]]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [["a", "31123:<author-pubkey>:long_form_note"], ["app", "didactyl"], ["scope", "public"]]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Edit `SYSTEM.md` to define the agent's personality and instructions.
|
||||
`startup_events[].content_fields` is accepted for human-readable authoring and encoded to JSON string content at runtime.
|
||||
|
||||
### Run
|
||||
|
||||
@@ -82,7 +114,7 @@ Edit `SYSTEM.md` to define the agent's personality and instructions.
|
||||
Options:
|
||||
```
|
||||
./didactyl --config <path> # custom config file (default: ./config.json)
|
||||
./didactyl --context <path> # custom context file (default: ./SYSTEM.md)
|
||||
./didactyl --debug <0-5> # log verbosity (0 none, 3 info, 5 trace)
|
||||
```
|
||||
|
||||
### Talk to it
|
||||
@@ -92,45 +124,116 @@ Send an encrypted DM to the agent's pubkey from the admin account using any Nost
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Didactyl │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌────────────┐ │
|
||||
│ │ config │ │ context │ │ agent │ │
|
||||
│ │ loader │ │ loader │ │ loop │ │
|
||||
│ └────┬────┘ └────┬────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ nostr_handler │ │
|
||||
│ │ relay pool · subscribe · publish │ │
|
||||
│ └──────────────────┬──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────┴──────────────────┐ │
|
||||
│ │ LLM client │ │
|
||||
│ │ OpenAI-compatible chat API │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Didactyl │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
|
||||
│ │ config │ │ skills │ │ agent │ │
|
||||
│ │ loader │ │ loader │ │ loop │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ nostr_handler │ │
|
||||
│ │ relay pool · subscribe · publish │ │
|
||||
│ └──────────────────┬──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────┴──────────────────┐ │
|
||||
│ │ LLM client │ │
|
||||
│ │ OpenAI-compatible chat API │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
Nostr Relays LLM API
|
||||
```
|
||||
|
||||
## Didactyl Kinds (Nostr)
|
||||
|
||||
Didactyl uses a two-layer skill model: authors publish public skill definitions, and adopters publish which skills they use.
|
||||
|
||||
- `31120` — **Soul** (private instruction baseline)
|
||||
- `d=soul`
|
||||
- `31123` — **Public Skill Definition** (markdown skill body in `content` or structured JSON in `content_fields`)
|
||||
- `d=<skill_slug>` (example: `d=long_form_note`)
|
||||
- `31124` — **Private Skill Definition** (private/internal procedures)
|
||||
- `d=<skill_slug>` (example: `d=admin_ops`)
|
||||
- `10123` — **Public Skill Adoption List**
|
||||
- tags contain one or more `a` references to selected `31123` skills
|
||||
|
||||
## Skill Sharing & Discovery
|
||||
|
||||
Skills are shared across Nostr without any centralized registry or approval process.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
- **Censorship resistant**: Skills live on relays. No single entity can remove a skill from the network.
|
||||
|
||||
## Startup
|
||||
|
||||
Didactyl startup behavior is configured in [`config.json`](config.json) under `startup_events`.
|
||||
|
||||
Also used at startup:
|
||||
|
||||
- `0` — profile metadata
|
||||
- `10002` — relay list
|
||||
- `1` — optional startup note/status
|
||||
- `3` — contacts/follows (optional placeholder)
|
||||
|
||||
On boot, Didactyl attempts startup publishes to each relay as that relay transitions to connected state.
|
||||
|
||||
## Runtime Context Model
|
||||
|
||||
For each admin DM request, Didactyl builds message context in this order:
|
||||
|
||||
1. Soul message from kind `31120` (or fallback default)
|
||||
2. Startup events memory block (`kinds/content/tags` snapshot)
|
||||
3. Last 12 decrypted DM turns between admin and agent
|
||||
4. Current user message
|
||||
|
||||
Every serialized LLM context payload is appended to [`context.log`](context.log).
|
||||
|
||||
## Tooling Interface
|
||||
|
||||
Current tool schema exposed to the LLM in [`tools_build_openai_schema_json()`](src/tools.c:72):
|
||||
|
||||
- `nostr_post`
|
||||
- `nostr_query`
|
||||
- `shell_exec`
|
||||
- `file_read`
|
||||
- `file_write`
|
||||
|
||||
Execution entrypoint: [`tools_execute()`](src/tools.c:434).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── config.json # Agent configuration
|
||||
├── SYSTEM.md # Agent personality/instructions for LLM
|
||||
├── config.json # Agent/runtime config including startup_events + tools
|
||||
├── context.log # Appended outbound LLM context payloads
|
||||
├── Makefile # Build system
|
||||
├── build_static.sh # Preferred final build validation
|
||||
├── src/
|
||||
│ ├── main.c # Entry point, signal handling, daemon loop
|
||||
│ ├── config.c / .h # JSON config parsing, key decoding
|
||||
│ ├── context.c / .h # SYSTEM.md file loader
|
||||
│ ├── agent.c / .h # Core agent logic: receive → LLM → respond
|
||||
│ ├── main.c # Entry point, args (--config/--debug), lifecycle
|
||||
│ ├── config.c / .h # JSON config parsing, key decode, startup events
|
||||
│ ├── agent.c / .h # Context assembly, tool loop, DM response flow
|
||||
│ ├── tools.c / .h # LLM tool schema and tool execution
|
||||
│ ├── llm.c / .h # LLM HTTP API client (OpenAI-compatible)
|
||||
│ ├── nostr_handler.c / .h # Relay pool, subscriptions, publish, DMs
|
||||
│ └── secp_compat.c # secp256k1 API compatibility shim
|
||||
│ ├── nostr_handler.c / .h # Relay pool, subscriptions, publish, startup reconcile
|
||||
│ └── debug.c / .h # Runtime log levels/macros
|
||||
├── plans/ # Architecture and planning documents
|
||||
│ ├── didactyl_mvp.md
|
||||
│ └── didactyl_agentic.md
|
||||
@@ -139,26 +242,33 @@ Send an encrypted DM to the agent's pubkey from the admin account using any Nost
|
||||
|
||||
## Dependencies
|
||||
|
||||
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 | System package |
|
||||
| libssl / libcrypto | TLS for WebSocket relay connections | System package |
|
||||
| libsecp256k1 | Schnorr signatures, ECDH | System package |
|
||||
| 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
|
||||
|
||||
- [x] MVP chat agent — DM in, LLM response out
|
||||
- [x] Relay pool with auto-reconnect and status logging
|
||||
- [x] Runtime diagnostics — relay health, message flow, LLM calls
|
||||
- [ ] **Agentic tool-use** — LLM can call tools (nostr_post, nostr_query, shell_exec)
|
||||
- [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, shell_exec, file_read, file_write)
|
||||
- [x] Context assembly with startup events + recent DM history
|
||||
- [x] Context payload logging to [`context.log`](context.log)
|
||||
- [x] Skill kind definitions (`31120` Soul, `31123` Public Skill, `31124` Private Skill)
|
||||
- [x] Skill adoption list (`10123`) for WoT-driven discovery
|
||||
- [ ] Runtime skill loading from adopted `31123` events on relays
|
||||
- [ ] Skill discovery CLI/tool (query WoT adoption lists)
|
||||
- [ ] Upgrade to NIP-17 gift-wrapped DMs
|
||||
- [ ] NIP-44 encrypted private skills (`31124`)
|
||||
- [ ] Nostr-native data storage (kind 30078 app-specific events)
|
||||
- [ ] Blossom blob storage integration
|
||||
- [ ] Conversation memory on Nostr
|
||||
- [ ] Config and SYSTEM.md stored as Nostr events
|
||||
- [ ] Multi-turn conversation context window
|
||||
- [ ] Agent-to-agent communication
|
||||
|
||||
## License
|
||||
|
||||
+116
@@ -161,6 +161,119 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
|
||||
if (!tags || !cJSON_IsArray(tags) || !tag_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key->valuestring, tag_key) == 0) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* tag_value) {
|
||||
if (!tags || !cJSON_IsArray(tags) || !tag_key || !tag_value || tag_value[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !val || !cJSON_IsString(key) || !key->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key->valuestring, tag_key) == 0) {
|
||||
if (cJSON_IsString(val)) {
|
||||
if (!cJSON_SetValuestring(val, tag_value)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* new_val = cJSON_CreateString(tag_value);
|
||||
if (!new_val) {
|
||||
return -1;
|
||||
}
|
||||
cJSON_ReplaceItemInArray(tag, 1, new_val);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* new_tag = cJSON_CreateArray();
|
||||
if (!new_tag) {
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_key));
|
||||
cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_value));
|
||||
cJSON_AddItemToArray(tags, new_tag);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
|
||||
if (!item || !tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
if (event_kind != 31123 && event_kind != 31124) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* d_val = find_tag_value_string(tags, "d");
|
||||
if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int needs_normalize =
|
||||
(strcmp(d_val->valuestring, "skill") == 0 || strcmp(d_val->valuestring, "private_skill") == 0);
|
||||
if (!needs_normalize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* slug = NULL;
|
||||
cJSON* slug_val = find_tag_value_string(tags, "slug");
|
||||
if (slug_val && cJSON_IsString(slug_val) && slug_val->valuestring && slug_val->valuestring[0] != '\0') {
|
||||
slug = slug_val->valuestring;
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
|
||||
if (content_fields && cJSON_IsObject(content_fields)) {
|
||||
cJSON* name = cJSON_GetObjectItemCaseSensitive(content_fields, "name");
|
||||
if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
|
||||
slug = name->valuestring;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return set_tag_value_string(tags, "d", slug);
|
||||
}
|
||||
|
||||
static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "startup_events");
|
||||
if (!arr || !cJSON_IsArray(arr)) {
|
||||
@@ -209,6 +322,9 @@ static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
|
||||
|
||||
if (tags) {
|
||||
if (!cJSON_IsArray(tags)) return -1;
|
||||
if (normalize_skill_d_tag(config->startup_events[i].kind, item, tags) != 0) {
|
||||
return -1;
|
||||
}
|
||||
config->startup_events[i].tags_json = cJSON_PrintUnformatted(tags);
|
||||
if (!config->startup_events[i].tags_json) return -1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user