# Didactyl — MVP Architecture Plan

## Vision

**Didactyl** is a sovereign AI agent daemon written in C99 that uses Nostr as its sole communication and memory layer. The agent boots on any internet-connected machine, connects to Nostr relays, listens for encrypted commands from its administrator, forwards them to an LLM, and replies via encrypted Nostr messages.

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).

---

## MVP Scope — Proof of Concept

The first version does exactly this:

1. Read config from `config.json` (keys, relays, LLM settings, profile metadata)
2. Read agent context/personality from `SYSTEM.md`
3. Connect to configured relays and stay connected (relay pool with auto-reconnect)
4. Publish a **kind 0** metadata event (agent profile)
5. Subscribe to **kind 4** NIP-04 encrypted DMs addressed to the agent
6. When a DM arrives from the authorized admin pubkey:
   - Decrypt it
   - Send the message + SYSTEM.md context to the configured LLM API
   - Get the LLM response
   - Encrypt and send the response back as a kind 4 DM to the admin
7. Loop forever (daemon)

---

## Architecture

```mermaid
flowchart TD
    A[config.json] -->|keys, relays, LLM config| D[didactyl daemon]
    B[SYSTEM.md] -->|agent context/personality| D
    D -->|1. Connect| R[Nostr Relay Pool]
    D -->|2. Publish kind 0| R
    D -->|3. Subscribe kind 4 + p-tag filter| R
    R -->|4. Incoming encrypted DM| D
    D -->|5. Decrypt with NIP-04| V{From admin pubkey?}
    V -->|No| X[Ignore]
    V -->|Yes| L[LLM API via HTTPS]
    L -->|Response| D
    D -->|6. Encrypt response with NIP-04| R
    R -->|7. Deliver kind 4 DM to admin| ADM[Admin Nostr Client]
```

---

## File Structure

```
didactyl/
├── config.json          # Agent configuration
├── SYSTEM.md            # Agent context/personality for LLM
├── src/
│   ├── main.c           # Entry point, daemon loop
│   ├── config.c          # JSON config parsing
│   ├── config.h          # Config structures and API
│   ├── agent.c           # Core agent logic: receive → LLM → respond
│   ├── agent.h           # Agent API
│   ├── llm.c             # LLM HTTP API client (simple HTTPS POST)
│   ├── llm.h             # LLM API
│   ├── nostr_handler.c   # Nostr relay pool, subscriptions, publish
│   ├── nostr_handler.h   # Nostr handler API
│   └── context.c         # Load SYSTEM.md into memory
│   └── context.h         # Context API
├── Makefile             # Build system
└── README.md            # Project documentation
```

---

## Configuration — `config.json`

```json
{
  "agent": {
    "name": "Didactyl Agent",
    "display_name": "Didactyl",
    "about": "A sovereign AI agent on Nostr",
    "picture": "",
    "nip05": ""
  },
  "keys": {
    "nsec": "nsec1..."
  },
  "admin": {
    "pubkey": "hex-pubkey-of-admin"
  },
  "relays": [
    "wss://relay.damus.io",
    "wss://nos.lol",
    "wss://relay.laantungir.net"
  ],
  "llm": {
    "provider": "openai",
    "api_key": "sk-...",
    "model": "gpt-4o-mini",
    "base_url": "https://api.openai.com/v1",
    "max_tokens": 2048,
    "temperature": 0.7
  }
}
```

---

## Agent Context — `SYSTEM.md`

Recommended name: **`SYSTEM.md`** — this is the universal term LLMs understand as their foundational instructions. Every major LLM API uses "system" as the role for context/personality instructions.

Example content:

```markdown
# Didactyl Agent

You are Didactyl, a sovereign AI agent that lives on the Nostr protocol.

## Personality
- You are helpful, concise, and technically knowledgeable
- You communicate via Nostr encrypted DMs

## Capabilities
- You can answer questions and have conversations
- Your memory persists on Nostr relays

## Rules
- Always be honest about your capabilities
- Keep responses concise for the DM format
```

---

## Component Details

### 1. `main.c` — Entry Point

