Files
didactyl/plans/admin_api.md
T

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/:slug` | Fetch a single skill by slug |
| PUT | `/api/events/skills/:slug` | Update skill content/tags, republish |
| DELETE | `/api/events/skills/:slug` | 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.