470 lines
14 KiB
Markdown
470 lines
14 KiB
Markdown
# 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.
|