Responsibilities:
- Parse CLI arguments (config path, debug level, context file path)
- Call `config_load()` to parse `config.json`
- Call `context_load()` to read `SYSTEM.md` into memory
- Call `nostr_init()` to initialize the crypto subsystem
- Decode the nsec from config into a 32-byte private key
- Derive the public key
- Call `nostr_handler_init()` to set up relay pool and connect
- Publish kind 0 metadata event
- Subscribe to kind 4 events with a `#p` tag filter for the agent's pubkey
- Enter the main daemon loop: `nostr_relay_pool_poll()` in a loop
- Handle SIGINT/SIGTERM for clean shutdown
- Call cleanup functions on exit

### 2. `config.c` / `config.h` — Configuration

Structures:
```c
typedef struct {
    char name[128];
    char display_name[128];
    char about[512];
    char picture[256];
    char nip05[256];
} agent_profile_t;

typedef struct {
    char nsec[100];
    unsigned char private_key[32];
    unsigned char public_key[32];
    char public_key_hex[65];
} agent_keys_t;

typedef struct {
    char pubkey[65];  // hex
} admin_config_t;

typedef struct {
    char provider[32];
    char api_key[256];
    char model[64];
    char base_url[256];
    int max_tokens;
    double temperature;
} llm_config_t;

typedef struct {
    agent_profile_t profile;
    agent_keys_t keys;
    admin_config_t admin;
    char** relays;
    int relay_count;
    llm_config_t llm;
} didactyl_config_t;
```

Functions:
- `int config_load(const char* path, didactyl_config_t* config)` — Parse JSON, populate struct
- `void config_free(didactyl_config_t* config)` — Free allocated memory

Uses `cJSON` (already bundled in nostr_core_lib) for parsing.

### 3. `context.c` / `context.h` — System Context

Simple file reader:
- `char* context_load(const char* path)` — Read entire file into malloc'd string
- `void context_free(char* context)` — Free the string

### 4. `nostr_handler.c` / `nostr_handler.h` — Nostr Communication

This wraps the `nostr_core_lib` relay pool API:

Functions:
- `int nostr_handler_init(didactyl_config_t* config)` — Create relay pool, add relays, connect
- `int nostr_handler_publish_profile(didactyl_config_t* config)` — Create and publish kind 0 event
- `int nostr_handler_subscribe_dms(didactyl_config_t* config, dm_callback_t callback, void* user_data)` — Subscribe to kind 4 events tagged with agent's pubkey
- `int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message, didactyl_config_t* config)` — Encrypt and publish a kind 4 DM
- `int nostr_handler_poll(int timeout_ms)` — Drive the event loop
- `void nostr_handler_cleanup(void)` — Destroy pool and cleanup

The DM subscription callback:
```c
typedef void (*dm_callback_t)(const char* sender_pubkey, const char* decrypted_message, void* user_data);
```

When a kind 4 event arrives:
1. Extract the sender pubkey from the event
2. Check if sender matches `config.admin.pubkey` — if not, ignore
3. Decrypt the content using `nostr_nip04_decrypt()`
4. Call the registered callback with the decrypted message

### 5. `agent.c` / `agent.h` — Agent Logic

The brain that ties everything together:

Functions:
- `int agent_init(didactyl_config_t* config, const char* system_context)` — Store config and context
- `void agent_on_message(const char* sender_pubkey, const char* message, void* user_data)` — The DM callback implementation:
  1. Log the incoming message
  2. Call `llm_chat()` with the system context + user message
  3. Send the LLM response back via `nostr_handler_send_dm()`
- `void agent_cleanup(void)` — Cleanup

### 6. `llm.c` / `llm.h` — LLM API Client

A minimal HTTP client for the OpenAI-compatible chat completions API:

Functions:
- `int llm_init(llm_config_t* config)` — Store config
- `char* llm_chat(const char* system_prompt, const char* user_message)` — Send chat completion request, return response string (caller frees)
- `void llm_cleanup(void)` — Cleanup

Implementation approach:
- Use `libcurl` for HTTPS requests (widely available, simple C API)
- Build the JSON request body with `cJSON`:
  ```json
  {
    "model": "gpt-4o-mini",
    "messages": [
      {"role": "system", "content": "<SYSTEM.md content>"},
      {"role": "user", "content": "<decrypted DM message>"}
    ],
    "max_tokens": 2048,
    "temperature": 0.7
  }
  ```
- Parse the JSON response with `cJSON` to extract `choices[0].message.content`

---

## Dependencies

