# Didactyl — Agentic Tool-Use Architecture Plan

## Vision

Didactyl is a **Nostr-first sovereign AI agent**. Where traditional agents ride on top of Linux — reading files, writing to disk — Didactyl rides on top of Nostr. Events are its files. Relays are its network bus. Blossom is its blob storage. The Linux host is just the runtime substrate.

The agent receives commands via encrypted Nostr DMs, uses an LLM to reason about them, and can take actions in its environment by calling **tools** — primarily Nostr-native tools, with shell access as an escape hatch.

---

## Current State (MVP)

The MVP is a linear chat pipeline:

```
DM in → LLM chat → DM out
```

The agent can only talk. It cannot act.

---

## Target State (Agentic)

The agent has an **agent loop** with tool-calling:

```
DM in → [LLM decides → execute tool → observe result → repeat] → DM out
```

The LLM can request actions, observe results, and iterate until it has a final answer.

---

## Architecture

```mermaid
flowchart TD
    subgraph NOSTR[Nostr Layer]
        EVENTS[Events = Data/Files]
        RELAYS[Relays = Network]
        DMS[DMs = Command Channel]
        BLOSSOM[Blossom = Blob Storage]
    end

    subgraph LINUX[Linux Layer]
        SHELL[Shell = Escape Hatch]
    end

    subgraph AGENT[Didactyl Agent Loop]
        LLM[LLM Brain]
        TOOLS[Tool Registry]
        LOOP{Agent Loop}
    end

    DMS -->|incoming command| LOOP
    LOOP --> LLM
    LLM -->|tool_call| TOOLS
    TOOLS -->|nostr tools| NOSTR
    TOOLS -->|local_shell_exec| SHELL
    TOOLS -->|result| LOOP
    LLM -->|final answer| DMS
```

---

## How Tool Calling Works

### The Protocol

All communication with the LLM is JSON via the OpenAI-compatible chat completions API. Tool definitions are sent as a structured `tools` parameter in every request — not in SYSTEM.md.

### Request with tools

```json
{
  "model": "gpt-4o-mini",
  "messages": [
    {"role": "system", "content": "You are Didactyl..."},
    {"role": "user", "content": "Post a joke"}
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "nostr_post",
        "description": "Publish a Nostr event to connected relays",
        "parameters": {
          "type": "object",
          "properties": {
            "kind": {"type": "integer", "description": "Event kind number"},
            "content": {"type": "string", "description": "Event content"}
          },
          "required": ["kind", "content"]
        }
      }
    }
  ]
}
```

### LLM responds with tool_call

```json
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "nostr_post",
          "arguments": "{\"kind\": 1, \"content\": \"Why do programmers prefer dark mode? Because light attracts bugs!\"}"
        }
      }]
    }
  }]
}
```

When `content` is null and `tool_calls` is present: **execute the tool, don't reply yet**.

### Didactyl sends tool result back

```json
{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "{\"success\": true, \"event_id\": \"abc...\", \"relays_published\": 3}"
}
```

### LLM gives final answer

```json
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "Done! I posted a joke to 3 relays."
    }
  }]
}
```

When `content` has text and no `tool_calls`: **this is the final answer, DM it back**.

---

## Message Lifecycle

```mermaid
sequenceDiagram
    participant Admin as Admin via Nostr DM
    participant Agent as Didactyl Agent Loop
    participant LLM as LLM API
    participant Tools as Tool Registry
    participant Nostr as Nostr Relays
    participant Shell as Local Shell

    Admin->>Agent: Encrypted DM: Post a joke
    Agent->>LLM: messages + tool definitions

    loop Until final answer
        LLM->>Agent: tool_call: nostr_post kind=1 content=joke
        Agent->>Tools: dispatch nostr_post
        Tools->>Nostr: Publish kind 1 event
        Nostr->>Tools: OK
        Tools->>Agent: result JSON
        Agent->>LLM: tool result + continue
    end

    LLM->>Agent: final text: Done! Posted to 3 relays
    Agent->>Admin: Encrypted DM reply
```

---

## Tool Inventory

### Tier 1: Core Nostr Tools

| Tool | Description | Underlying API |
|---|---|---|
| `nostr_post` | Publish any kind event to relays | `nostr_create_and_sign_event()` + `nostr_relay_pool_publish_async()` |
| `nostr_query` | Query relays with filters, return matching events | `nostr_relay_pool_query_sync()` |
| `nostr_dm` | Send a private DM via NIP-17 gift wrap | `nostr_nip17_send_dm()` |
| `nostr_profile` | Update the agent's kind 0 metadata | `nostr_create_and_sign_event(0, ...)` |

### Tier 2: Extended Nostr Tools

| Tool | Description | Notes |
|---|---|---|
| `nostr_follow` | Publish/update kind 3 contact list | Build tags array of p-tags |
| `nostr_encrypt` | Encrypt content for a pubkey | NIP-44 |
| `nostr_decrypt` | Decrypt content from a pubkey | NIP-44 |
| `nostr_relay_info` | Get relay information document | NIP-11 |
| `nostr_verify_nip05` | Verify a NIP-05 identifier | NIP-05 |

### Tier 3: Storage Tools

| Tool | Description | Notes |
|---|---|---|
| `nostr_note_save` | Store agent data as kind 30078 event | Agent's filesystem on Nostr |
| `nostr_note_load` | Retrieve stored data events | Query own events |
| `blossom_upload` | Upload blob to Blossom server | Future |
| `blossom_download` | Download blob from Blossom | Future |

### Tier 4: Linux Escape Hatch

