# Didactyl Admin HTTP API — Architecture & Implementation Plan ## Overview Add a localhost-only HTTP API to didactyl so an external web dashboard can inspect and manage the agent at runtime. No authentication required — binding to `127.0.0.1` only. All responses are JSON. CORS headers included for browser access from any local origin. The web frontend is a separate project; this plan covers only the C-side HTTP server and API endpoints. --- ## Architecture ```mermaid flowchart LR subgraph didactyl process MAIN[main loop] --> POLL[nostr_handler_poll] MAIN --> TPOLL[trigger_manager_poll] MAIN --> HPOLL[http_api_poll] HPOLL --> ROUTER[request router] ROUTER --> AGENT[agent internals] ROUTER --> NOSTR[nostr_handler] ROUTER --> TOOLS[tools context] ROUTER --> CONFIG[config] ROUTER --> TRIGGERS[trigger_manager] end BROWSER[Web Dashboard] -- HTTP localhost:8484 --> HPOLL ``` ### HTTP Library Choice Use a minimal embedded HTTP server. Two good options for C with no extra dependencies: 1. **mongoose** (single `mongoose.c` + `mongoose.h`) — battle-tested, MIT license, supports polling model 2. **microhttpd** (libmicrohttpd) — GNU project, available as system package **Recommendation: mongoose** — it is a single-file drop-in, works with the existing poll-based main loop, and requires zero system dependencies. Just add `mongoose.c` and `mongoose.h` to the project. ### Integration Pattern The HTTP server runs in the same thread as the main poll loop. Each iteration calls `http_api_poll()` which does non-blocking accept/read/write via mongoose's `mg_mgr_poll()`. This avoids threading complexity and gives the API direct access to all agent state. --- ## Config Extension ```json { "api": { "enabled": true, "port": 8484, "bind_address": "127.0.0.1" } } ``` Defaults: enabled=false, port=8484, bind=127.0.0.1. --- ## API Endpoints All endpoints return JSON. All mutations use POST/PUT/DELETE. All reads use GET. ### Agent Identity & Status | Method | Path | Description | |---|---|---| | GET | `/api/status` | Agent runtime status: pubkey, display name, version, uptime, connected relay count, trigger count | | GET | `/api/config` | Current runtime config (redacted: nsec/api_key masked) | ### Nostr Events — Read & Edit | Method | Path | Description | |---|---|---| | GET | `/api/events/soul` | Fetch the agent soul event (kind 31120, d=soul) | | PUT | `/api/events/soul` | Update soul content, republish to relays | | GET | `/api/events/skills` | List all published skills (kind 31123/31124 by own pubkey) | | GET | `/api/events/skills/:d_tag` | Fetch a single skill by d_tag | | PUT | `/api/events/skills/:d_tag` | Update skill content/tags, republish | | DELETE | `/api/events/skills/:d_tag` | Remove skill from adoption list | | GET | `/api/events/adoption` | Fetch kind 10123 adoption list | | GET | `/api/events/startup` | List startup events from config | | GET | `/api/events/profile` | Fetch agent kind 0 profile | | PUT | `/api/events/profile` | Update agent kind 0 profile, republish | | GET | `/api/events/query` | Generic Nostr query — pass filter as query params or JSON body | ### Context Inspector | Method | Path | Description | |---|---|---| | GET | `/api/context/current` | Build and return the full context that would be sent to the LLM right now, broken into labeled parts | | GET | `/api/context/parts` | Return context parts with individual sizes (bytes and estimated tokens) | | GET | `/api/context/log` | Return recent context.log entries (last N blocks, configurable via ?limit=) | | POST | `/api/context/preview` | Accept a modified context structure, return what the LLM payload would look like (dry run, no send) | ### Context Parts Response Shape ```json { "total_chars": 12450, "total_estimated_tokens": 3112, "parts": [ { "name": "system_prompt", "role": "system", "chars": 1200, "estimated_tokens": 300, "content": "# Didactyl Agent..." }, { "name": "admin_identity", "role": "system", "chars": 450, "estimated_tokens": 112, "content": "This is your administrator!..." }, { "name": "admin_kind0", "role": "system", "chars": 320, "estimated_tokens": 80, "content": "Administrator kind 0 profile..." }, { "name": "startup_events", "role": "system", "chars": 4800, "estimated_tokens": 1200, "content": "Startup events memory..." }, { "name": "adopted_skills", "role": "system", "chars": 2100, "estimated_tokens": 525, "content": "Adopted skills memory..." }, { "name": "dm_history", "role": "mixed", "chars": 2400, "estimated_tokens": 600, "turns": 8 }, { "name": "admin_notes", "role": "system", "chars": 680, "estimated_tokens": 170, "content": "Administrator recent public notes..." }, { "name": "tools_schema", "chars": 500, "estimated_tokens": 125, "tool_count": 28 } ] } ``` ### Triggers | Method | Path | Description | |---|---|---| | GET | `/api/triggers` | List active triggers with status (wraps existing trigger_manager_status_json) | ### Model / LLM | Method | Path | Description | |---|---|---| | GET | `/api/model` | Current model config (wraps existing model_get) | | PUT | `/api/model` | Update model config (wraps existing model_set) | | GET | `/api/models` | List available models from provider (wraps existing model_list) | ### Relays | Method | Path | Description | |---|---|---| | GET | `/api/relays` | Relay connection status (wraps existing relay_status tool) | ### Prompt Crafting & Execution | Method | Path | Description | |---|---|---| | POST | `/api/prompt/run` | Submit a custom messages array with tools enabled; returns full LLM response including tool calls and results | | POST | `/api/prompt/run-simple` | Submit system prompt + user message; returns LLM text response (no tools) | | POST | `/api/prompt/compare` | A/B test: submit two prompt variants, run both, return side-by-side responses | #### POST /api/prompt/run Send a fully crafted messages array to the LLM with the full tool set enabled. The agent executes tool calls and returns the complete conversation. ```json { "messages": [ {"role": "system", "content": "You are Didactyl..."}, {"role": "system", "content": "Adopted skills memory..."}, {"role": "user", "content": "Tweet about the weather"} ], "model": "claude-haiku-4.5", "max_turns": 5, "tools_enabled": true } ``` Response: ```json { "success": true, "final_response": "Done! I posted a tweet about the weather.", "turns": [ { "turn": 1, "tool_calls": [ {"name": "nostr_post", "arguments": "...", "result": "..."} ] } ], "model_used": "claude-haiku-4.5", "total_input_tokens_estimate": 3200, "total_output_tokens_estimate": 180 } ``` #### POST /api/prompt/run-simple Quick iteration on prompt wording without tools. ```json { "system": "You are a helpful assistant that writes tweets...", "user": "Write a tweet about AI agents on Nostr", "model": "claude-haiku-4.5" } ``` Response: ```json { "success": true, "response": "AI agents are finding their home on Nostr...", "model_used": "claude-haiku-4.5", "input_tokens_estimate": 85, "output_tokens_estimate": 42 } ``` #### POST /api/prompt/compare A/B testing: submit two prompt variants, both are executed, responses returned side-by-side. ```json { "variant_a": { "messages": [ {"role": "system", "content": "You are Didactyl. Keep responses under 280 chars."}, {"role": "user", "content": "Tweet about your new skill"} ], "model": "claude-haiku-4.5", "tools_enabled": true }, "variant_b": { "messages": [ {"role": "system", "content": "You are Didactyl. Be concise. No markdown. No emoji."}, {"role": "user", "content": "Tweet about your new skill"} ], "model": "claude-haiku-4.5", "tools_enabled": true } } ``` Response: ```json { "success": true, "variant_a": { "final_response": "Just picked up the tweet-composer skill! ...", "turns": [], "model_used": "claude-haiku-4.5", "total_input_tokens_estimate": 3200, "total_output_tokens_estimate": 95 }, "variant_b": { "final_response": "New skill acquired: tweet-composer. ...", "turns": [], "model_used": "claude-haiku-4.5", "total_input_tokens_estimate": 3100, "total_output_tokens_estimate": 78 } } ``` The compare endpoint runs variant_a first, then variant_b sequentially. Each variant can optionally use a different model for cross-model comparison. #### Prompt Crafting Workflow ```mermaid flowchart TD LOAD[GET /api/context/parts] --> EDIT[Edit parts in UI] EDIT --> PREVIEW[POST /api/context/preview] PREVIEW --> FIRE[POST /api/prompt/run] FIRE --> COMPARE{Want to compare?} COMPARE -- Yes --> AB[POST /api/prompt/compare] COMPARE -- No --> PERSIST{Like the result?} AB --> PERSIST PERSIST -- Yes --> SAVE[PUT /api/events/soul or skills] PERSIST -- No --> EDIT ``` ### Tools | Method | Path | Description | |---|---|---| | GET | `/api/tools` | List all registered tool schemas | | POST | `/api/tools/:name/execute` | Execute a tool by name with JSON body as args (admin-only equivalent) | --- ## Implementation Plan ### New Files | File | Purpose | |---|---| | `src/http_api.c` | HTTP server, request router, endpoint handlers | | `src/http_api.h` | Public API: init, poll, cleanup | | `vendor/mongoose.c` | Mongoose HTTP library (single file) | | `vendor/mongoose.h` | Mongoose header | ### Modified Files | File | Change | |---|---| | `src/config.h` | Add `api_config_t` struct to `didactyl_config_t` | | `src/config.c` | Parse `api` config section | | `src/main.c` | Call `http_api_init()`, add `http_api_poll()` to main loop, call `http_api_cleanup()` on shutdown | | `src/agent.h` | Expose `agent_build_context_parts_json()` for context inspector | | `src/agent.c` | Implement `agent_build_context_parts_json()` that builds context and returns labeled parts with sizes | | `Makefile` | Add `vendor/mongoose.c` and `src/http_api.c` to SRCS, add `-Ivendor` to INCLUDES | ### http_api.h ```c #ifndef DIDACTYL_HTTP_API_H #define DIDACTYL_HTTP_API_H #include "config.h" #include "tools.h" struct trigger_manager; typedef struct { didactyl_config_t* cfg; tools_context_t* tools_ctx; struct trigger_manager* trigger_manager; } http_api_context_t; int http_api_init(http_api_context_t* ctx); int http_api_poll(int timeout_ms); void http_api_cleanup(void); #endif ``` ### Main Loop Integration ```c // In main.c, after agent_init and trigger_manager_init: http_api_context_t api_ctx = { .cfg = &cfg, .tools_ctx = &g_tools_ctx, // need to expose from agent .trigger_manager = &trigger_manager }; if (cfg.api.enabled) { if (http_api_init(&api_ctx) != 0) { DEBUG_WARN("HTTP API failed to start"); } } // In main loop: while (g_running) { nostr_handler_poll(100); trigger_manager_poll(&trigger_manager); if (cfg.api.enabled) { http_api_poll(0); // non-blocking } nanosleep(...); } // On shutdown: if (cfg.api.enabled) { http_api_cleanup(); } ``` ### Request Router Pattern ```c static void http_handler(struct mg_connection* c, int ev, void* ev_data) { if (ev == MG_EV_HTTP_MSG) { struct mg_http_message* hm = ev_data; // Add CORS headers to all responses // Route by method + path prefix if (mg_match(hm->uri, mg_str("/api/status"), NULL) && is_get(hm)) { handle_status(c, hm); } else if (mg_match(hm->uri, mg_str("/api/context/parts"), NULL) && is_get(hm)) { handle_context_parts(c, hm); } else if (mg_match(hm->uri, mg_str("/api/events/skills/*"), NULL)) { handle_skill_by_slug(c, hm); } // ... etc } } ``` --- ## Implementation Order 1. Add `api_config_t` to config and parse it 2. Vendor mongoose.c/mongoose.h, update Makefile 3. Create `src/http_api.c` with init/poll/cleanup skeleton + CORS 4. Wire into main.c poll loop 5. Implement read-only endpoints first: `/api/status`, `/api/config`, `/api/relays`, `/api/model`, `/api/tools`, `/api/triggers` 6. Implement Nostr event endpoints: `/api/events/soul`, `/api/events/skills`, `/api/events/adoption`, `/api/events/profile`, `/api/events/startup` 7. Implement context inspector: `/api/context/parts`, `/api/context/current`, `/api/context/log` 8. Implement mutation endpoints: PUT soul, PUT skills, PUT model, PUT profile 9. Implement tool execution endpoint: POST `/api/tools/:name/execute` 10. Implement context preview: POST `/api/context/preview` 11. Test all endpoints via curl 12. Update documentation --- ## CORS Headers Every response includes: ``` Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type ``` OPTIONS requests return 204 with these headers (preflight support). --- ## Security Notes - Binds to `127.0.0.1` only — not accessible from network - No authentication — this is a local dev tool - The `api.enabled` config flag defaults to `false` so it must be explicitly opted in - Tool execution endpoint gives full admin-tier access — acceptable for localhost dev dashboard - Config endpoint redacts `nsec` and `api_key` fields --- ## Token Estimation For the context size display, use a simple heuristic: `estimated_tokens = chars / 4`. This is a rough approximation that works well enough for English text with the major model families. No need for a real tokenizer.