| Dependency | Purpose | Source |
|---|---|---|
| `nostr_core_lib` | Nostr protocol: keys, events, NIP-04, relay pool | In workspace |
| `c_utils_lib` | Debug logging | In workspace |
| `cJSON` | JSON parsing | Bundled in nostr_core_lib |
| `libcurl` | HTTPS for LLM API calls | System package |
| `libssl/libcrypto` | TLS for WebSocket connections | System package (already required by nostr_core_lib) |

---

## Build System — `Makefile`

```makefile
CC = gcc
CFLAGS = -std=c99 -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L
LIBS = -lcurl -lssl -lcrypto -lm -lpthread

# Paths to workspace libraries
NOSTR_LIB = ../nostr_core_lib
C_UTILS_LIB = ../c_utils_lib
CJSON = $(NOSTR_LIB)/cjson

INCLUDES = -I$(NOSTR_LIB) -I$(C_UTILS_LIB)/src -I$(CJSON)

SRCS = src/main.c src/config.c src/context.c src/nostr_handler.c src/agent.c src/llm.c
TARGET = didactyl

all: $(TARGET)

$(TARGET): $(SRCS)
	$(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ \
		$(NOSTR_LIB)/libnostr_core.a \
		$(C_UTILS_LIB)/libc_utils.a \
		$(LIBS)

clean:
	rm -f $(TARGET)
```

---

## Data Flow — Message Lifecycle

```mermaid
sequenceDiagram
    participant Admin as Admin Nostr Client
    participant Relay as Nostr Relays
    participant OW as Didactyl Daemon
    participant LLM as LLM API

    Note over OW: Boot: load config.json + SYSTEM.md
    OW->>Relay: Connect to relay pool
    OW->>Relay: Publish kind 0 profile
    OW->>Relay: Subscribe kind 4 where #p = agent_pubkey

    Admin->>Relay: Send kind 4 encrypted DM to agent
    Relay->>OW: Deliver kind 4 event
    OW->>OW: Verify sender == admin pubkey
    OW->>OW: Decrypt with NIP-04
    OW->>LLM: POST /v1/chat/completions with SYSTEM.md + message
    LLM->>OW: Response text
    OW->>OW: Encrypt response with NIP-04
    OW->>Relay: Publish kind 4 DM to admin
    Relay->>Admin: Deliver encrypted response
```

---

## Main Loop Pseudocode

```c
int main(int argc, char* argv[]) {
    // 1. Parse args (--config, --context, --debug)
    // 2. Load config
    didactyl_config_t config;
    config_load("config.json", &config);
    
    // 3. Load system context
    char* system_context = context_load("SYSTEM.md");
    
    // 4. Init nostr
    nostr_init();
    debug_init(debug_level);
    
    // 5. Decode keys
    nostr_decode_nsec(config.keys.nsec, config.keys.private_key);
    nostr_ec_public_key_from_private_key(config.keys.private_key, config.keys.public_key);
    
    // 6. Init components
    nostr_handler_init(&config);
    llm_init(&config.llm);
    agent_init(&config, system_context);
    
    // 7. Publish profile
    nostr_handler_publish_profile(&config);
    
    // 8. Subscribe to DMs
    nostr_handler_subscribe_dms(&config, agent_on_message, NULL);
    
    // 9. Daemon loop
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    
    while (running) {
        nostr_handler_poll(100);  // 100ms poll interval
    }
    
    // 10. Cleanup
    agent_cleanup();
    llm_cleanup();
    nostr_handler_cleanup();
    config_free(&config);
    context_free(system_context);
    nostr_cleanup();
    
    return 0;
}
```

---

## Implementation Order

1. **Config loader** — parse `config.json`, decode keys
2. **Context loader** — read `SYSTEM.md` into string
3. **Nostr handler** — relay pool connect, kind 0 publish, kind 4 subscribe + send
4. **LLM client** — HTTP POST to chat completions API, parse response
5. **Agent logic** — wire DM callback to LLM and back
6. **Main** — tie it all together with the daemon loop
7. **Makefile** — build system
8. **Test** — manual test with a Nostr client sending DMs to the agent

---

## Future Enhancements (Not MVP)

- Upgrade to NIP-17 gift-wrapped DMs for better privacy
- Conversation history/memory stored as Nostr events
- Agent can store and retrieve its own notes on Nostr
- Multi-turn conversation context window
- Shell command execution
- File storage on Nostr (NIP-94/NIP-96)
- Mnemonic-based key generation for portability
- Multiple admin support with role-based access
- Agent-to-agent communication
