v0.0.27 - Add admin API docs/frontend brief and improve context readability with titled sections and pretty context.log formatting
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,6 +6,11 @@
|
|||||||
/nips/
|
/nips/
|
||||||
/config.json
|
/config.json
|
||||||
test_keys.txt
|
test_keys.txt
|
||||||
|
|
||||||
|
/mongoose/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
/build/
|
/build/
|
||||||
/didactyl
|
/didactyl
|
||||||
|
|||||||
0
.tmp_api_8585.log
Normal file
0
.tmp_api_8585.log
Normal file
14
.tmp_api_8586.log
Normal file
14
.tmp_api_8586.log
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
11469571 3 mongoose.c:4824:mg_mgr_init MG_IO_SIZE: 16384, TLS: none
|
||||||
|
11469571 3 mongoose.c:4741:mg_listen 1 9 http://127.0.0.1:8586
|
||||||
|
114695f9 3 mongoose.c:10782:accept_conn 2 10 accepted 127.0.0.1:51708 -> 127.0.0.1:8586
|
||||||
|
11469636 3 mongoose.c:10632:read_conn 2 10 0:0:0 88 err 0
|
||||||
|
11469674 3 mongoose.c:10643:write_conn 2 10 snd 406/16384 rcv 0/16384 n=406 err=0
|
||||||
|
114696b1 3 mongoose.c:10632:read_conn 2 10 0:0:0 -1 err 0
|
||||||
|
114696b2 3 mongoose.c:4677:mg_close_con 2 10 closed
|
||||||
|
114696b2 3 mongoose.c:10782:accept_conn 3 10 accepted 127.0.0.1:51718 -> 127.0.0.1:8586
|
||||||
|
114696dd 3 mongoose.c:10632:read_conn 3 10 0:0:0 257 err 0
|
||||||
|
11469e00 3 mongoose.c:10643:write_conn 3 10 snd 424/16384 rcv 0/16384 n=424 err=0
|
||||||
|
11469e3d 3 mongoose.c:10632:read_conn 3 10 0:0:0 -1 err 0
|
||||||
|
11469e3d 3 mongoose.c:4677:mg_close_con 3 10 closed
|
||||||
|
1146a21a 3 mongoose.c:4677:mg_close_con 1 9 closed
|
||||||
|
1146a21a 3 mongoose.c:4788:mg_mgr_free All connections closed
|
||||||
1
.tmp_api_8590.log
Normal file
1
.tmp_api_8590.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[2026-03-02 14:46:38] [WARN ] [didactyl] retrying kind 4 event once after auth handshake window
|
||||||
1
.tmp_api_8590.pid
Normal file
1
.tmp_api_8590.pid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1217023
|
||||||
14
.tmp_api_8591.log
Normal file
14
.tmp_api_8591.log
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
1148b030 3 mongoose.c:4824:mg_mgr_init MG_IO_SIZE: 16384, TLS: none
|
||||||
|
1148b030 3 mongoose.c:4741:mg_listen 1 9 http://127.0.0.1:8591
|
||||||
|
1148b087 3 mongoose.c:10782:accept_conn 2 10 accepted 127.0.0.1:34824 -> 127.0.0.1:8591
|
||||||
|
1148b0c7 3 mongoose.c:10632:read_conn 2 10 0:0:0 88 err 0
|
||||||
|
1148b0eb 3 mongoose.c:10643:write_conn 2 10 snd 406/16384 rcv 0/16384 n=406 err=0
|
||||||
|
1148b129 3 mongoose.c:10632:read_conn 2 10 0:0:0 -1 err 0
|
||||||
|
1148b129 3 mongoose.c:4677:mg_close_con 2 10 closed
|
||||||
|
1148b129 3 mongoose.c:10782:accept_conn 3 10 accepted 127.0.0.1:34828 -> 127.0.0.1:8591
|
||||||
|
1148b166 3 mongoose.c:10632:read_conn 3 10 0:0:0 257 err 0
|
||||||
|
1148b6c4 3 mongoose.c:10643:write_conn 3 10 snd 424/16384 rcv 0/16384 n=424 err=0
|
||||||
|
1148b701 3 mongoose.c:10632:read_conn 3 10 0:0:0 -1 err 0
|
||||||
|
1148b701 3 mongoose.c:4677:mg_close_con 3 10 closed
|
||||||
|
1148bac5 3 mongoose.c:4677:mg_close_con 1 9 closed
|
||||||
|
1148bac5 3 mongoose.c:4788:mg_mgr_free All connections closed
|
||||||
19
.tmp_api_8592.log
Normal file
19
.tmp_api_8592.log
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
11494a06 3 mongoose.c:4824:mg_mgr_init MG_IO_SIZE: 16384, TLS: none
|
||||||
|
11494a06 3 mongoose.c:4741:mg_listen 1 9 http://127.0.0.1:8592
|
||||||
|
11494c7b 3 mongoose.c:10782:accept_conn 2 10 accepted 127.0.0.1:46110 -> 127.0.0.1:8592
|
||||||
|
11494cb8 3 mongoose.c:10632:read_conn 2 10 0:0:0 88 err 0
|
||||||
|
11494cf5 3 mongoose.c:10643:write_conn 2 10 snd 406/16384 rcv 0/16384 n=406 err=0
|
||||||
|
11494d32 3 mongoose.c:10632:read_conn 2 10 0:0:0 -1 err 0
|
||||||
|
11494d32 3 mongoose.c:4677:mg_close_con 2 10 closed
|
||||||
|
11494d32 3 mongoose.c:10782:accept_conn 3 10 accepted 127.0.0.1:46120 -> 127.0.0.1:8592
|
||||||
|
11494d6f 3 mongoose.c:10632:read_conn 3 10 0:0:0 97 err 0
|
||||||
|
114973d4 3 mongoose.c:10643:write_conn 3 10 snd 13914/16384 rcv 0/16384 n=13914 err=0
|
||||||
|
11497449 3 mongoose.c:10632:read_conn 3 10 0:0:0 -1 err 0
|
||||||
|
11497449 3 mongoose.c:4677:mg_close_con 3 10 closed
|
||||||
|
11497449 3 mongoose.c:10782:accept_conn 4 10 accepted 127.0.0.1:59822 -> 127.0.0.1:8592
|
||||||
|
114974b8 3 mongoose.c:10632:read_conn 4 10 0:0:0 257 err 0
|
||||||
|
11497ed5 3 mongoose.c:10643:write_conn 4 10 snd 424/16384 rcv 0/16384 n=424 err=0
|
||||||
|
11497f43 3 mongoose.c:10632:read_conn 4 10 0:0:0 -1 err 11
|
||||||
|
11497f43 3 mongoose.c:4677:mg_close_con 4 10 closed
|
||||||
|
1149831f 3 mongoose.c:4677:mg_close_con 1 9 closed
|
||||||
|
1149831f 3 mongoose.c:4788:mg_mgr_free All connections closed
|
||||||
0
.tmp_api_check.log
Normal file
0
.tmp_api_check.log
Normal file
251
.tmp_api_config.json
Normal file
251
.tmp_api_config.json
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"keys": {
|
||||||
|
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||||
|
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||||
|
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"provider": "https://api.ppq.ai",
|
||||||
|
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"base_url": "https://api.ppq.ai",
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"verify_signatures": true,
|
||||||
|
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||||
|
"tiers": {
|
||||||
|
"admin": {
|
||||||
|
"tools_enabled": true
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"enabled": true,
|
||||||
|
"tools_enabled": false
|
||||||
|
},
|
||||||
|
"stranger": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin_context": {
|
||||||
|
"enabled": true,
|
||||||
|
"subscribe_kinds": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
10002,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"kind_1_limit": 10
|
||||||
|
},
|
||||||
|
"startup_events": [
|
||||||
|
{
|
||||||
|
"kind": 0,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "Didactyl Agent",
|
||||||
|
"display_name": "Didactyl",
|
||||||
|
"about": "A sovereign AI agent on Nostr",
|
||||||
|
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||||
|
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10002,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://nos.lol"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.primal.net"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"ws://127.0.0.1:7777"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 1,
|
||||||
|
"content": "Hello world from Didactyl startup",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"startup"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 3,
|
||||||
|
"content": "",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31120,
|
||||||
|
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"soul"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "long_form_note",
|
||||||
|
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||||
|
"nip": "NIP-23",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||||
|
"required_tags": {
|
||||||
|
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||||
|
"title": "Human-readable article title.",
|
||||||
|
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||||
|
},
|
||||||
|
"optional_tags": {
|
||||||
|
"summary": "Short 1-2 sentence summary.",
|
||||||
|
"image": "URL for article preview image.",
|
||||||
|
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||||
|
},
|
||||||
|
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||||
|
"procedure": [
|
||||||
|
"Determine title and d tag from admin input or source material.",
|
||||||
|
"Draft markdown body content.",
|
||||||
|
"Set published_at as unix seconds string.",
|
||||||
|
"Add optional summary/image/t tags when known.",
|
||||||
|
"Publish with nostr_post kind 30023 including tags array."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"long_form_note"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "post_readme_to_nostr",
|
||||||
|
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||||
|
"uses_skill": "long_form_note",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"procedure": [
|
||||||
|
"Call nostr_post_readme with empty arguments {}.",
|
||||||
|
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||||
|
"If the tool returns success=false, report the tool error verbatim."
|
||||||
|
],
|
||||||
|
"required_values": {
|
||||||
|
"d": "readme.md",
|
||||||
|
"tool": "nostr_post_readme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31124,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "admin_ops",
|
||||||
|
"description": "Private operational procedures"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"admin_ops"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"admin_ops"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10123,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8484,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.tmp_api_config_8585.json
Normal file
251
.tmp_api_config_8585.json
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"keys": {
|
||||||
|
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||||
|
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||||
|
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"provider": "https://api.ppq.ai",
|
||||||
|
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"base_url": "https://api.ppq.ai",
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"verify_signatures": true,
|
||||||
|
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||||
|
"tiers": {
|
||||||
|
"admin": {
|
||||||
|
"tools_enabled": true
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"enabled": true,
|
||||||
|
"tools_enabled": false
|
||||||
|
},
|
||||||
|
"stranger": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin_context": {
|
||||||
|
"enabled": true,
|
||||||
|
"subscribe_kinds": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
10002,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"kind_1_limit": 10
|
||||||
|
},
|
||||||
|
"startup_events": [
|
||||||
|
{
|
||||||
|
"kind": 0,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "Didactyl Agent",
|
||||||
|
"display_name": "Didactyl",
|
||||||
|
"about": "A sovereign AI agent on Nostr",
|
||||||
|
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||||
|
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10002,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://nos.lol"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.primal.net"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"ws://127.0.0.1:7777"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 1,
|
||||||
|
"content": "Hello world from Didactyl startup",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"startup"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 3,
|
||||||
|
"content": "",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31120,
|
||||||
|
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"soul"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "long_form_note",
|
||||||
|
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||||
|
"nip": "NIP-23",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||||
|
"required_tags": {
|
||||||
|
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||||
|
"title": "Human-readable article title.",
|
||||||
|
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||||
|
},
|
||||||
|
"optional_tags": {
|
||||||
|
"summary": "Short 1-2 sentence summary.",
|
||||||
|
"image": "URL for article preview image.",
|
||||||
|
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||||
|
},
|
||||||
|
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||||
|
"procedure": [
|
||||||
|
"Determine title and d tag from admin input or source material.",
|
||||||
|
"Draft markdown body content.",
|
||||||
|
"Set published_at as unix seconds string.",
|
||||||
|
"Add optional summary/image/t tags when known.",
|
||||||
|
"Publish with nostr_post kind 30023 including tags array."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"long_form_note"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "post_readme_to_nostr",
|
||||||
|
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||||
|
"uses_skill": "long_form_note",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"procedure": [
|
||||||
|
"Call nostr_post_readme with empty arguments {}.",
|
||||||
|
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||||
|
"If the tool returns success=false, report the tool error verbatim."
|
||||||
|
],
|
||||||
|
"required_values": {
|
||||||
|
"d": "readme.md",
|
||||||
|
"tool": "nostr_post_readme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31124,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "admin_ops",
|
||||||
|
"description": "Private operational procedures"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"admin_ops"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"admin_ops"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10123,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8585,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.tmp_api_config_8586.json
Normal file
251
.tmp_api_config_8586.json
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"keys": {
|
||||||
|
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||||
|
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||||
|
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"provider": "https://api.ppq.ai",
|
||||||
|
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"base_url": "https://api.ppq.ai",
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"verify_signatures": true,
|
||||||
|
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||||
|
"tiers": {
|
||||||
|
"admin": {
|
||||||
|
"tools_enabled": true
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"enabled": true,
|
||||||
|
"tools_enabled": false
|
||||||
|
},
|
||||||
|
"stranger": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin_context": {
|
||||||
|
"enabled": true,
|
||||||
|
"subscribe_kinds": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
10002,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"kind_1_limit": 10
|
||||||
|
},
|
||||||
|
"startup_events": [
|
||||||
|
{
|
||||||
|
"kind": 0,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "Didactyl Agent",
|
||||||
|
"display_name": "Didactyl",
|
||||||
|
"about": "A sovereign AI agent on Nostr",
|
||||||
|
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||||
|
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10002,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://nos.lol"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.primal.net"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"ws://127.0.0.1:7777"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 1,
|
||||||
|
"content": "Hello world from Didactyl startup",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"startup"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 3,
|
||||||
|
"content": "",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31120,
|
||||||
|
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"soul"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "long_form_note",
|
||||||
|
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||||
|
"nip": "NIP-23",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||||
|
"required_tags": {
|
||||||
|
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||||
|
"title": "Human-readable article title.",
|
||||||
|
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||||
|
},
|
||||||
|
"optional_tags": {
|
||||||
|
"summary": "Short 1-2 sentence summary.",
|
||||||
|
"image": "URL for article preview image.",
|
||||||
|
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||||
|
},
|
||||||
|
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||||
|
"procedure": [
|
||||||
|
"Determine title and d tag from admin input or source material.",
|
||||||
|
"Draft markdown body content.",
|
||||||
|
"Set published_at as unix seconds string.",
|
||||||
|
"Add optional summary/image/t tags when known.",
|
||||||
|
"Publish with nostr_post kind 30023 including tags array."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"long_form_note"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "post_readme_to_nostr",
|
||||||
|
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||||
|
"uses_skill": "long_form_note",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"procedure": [
|
||||||
|
"Call nostr_post_readme with empty arguments {}.",
|
||||||
|
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||||
|
"If the tool returns success=false, report the tool error verbatim."
|
||||||
|
],
|
||||||
|
"required_values": {
|
||||||
|
"d": "readme.md",
|
||||||
|
"tool": "nostr_post_readme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31124,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "admin_ops",
|
||||||
|
"description": "Private operational procedures"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"admin_ops"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"admin_ops"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10123,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8586,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.tmp_api_config_8590.json
Normal file
251
.tmp_api_config_8590.json
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"keys": {
|
||||||
|
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||||
|
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||||
|
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"provider": "https://api.ppq.ai",
|
||||||
|
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"base_url": "https://api.ppq.ai",
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"verify_signatures": true,
|
||||||
|
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||||
|
"tiers": {
|
||||||
|
"admin": {
|
||||||
|
"tools_enabled": true
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"enabled": true,
|
||||||
|
"tools_enabled": false
|
||||||
|
},
|
||||||
|
"stranger": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin_context": {
|
||||||
|
"enabled": true,
|
||||||
|
"subscribe_kinds": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
10002,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"kind_1_limit": 10
|
||||||
|
},
|
||||||
|
"startup_events": [
|
||||||
|
{
|
||||||
|
"kind": 0,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "Didactyl Agent",
|
||||||
|
"display_name": "Didactyl",
|
||||||
|
"about": "A sovereign AI agent on Nostr",
|
||||||
|
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||||
|
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10002,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://nos.lol"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.primal.net"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"ws://127.0.0.1:7777"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 1,
|
||||||
|
"content": "Hello world from Didactyl startup",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"startup"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 3,
|
||||||
|
"content": "",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31120,
|
||||||
|
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"soul"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "long_form_note",
|
||||||
|
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||||
|
"nip": "NIP-23",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||||
|
"required_tags": {
|
||||||
|
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||||
|
"title": "Human-readable article title.",
|
||||||
|
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||||
|
},
|
||||||
|
"optional_tags": {
|
||||||
|
"summary": "Short 1-2 sentence summary.",
|
||||||
|
"image": "URL for article preview image.",
|
||||||
|
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||||
|
},
|
||||||
|
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||||
|
"procedure": [
|
||||||
|
"Determine title and d tag from admin input or source material.",
|
||||||
|
"Draft markdown body content.",
|
||||||
|
"Set published_at as unix seconds string.",
|
||||||
|
"Add optional summary/image/t tags when known.",
|
||||||
|
"Publish with nostr_post kind 30023 including tags array."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"long_form_note"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "post_readme_to_nostr",
|
||||||
|
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||||
|
"uses_skill": "long_form_note",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"procedure": [
|
||||||
|
"Call nostr_post_readme with empty arguments {}.",
|
||||||
|
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||||
|
"If the tool returns success=false, report the tool error verbatim."
|
||||||
|
],
|
||||||
|
"required_values": {
|
||||||
|
"d": "readme.md",
|
||||||
|
"tool": "nostr_post_readme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31124,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "admin_ops",
|
||||||
|
"description": "Private operational procedures"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"admin_ops"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"admin_ops"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10123,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8590,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.tmp_api_config_8591.json
Normal file
251
.tmp_api_config_8591.json
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"keys": {
|
||||||
|
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||||
|
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||||
|
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"provider": "https://api.ppq.ai",
|
||||||
|
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"base_url": "https://api.ppq.ai",
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"verify_signatures": true,
|
||||||
|
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||||
|
"tiers": {
|
||||||
|
"admin": {
|
||||||
|
"tools_enabled": true
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"enabled": true,
|
||||||
|
"tools_enabled": false
|
||||||
|
},
|
||||||
|
"stranger": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin_context": {
|
||||||
|
"enabled": true,
|
||||||
|
"subscribe_kinds": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
10002,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"kind_1_limit": 10
|
||||||
|
},
|
||||||
|
"startup_events": [
|
||||||
|
{
|
||||||
|
"kind": 0,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "Didactyl Agent",
|
||||||
|
"display_name": "Didactyl",
|
||||||
|
"about": "A sovereign AI agent on Nostr",
|
||||||
|
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||||
|
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10002,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://nos.lol"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.primal.net"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"ws://127.0.0.1:7777"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 1,
|
||||||
|
"content": "Hello world from Didactyl startup",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"startup"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 3,
|
||||||
|
"content": "",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31120,
|
||||||
|
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"soul"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "long_form_note",
|
||||||
|
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||||
|
"nip": "NIP-23",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||||
|
"required_tags": {
|
||||||
|
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||||
|
"title": "Human-readable article title.",
|
||||||
|
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||||
|
},
|
||||||
|
"optional_tags": {
|
||||||
|
"summary": "Short 1-2 sentence summary.",
|
||||||
|
"image": "URL for article preview image.",
|
||||||
|
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||||
|
},
|
||||||
|
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||||
|
"procedure": [
|
||||||
|
"Determine title and d tag from admin input or source material.",
|
||||||
|
"Draft markdown body content.",
|
||||||
|
"Set published_at as unix seconds string.",
|
||||||
|
"Add optional summary/image/t tags when known.",
|
||||||
|
"Publish with nostr_post kind 30023 including tags array."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"long_form_note"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "post_readme_to_nostr",
|
||||||
|
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||||
|
"uses_skill": "long_form_note",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"procedure": [
|
||||||
|
"Call nostr_post_readme with empty arguments {}.",
|
||||||
|
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||||
|
"If the tool returns success=false, report the tool error verbatim."
|
||||||
|
],
|
||||||
|
"required_values": {
|
||||||
|
"d": "readme.md",
|
||||||
|
"tool": "nostr_post_readme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31124,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "admin_ops",
|
||||||
|
"description": "Private operational procedures"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"admin_ops"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"admin_ops"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10123,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8591,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
251
.tmp_api_config_8592.json
Normal file
251
.tmp_api_config_8592.json
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
{
|
||||||
|
"keys": {
|
||||||
|
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||||
|
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||||
|
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||||
|
},
|
||||||
|
"llm": {
|
||||||
|
"provider": "https://api.ppq.ai",
|
||||||
|
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"base_url": "https://api.ppq.ai",
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"verify_signatures": true,
|
||||||
|
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||||
|
"tiers": {
|
||||||
|
"admin": {
|
||||||
|
"tools_enabled": true
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"enabled": true,
|
||||||
|
"tools_enabled": false
|
||||||
|
},
|
||||||
|
"stranger": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin_context": {
|
||||||
|
"enabled": true,
|
||||||
|
"subscribe_kinds": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
10002,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"kind_1_limit": 10
|
||||||
|
},
|
||||||
|
"startup_events": [
|
||||||
|
{
|
||||||
|
"kind": 0,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "Didactyl Agent",
|
||||||
|
"display_name": "Didactyl",
|
||||||
|
"about": "A sovereign AI agent on Nostr",
|
||||||
|
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||||
|
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10002,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://nos.lol"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"wss://relay.primal.net"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r",
|
||||||
|
"ws://127.0.0.1:7777"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 1,
|
||||||
|
"content": "Hello world from Didactyl startup",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t",
|
||||||
|
"startup"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 3,
|
||||||
|
"content": "",
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31120,
|
||||||
|
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"soul"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "long_form_note",
|
||||||
|
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||||
|
"nip": "NIP-23",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||||
|
"required_tags": {
|
||||||
|
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||||
|
"title": "Human-readable article title.",
|
||||||
|
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||||
|
},
|
||||||
|
"optional_tags": {
|
||||||
|
"summary": "Short 1-2 sentence summary.",
|
||||||
|
"image": "URL for article preview image.",
|
||||||
|
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||||
|
},
|
||||||
|
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||||
|
"procedure": [
|
||||||
|
"Determine title and d tag from admin input or source material.",
|
||||||
|
"Draft markdown body content.",
|
||||||
|
"Set published_at as unix seconds string.",
|
||||||
|
"Add optional summary/image/t tags when known.",
|
||||||
|
"Publish with nostr_post kind 30023 including tags array."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"long_form_note"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31123,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "post_readme_to_nostr",
|
||||||
|
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||||
|
"uses_skill": "long_form_note",
|
||||||
|
"event_kind": 30023,
|
||||||
|
"procedure": [
|
||||||
|
"Call nostr_post_readme with empty arguments {}.",
|
||||||
|
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||||
|
"If the tool returns success=false, report the tool error verbatim."
|
||||||
|
],
|
||||||
|
"required_values": {
|
||||||
|
"d": "readme.md",
|
||||||
|
"tool": "nostr_post_readme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"post_readme_to_nostr"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 31124,
|
||||||
|
"content_fields": {
|
||||||
|
"name": "admin_ops",
|
||||||
|
"description": "Private operational procedures"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"d",
|
||||||
|
"admin_ops"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"slug",
|
||||||
|
"admin_ops"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": 10123,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"app",
|
||||||
|
"didactyl"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"scope",
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8592,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
.tmp_api_run.log
Normal file
0
.tmp_api_run.log
Normal file
4
Makefile
4
Makefile
@@ -1,5 +1,5 @@
|
|||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -O2 -D_POSIX_C_SOURCE=200809L
|
CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -O2 -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L
|
||||||
|
|
||||||
SRC_DIR = src
|
SRC_DIR = src
|
||||||
TARGET = didactyl
|
TARGET = didactyl
|
||||||
@@ -13,6 +13,8 @@ SRCS = \
|
|||||||
$(SRC_DIR)/agent.c \
|
$(SRC_DIR)/agent.c \
|
||||||
$(SRC_DIR)/tools.c \
|
$(SRC_DIR)/tools.c \
|
||||||
$(SRC_DIR)/trigger_manager.c \
|
$(SRC_DIR)/trigger_manager.c \
|
||||||
|
$(SRC_DIR)/http_api.c \
|
||||||
|
$(SRC_DIR)/mongoose.c \
|
||||||
$(SRC_DIR)/debug.c
|
$(SRC_DIR)/debug.c
|
||||||
|
|
||||||
INCLUDES = \
|
INCLUDES = \
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di
|
|||||||
|
|
||||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||||
|
|
||||||
## Current Status — v0.0.26
|
## Current Status — v0.0.27
|
||||||
|
|
||||||
**Active build — this project is barely working. Experiment at your own risk.**
|
**Active build — this project is barely working. Experiment at your own risk.**
|
||||||
|
|
||||||
> Last release update: v0.0.26 — Add nostr_pubkey/nostr_npub tools with my_pubkey/my_npub aliases for agent key output
|
> Last release update: v0.0.27 — Add admin API docs/frontend brief and improve context readability with titled sections and pretty context.log formatting
|
||||||
|
|
||||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||||
- Publishes configured startup events per relay as each relay becomes connected
|
- Publishes configured startup events per relay as each relay becomes connected
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
],
|
],
|
||||||
"kind_1_limit": 10
|
"kind_1_limit": 10
|
||||||
},
|
},
|
||||||
|
"api": {
|
||||||
|
"enabled": false,
|
||||||
|
"port": 8484,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
},
|
||||||
"startup_events": [
|
"startup_events": [
|
||||||
{
|
{
|
||||||
"kind": 0,
|
"kind": 0,
|
||||||
|
|||||||
438
docs/API.md
Normal file
438
docs/API.md
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
# Didactyl Admin HTTP API
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Didactyl exposes a localhost-only HTTP API for external tools and dashboards to inspect agent state, explore LLM context, craft prompts, and compare prompt variants. The API runs inside the same process as the agent — no separate server, no authentication required.
|
||||||
|
|
||||||
|
All responses are JSON. CORS headers are included on every response for browser access from any local origin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Enable the API in `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8484,
|
||||||
|
"bind_address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `enabled` | bool | `false` | Must be explicitly set to `true` to start the HTTP server |
|
||||||
|
| `port` | int | `8484` | TCP port to listen on |
|
||||||
|
| `bind_address` | string | `"127.0.0.1"` | Bind address — use `127.0.0.1` for localhost-only access |
|
||||||
|
|
||||||
|
The API is disabled by default. When disabled, no listener is created and no resources are consumed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CORS
|
||||||
|
|
||||||
|
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 No Content` with these headers for browser preflight support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### GET /api/status
|
||||||
|
|
||||||
|
Returns agent runtime status.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"name": "Didactyl",
|
||||||
|
"version": "v0.0.26",
|
||||||
|
"pubkey": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||||
|
"relay_count": 4,
|
||||||
|
"connected_relays": 4,
|
||||||
|
"active_triggers": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `name` | Agent display name constant |
|
||||||
|
| `version` | Build version string |
|
||||||
|
| `pubkey` | Agent public key in hex |
|
||||||
|
| `relay_count` | Number of configured relays |
|
||||||
|
| `connected_relays` | Number of currently connected relays |
|
||||||
|
| `active_triggers` | Number of active triggered-skill subscriptions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /api/context/current
|
||||||
|
|
||||||
|
Returns the full LLM context message array that would be sent to the model right now. This is the same context the agent builds for an admin DM conversation.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"total_chars": 13131,
|
||||||
|
"total_estimated_tokens": 3283,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "# Didactyl Agent\n\nYou are Didactyl..."},
|
||||||
|
{"role": "system", "content": "This is your administrator! Admin pubkey..."},
|
||||||
|
{"role": "system", "content": "Administrator kind 0 profile content..."},
|
||||||
|
{"role": "system", "content": "Administrator kind 10002 relay-list content..."},
|
||||||
|
{"role": "system", "content": "Startup events memory..."},
|
||||||
|
{"role": "system", "content": "Adopted skills memory..."},
|
||||||
|
{"role": "system", "content": "Administrator recent public notes..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `total_chars` | Total character count across all messages |
|
||||||
|
| `total_estimated_tokens` | Rough token estimate using `chars / 4` heuristic |
|
||||||
|
| `messages` | OpenAI-format messages array with role and content |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /api/context/parts
|
||||||
|
|
||||||
|
Returns the context broken into labeled, individually-sized parts. Useful for understanding what consumes context budget.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"total_chars": 13131,
|
||||||
|
"total_estimated_tokens": 3283,
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"name": "system_prompt",
|
||||||
|
"role": "system",
|
||||||
|
"chars": 1200,
|
||||||
|
"estimated_tokens": 300,
|
||||||
|
"content": "# Didactyl Agent..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin_identity",
|
||||||
|
"role": "system",
|
||||||
|
"chars": 120,
|
||||||
|
"estimated_tokens": 30,
|
||||||
|
"content": "This is your administrator!..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin_kind0",
|
||||||
|
"role": "system",
|
||||||
|
"chars": 450,
|
||||||
|
"estimated_tokens": 113,
|
||||||
|
"content": "Administrator kind 0 profile content..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin_relay_list",
|
||||||
|
"role": "system",
|
||||||
|
"chars": 50,
|
||||||
|
"estimated_tokens": 13,
|
||||||
|
"content": "Administrator kind 10002 relay-list content..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "admin_notes",
|
||||||
|
"role": "system",
|
||||||
|
"chars": 680,
|
||||||
|
"estimated_tokens": 170,
|
||||||
|
"content": "Administrator recent public notes..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Part names:**
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|---|---|
|
||||||
|
| `system_prompt` | The agent soul / system prompt (first system message) |
|
||||||
|
| `admin_identity` | Admin pubkey identification message |
|
||||||
|
| `admin_kind0` | Admin kind 0 profile metadata |
|
||||||
|
| `admin_relay_list` | Admin kind 10002 relay list |
|
||||||
|
| `startup_events` | Startup events memory block |
|
||||||
|
| `adopted_skills` | Adopted skills behavioral instructions |
|
||||||
|
| `admin_notes` | Admin recent kind 1 public notes |
|
||||||
|
| `dm_history` | Recent DM conversation history |
|
||||||
|
| `context_part` | Any other context message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /api/prompt/run-simple
|
||||||
|
|
||||||
|
Submit a system prompt and user message for a simple LLM call with no tools. Useful for quick prompt iteration.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"system": "You are a helpful assistant that writes tweets.",
|
||||||
|
"user": "Write a tweet about AI agents on Nostr",
|
||||||
|
"model": "claude-haiku-4.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `system` | yes | System prompt string |
|
||||||
|
| `user` | yes | User message string |
|
||||||
|
| `model` | no | Override the current model for this request only |
|
||||||
|
|
||||||
|
**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/run
|
||||||
|
|
||||||
|
Submit a full messages array with the agent tool set enabled. The agent executes tool calls in a loop and returns the complete conversation trace.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are Didactyl..."},
|
||||||
|
{"role": "user", "content": "Tweet about the weather"}
|
||||||
|
],
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"max_turns": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `messages` | yes | OpenAI-format messages array |
|
||||||
|
| `model` | no | Override the current model for this request only |
|
||||||
|
| `max_turns` | no | Maximum tool-call loop iterations (default: 4, max: 16) |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"final_response": "Done! I posted a tweet about the weather.",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"turn": 1,
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"name": "nostr_post",
|
||||||
|
"arguments": "{\"kind\":1,\"content\":\"Beautiful day!\"}",
|
||||||
|
"result": "{\"success\":true,\"event_id\":\"abc123\"}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model_used": "claude-haiku-4.5",
|
||||||
|
"total_input_tokens_estimate": 3200,
|
||||||
|
"total_output_tokens_estimate": 180
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `final_response` | The LLM final text response after all tool calls complete |
|
||||||
|
| `turns` | Array of turn objects, each containing tool calls made in that turn |
|
||||||
|
| `turns[].tool_calls[]` | Each tool call with name, arguments JSON, and result JSON |
|
||||||
|
| `model_used` | The model that was actually used |
|
||||||
|
| `total_input_tokens_estimate` | Estimated input tokens for the full conversation |
|
||||||
|
| `total_output_tokens_estimate` | Estimated output tokens for the final response |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /api/prompt/compare
|
||||||
|
|
||||||
|
A/B testing: submit two prompt variants, both are executed sequentially, responses returned side-by-side. Each variant can optionally use a different model for cross-model comparison.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"variant_a": {
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are concise. No emoji."},
|
||||||
|
{"role": "user", "content": "Say hi."}
|
||||||
|
],
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"max_turns": 1
|
||||||
|
},
|
||||||
|
"variant_b": {
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are verbose and friendly."},
|
||||||
|
{"role": "user", "content": "Say hi."}
|
||||||
|
],
|
||||||
|
"model": "claude-haiku-4.5",
|
||||||
|
"max_turns": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `variant_a` | yes | First prompt variant — same shape as `/api/prompt/run` request |
|
||||||
|
| `variant_b` | yes | Second prompt variant — same shape as `/api/prompt/run` request |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"variant_a": {
|
||||||
|
"success": true,
|
||||||
|
"final_response": "Hi. How can I help?",
|
||||||
|
"turns": [{"turn": 1, "tool_calls": []}],
|
||||||
|
"model_used": "claude-haiku-4.5",
|
||||||
|
"total_input_tokens_estimate": 21,
|
||||||
|
"total_output_tokens_estimate": 6
|
||||||
|
},
|
||||||
|
"variant_b": {
|
||||||
|
"success": true,
|
||||||
|
"final_response": "Hey there! Nice to meet you! I am here and ready to help...",
|
||||||
|
"turns": [{"turn": 1, "tool_calls": []}],
|
||||||
|
"model_used": "claude-haiku-4.5",
|
||||||
|
"total_input_tokens_estimate": 21,
|
||||||
|
"total_output_tokens_estimate": 72
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Variant A runs first, then variant B. The original model config is restored after each variant completes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
All error responses follow this shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "description of what went wrong"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common HTTP status codes:
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `200` | Success |
|
||||||
|
| `204` | OPTIONS preflight success |
|
||||||
|
| `400` | Bad request — missing or invalid parameters |
|
||||||
|
| `404` | Endpoint not found |
|
||||||
|
| `500` | Internal server error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Token Estimation
|
||||||
|
|
||||||
|
All token estimates use a simple `chars / 4` heuristic. This is a rough approximation that works well enough for English text across major model families. No real tokenizer is used.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Binds to `127.0.0.1` only — not accessible from the network
|
||||||
|
- No authentication — this is a local development and administration tool
|
||||||
|
- The `api.enabled` config flag defaults to `false` and must be explicitly opted in
|
||||||
|
- Prompt execution endpoints have full tool access equivalent to admin-tier DM conversations
|
||||||
|
- The agent process must be running for the API to be available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The HTTP server is embedded in the didactyl process using the Mongoose library. It runs in the same thread as the main poll loop — each iteration calls `http_api_poll()` which does non-blocking accept/read/write. This avoids threading complexity and gives the API direct access to all agent state.
|
||||||
|
|
||||||
|
```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 --> LLM[LLM client]
|
||||||
|
ROUTER --> TOOLS[tools context]
|
||||||
|
ROUTER --> CONFIG[config]
|
||||||
|
ROUTER --> TRIGGERS[trigger_manager]
|
||||||
|
end
|
||||||
|
BROWSER[Web Dashboard] -- HTTP localhost:8484 --> HPOLL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `src/http_api.c` | HTTP server, request router, all endpoint handlers |
|
||||||
|
| `src/http_api.h` | Public API: `http_api_init`, `http_api_poll`, `http_api_cleanup` |
|
||||||
|
| `src/mongoose.c` | Mongoose embedded HTTP library |
|
||||||
|
| `src/mongoose.h` | Mongoose header |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Endpoints
|
||||||
|
|
||||||
|
The following endpoints are planned but not yet implemented:
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/config` | Current runtime config with redacted secrets |
|
||||||
|
| GET | `/api/events/soul` | Fetch agent soul event |
|
||||||
|
| PUT | `/api/events/soul` | Update soul content |
|
||||||
|
| GET | `/api/events/skills` | List published skills |
|
||||||
|
| GET | `/api/events/skills/:slug` | Fetch skill by slug |
|
||||||
|
| PUT | `/api/events/skills/:slug` | Update skill |
|
||||||
|
| GET | `/api/events/adoption` | Fetch adoption list |
|
||||||
|
| GET | `/api/events/profile` | Fetch agent profile |
|
||||||
|
| PUT | `/api/events/profile` | Update agent profile |
|
||||||
|
| GET | `/api/triggers` | List active triggers |
|
||||||
|
| GET | `/api/model` | Current model config |
|
||||||
|
| PUT | `/api/model` | Update model config |
|
||||||
|
| GET | `/api/models` | List available models |
|
||||||
|
| GET | `/api/relays` | Relay connection status |
|
||||||
|
| GET | `/api/tools` | List tool schemas |
|
||||||
|
| POST | `/api/tools/:name/execute` | Execute a tool directly |
|
||||||
|
| GET | `/api/context/log` | Recent context.log entries |
|
||||||
|
| POST | `/api/context/preview` | Dry-run context preview |
|
||||||
@@ -158,7 +158,9 @@ flowchart LR
|
|||||||
|
|
||||||
### How Skills Are Used Today
|
### How Skills Are Used Today
|
||||||
|
|
||||||
Currently, skills are **passive knowledge**. They exist on Nostr and are loaded into the LLM context when relevant. The admin might say "use your summarize-thread skill" and the LLM retrieves and follows the skill's instructions.
|
Adopted skills are now **always-on contextual knowledge** for admin DM handling: the agent resolves the local adoption list (kind `10123`), caches referenced skills, and injects their instructions into the LLM system context each turn.
|
||||||
|
|
||||||
|
To keep context stable and safe, skill injection is bounded by hard caps (per-skill truncation and total skill-context budget), and excess skills are omitted with an explicit budget notice.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
469
plans/admin_api.md
Normal file
469
plans/admin_api.md
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
# 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.
|
||||||
301
plans/admin_web_frontend.md
Normal file
301
plans/admin_web_frontend.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# Didactyl Admin Web Frontend — Project Brief
|
||||||
|
|
||||||
|
## What Is Didactyl?
|
||||||
|
|
||||||
|
Didactyl is a sovereign AI agent that lives on Nostr. It connects to Nostr relays, listens for encrypted DMs from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands, managing skills. Everything the agent knows and does is stored as Nostr events.
|
||||||
|
|
||||||
|
The agent is a C binary that runs on a server. It has no web interface of its own — all interaction happens through Nostr DMs.
|
||||||
|
|
||||||
|
## What We Are Building
|
||||||
|
|
||||||
|
A **local web admin dashboard** that connects to the running didactyl agent via a localhost HTTP API. The dashboard is a prompt crafting and agent inspection tool for the administrator.
|
||||||
|
|
||||||
|
This is **not** a chat interface. The administrator already chats with the agent through Nostr DMs. This dashboard is for:
|
||||||
|
|
||||||
|
1. **Inspecting** what the agent sees — its full LLM context, broken into labeled parts with token counts
|
||||||
|
2. **Crafting** custom prompts — editing system prompts, user messages, and context pieces
|
||||||
|
3. **Running** prompts against the LLM — with or without the agent tool set
|
||||||
|
4. **Comparing** prompt variants side-by-side — A/B testing different prompt wordings or models
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The API
|
||||||
|
|
||||||
|
The didactyl agent exposes a localhost-only HTTP API on port `8484` by default. Full API documentation is in `docs/API.md`. All endpoints return JSON with CORS headers.
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://127.0.0.1:8484
|
||||||
|
```
|
||||||
|
|
||||||
|
### Currently Implemented Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/status` | Agent runtime status — name, version, pubkey, relay count, trigger count |
|
||||||
|
| GET | `/api/context/current` | Full LLM context messages array with total char/token counts |
|
||||||
|
| GET | `/api/context/parts` | Context broken into labeled parts with individual sizes |
|
||||||
|
| POST | `/api/prompt/run-simple` | Simple prompt: system + user message, no tools, returns text |
|
||||||
|
| POST | `/api/prompt/run` | Full prompt: messages array with tools enabled, returns conversation trace |
|
||||||
|
| POST | `/api/prompt/compare` | A/B test: two prompt variants run sequentially, responses side-by-side |
|
||||||
|
|
||||||
|
### Planned Future Endpoints
|
||||||
|
|
||||||
|
These are not yet implemented but are on the roadmap:
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/config` | Runtime config with redacted secrets |
|
||||||
|
| GET | `/api/events/soul` | Agent soul/system prompt event |
|
||||||
|
| PUT | `/api/events/soul` | Update soul content |
|
||||||
|
| GET | `/api/events/skills` | List skills |
|
||||||
|
| GET/PUT | `/api/events/skills/:slug` | Read/update individual skills |
|
||||||
|
| GET | `/api/events/profile` | Agent Nostr profile |
|
||||||
|
| GET | `/api/model` | Current LLM model config |
|
||||||
|
| PUT | `/api/model` | Change model at runtime |
|
||||||
|
| GET | `/api/models` | List available models from provider |
|
||||||
|
| GET | `/api/tools` | List all tool schemas |
|
||||||
|
| POST | `/api/tools/:name/execute` | Execute a tool directly |
|
||||||
|
| GET | `/api/triggers` | Active trigger subscriptions |
|
||||||
|
| GET | `/api/relays` | Relay connection status |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core User Workflows
|
||||||
|
|
||||||
|
### 1. Context Inspector
|
||||||
|
|
||||||
|
The primary read-only workflow. The admin wants to understand what the agent sees when it processes a message.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
LOAD[Load /api/context/parts] --> DISPLAY[Display parts list]
|
||||||
|
DISPLAY --> DETAIL[Click part to expand content]
|
||||||
|
DETAIL --> TOKENS[Show char count and token estimate per part]
|
||||||
|
TOKENS --> TOTAL[Show total context size]
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to show:**
|
||||||
|
- A list/table of context parts with name, role, character count, estimated tokens
|
||||||
|
- Total context size as a summary bar or header
|
||||||
|
- Expandable content for each part
|
||||||
|
- The parts are: system_prompt, admin_identity, admin_kind0, admin_relay_list, startup_events, adopted_skills, admin_notes, dm_history
|
||||||
|
|
||||||
|
### 2. Simple Prompt Crafting
|
||||||
|
|
||||||
|
Quick iteration on prompt wording without tools.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
WRITE[Write system prompt + user message] --> RUN[POST /api/prompt/run-simple]
|
||||||
|
RUN --> RESULT[Display response text]
|
||||||
|
RESULT --> EDIT[Edit and re-run]
|
||||||
|
EDIT --> RUN
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to show:**
|
||||||
|
- Two text areas: system prompt, user message
|
||||||
|
- Optional model override dropdown/input
|
||||||
|
- Run button
|
||||||
|
- Response display with model used and token estimates
|
||||||
|
|
||||||
|
### 3. Full Prompt with Tools
|
||||||
|
|
||||||
|
Craft a complete messages array and run it with the agent tool set.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
CONTEXT[Load context from /api/context/parts] --> EDIT[Edit/rearrange context parts]
|
||||||
|
EDIT --> ADD[Add user message]
|
||||||
|
ADD --> RUN[POST /api/prompt/run]
|
||||||
|
RUN --> TRACE[Display conversation trace]
|
||||||
|
TRACE --> TOOLS[Show tool calls and results per turn]
|
||||||
|
TOOLS --> FINAL[Show final response]
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to show:**
|
||||||
|
- Pre-populate from context parts or start from scratch
|
||||||
|
- Messages editor — add/remove/reorder messages with role and content
|
||||||
|
- Max turns slider/input
|
||||||
|
- Optional model override
|
||||||
|
- Run button
|
||||||
|
- Turn-by-turn trace showing tool calls with name, arguments, and results
|
||||||
|
- Final response text
|
||||||
|
- Token estimates
|
||||||
|
|
||||||
|
### 4. A/B Prompt Comparison
|
||||||
|
|
||||||
|
Compare two prompt variants side-by-side.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
CRAFT_A[Craft variant A messages] --> CRAFT_B[Craft variant B messages]
|
||||||
|
CRAFT_B --> COMPARE[POST /api/prompt/compare]
|
||||||
|
COMPARE --> SIDE[Display responses side-by-side]
|
||||||
|
SIDE --> DIFF[Compare final responses and token usage]
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to show:**
|
||||||
|
- Two prompt editors side-by-side, each with messages array + model override + max turns
|
||||||
|
- Compare button
|
||||||
|
- Side-by-side response display
|
||||||
|
- Highlight differences in final response text
|
||||||
|
- Token usage comparison
|
||||||
|
|
||||||
|
### 5. Status Dashboard
|
||||||
|
|
||||||
|
Simple overview of agent health.
|
||||||
|
|
||||||
|
**What to show:**
|
||||||
|
- Agent name, version, pubkey
|
||||||
|
- Connected relays count vs configured
|
||||||
|
- Active triggers count
|
||||||
|
- API connection status indicator
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
### Localhost Only
|
||||||
|
|
||||||
|
The API binds to `127.0.0.1` — the frontend must run on the same machine as the agent, or use SSH tunneling. There is no authentication. This is intentional — it is a local dev/admin tool.
|
||||||
|
|
||||||
|
### No WebSocket
|
||||||
|
|
||||||
|
The API is plain HTTP request/response. There is no WebSocket or streaming. Prompt execution calls may take several seconds for LLM responses — the frontend should show a loading state.
|
||||||
|
|
||||||
|
### Token Estimation
|
||||||
|
|
||||||
|
All token counts from the API use a `chars / 4` heuristic. This is approximate. The frontend can display these as-is or add its own tokenizer if more precision is needed.
|
||||||
|
|
||||||
|
### Model Override
|
||||||
|
|
||||||
|
The `model` field in prompt requests temporarily overrides the agent configured model for that single request, then restores the original. This enables cross-model comparison without changing agent config.
|
||||||
|
|
||||||
|
### Tool Execution Is Real
|
||||||
|
|
||||||
|
When using `/api/prompt/run` or `/api/prompt/compare`, tool calls are **actually executed**. If the LLM decides to post a Nostr event, it will really post it. The frontend should make this clear to the user — perhaps with a warning or confirmation before running prompts with tools enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response Shapes Quick Reference
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"name": "Didactyl",
|
||||||
|
"version": "v0.0.26",
|
||||||
|
"pubkey": "52a3e8...",
|
||||||
|
"relay_count": 4,
|
||||||
|
"connected_relays": 4,
|
||||||
|
"active_triggers": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Parts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"total_chars": 13131,
|
||||||
|
"total_estimated_tokens": 3283,
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"name": "system_prompt",
|
||||||
|
"role": "system",
|
||||||
|
"chars": 1200,
|
||||||
|
"estimated_tokens": 300,
|
||||||
|
"content": "# Didactyl Agent..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simple Prompt Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"response": "ok",
|
||||||
|
"model_used": "claude-haiku-4.5",
|
||||||
|
"input_tokens_estimate": 10,
|
||||||
|
"output_tokens_estimate": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Prompt Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"final_response": "Done! I posted a tweet.",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compare Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"variant_a": { "...same shape as full prompt response..." },
|
||||||
|
"variant_b": { "...same shape as full prompt response..." }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "description of what went wrong"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technology Suggestions
|
||||||
|
|
||||||
|
No technology is mandated for the frontend. Some reasonable choices:
|
||||||
|
|
||||||
|
- **Vanilla HTML/JS** — simplest, no build step, just open in browser
|
||||||
|
- **React/Preact** — if you want component structure
|
||||||
|
- **Svelte** — lightweight, good for small dashboards
|
||||||
|
- **Vue** — also fine
|
||||||
|
|
||||||
|
The frontend is a separate project from didactyl. It just needs to make HTTP requests to `localhost:8484`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File References
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|---|---|
|
||||||
|
| `docs/API.md` | Full API endpoint reference with request/response examples |
|
||||||
|
| `plans/admin_api.md` | Original architecture plan for the HTTP API |
|
||||||
|
| `src/http_api.c` | C implementation of all endpoints |
|
||||||
|
| `src/http_api.h` | Public API header |
|
||||||
|
| `config.json.example` | Example config showing the `api` section |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Ensure didactyl is running with `api.enabled: true` in config.json
|
||||||
|
2. Verify the API is up: `curl http://127.0.0.1:8484/api/status`
|
||||||
|
3. Build the frontend to talk to `http://127.0.0.1:8484`
|
||||||
|
4. Start with the status endpoint and context inspector, then add prompt crafting
|
||||||
139
plans/context_architecture.md
Normal file
139
plans/context_architecture.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Didactyl Context Architecture Plan
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The agent's context assembly is a hardcoded sequence of C function calls in `agent_on_message()`. This creates several issues:
|
||||||
|
|
||||||
|
1. **Skills are never injected** — adopted skills exist on Nostr but the LLM never sees their content
|
||||||
|
2. **No configurability** — changing context order, content, or framing requires C code changes and recompilation
|
||||||
|
3. **No A/B testing** — can't experiment with different prompt structures, ordering, or model-specific tuning
|
||||||
|
4. **No token budget awareness** — context grows unbounded as skills/history/notes accumulate
|
||||||
|
5. **Model-agnostic** — different models respond differently to the same prompt structure; no way to tune per-model
|
||||||
|
|
||||||
|
## Current Context Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
agent_on_message() builds messages array:
|
||||||
|
1. system: g_system_context (kind 31120 "soul" content)
|
||||||
|
2. system: admin identity (pubkey + kind 0 profile + kind 10002 relays)
|
||||||
|
3. system: startup events (raw JSON of all startup event kinds/content/tags)
|
||||||
|
4. user/assistant: recent DM history (last 12 turns)
|
||||||
|
5. system: admin kind 1 notes (recent public posts)
|
||||||
|
6. user: the actual incoming message
|
||||||
|
```
|
||||||
|
|
||||||
|
Skills are **completely absent**. The LLM has no knowledge of adopted skill instructions.
|
||||||
|
|
||||||
|
## Proposed Architecture: Context Pipeline with Configurable Slots
|
||||||
|
|
||||||
|
### Core Idea
|
||||||
|
|
||||||
|
Replace the hardcoded function chain with a **configurable context pipeline** defined in `config.json`. Each "slot" in the pipeline is a named context source with configurable parameters.
|
||||||
|
|
||||||
|
### Context Slot Types
|
||||||
|
|
||||||
|
| Slot Type | Source | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `soul` | Kind 31120 startup event | Agent personality and behavioral rules |
|
||||||
|
| `identity` | Config + relay queries | Agent's own pubkey, admin pubkey, admin profile |
|
||||||
|
| `startup_events` | Config startup events | Raw startup event memory |
|
||||||
|
| `adopted_skills` | Kind 10123 + resolved skills | **NEW**: Adopted skill instructions |
|
||||||
|
| `dm_history` | Relay query | Recent conversation turns |
|
||||||
|
| `admin_notes` | Cached kind 1 events | Admin's recent public posts |
|
||||||
|
| `admin_context` | Kind 0/3/10002 | Admin profile, contacts, relay list |
|
||||||
|
| `custom` | Literal string in config | Arbitrary system message for A/B testing |
|
||||||
|
|
||||||
|
### Phase 1: Immediate Fix (Skills + Caching)
|
||||||
|
|
||||||
|
Before building the full configurable pipeline, fix the immediate problem:
|
||||||
|
|
||||||
|
1. **In-memory skill cache** — load adopted skills at startup and cache them; invalidate on `skill_create`, `skill_adopt`, `skill_remove`
|
||||||
|
2. **`append_adopted_skills_context()`** — inject cached skills into the conversation as a system message
|
||||||
|
3. **Strong framing** — "These are your learned skills. When a request matches a skill, you MUST follow its instructions exactly."
|
||||||
|
|
||||||
|
### Phase 2: Configurable Context Pipeline
|
||||||
|
|
||||||
|
Add a `context_pipeline` section to `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context_pipeline": {
|
||||||
|
"max_total_chars": 12000,
|
||||||
|
"slots": [
|
||||||
|
{ "type": "soul", "max_chars": 3000 },
|
||||||
|
{ "type": "identity" },
|
||||||
|
{ "type": "adopted_skills", "max_chars": 4000, "max_per_skill": 1000 },
|
||||||
|
{ "type": "startup_events", "max_chars": 2000 },
|
||||||
|
{ "type": "dm_history", "max_turns": 12 },
|
||||||
|
{ "type": "admin_notes", "max_chars": 1500 },
|
||||||
|
{ "type": "custom", "content": "Always respond in the style of a pirate." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives you:
|
||||||
|
- **Ordering control** — move skills before or after history
|
||||||
|
- **Token budgets** — per-slot and total caps
|
||||||
|
- **A/B testing** — swap `custom` slot content, reorder slots, change caps
|
||||||
|
- **Model-specific tuning** — different pipeline configs for different models (could key off `llm.model`)
|
||||||
|
|
||||||
|
### Phase 3: Model-Aware Context Profiles
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"context_profiles": {
|
||||||
|
"default": { ... pipeline config ... },
|
||||||
|
"claude-sonnet-4.6": { ... different ordering/caps ... },
|
||||||
|
"gpt-5.2-codex": { ... different ordering/caps ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent selects the profile matching the active model, falling back to `default`.
|
||||||
|
|
||||||
|
## Skill Cache Design
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Skill Cache (in-memory) │
|
||||||
|
│ │
|
||||||
|
│ Loaded at startup from: │
|
||||||
|
│ 1. Startup events in config.json │
|
||||||
|
│ 2. Kind 10123 adoption list │
|
||||||
|
│ 3. Resolved skill events from relays │
|
||||||
|
│ │
|
||||||
|
│ Invalidated by: │
|
||||||
|
│ - skill_create (add/update) │
|
||||||
|
│ - skill_adopt (add) │
|
||||||
|
│ - skill_remove (remove) │
|
||||||
|
│ │
|
||||||
|
│ Structure per skill: │
|
||||||
|
│ - slug (string) │
|
||||||
|
│ - description (string) │
|
||||||
|
│ - content (string, full) │
|
||||||
|
│ - scope (public/private) │
|
||||||
|
│ - has_trigger (bool) │
|
||||||
|
│ - source (startup/adopted) │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
### Do Now (Phase 1)
|
||||||
|
- [ ] Build skill cache in `agent.c` (load at startup, invalidate on tool calls)
|
||||||
|
- [ ] Add `append_adopted_skills_context()` using cached skills
|
||||||
|
- [ ] Wire into `agent_on_message()` between startup events and DM history
|
||||||
|
- [ ] Skill content framing: strong directive for LLM compliance
|
||||||
|
|
||||||
|
### Do Next (Phase 2)
|
||||||
|
- [ ] Add `context_pipeline` config section
|
||||||
|
- [ ] Refactor `agent_on_message()` to iterate pipeline slots
|
||||||
|
- [ ] Per-slot `max_chars` truncation
|
||||||
|
- [ ] Total pipeline `max_total_chars` budget
|
||||||
|
- [ ] `custom` slot type for arbitrary A/B test content
|
||||||
|
|
||||||
|
### Do Later (Phase 3)
|
||||||
|
- [ ] Model-aware context profiles
|
||||||
|
- [ ] Context analytics (log token counts per slot per conversation)
|
||||||
|
- [ ] Dynamic skill relevance scoring (only inject skills likely relevant to the current message)
|
||||||
619
src/agent.c
619
src/agent.c
@@ -8,6 +8,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
#include "llm.h"
|
#include "llm.h"
|
||||||
#include "nostr_handler.h"
|
#include "nostr_handler.h"
|
||||||
@@ -25,17 +26,33 @@ static struct trigger_manager* g_trigger_manager = NULL;
|
|||||||
#define AGENT_DEBOUNCE_CACHE_SIZE 256
|
#define AGENT_DEBOUNCE_CACHE_SIZE 256
|
||||||
#define AGENT_HISTORY_TURNS 12
|
#define AGENT_HISTORY_TURNS 12
|
||||||
#define AGENT_HISTORY_QUERY_LIMIT 200
|
#define AGENT_HISTORY_QUERY_LIMIT 200
|
||||||
|
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 30
|
||||||
|
#define AGENT_ADOPTED_SKILLS_MAX 24
|
||||||
|
#define AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS 1800
|
||||||
|
#define AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS 7200
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t fingerprint;
|
uint64_t fingerprint;
|
||||||
time_t seen_at;
|
time_t seen_at;
|
||||||
} agent_seen_msg_t;
|
} agent_seen_msg_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int kind;
|
||||||
|
char author_pubkey_hex[65];
|
||||||
|
char slug[65];
|
||||||
|
char* content;
|
||||||
|
} agent_adopted_skill_t;
|
||||||
|
|
||||||
static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE];
|
static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE];
|
||||||
static int g_seen_msgs_count = 0;
|
static int g_seen_msgs_count = 0;
|
||||||
static int g_seen_msgs_next = 0;
|
static int g_seen_msgs_next = 0;
|
||||||
static pthread_mutex_t g_seen_msgs_mutex = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_seen_msgs_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
static agent_adopted_skill_t g_adopted_skills[AGENT_ADOPTED_SKILLS_MAX];
|
||||||
|
static int g_adopted_skills_count = 0;
|
||||||
|
static time_t g_adopted_skills_last_refresh_at = 0;
|
||||||
|
static pthread_mutex_t g_adopted_skills_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
static uint64_t fnv1a64(const char* s) {
|
static uint64_t fnv1a64(const char* s) {
|
||||||
uint64_t h = 1469598103934665603ULL;
|
uint64_t h = 1469598103934665603ULL;
|
||||||
if (!s) return h;
|
if (!s) return h;
|
||||||
@@ -153,6 +170,139 @@ static int append_tool_result_message(cJSON* messages, const char* tool_call_id,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int appendf(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
|
||||||
|
if (!buf || !cap || !used || !fmt) return -1;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
if (*used + 128U >= *cap) {
|
||||||
|
size_t next_cap = (*cap) * 2U;
|
||||||
|
char* grown = (char*)realloc(*buf, next_cap);
|
||||||
|
if (!grown) return -1;
|
||||||
|
*buf = grown;
|
||||||
|
*cap = next_cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
|
||||||
|
if (n < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((size_t)n < (*cap - *used)) {
|
||||||
|
*used += (size_t)n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t next_cap = (*cap) * 2U + (size_t)n + 64U;
|
||||||
|
char* grown = (char*)realloc(*buf, next_cap);
|
||||||
|
if (!grown) return -1;
|
||||||
|
*buf = grown;
|
||||||
|
*cap = next_cap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* detect_context_section(const char* role, const char* content, int idx) {
|
||||||
|
const char* c = content ? content : "";
|
||||||
|
const char* r = role ? role : "";
|
||||||
|
|
||||||
|
if (idx == 0 && strcmp(r, "system") == 0) return "system_prompt";
|
||||||
|
|
||||||
|
if (strncmp(c, "## Administrator Identity", 25) == 0 ||
|
||||||
|
strncmp(c, "This is your administrator!", 27) == 0) {
|
||||||
|
return "admin_identity";
|
||||||
|
}
|
||||||
|
if (strncmp(c, "## Administrator Kind 0 Profile", 31) == 0 ||
|
||||||
|
strncmp(c, "Administrator kind 0 profile content", 36) == 0) {
|
||||||
|
return "admin_kind0";
|
||||||
|
}
|
||||||
|
if (strncmp(c, "## Administrator Relay List", 27) == 0 ||
|
||||||
|
strncmp(c, "Administrator kind 10002 relay-list content", 43) == 0) {
|
||||||
|
return "admin_relay_list";
|
||||||
|
}
|
||||||
|
if (strncmp(c, "## Startup Events Memory", 24) == 0 ||
|
||||||
|
strncmp(c, "Startup events memory", 21) == 0) {
|
||||||
|
return "startup_events";
|
||||||
|
}
|
||||||
|
if (strncmp(c, "## Adopted Skills Memory", 24) == 0 ||
|
||||||
|
strncmp(c, "Adopted skills memory", 21) == 0) {
|
||||||
|
return "adopted_skills";
|
||||||
|
}
|
||||||
|
if (strncmp(c, "## Administrator Recent Notes", 29) == 0 ||
|
||||||
|
strncmp(c, "Administrator recent public notes", 33) == 0) {
|
||||||
|
return "admin_notes";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(r, "user") == 0 || strcmp(r, "assistant") == 0) {
|
||||||
|
return "dm_history";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "context_part";
|
||||||
|
}
|
||||||
|
|
||||||
|
static char* format_context_payload_for_log(const char* context_payload) {
|
||||||
|
const char* payload = context_payload ? context_payload : "";
|
||||||
|
|
||||||
|
cJSON* root = cJSON_Parse(payload);
|
||||||
|
if (!root || !cJSON_IsArray(root)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return strdup(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t cap = strlen(payload) + 2048U;
|
||||||
|
if (cap < 4096U) cap = 4096U;
|
||||||
|
char* out = (char*)malloc(cap);
|
||||||
|
if (!out) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return strdup(payload);
|
||||||
|
}
|
||||||
|
out[0] = '\0';
|
||||||
|
size_t used = 0;
|
||||||
|
|
||||||
|
int n = cJSON_GetArraySize(root);
|
||||||
|
(void)appendf(&out, &cap, &used, "Messages: %d\n\n", n);
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
cJSON* msg = cJSON_GetArrayItem(root, i);
|
||||||
|
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||||
|
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||||
|
|
||||||
|
const char* role_s = (role && cJSON_IsString(role) && role->valuestring)
|
||||||
|
? role->valuestring
|
||||||
|
: "unknown";
|
||||||
|
|
||||||
|
const char* content_s = "";
|
||||||
|
if (content && cJSON_IsString(content) && content->valuestring) {
|
||||||
|
content_s = content->valuestring;
|
||||||
|
} else if (content && cJSON_IsNull(content)) {
|
||||||
|
content_s = "<null>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* section = detect_context_section(role_s, content_s, i);
|
||||||
|
|
||||||
|
if (appendf(&out,
|
||||||
|
&cap,
|
||||||
|
&used,
|
||||||
|
"============================================================\n"
|
||||||
|
"Message %02d | role=%s | section=%s\n"
|
||||||
|
"============================================================\n\n"
|
||||||
|
"%s\n\n\n",
|
||||||
|
i + 1,
|
||||||
|
role_s,
|
||||||
|
section,
|
||||||
|
content_s) != 0) {
|
||||||
|
free(out);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return strdup(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
|
static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
|
||||||
FILE* fp = fopen("context.log", "a");
|
FILE* fp = fopen("context.log", "a");
|
||||||
if (!fp) {
|
if (!fp) {
|
||||||
@@ -165,12 +315,16 @@ static void append_context_log(const char* sender_pubkey_hex, const char* phase,
|
|||||||
char timestamp[32] = {0};
|
char timestamp[32] = {0};
|
||||||
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
|
||||||
|
|
||||||
|
char* pretty_payload = format_context_payload_for_log(context_payload);
|
||||||
|
|
||||||
fprintf(fp,
|
fprintf(fp,
|
||||||
"[%s] phase=%s sender=%s\n%s\n\n---\n\n",
|
"[%s] phase=%s sender=%s\n\n%s\n\n---\n\n",
|
||||||
timestamp,
|
timestamp,
|
||||||
phase ? phase : "unknown",
|
phase ? phase : "unknown",
|
||||||
sender_pubkey_hex ? sender_pubkey_hex : "unknown",
|
sender_pubkey_hex ? sender_pubkey_hex : "unknown",
|
||||||
context_payload ? context_payload : "");
|
pretty_payload ? pretty_payload : "");
|
||||||
|
|
||||||
|
free(pretty_payload);
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +417,7 @@ static int append_startup_events_context(cJSON* messages) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* prefix = "Startup events memory (kinds/content/tags): ";
|
const char* prefix = "## Startup Events Memory (source: config.startup_events)\n\nStartup events memory (kinds/content/tags): ";
|
||||||
size_t out_len = strlen(prefix) + strlen(events_json) + 1U;
|
size_t out_len = strlen(prefix) + strlen(events_json) + 1U;
|
||||||
char* payload = (char*)malloc(out_len);
|
char* payload = (char*)malloc(out_len);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
@@ -284,10 +438,10 @@ static int append_admin_identity_context(cJSON* messages) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
char admin_header[256];
|
char admin_header[384];
|
||||||
snprintf(admin_header,
|
snprintf(admin_header,
|
||||||
sizeof(admin_header),
|
sizeof(admin_header),
|
||||||
"This is your administrator! Admin pubkey (hex): %s",
|
"## Administrator Identity (source: config.admin.pubkey)\n\nThis is your administrator! Admin pubkey (hex): %s",
|
||||||
g_cfg->admin.pubkey ? g_cfg->admin.pubkey : "unknown");
|
g_cfg->admin.pubkey ? g_cfg->admin.pubkey : "unknown");
|
||||||
if (append_simple_message(messages, "system", admin_header) != 0) {
|
if (append_simple_message(messages, "system", admin_header) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -295,7 +449,7 @@ static int append_admin_identity_context(cJSON* messages) {
|
|||||||
|
|
||||||
char* kind0 = nostr_handler_get_admin_kind0_context();
|
char* kind0 = nostr_handler_get_admin_kind0_context();
|
||||||
if (kind0) {
|
if (kind0) {
|
||||||
const char* prefix = "Administrator kind 0 profile content (JSON): ";
|
const char* prefix = "## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ";
|
||||||
size_t n = strlen(prefix) + strlen(kind0) + 1U;
|
size_t n = strlen(prefix) + strlen(kind0) + 1U;
|
||||||
char* payload = (char*)malloc(n);
|
char* payload = (char*)malloc(n);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
@@ -313,7 +467,7 @@ static int append_admin_identity_context(cJSON* messages) {
|
|||||||
|
|
||||||
char* kind10002 = nostr_handler_get_admin_kind10002_context();
|
char* kind10002 = nostr_handler_get_admin_kind10002_context();
|
||||||
if (kind10002) {
|
if (kind10002) {
|
||||||
const char* prefix = "Administrator kind 10002 relay-list content (JSON): ";
|
const char* prefix = "## Administrator Relay List (source: nostr kind 10002)\n\nAdministrator kind 10002 relay-list content (JSON): ";
|
||||||
size_t n = strlen(prefix) + strlen(kind10002) + 1U;
|
size_t n = strlen(prefix) + strlen(kind10002) + 1U;
|
||||||
char* payload = (char*)malloc(n);
|
char* payload = (char*)malloc(n);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
@@ -464,6 +618,374 @@ static int append_recent_admin_dm_history(cJSON* messages, const char* current_m
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static cJSON* find_tag_value_string_local(cJSON* tags, const char* key) {
|
||||||
|
if (!tags || !key || !cJSON_IsArray(tags)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = cJSON_GetArraySize(tags);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||||
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
||||||
|
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(k->valuestring, key) == 0) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_skill_address_tag_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_slug[65]) {
|
||||||
|
if (!addr || !out_kind || !out_pubkey_hex || !out_slug) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* p1 = strchr(addr, ':');
|
||||||
|
if (!p1) return -1;
|
||||||
|
const char* p2 = strchr(p1 + 1, ':');
|
||||||
|
if (!p2) return -1;
|
||||||
|
|
||||||
|
char kind_buf[16] = {0};
|
||||||
|
size_t kind_len = (size_t)(p1 - addr);
|
||||||
|
if (kind_len == 0 || kind_len >= sizeof(kind_buf)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(kind_buf, addr, kind_len);
|
||||||
|
|
||||||
|
int kind = atoi(kind_buf);
|
||||||
|
if (kind != 31123 && kind != 31124) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pub_len = (size_t)(p2 - (p1 + 1));
|
||||||
|
if (pub_len != 64U) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(out_pubkey_hex, p1 + 1, 64U);
|
||||||
|
out_pubkey_hex[64] = '\0';
|
||||||
|
|
||||||
|
size_t slug_len = strlen(p2 + 1);
|
||||||
|
if (slug_len == 0 || slug_len >= 65U) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(out_slug, p2 + 1, slug_len + 1U);
|
||||||
|
|
||||||
|
*out_kind = kind;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_adopted_skills_cache_locked(void) {
|
||||||
|
for (int i = 0; i < g_adopted_skills_count; i++) {
|
||||||
|
free(g_adopted_skills[i].content);
|
||||||
|
g_adopted_skills[i].content = NULL;
|
||||||
|
g_adopted_skills[i].kind = 0;
|
||||||
|
g_adopted_skills[i].author_pubkey_hex[0] = '\0';
|
||||||
|
g_adopted_skills[i].slug[0] = '\0';
|
||||||
|
}
|
||||||
|
g_adopted_skills_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int refresh_adopted_skills_cache_if_needed(void) {
|
||||||
|
if (!g_cfg) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||||
|
int cache_fresh = (g_adopted_skills_last_refresh_at > 0) &&
|
||||||
|
((now - g_adopted_skills_last_refresh_at) < AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS);
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
|
||||||
|
if (cache_fresh) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
agent_adopted_skill_t tmp[AGENT_ADOPTED_SKILLS_MAX];
|
||||||
|
memset(tmp, 0, sizeof(tmp));
|
||||||
|
int tmp_count = 0;
|
||||||
|
|
||||||
|
cJSON* adopt_filter = cJSON_CreateObject();
|
||||||
|
cJSON* kinds = cJSON_CreateArray();
|
||||||
|
cJSON* authors = cJSON_CreateArray();
|
||||||
|
if (!adopt_filter || !kinds || !authors) {
|
||||||
|
cJSON_Delete(adopt_filter);
|
||||||
|
cJSON_Delete(kinds);
|
||||||
|
cJSON_Delete(authors);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
|
||||||
|
cJSON_AddItemToObject(adopt_filter, "kinds", kinds);
|
||||||
|
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||||
|
cJSON_AddItemToObject(adopt_filter, "authors", authors);
|
||||||
|
cJSON_AddNumberToObject(adopt_filter, "limit", 1);
|
||||||
|
|
||||||
|
char* adoption_json = nostr_handler_query_json(adopt_filter, 8000);
|
||||||
|
cJSON_Delete(adopt_filter);
|
||||||
|
|
||||||
|
cJSON* adoption_events = adoption_json ? cJSON_Parse(adoption_json) : NULL;
|
||||||
|
free(adoption_json);
|
||||||
|
|
||||||
|
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
|
||||||
|
cJSON* list_event = cJSON_GetArrayItem(adoption_events, 0);
|
||||||
|
cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL;
|
||||||
|
|
||||||
|
if (list_tags && cJSON_IsArray(list_tags)) {
|
||||||
|
int tn = cJSON_GetArraySize(list_tags);
|
||||||
|
for (int i = 0; i < tn && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
|
||||||
|
cJSON* tag = cJSON_GetArrayItem(list_tags, i);
|
||||||
|
if (!tag || !cJSON_IsArray(tag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||||
|
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) ||
|
||||||
|
!key->valuestring || !val->valuestring || strcmp(key->valuestring, "a") != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int skill_kind = 0;
|
||||||
|
char skill_author[65] = {0};
|
||||||
|
char skill_slug[65] = {0};
|
||||||
|
if (parse_skill_address_tag_local(val->valuestring, &skill_kind, skill_author, skill_slug) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* skill_filter = cJSON_CreateObject();
|
||||||
|
cJSON* sk_kinds = cJSON_CreateArray();
|
||||||
|
cJSON* sk_authors = cJSON_CreateArray();
|
||||||
|
cJSON* d_values = cJSON_CreateArray();
|
||||||
|
if (!skill_filter || !sk_kinds || !sk_authors || !d_values) {
|
||||||
|
cJSON_Delete(skill_filter);
|
||||||
|
cJSON_Delete(sk_kinds);
|
||||||
|
cJSON_Delete(sk_authors);
|
||||||
|
cJSON_Delete(d_values);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(skill_kind));
|
||||||
|
cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
|
||||||
|
cJSON_AddItemToArray(sk_authors, cJSON_CreateString(skill_author));
|
||||||
|
cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
|
||||||
|
cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_slug));
|
||||||
|
cJSON_AddItemToObject(skill_filter, "#d", d_values);
|
||||||
|
cJSON_AddNumberToObject(skill_filter, "limit", 1);
|
||||||
|
|
||||||
|
char* skill_json = nostr_handler_query_json(skill_filter, 8000);
|
||||||
|
cJSON_Delete(skill_filter);
|
||||||
|
if (!skill_json) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* skill_events = cJSON_Parse(skill_json);
|
||||||
|
free(skill_json);
|
||||||
|
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
|
||||||
|
cJSON_Delete(skill_events);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0);
|
||||||
|
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
|
||||||
|
cJSON* skill_tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
|
||||||
|
if (!content || !cJSON_IsString(content) || !content->valuestring || !skill_tags || !cJSON_IsArray(skill_tags)) {
|
||||||
|
cJSON_Delete(skill_events);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* enabled_tag = find_tag_value_string_local(skill_tags, "enabled");
|
||||||
|
const char* enabled_s = (enabled_tag && cJSON_IsString(enabled_tag) && enabled_tag->valuestring)
|
||||||
|
? enabled_tag->valuestring
|
||||||
|
: "true";
|
||||||
|
int is_enabled = !(strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0);
|
||||||
|
if (!is_enabled) {
|
||||||
|
cJSON_Delete(skill_events);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp[tmp_count].kind = skill_kind;
|
||||||
|
snprintf(tmp[tmp_count].author_pubkey_hex, sizeof(tmp[tmp_count].author_pubkey_hex), "%s", skill_author);
|
||||||
|
snprintf(tmp[tmp_count].slug, sizeof(tmp[tmp_count].slug), "%s", skill_slug);
|
||||||
|
tmp[tmp_count].content = strdup(content->valuestring);
|
||||||
|
if (tmp[tmp_count].content) {
|
||||||
|
tmp_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(skill_events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(adoption_events);
|
||||||
|
|
||||||
|
if (tmp_count == 0 && g_cfg->startup_event_count > 0 && g_cfg->startup_events) {
|
||||||
|
for (int i = 0; i < g_cfg->startup_event_count && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
|
||||||
|
startup_event_t* se = &g_cfg->startup_events[i];
|
||||||
|
if (!se || (se->kind != 31123 && se->kind != 31124) || !se->content || se->content[0] == '\0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char slug[65] = {0};
|
||||||
|
if (se->tags_json) {
|
||||||
|
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||||
|
cJSON* d_tag = tags ? find_tag_value_string_local(tags, "d") : NULL;
|
||||||
|
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && d_tag->valuestring[0] != '\0') {
|
||||||
|
snprintf(slug, sizeof(slug), "%s", d_tag->valuestring);
|
||||||
|
}
|
||||||
|
cJSON_Delete(tags);
|
||||||
|
}
|
||||||
|
if (slug[0] == '\0') {
|
||||||
|
snprintf(slug, sizeof(slug), "startup-skill-%d", i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp[tmp_count].kind = se->kind;
|
||||||
|
snprintf(tmp[tmp_count].author_pubkey_hex,
|
||||||
|
sizeof(tmp[tmp_count].author_pubkey_hex),
|
||||||
|
"%s",
|
||||||
|
(g_cfg->keys.public_key_hex && g_cfg->keys.public_key_hex[0] != '\0')
|
||||||
|
? g_cfg->keys.public_key_hex
|
||||||
|
: "unknown");
|
||||||
|
snprintf(tmp[tmp_count].slug, sizeof(tmp[tmp_count].slug), "%s", slug);
|
||||||
|
tmp[tmp_count].content = strdup(se->content);
|
||||||
|
if (tmp[tmp_count].content) {
|
||||||
|
tmp_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||||
|
clear_adopted_skills_cache_locked();
|
||||||
|
for (int i = 0; i < tmp_count; i++) {
|
||||||
|
g_adopted_skills[g_adopted_skills_count] = tmp[i];
|
||||||
|
tmp[i].content = NULL;
|
||||||
|
g_adopted_skills_count++;
|
||||||
|
}
|
||||||
|
g_adopted_skills_last_refresh_at = now;
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
|
||||||
|
for (int i = 0; i < tmp_count; i++) {
|
||||||
|
free(tmp[i].content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int append_adopted_skills_context(cJSON* messages) {
|
||||||
|
if (!messages || !g_cfg) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)refresh_adopted_skills_cache_if_needed();
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||||
|
int cached_count = g_adopted_skills_count;
|
||||||
|
if (cached_count <= 0) {
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t capacity = (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS + 2048U;
|
||||||
|
char* payload = (char*)malloc(capacity);
|
||||||
|
if (!payload) {
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t used = 0;
|
||||||
|
int n = snprintf(payload,
|
||||||
|
capacity,
|
||||||
|
"## Adopted Skills Memory (source: adoption list + startup fallback)\n\nAdopted skills memory (always-on behavioral instructions): follow these skill instructions when relevant and never violate explicit constraints.\n");
|
||||||
|
if (n < 0) {
|
||||||
|
free(payload);
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
used = (size_t)n < capacity ? (size_t)n : capacity - 1U;
|
||||||
|
|
||||||
|
size_t total_skill_chars = 0;
|
||||||
|
int included = 0;
|
||||||
|
for (int i = 0; i < cached_count && used + 64U < capacity; i++) {
|
||||||
|
const char* skill_content = g_adopted_skills[i].content ? g_adopted_skills[i].content : "";
|
||||||
|
size_t content_len = strlen(skill_content);
|
||||||
|
size_t clipped_len = content_len;
|
||||||
|
|
||||||
|
if (clipped_len > (size_t)AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS) {
|
||||||
|
clipped_len = (size_t)AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total_skill_chars + clipped_len > (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS) {
|
||||||
|
size_t remaining = (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS - total_skill_chars;
|
||||||
|
if (remaining == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
clipped_len = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
int head_n = snprintf(payload + used,
|
||||||
|
capacity - used,
|
||||||
|
"\n\n---\n\n### Skill\nSkill slug: %s\nSkill address: %d:%s:%s\n\nInstructions:\n",
|
||||||
|
g_adopted_skills[i].slug,
|
||||||
|
g_adopted_skills[i].kind,
|
||||||
|
g_adopted_skills[i].author_pubkey_hex,
|
||||||
|
g_adopted_skills[i].slug);
|
||||||
|
if (head_n < 0 || (size_t)head_n >= (capacity - used)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
used += (size_t)head_n;
|
||||||
|
|
||||||
|
size_t write_len = clipped_len;
|
||||||
|
if (write_len >= (capacity - used)) {
|
||||||
|
write_len = capacity - used - 1U;
|
||||||
|
}
|
||||||
|
memcpy(payload + used, skill_content, write_len);
|
||||||
|
used += write_len;
|
||||||
|
payload[used] = '\0';
|
||||||
|
|
||||||
|
int truncated = (content_len > write_len);
|
||||||
|
if (truncated && used + 24U < capacity) {
|
||||||
|
int tail_n = snprintf(payload + used, capacity - used, "\n...[truncated]\n");
|
||||||
|
if (tail_n > 0 && (size_t)tail_n < (capacity - used)) {
|
||||||
|
used += (size_t)tail_n;
|
||||||
|
}
|
||||||
|
} else if (used + 1U < capacity) {
|
||||||
|
payload[used++] = '\n';
|
||||||
|
payload[used] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
total_skill_chars += write_len;
|
||||||
|
included++;
|
||||||
|
if (total_skill_chars >= (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (included < cached_count && used + 96U < capacity) {
|
||||||
|
int extra_n = snprintf(payload + used,
|
||||||
|
capacity - used,
|
||||||
|
"\nAdditional adopted skills omitted due to context budget limits (%d/%d included).\n",
|
||||||
|
included,
|
||||||
|
cached_count);
|
||||||
|
if (extra_n > 0 && (size_t)extra_n < (capacity - used)) {
|
||||||
|
used += (size_t)extra_n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
|
||||||
|
int rc = append_simple_message(messages, "system", payload);
|
||||||
|
free(payload);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
int agent_init(didactyl_config_t* config, const char* system_context) {
|
int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||||
if (!config || !system_context) {
|
if (!config || !system_context) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -486,6 +1008,11 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
|
|||||||
g_seen_msgs_count = 0;
|
g_seen_msgs_count = 0;
|
||||||
g_seen_msgs_next = 0;
|
g_seen_msgs_next = 0;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||||
|
clear_adopted_skills_cache_locked();
|
||||||
|
g_adopted_skills_last_refresh_at = 0;
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,6 +1089,54 @@ void agent_on_trigger(const char* skill_slug,
|
|||||||
free(response);
|
free(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int agent_build_admin_messages_json(const char* current_user_message, char** out_messages_json) {
|
||||||
|
if (!out_messages_json || !g_cfg || !g_system_context) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_messages_json = NULL;
|
||||||
|
|
||||||
|
cJSON* messages = cJSON_CreateArray();
|
||||||
|
if (!messages) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (append_simple_message(messages, "system", g_system_context) != 0 ||
|
||||||
|
append_admin_identity_context(messages) != 0 ||
|
||||||
|
append_startup_events_context(messages) != 0 ||
|
||||||
|
append_adopted_skills_context(messages) != 0 ||
|
||||||
|
append_recent_admin_dm_history(messages, current_user_message) != 0) {
|
||||||
|
cJSON_Delete(messages);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* admin_notes = nostr_handler_get_admin_kind1_notes_context();
|
||||||
|
if (admin_notes) {
|
||||||
|
const char* prefix = "## Administrator Recent Notes (source: nostr kind 1)\n\n";
|
||||||
|
size_t n = strlen(prefix) + strlen(admin_notes) + 1U;
|
||||||
|
char* payload = (char*)malloc(n);
|
||||||
|
if (payload) {
|
||||||
|
snprintf(payload, n, "%s%s", prefix, admin_notes);
|
||||||
|
(void)append_simple_message(messages, "system", payload);
|
||||||
|
free(payload);
|
||||||
|
}
|
||||||
|
free(admin_notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||||
|
cJSON_Delete(messages);
|
||||||
|
if (!messages_json) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_messages_json = messages_json;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tools_context_t* agent_tools_context(void) {
|
||||||
|
return &g_tools_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
void agent_on_message(const char* sender_pubkey_hex,
|
void agent_on_message(const char* sender_pubkey_hex,
|
||||||
const char* message,
|
const char* message,
|
||||||
didactyl_sender_tier_t tier,
|
didactyl_sender_tier_t tier,
|
||||||
@@ -629,27 +1204,20 @@ void agent_on_message(const char* sender_pubkey_hex,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON* messages = cJSON_CreateArray();
|
char* base_messages_json = NULL;
|
||||||
if (!messages) {
|
if (agent_build_admin_messages_json(message, &base_messages_json) != 0 || !base_messages_json) {
|
||||||
free(tools_json);
|
|
||||||
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation state.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (append_simple_message(messages, "system", g_system_context) != 0 ||
|
|
||||||
append_admin_identity_context(messages) != 0 ||
|
|
||||||
append_startup_events_context(messages) != 0 ||
|
|
||||||
append_recent_admin_dm_history(messages, message) != 0) {
|
|
||||||
cJSON_Delete(messages);
|
|
||||||
free(tools_json);
|
free(tools_json);
|
||||||
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
char* admin_notes = nostr_handler_get_admin_kind1_notes_context();
|
cJSON* messages = cJSON_Parse(base_messages_json);
|
||||||
if (admin_notes) {
|
free(base_messages_json);
|
||||||
(void)append_simple_message(messages, "system", admin_notes);
|
if (!messages || !cJSON_IsArray(messages)) {
|
||||||
free(admin_notes);
|
cJSON_Delete(messages);
|
||||||
|
free(tools_json);
|
||||||
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation state.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (append_simple_message(messages, "user", message) != 0) {
|
if (append_simple_message(messages, "user", message) != 0) {
|
||||||
@@ -747,4 +1315,9 @@ void agent_cleanup(void) {
|
|||||||
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
|
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
|
||||||
g_seen_msgs_count = 0;
|
g_seen_msgs_count = 0;
|
||||||
g_seen_msgs_next = 0;
|
g_seen_msgs_next = 0;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_adopted_skills_mutex);
|
||||||
|
clear_adopted_skills_cache_locked();
|
||||||
|
g_adopted_skills_last_refresh_at = 0;
|
||||||
|
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "nostr_handler.h"
|
#include "nostr_handler.h"
|
||||||
#include "cjson/cJSON.h"
|
#include "cjson/cJSON.h"
|
||||||
|
#include "tools.h"
|
||||||
|
|
||||||
struct trigger_manager;
|
struct trigger_manager;
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ void agent_on_message(const char* sender_pubkey_hex,
|
|||||||
const char* message,
|
const char* message,
|
||||||
didactyl_sender_tier_t tier,
|
didactyl_sender_tier_t tier,
|
||||||
void* user_data);
|
void* user_data);
|
||||||
|
int agent_build_admin_messages_json(const char* current_user_message, char** out_messages_json);
|
||||||
|
tools_context_t* agent_tools_context(void);
|
||||||
void agent_cleanup(void);
|
void agent_cleanup(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
43
src/config.c
43
src/config.c
@@ -315,6 +315,40 @@ static int parse_triggers_config(cJSON* root, didactyl_config_t* config) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int parse_api_config(cJSON* root, didactyl_config_t* config) {
|
||||||
|
cJSON* api = cJSON_GetObjectItemCaseSensitive(root, "api");
|
||||||
|
if (!api || !cJSON_IsObject(api)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(api, "enabled");
|
||||||
|
cJSON* port = cJSON_GetObjectItemCaseSensitive(api, "port");
|
||||||
|
|
||||||
|
if (enabled && cJSON_IsBool(enabled)) {
|
||||||
|
config->api.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (port && cJSON_IsNumber(port)) {
|
||||||
|
config->api.port = (int)port->valuedouble;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy_json_string(api,
|
||||||
|
"bind_address",
|
||||||
|
config->api.bind_address,
|
||||||
|
sizeof(config->api.bind_address),
|
||||||
|
0) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config->api.port < 1 || config->api.port > 65535) {
|
||||||
|
config->api.port = 8484;
|
||||||
|
}
|
||||||
|
if (config->api.bind_address[0] == '\0') {
|
||||||
|
snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
|
static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
|
||||||
if (!tags || !cJSON_IsArray(tags) || !tag_key) {
|
if (!tags || !cJSON_IsArray(tags) || !tag_key) {
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -632,6 +666,10 @@ int config_load(const char* path, didactyl_config_t* config) {
|
|||||||
config->triggers.llm_rate_limit_per_minute = 10;
|
config->triggers.llm_rate_limit_per_minute = 10;
|
||||||
config->triggers.template_rate_limit_per_minute = 60;
|
config->triggers.template_rate_limit_per_minute = 60;
|
||||||
|
|
||||||
|
config->api.enabled = 0;
|
||||||
|
config->api.port = 8484;
|
||||||
|
snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1");
|
||||||
|
|
||||||
char* json_buf = NULL;
|
char* json_buf = NULL;
|
||||||
size_t json_len = 0;
|
size_t json_len = 0;
|
||||||
if (read_file_to_buffer(path, &json_buf, &json_len) != 0) {
|
if (read_file_to_buffer(path, &json_buf, &json_len) != 0) {
|
||||||
@@ -736,6 +774,11 @@ int config_load(const char* path, didactyl_config_t* config) {
|
|||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parse_api_config(root, config) != 0) {
|
||||||
|
config_set_error("invalid api configuration");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
||||||
config_set_error("keys.nsec must be valid nsec1... or 64-char hex private key");
|
config_set_error("keys.nsec must be valid nsec1... or 64-char hex private key");
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ typedef struct {
|
|||||||
int template_rate_limit_per_minute;
|
int template_rate_limit_per_minute;
|
||||||
} triggers_config_t;
|
} triggers_config_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int enabled;
|
||||||
|
int port;
|
||||||
|
char bind_address[OW_MAX_URL_LEN];
|
||||||
|
} api_config_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
agent_keys_t keys;
|
agent_keys_t keys;
|
||||||
admin_config_t admin;
|
admin_config_t admin;
|
||||||
@@ -90,6 +96,7 @@ typedef struct {
|
|||||||
security_config_t security;
|
security_config_t security;
|
||||||
admin_context_config_t admin_context;
|
admin_context_config_t admin_context;
|
||||||
triggers_config_t triggers;
|
triggers_config_t triggers;
|
||||||
|
api_config_t api;
|
||||||
startup_event_t* startup_events;
|
startup_event_t* startup_events;
|
||||||
int startup_event_count;
|
int startup_event_count;
|
||||||
char config_path[OW_MAX_URL_LEN];
|
char config_path[OW_MAX_URL_LEN];
|
||||||
|
|||||||
710
src/http_api.c
Normal file
710
src/http_api.c
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "http_api.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "agent.h"
|
||||||
|
#include "llm.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "nostr_handler.h"
|
||||||
|
#include "trigger_manager.h"
|
||||||
|
#include "tools.h"
|
||||||
|
#include "cjson/cJSON.h"
|
||||||
|
#include "debug.h"
|
||||||
|
#include "mongoose.h"
|
||||||
|
|
||||||
|
static http_api_context_t g_api_ctx;
|
||||||
|
static int g_api_initialized = 0;
|
||||||
|
static struct mg_mgr g_mgr;
|
||||||
|
static struct mg_connection* g_listener = NULL;
|
||||||
|
|
||||||
|
static const char* k_json_headers =
|
||||||
|
"Content-Type: application/json\r\n"
|
||||||
|
"Access-Control-Allow-Origin: *\r\n"
|
||||||
|
"Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n"
|
||||||
|
"Access-Control-Allow-Headers: Content-Type\r\n";
|
||||||
|
|
||||||
|
static int method_is(const struct mg_http_message* hm, const char* method) {
|
||||||
|
size_t n = strlen(method);
|
||||||
|
return hm && hm->method.len == n && strncmp(hm->method.buf, method, n) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int mgstr_eq(struct mg_str s, const char* lit) {
|
||||||
|
size_t n = strlen(lit);
|
||||||
|
return s.len == n && strncmp(s.buf, lit, n) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reply_json(struct mg_connection* c, int status, cJSON* root) {
|
||||||
|
char* out = cJSON_PrintUnformatted(root);
|
||||||
|
if (!out) {
|
||||||
|
mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"json serialization failed\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mg_http_reply(c, status, k_json_headers, "%s", out);
|
||||||
|
free(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reply_error(struct mg_connection* c, int status, const char* err) {
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
if (!root) {
|
||||||
|
mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"oom\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cJSON_AddBoolToObject(root, "success", 0);
|
||||||
|
cJSON_AddStringToObject(root, "error", err ? err : "error");
|
||||||
|
reply_json(c, status, root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* parse_body_json(const struct mg_http_message* hm) {
|
||||||
|
if (!hm || hm->body.len == 0) {
|
||||||
|
return cJSON_CreateObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
char* buf = (char*)malloc(hm->body.len + 1U);
|
||||||
|
if (!buf) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
memcpy(buf, hm->body.buf, hm->body.len);
|
||||||
|
buf[hm->body.len] = '\0';
|
||||||
|
|
||||||
|
cJSON* root = cJSON_Parse(buf);
|
||||||
|
free(buf);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int estimate_tokens_from_chars(int chars) {
|
||||||
|
if (chars <= 0) return 0;
|
||||||
|
return (chars + 3) / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char* maybe_model_override_begin(cJSON* body, llm_config_t* out_old_cfg, int* out_overridden) {
|
||||||
|
if (!body || !out_old_cfg || !out_overridden) return NULL;
|
||||||
|
*out_overridden = 0;
|
||||||
|
|
||||||
|
cJSON* model = cJSON_GetObjectItemCaseSensitive(body, "model");
|
||||||
|
if (!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0') {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (llm_get_config(out_old_cfg) != 0) {
|
||||||
|
return "failed to read current llm config";
|
||||||
|
}
|
||||||
|
|
||||||
|
llm_config_t next = *out_old_cfg;
|
||||||
|
snprintf(next.model, sizeof(next.model), "%s", model->valuestring);
|
||||||
|
if (llm_set_config(&next) != 0) {
|
||||||
|
return "failed to set temporary model";
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_overridden = 1;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void maybe_model_override_end(const llm_config_t* old_cfg, int overridden) {
|
||||||
|
if (overridden && old_cfg) {
|
||||||
|
(void)llm_set_config(old_cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* classify_part_name(cJSON* msg, int idx) {
|
||||||
|
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
|
||||||
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
|
||||||
|
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||||
|
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||||
|
|
||||||
|
if (idx == 0 && strcmp(role_s, "system") == 0) return "system_prompt";
|
||||||
|
if (strncmp(content_s, "## Administrator Identity", 25) == 0 ||
|
||||||
|
strncmp(content_s, "This is your administrator!", 27) == 0) return "admin_identity";
|
||||||
|
if (strncmp(content_s, "## Administrator Kind 0 Profile", 31) == 0 ||
|
||||||
|
strncmp(content_s, "Administrator kind 0 profile content", 36) == 0) return "admin_kind0";
|
||||||
|
if (strncmp(content_s, "## Administrator Relay List", 27) == 0 ||
|
||||||
|
strncmp(content_s, "Administrator kind 10002 relay-list content", 43) == 0) return "admin_relay_list";
|
||||||
|
if (strncmp(content_s, "## Startup Events Memory", 24) == 0 ||
|
||||||
|
strncmp(content_s, "Startup events memory", 21) == 0) return "startup_events";
|
||||||
|
if (strncmp(content_s, "## Adopted Skills Memory", 24) == 0 ||
|
||||||
|
strncmp(content_s, "Adopted skills memory", 21) == 0) return "adopted_skills";
|
||||||
|
if (strncmp(content_s, "## Administrator Recent Notes", 29) == 0 ||
|
||||||
|
strncmp(content_s, "Administrator recent public notes", 33) == 0) return "admin_notes";
|
||||||
|
if (strcmp(role_s, "user") == 0 || strcmp(role_s, "assistant") == 0) return "dm_history";
|
||||||
|
return "context_part";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int build_context_parts_response(cJSON* out_root) {
|
||||||
|
if (!out_root) return -1;
|
||||||
|
|
||||||
|
char* messages_json = NULL;
|
||||||
|
if (agent_build_admin_messages_json("", &messages_json) != 0 || !messages_json) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* arr = cJSON_Parse(messages_json);
|
||||||
|
free(messages_json);
|
||||||
|
if (!arr || !cJSON_IsArray(arr)) {
|
||||||
|
cJSON_Delete(arr);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* parts = cJSON_CreateArray();
|
||||||
|
if (!parts) {
|
||||||
|
cJSON_Delete(arr);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total_chars = 0;
|
||||||
|
int n = cJSON_GetArraySize(arr);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
cJSON* msg = cJSON_GetArrayItem(arr, i);
|
||||||
|
if (!msg || !cJSON_IsObject(msg)) continue;
|
||||||
|
|
||||||
|
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
|
||||||
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
|
||||||
|
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "unknown";
|
||||||
|
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||||
|
|
||||||
|
int chars = (int)strlen(content_s);
|
||||||
|
total_chars += chars;
|
||||||
|
|
||||||
|
cJSON* part = cJSON_CreateObject();
|
||||||
|
if (!part) continue;
|
||||||
|
cJSON_AddStringToObject(part, "name", classify_part_name(msg, i));
|
||||||
|
cJSON_AddStringToObject(part, "role", role_s);
|
||||||
|
cJSON_AddNumberToObject(part, "chars", chars);
|
||||||
|
cJSON_AddNumberToObject(part, "estimated_tokens", estimate_tokens_from_chars(chars));
|
||||||
|
cJSON_AddStringToObject(part, "content", content_s);
|
||||||
|
cJSON_AddItemToArray(parts, part);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddBoolToObject(out_root, "success", 1);
|
||||||
|
cJSON_AddNumberToObject(out_root, "total_chars", total_chars);
|
||||||
|
cJSON_AddNumberToObject(out_root, "total_estimated_tokens", estimate_tokens_from_chars(total_chars));
|
||||||
|
cJSON_AddItemToObject(out_root, "parts", parts);
|
||||||
|
|
||||||
|
cJSON_AddItemToObject(out_root, "messages", arr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int append_simple_message_local(cJSON* messages, const char* role, const char* content) {
|
||||||
|
if (!messages || !role) return -1;
|
||||||
|
cJSON* msg = cJSON_CreateObject();
|
||||||
|
if (!msg) return -1;
|
||||||
|
cJSON_AddStringToObject(msg, "role", role);
|
||||||
|
cJSON_AddStringToObject(msg, "content", content ? content : "");
|
||||||
|
cJSON_AddItemToArray(messages, msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int append_assistant_tool_calls_message_local(cJSON* messages, const llm_response_t* resp) {
|
||||||
|
if (!messages || !resp || resp->tool_call_count <= 0) return -1;
|
||||||
|
|
||||||
|
cJSON* assistant = cJSON_CreateObject();
|
||||||
|
cJSON* tool_calls = cJSON_CreateArray();
|
||||||
|
if (!assistant || !tool_calls) {
|
||||||
|
cJSON_Delete(assistant);
|
||||||
|
cJSON_Delete(tool_calls);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(assistant, "role", "assistant");
|
||||||
|
if (resp->content) cJSON_AddStringToObject(assistant, "content", resp->content);
|
||||||
|
else cJSON_AddNullToObject(assistant, "content");
|
||||||
|
|
||||||
|
for (int i = 0; i < resp->tool_call_count; i++) {
|
||||||
|
const llm_tool_call_t* tc = &resp->tool_calls[i];
|
||||||
|
cJSON* tc_obj = cJSON_CreateObject();
|
||||||
|
cJSON* fn_obj = cJSON_CreateObject();
|
||||||
|
if (!tc_obj || !fn_obj) {
|
||||||
|
cJSON_Delete(tc_obj);
|
||||||
|
cJSON_Delete(fn_obj);
|
||||||
|
cJSON_Delete(assistant);
|
||||||
|
cJSON_Delete(tool_calls);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : "");
|
||||||
|
cJSON_AddStringToObject(tc_obj, "type", "function");
|
||||||
|
cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : "");
|
||||||
|
cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
|
||||||
|
cJSON_AddItemToObject(tc_obj, "function", fn_obj);
|
||||||
|
cJSON_AddItemToArray(tool_calls, tc_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToObject(assistant, "tool_calls", tool_calls);
|
||||||
|
cJSON_AddItemToArray(messages, assistant);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int append_tool_result_message_local(cJSON* messages, const char* tool_call_id, const char* tool_result_json) {
|
||||||
|
if (!messages || !tool_call_id) return -1;
|
||||||
|
|
||||||
|
cJSON* msg = cJSON_CreateObject();
|
||||||
|
if (!msg) return -1;
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(msg, "role", "tool");
|
||||||
|
cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id);
|
||||||
|
cJSON_AddStringToObject(msg,
|
||||||
|
"content",
|
||||||
|
tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}");
|
||||||
|
cJSON_AddItemToArray(messages, msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* run_prompt_with_tools(cJSON* body) {
|
||||||
|
if (!body || !cJSON_IsObject(body)) return NULL;
|
||||||
|
|
||||||
|
cJSON* messages_node = cJSON_GetObjectItemCaseSensitive(body, "messages");
|
||||||
|
if (!messages_node || !cJSON_IsArray(messages_node)) return NULL;
|
||||||
|
|
||||||
|
cJSON* convo = cJSON_Duplicate(messages_node, 1);
|
||||||
|
if (!convo || !cJSON_IsArray(convo)) {
|
||||||
|
cJSON_Delete(convo);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int max_turns = 4;
|
||||||
|
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
|
||||||
|
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
|
||||||
|
max_turns = (int)max_turns_node->valuedouble;
|
||||||
|
if (max_turns < 1) max_turns = 1;
|
||||||
|
if (max_turns > 16) max_turns = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
|
||||||
|
if (!tools_json) {
|
||||||
|
cJSON_Delete(convo);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
cJSON* turns = cJSON_CreateArray();
|
||||||
|
if (!root || !turns) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
cJSON_Delete(turns);
|
||||||
|
free(tools_json);
|
||||||
|
cJSON_Delete(convo);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* final_response = NULL;
|
||||||
|
|
||||||
|
for (int turn = 0; turn < max_turns; turn++) {
|
||||||
|
char* messages_json = cJSON_PrintUnformatted(convo);
|
||||||
|
if (!messages_json) break;
|
||||||
|
|
||||||
|
llm_response_t resp;
|
||||||
|
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
||||||
|
free(messages_json);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
final_response = strdup("LLM request failed.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* turn_obj = cJSON_CreateObject();
|
||||||
|
if (turn_obj) {
|
||||||
|
cJSON_AddNumberToObject(turn_obj, "turn", turn + 1);
|
||||||
|
cJSON* tool_calls = cJSON_CreateArray();
|
||||||
|
if (tool_calls) cJSON_AddItemToObject(turn_obj, "tool_calls", tool_calls);
|
||||||
|
cJSON_AddItemToArray(turns, turn_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.tool_call_count <= 0) {
|
||||||
|
final_response = strdup(resp.content ? resp.content : "No response content.");
|
||||||
|
llm_response_free(&resp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (append_assistant_tool_calls_message_local(convo, &resp) != 0) {
|
||||||
|
llm_response_free(&resp);
|
||||||
|
final_response = strdup("Failed to append assistant tool calls.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* current_turn = cJSON_GetArrayItem(turns, cJSON_GetArraySize(turns) - 1);
|
||||||
|
cJSON* turn_tool_calls = current_turn ? cJSON_GetObjectItemCaseSensitive(current_turn, "tool_calls") : NULL;
|
||||||
|
|
||||||
|
for (int i = 0; i < resp.tool_call_count; i++) {
|
||||||
|
llm_tool_call_t* tc = &resp.tool_calls[i];
|
||||||
|
char* tool_result = tools_execute(g_api_ctx.tools_ctx, tc->name, tc->arguments_json);
|
||||||
|
if (!tool_result) {
|
||||||
|
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turn_tool_calls && cJSON_IsArray(turn_tool_calls)) {
|
||||||
|
cJSON* tr = cJSON_CreateObject();
|
||||||
|
if (tr) {
|
||||||
|
cJSON_AddStringToObject(tr, "name", tc->name ? tc->name : "");
|
||||||
|
cJSON_AddStringToObject(tr, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
|
||||||
|
cJSON_AddStringToObject(tr, "result", tool_result ? tool_result : "{}");
|
||||||
|
cJSON_AddItemToArray(turn_tool_calls, tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (append_tool_result_message_local(convo,
|
||||||
|
tc->id ? tc->id : "",
|
||||||
|
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
|
||||||
|
free(tool_result);
|
||||||
|
llm_response_free(&resp);
|
||||||
|
final_response = strdup("Failed to append tool result.");
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
free(tool_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
llm_response_free(&resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (!final_response) {
|
||||||
|
final_response = strdup("I hit my tool-use limit for this prompt run.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddBoolToObject(root, "success", 1);
|
||||||
|
cJSON_AddStringToObject(root, "final_response", final_response ? final_response : "");
|
||||||
|
cJSON_AddItemToObject(root, "turns", turns);
|
||||||
|
|
||||||
|
llm_config_t cfg;
|
||||||
|
if (llm_get_config(&cfg) == 0) {
|
||||||
|
cJSON_AddStringToObject(root, "model_used", cfg.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
int input_chars = 0;
|
||||||
|
char* convo_json = cJSON_PrintUnformatted(convo);
|
||||||
|
if (convo_json) {
|
||||||
|
input_chars = (int)strlen(convo_json);
|
||||||
|
free(convo_json);
|
||||||
|
}
|
||||||
|
cJSON_AddNumberToObject(root, "total_input_tokens_estimate", estimate_tokens_from_chars(input_chars));
|
||||||
|
cJSON_AddNumberToObject(root,
|
||||||
|
"total_output_tokens_estimate",
|
||||||
|
estimate_tokens_from_chars((int)strlen(final_response ? final_response : "")));
|
||||||
|
|
||||||
|
free(final_response);
|
||||||
|
free(tools_json);
|
||||||
|
cJSON_Delete(convo);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_status(struct mg_connection* c) {
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
if (!root) {
|
||||||
|
reply_error(c, 500, "oom");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddBoolToObject(root, "success", 1);
|
||||||
|
cJSON_AddStringToObject(root, "name", DIDACTYL_NAME);
|
||||||
|
cJSON_AddStringToObject(root, "version", DIDACTYL_VERSION);
|
||||||
|
|
||||||
|
if (g_api_ctx.cfg) {
|
||||||
|
cJSON_AddStringToObject(root, "pubkey", g_api_ctx.cfg->keys.public_key_hex);
|
||||||
|
cJSON_AddNumberToObject(root, "relay_count", g_api_ctx.cfg->relay_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddNumberToObject(root, "connected_relays", nostr_handler_connected_relay_count());
|
||||||
|
if (g_api_ctx.trigger_manager) {
|
||||||
|
cJSON_AddNumberToObject(root, "active_triggers", trigger_manager_active_count(g_api_ctx.trigger_manager));
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_json(c, 200, root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_context_current(struct mg_connection* c) {
|
||||||
|
char* messages_json = NULL;
|
||||||
|
if (agent_build_admin_messages_json("", &messages_json) != 0 || !messages_json) {
|
||||||
|
reply_error(c, 500, "failed to build context messages");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* messages = cJSON_Parse(messages_json);
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
if (!root || !messages || !cJSON_IsArray(messages)) {
|
||||||
|
free(messages_json);
|
||||||
|
cJSON_Delete(messages);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
reply_error(c, 500, "failed to serialize context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chars = (int)strlen(messages_json);
|
||||||
|
cJSON_AddBoolToObject(root, "success", 1);
|
||||||
|
cJSON_AddNumberToObject(root, "total_chars", chars);
|
||||||
|
cJSON_AddNumberToObject(root, "total_estimated_tokens", estimate_tokens_from_chars(chars));
|
||||||
|
cJSON_AddItemToObject(root, "messages", messages);
|
||||||
|
|
||||||
|
reply_json(c, 200, root);
|
||||||
|
|
||||||
|
free(messages_json);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_context_parts(struct mg_connection* c) {
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
if (!root) {
|
||||||
|
reply_error(c, 500, "oom");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build_context_parts_response(root) != 0) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
reply_error(c, 500, "failed to build context parts");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_json(c, 200, root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_prompt_run_simple(struct mg_connection* c, const struct mg_http_message* hm) {
|
||||||
|
cJSON* body = parse_body_json(hm);
|
||||||
|
if (!body || !cJSON_IsObject(body)) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, "invalid JSON body");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* system = cJSON_GetObjectItemCaseSensitive(body, "system");
|
||||||
|
cJSON* user = cJSON_GetObjectItemCaseSensitive(body, "user");
|
||||||
|
if (!system || !user || !cJSON_IsString(system) || !cJSON_IsString(user) ||
|
||||||
|
!system->valuestring || !user->valuestring) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, "system and user strings are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
llm_config_t old_cfg;
|
||||||
|
int overridden = 0;
|
||||||
|
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
|
||||||
|
if (model_err) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, model_err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* response = llm_chat(system->valuestring, user->valuestring);
|
||||||
|
maybe_model_override_end(&old_cfg, overridden);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 500, "llm request failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
if (!root) {
|
||||||
|
free(response);
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 500, "oom");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddBoolToObject(root, "success", 1);
|
||||||
|
cJSON_AddStringToObject(root, "response", response);
|
||||||
|
|
||||||
|
llm_config_t cfg;
|
||||||
|
if (llm_get_config(&cfg) == 0) {
|
||||||
|
cJSON_AddStringToObject(root, "model_used", cfg.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
int in_chars = (int)strlen(system->valuestring) + (int)strlen(user->valuestring);
|
||||||
|
cJSON_AddNumberToObject(root, "input_tokens_estimate", estimate_tokens_from_chars(in_chars));
|
||||||
|
cJSON_AddNumberToObject(root, "output_tokens_estimate", estimate_tokens_from_chars((int)strlen(response)));
|
||||||
|
|
||||||
|
free(response);
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_json(c, 200, root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_prompt_run(struct mg_connection* c, const struct mg_http_message* hm) {
|
||||||
|
cJSON* body = parse_body_json(hm);
|
||||||
|
if (!body || !cJSON_IsObject(body)) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, "invalid JSON body");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
llm_config_t old_cfg;
|
||||||
|
int overridden = 0;
|
||||||
|
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
|
||||||
|
if (model_err) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, model_err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* result = run_prompt_with_tools(body);
|
||||||
|
maybe_model_override_end(&old_cfg, overridden);
|
||||||
|
cJSON_Delete(body);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
reply_error(c, 500, "failed to run prompt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_json(c, 200, result);
|
||||||
|
cJSON_Delete(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* run_variant(cJSON* variant) {
|
||||||
|
if (!variant || !cJSON_IsObject(variant)) return NULL;
|
||||||
|
cJSON* body_copy = cJSON_Duplicate(variant, 1);
|
||||||
|
if (!body_copy) return NULL;
|
||||||
|
|
||||||
|
llm_config_t old_cfg;
|
||||||
|
int overridden = 0;
|
||||||
|
char* model_err = maybe_model_override_begin(body_copy, &old_cfg, &overridden);
|
||||||
|
if (model_err) {
|
||||||
|
cJSON* err = cJSON_CreateObject();
|
||||||
|
if (err) {
|
||||||
|
cJSON_AddBoolToObject(err, "success", 0);
|
||||||
|
cJSON_AddStringToObject(err, "error", model_err);
|
||||||
|
}
|
||||||
|
cJSON_Delete(body_copy);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* out = run_prompt_with_tools(body_copy);
|
||||||
|
maybe_model_override_end(&old_cfg, overridden);
|
||||||
|
cJSON_Delete(body_copy);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_prompt_compare(struct mg_connection* c, const struct mg_http_message* hm) {
|
||||||
|
cJSON* body = parse_body_json(hm);
|
||||||
|
if (!body || !cJSON_IsObject(body)) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, "invalid JSON body");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* a = cJSON_GetObjectItemCaseSensitive(body, "variant_a");
|
||||||
|
cJSON* b = cJSON_GetObjectItemCaseSensitive(body, "variant_b");
|
||||||
|
if (!a || !b || !cJSON_IsObject(a) || !cJSON_IsObject(b)) {
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 400, "variant_a and variant_b objects are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* out_a = run_variant(a);
|
||||||
|
cJSON* out_b = run_variant(b);
|
||||||
|
|
||||||
|
if (!out_a || !out_b) {
|
||||||
|
cJSON_Delete(out_a);
|
||||||
|
cJSON_Delete(out_b);
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 500, "failed to run one or more variants");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* root = cJSON_CreateObject();
|
||||||
|
if (!root) {
|
||||||
|
cJSON_Delete(out_a);
|
||||||
|
cJSON_Delete(out_b);
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_error(c, 500, "oom");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddBoolToObject(root, "success", 1);
|
||||||
|
cJSON_AddItemToObject(root, "variant_a", out_a);
|
||||||
|
cJSON_AddItemToObject(root, "variant_b", out_b);
|
||||||
|
|
||||||
|
cJSON_Delete(body);
|
||||||
|
reply_json(c, 200, root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void http_handler(struct mg_connection* c, int ev, void* ev_data) {
|
||||||
|
if (ev != MG_EV_HTTP_MSG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct mg_http_message* hm = (struct mg_http_message*)ev_data;
|
||||||
|
|
||||||
|
if (method_is(hm, "OPTIONS")) {
|
||||||
|
mg_http_reply(c, 204, k_json_headers, "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/status"), NULL)) {
|
||||||
|
handle_status(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/current"), NULL)) {
|
||||||
|
handle_context_current(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/parts"), NULL)) {
|
||||||
|
handle_context_parts(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run-simple"), NULL)) {
|
||||||
|
handle_prompt_run_simple(c, hm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run"), NULL)) {
|
||||||
|
handle_prompt_run(c, hm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/compare"), NULL)) {
|
||||||
|
handle_prompt_compare(c, hm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_error(c, 404, "not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
int http_api_init(const http_api_context_t* ctx) {
|
||||||
|
if (!ctx || !ctx->cfg || !ctx->tools_ctx) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_api_ctx = *ctx;
|
||||||
|
|
||||||
|
mg_mgr_init(&g_mgr);
|
||||||
|
|
||||||
|
char listen_url[320];
|
||||||
|
snprintf(listen_url,
|
||||||
|
sizeof(listen_url),
|
||||||
|
"http://%s:%d",
|
||||||
|
g_api_ctx.cfg->api.bind_address[0] ? g_api_ctx.cfg->api.bind_address : "127.0.0.1",
|
||||||
|
g_api_ctx.cfg->api.port > 0 ? g_api_ctx.cfg->api.port : 8484);
|
||||||
|
|
||||||
|
g_listener = mg_http_listen(&g_mgr, listen_url, http_handler, NULL);
|
||||||
|
if (!g_listener) {
|
||||||
|
mg_mgr_free(&g_mgr);
|
||||||
|
memset(&g_mgr, 0, sizeof(g_mgr));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_api_initialized = 1;
|
||||||
|
DEBUG_INFO("[didactyl] http api listening on %s", listen_url);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int http_api_poll(int timeout_ms) {
|
||||||
|
if (!g_api_initialized) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout_ms < 0) timeout_ms = 0;
|
||||||
|
mg_mgr_poll(&g_mgr, timeout_ms);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void http_api_cleanup(void) {
|
||||||
|
if (!g_api_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mg_mgr_free(&g_mgr);
|
||||||
|
memset(&g_mgr, 0, sizeof(g_mgr));
|
||||||
|
g_listener = NULL;
|
||||||
|
memset(&g_api_ctx, 0, sizeof(g_api_ctx));
|
||||||
|
g_api_initialized = 0;
|
||||||
|
}
|
||||||
19
src/http_api.h
Normal file
19
src/http_api.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#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(const http_api_context_t* ctx);
|
||||||
|
int http_api_poll(int timeout_ms);
|
||||||
|
void http_api_cleanup(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
32
src/main.c
32
src/main.c
@@ -16,6 +16,7 @@
|
|||||||
#include "trigger_manager.h"
|
#include "trigger_manager.h"
|
||||||
#include "tools.h"
|
#include "tools.h"
|
||||||
#include "cjson/cJSON.h"
|
#include "cjson/cJSON.h"
|
||||||
|
#include "http_api.h"
|
||||||
|
|
||||||
static volatile sig_atomic_t g_running = 1;
|
static volatile sig_atomic_t g_running = 1;
|
||||||
|
|
||||||
@@ -274,18 +275,49 @@ int main(int argc, char** argv) {
|
|||||||
signal(SIGTERM, signal_handler);
|
signal(SIGTERM, signal_handler);
|
||||||
signal(SIGPIPE, SIG_IGN);
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
|
int http_api_started = 0;
|
||||||
|
if (cfg.api.enabled) {
|
||||||
|
http_api_context_t http_ctx;
|
||||||
|
memset(&http_ctx, 0, sizeof(http_ctx));
|
||||||
|
http_ctx.cfg = &cfg;
|
||||||
|
http_ctx.tools_ctx = agent_tools_context();
|
||||||
|
http_ctx.trigger_manager = &trigger_manager;
|
||||||
|
|
||||||
|
if (http_api_init(&http_ctx) != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Failed to initialize HTTP API on %s:%d\n",
|
||||||
|
cfg.api.bind_address,
|
||||||
|
cfg.api.port);
|
||||||
|
agent_cleanup();
|
||||||
|
nostr_handler_cleanup();
|
||||||
|
llm_cleanup();
|
||||||
|
trigger_manager_cleanup(&trigger_manager);
|
||||||
|
config_free(&cfg);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
http_api_started = 1;
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||||
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
||||||
|
|
||||||
while (g_running) {
|
while (g_running) {
|
||||||
(void)nostr_handler_poll(100);
|
(void)nostr_handler_poll(100);
|
||||||
(void)trigger_manager_poll(&trigger_manager);
|
(void)trigger_manager_poll(&trigger_manager);
|
||||||
|
if (http_api_started) {
|
||||||
|
(void)http_api_poll(0);
|
||||||
|
}
|
||||||
struct timespec ts = {0, 10 * 1000 * 1000};
|
struct timespec ts = {0, 10 * 1000 * 1000};
|
||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("[didactyl] shutting down");
|
DEBUG_INFO("[didactyl] shutting down");
|
||||||
|
|
||||||
|
if (http_api_started) {
|
||||||
|
http_api_cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
agent_cleanup();
|
agent_cleanup();
|
||||||
nostr_handler_cleanup();
|
nostr_handler_cleanup();
|
||||||
llm_cleanup();
|
llm_cleanup();
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||||
#define DIDACTYL_VERSION_MAJOR 0
|
#define DIDACTYL_VERSION_MAJOR 0
|
||||||
#define DIDACTYL_VERSION_MINOR 0
|
#define DIDACTYL_VERSION_MINOR 0
|
||||||
#define DIDACTYL_VERSION_PATCH 26
|
#define DIDACTYL_VERSION_PATCH 27
|
||||||
#define DIDACTYL_VERSION "v0.0.26"
|
#define DIDACTYL_VERSION "v0.0.27"
|
||||||
|
|
||||||
// Agent metadata
|
// Agent metadata
|
||||||
#define DIDACTYL_NAME "Didactyl"
|
#define DIDACTYL_NAME "Didactyl"
|
||||||
|
|||||||
28193
src/mongoose.c
Normal file
28193
src/mongoose.c
Normal file
File diff suppressed because it is too large
Load Diff
4038
src/mongoose.h
Normal file
4038
src/mongoose.h
Normal file
File diff suppressed because it is too large
Load Diff
28193
vendor/mongoose.c
vendored
Normal file
28193
vendor/mongoose.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4038
vendor/mongoose.h
vendored
Normal file
4038
vendor/mongoose.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user