v0.0.5 - Release the first binary

This commit is contained in:
Your Name
2026-02-28 08:10:33 -04:00
parent 2209629f23
commit 76842627dc
2 changed files with 280 additions and 54 deletions
+164 -54
View File
@@ -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
View File
@@ -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;
}