| Tool | Description | Notes |
|---|---|---|
| `local_shell_exec` | Run a shell command, capture stdout/stderr | Sandboxed with timeouts |

---

## File Structure

```
src/
├── main.c              # Entry point, daemon loop (existing)
├── config.c / .h       # Configuration parsing (existing, extended)
├── context.c / .h      # SYSTEM.md loader (existing)
├── nostr_handler.c / .h # Relay pool, subscriptions (existing)
├── llm.c / .h          # LLM client (existing, extended for tool calling)
├── agent.c / .h        # Agent logic (existing, rewritten for agent loop)
├── tools.h             # Tool definition struct, registry API (NEW)
├── tools.c             # Tool registry, dispatch, JSON helpers (NEW)
├── tool_nostr.c        # Nostr-native tools implementation (NEW)
├── tool_shell.c        # Shell execution with sandboxing (NEW)
└── secp_compat.c       # secp256k1 compatibility (existing)
```

---

## Key Data Structures

### Tool definition

```c
typedef struct {
    const char* name;
    const char* description;
    const char* parameters_json;  // JSON Schema for parameters
    char* (*execute)(const char* args_json, void* ctx);
} tool_t;
```

### Tool registry

```c
typedef struct {
    tool_t* tools;
    int count;
    int capacity;
    didactyl_config_t* cfg;
    nostr_relay_pool_t* pool;
} tool_registry_t;
```

### LLM tool call (parsed from response)

```c
typedef struct {
    char id[64];
    char name[64];
    char* arguments_json;
} llm_tool_call_t;
```

### LLM response (extended)

```c
typedef struct {
    char* content;              // final text, or NULL if tool calls
    llm_tool_call_t* tool_calls;
    int tool_call_count;
} llm_response_t;
```

### Conversation message (for multi-turn)

```c
typedef enum {
    MSG_ROLE_SYSTEM,
    MSG_ROLE_USER,
    MSG_ROLE_ASSISTANT,
    MSG_ROLE_TOOL
} msg_role_t;

typedef struct {
    msg_role_t role;
    char* content;
    llm_tool_call_t* tool_calls;  // for assistant messages
    int tool_call_count;
    char tool_call_id[64];        // for tool result messages
} conversation_msg_t;
```

---

## Agent Loop Pseudocode

```c
void agent_on_message(const char* sender, const char* message) {
    conversation_msg_t messages[MAX_TURNS * 2 + 2];
    int msg_count = 0;

    // Build initial messages
    messages[msg_count++] = {SYSTEM, system_context};
    messages[msg_count++] = {USER, message};

    for (int turn = 0; turn < MAX_TOOL_TURNS; turn++) {
        llm_response_t resp = llm_chat_with_tools(messages, msg_count, tool_registry);

        if (resp.content != NULL) {
            // Final answer — send DM and done
            nostr_handler_send_dm(sender, resp.content);
            return;
        }

        // Tool calls — execute each one
        messages[msg_count++] = {ASSISTANT, NULL, resp.tool_calls, resp.tool_call_count};

        for (int i = 0; i < resp.tool_call_count; i++) {
            char* result = tools_execute(registry, resp.tool_calls[i].name, resp.tool_calls[i].arguments_json);
            messages[msg_count++] = {TOOL, result, .tool_call_id = resp.tool_calls[i].id};
        }
    }

    // Max turns exceeded
    nostr_handler_send_dm(sender, "I hit my tool-use limit for this request.");
}
```

---

## Security Model

| Control | Description | Config key |
|---|---|---|
| Admin-only access | Only admin pubkey can send commands | `admin.pubkey` (existing) |
| Max tool turns | Limit iterations per request | `tools.max_turns` |
| Shell timeout | Kill shell commands after N seconds | `tools.shell_timeout_seconds` |
| Shell allowlist | Optional list of allowed commands | `tools.shell_allowlist` |
| Working directory | Confine file operations | `tools.working_directory` |
| Output size limit | Cap tool output to prevent memory issues | `tools.max_output_bytes` |

---

## Config Extension

```json
{
  "tools": {
    "enabled": true,
    "max_turns": 10,
    "shell": {
      "enabled": true,
      "timeout_seconds": 30,
      "max_output_bytes": 65536,
      "working_directory": "/home/didactyl",
      "allowlist": []
    }
  }
}
```

---

## Implementation Order

1. **Tool registry framework** — `tools.h`, `tools.c` with registration and dispatch
2. **LLM tool-calling protocol** — extend `llm.c` with `llm_chat_with_tools()`
3. **Agent loop** — rewrite `agent.c` with the tool-call loop
4. **`nostr_post` tool** — publish any kind event
5. **`nostr_query` tool** — query relays with filters
6. **`local_shell_exec` tool** — sandboxed shell command execution
7. **`nostr_dm` tool** — NIP-17 private DMs
8. **Config extension** — parse tools config section
9. **Security hardening** — timeouts, output limits, allowlists
10. **`nostr_note_save` / `nostr_note_load`** — agent data storage on Nostr

---

## Future: Nostr as the OS

Once the tool system is in place, the path to full Nostr-native operation:

- **Config on Nostr** — store config.json as a kind 30078 event, load on boot
- **SYSTEM.md on Nostr** — store system context as a Nostr event
- **Conversation memory** — store conversation history as Nostr events
- **Agent-to-agent** — agents can DM each other and use tools cooperatively
- **Blossom integration** — binary data storage via Blossom servers
- **Self-update** — agent can modify its own SYSTEM.md on Nostr
