v0.2.61 - Skill template uses agent name + admin npub; add skill picker step with toggle menu and custom skill creation

This commit is contained in:
Didactyl User
2026-08-07 08:03:35 -04:00
parent 8a168bf7ba
commit 6c24ba8d96
4 changed files with 239 additions and 34 deletions
+2 -2
View File
@@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
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.2.60
## Current Status — v0.2.61
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.60Add FIPS relay to default relays after laantungir
> Last release update: v0.2.61Skill template uses agent name + admin npub; add skill picker step with toggle menu and custom skill creation
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+20 -15
View File
@@ -41,21 +41,26 @@ static const char* DIDACTYL_DEFAULT_RELAYS[] = {
#define DIDACTYL_DEFAULT_RELAY_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_RELAYS) / sizeof(DIDACTYL_DEFAULT_RELAYS[0])))
static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE =
"# Didactyl Agent\n\n"
"You are {{my_kind0_profile}}\n\n"
"Your npub: {{my_npub}}\n\n"
"## Rules\n\n"
"- Communicate through encrypted Nostr direct messages\n"
"- Keep responses concise and clear\n"
"- Be helpful and technically accurate\n"
"- If unsure, state uncertainty directly\n"
"- Use tools when a request requires taking action\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"
"- Maintain your task list as short-term working memory\n"
"- Never reveal your private key (nsec)\n"
"- You may share your public key (npub) with anyone";
/* Template for the identity_and_rules skill. Use %s placeholders:
* %s = agent name
* %s = admin npub (bech32)
* The content is built at wizard time by configure_default_skills_for_agent(). */
#define DIDACTYL_DEFAULT_SKILL_TEMPLATE_FMT \
"# %s\n\n" \
"You are {{my_kind0_profile}}\n\n" \
"Your npub: {{my_npub}}\n\n" \
"Admin npub: %s\n\n" \
"## Rules\n\n" \
"- Communicate through encrypted Nostr direct messages\n" \
"- Keep responses concise and clear\n" \
"- Be helpful and technically accurate\n" \
"- If unsure, state uncertainty directly\n" \
"- Use tools when a request requires taking action\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" \
"- Maintain your task list as short-term working memory\n" \
"- Never reveal your private key (nsec)\n" \
"- You may share your public key (npub) with anyone"
static const char* DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE =
"## Recent Conversation\n\n"
+2 -2
View File
@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 2
#define DIDACTYL_VERSION_PATCH 60
#define DIDACTYL_VERSION "v0.2.60"
#define DIDACTYL_VERSION_PATCH 61
#define DIDACTYL_VERSION "v0.2.61"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+215 -15
View File
@@ -588,26 +588,226 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen
return 0;
}
/* Build the identity_and_rules skill content from the template format string,
* substituting the agent name and admin npub. */
static char* build_identity_skill_content(const didactyl_config_t* cfg, const char* agent_name) {
if (!cfg || !agent_name) return NULL;
char admin_npub[OW_MAX_KEY_LEN] = {0};
unsigned char admin_pubkey[32] = {0};
if (nostr_hex_to_bytes(cfg->admin.pubkey, admin_pubkey, 32) == 0) {
(void)nostr_key_to_bech32(admin_pubkey, "npub", admin_npub);
}
if (admin_npub[0] == '\0') {
snprintf(admin_npub, sizeof(admin_npub), "%s", cfg->admin.pubkey);
}
size_t needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE_FMT,
agent_name, admin_npub) + 1;
char* content = (char*)malloc(needed);
if (!content) return NULL;
snprintf(content, needed, DIDACTYL_DEFAULT_SKILL_TEMPLATE_FMT,
agent_name, admin_npub);
return content;
}
/* Default skill descriptors used by the skill picker. */
#define SKILL_PICKER_MAX 32
typedef struct {
const char* d_tag;
const char* description;
const char* tags_json;
const char* content_template;
int kind;
int enabled_by_default;
} skill_descriptor_t;
static const skill_descriptor_t DIDACTYL_DEFAULT_SKILLS[] = {
{
.d_tag = "identity_and_rules",
.description = "Agent identity, admin npub, and behavioral rules",
.tags_json = DIDACTYL_DEFAULT_SKILL_TAGS_JSON,
.content_template = NULL,
.kind = DIDACTYL_DEFAULT_SKILL_KIND,
.enabled_by_default = 1
},
{
.d_tag = "dm_history",
.description = "DM conversation history for context continuity",
.tags_json = DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON,
.content_template = NULL, /* dm_history template used directly */
.kind = DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND,
.enabled_by_default = 1
}
};
#define DIDACTYL_DEFAULT_SKILL_COUNT \
((int)(sizeof(DIDACTYL_DEFAULT_SKILLS) / sizeof(DIDACTYL_DEFAULT_SKILLS[0])))
/* Prompt the operator to pick which skills to include, and optionally create
* custom skills. Shows a toggle-based menu similar to the relay picker. */
static int prompt_skill_configuration(didactyl_config_t* cfg, const char* agent_name) {
if (!cfg || !agent_name) return -1;
int skill_enabled[SKILL_PICKER_MAX];
char* skill_d_tags[SKILL_PICKER_MAX];
char* skill_contents[SKILL_PICKER_MAX];
char* skill_tags[SKILL_PICKER_MAX];
int skill_kinds[SKILL_PICKER_MAX];
int skill_count = 0;
for (int i = 0; i < DIDACTYL_DEFAULT_SKILL_COUNT && skill_count < SKILL_PICKER_MAX; i++) {
skill_enabled[skill_count] = DIDACTYL_DEFAULT_SKILLS[i].enabled_by_default;
skill_d_tags[skill_count] = strdup(DIDACTYL_DEFAULT_SKILLS[i].d_tag);
if (strcmp(DIDACTYL_DEFAULT_SKILLS[i].d_tag, "dm_history") == 0) {
skill_contents[skill_count] = strdup(DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE);
} else {
skill_contents[skill_count] = build_identity_skill_content(cfg, agent_name);
}
skill_tags[skill_count] = strdup(DIDACTYL_DEFAULT_SKILLS[i].tags_json);
skill_kinds[skill_count] = DIDACTYL_DEFAULT_SKILLS[i].kind;
skill_count++;
}
char input[WIZARD_LINE_MAX];
for (;;) {
render_wizard_page_header("Step 7 of 8", "New Agent Setup -- Skills");
fprintf(stderr, " Skills (enter a number to toggle, [X] = enabled):\n");
for (int i = 0; i < skill_count; i++) {
fprintf(stderr, " %d. [%c] %s\n", i + 1,
skill_enabled[i] ? 'X' : ' ',
skill_d_tags[i] ? skill_d_tags[i] : "");
}
fprintf(stderr, "\n");
fprintf(stderr, " Commands: a=add custom skill, d=done, b=back, q=quit\n");
if (read_line_prompt("> ", input, sizeof(input)) != 0) {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return -1;
}
if (line_is_quit(input)) {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return -1;
}
char* trimmed = input;
while (*trimmed == ' ') trimmed++;
if (trimmed[0] == '\0') continue;
int is_number = 1;
for (size_t i = 0; trimmed[i] != '\0'; i++) {
if (!isdigit((unsigned char)trimmed[i])) { is_number = 0; break; }
}
if (is_number) {
int idx = atoi(trimmed) - 1;
if (idx >= 0 && idx < skill_count) {
skill_enabled[idx] = !skill_enabled[idx];
} else {
fprintf(stderr, "%sInvalid skill number (1-%d).%s\n", ANSI_RED, skill_count, ANSI_RESET);
}
continue;
}
char c = (char)tolower((unsigned char)trimmed[0]);
if (c == 'b') {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return 1;
}
if (c == 'd') {
for (int i = 0; i < cfg->startup_event_count; i++) {
free(cfg->startup_events[i].content);
free(cfg->startup_events[i].tags_json);
}
free(cfg->startup_events);
cfg->startup_events = NULL;
cfg->startup_event_count = 0;
for (int i = 0; i < skill_count; i++) {
if (skill_enabled[i]) {
if (upsert_startup_event(cfg, skill_kinds[i],
skill_contents[i],
skill_tags[i]) != 0) {
fprintf(stderr, "%sFailed to add skill '%s'.%s\n",
ANSI_RED, skill_d_tags[i] ? skill_d_tags[i] : "", ANSI_RESET);
}
}
}
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return 0;
}
if (c == 'a') {
if (skill_count >= SKILL_PICKER_MAX) {
fprintf(stderr, "%sMaximum skill count reached.%s\n", ANSI_RED, ANSI_RESET);
continue;
}
char d_tag[WIZARD_LINE_MAX] = {0};
if (read_line_prompt(" Skill d_tag (identifier): ", d_tag, sizeof(d_tag)) != 0) {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return -1;
}
if (line_is_quit(d_tag)) {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return -1;
}
if (d_tag[0] == '\0') {
fprintf(stderr, "%sd_tag cannot be empty.%s\n", ANSI_RED, ANSI_RESET);
continue;
}
int dup = 0;
for (int i = 0; i < skill_count; i++) {
if (skill_d_tags[i] && strcmp(skill_d_tags[i], d_tag) == 0) { dup = 1; break; }
}
if (dup) {
fprintf(stderr, "%sA skill with d_tag '%s' already exists.%s\n", ANSI_RED, d_tag, ANSI_RESET);
continue;
}
char content[4096] = {0};
fprintf(stderr, " Enter skill content (markdown, end with '.' on its own line):\n");
size_t content_len = 0;
for (;;) {
char line[WIZARD_LINE_MAX];
if (read_line_prompt(" ", line, sizeof(line)) != 0) {
for (int i = 0; i < skill_count; i++) { free(skill_d_tags[i]); free(skill_contents[i]); free(skill_tags[i]); }
return -1;
}
if (strcmp(line, ".") == 0) break;
size_t line_len = strlen(line);
if (content_len + line_len + 2 > sizeof(content)) {
fprintf(stderr, "%sSkill content too long (max %zu bytes).%s\n", ANSI_RED, sizeof(content) - 1, ANSI_RESET);
break;
}
if (content_len > 0) content[content_len++] = '\n';
memcpy(content + content_len, line, line_len);
content_len += line_len;
}
if (content_len == 0) {
fprintf(stderr, "%sSkill content cannot be empty.%s\n", ANSI_RED, ANSI_RESET);
continue;
}
content[content_len] = '\0';
skill_d_tags[skill_count] = strdup(d_tag);
skill_contents[skill_count] = strdup(content);
char tags_json[512] = {0};
snprintf(tags_json, sizeof(tags_json),
"[[\"d\",\"%s\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"trigger\",\"dm\"]]",
d_tag);
skill_tags[skill_count] = strdup(tags_json);
skill_kinds[skill_count] = DIDACTYL_DEFAULT_SKILL_KIND;
skill_enabled[skill_count] = 1;
skill_count++;
fprintf(stderr, "%sCustom skill '%s' added.%s\n", ANSI_YELLOW, d_tag, ANSI_RESET);
continue;
}
}
}
static int configure_default_skills_for_agent(didactyl_config_t* cfg, const char* agent_name) {
if (!cfg || !agent_name || agent_name[0] == '\0') {
return -1;
}
if (upsert_startup_event(cfg,
DIDACTYL_DEFAULT_SKILL_KIND,
DIDACTYL_DEFAULT_SKILL_TEMPLATE,
DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) {
return -1;
}
if (upsert_startup_event(cfg,
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND,
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE,
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) {
return -1;
}
return 0;
return prompt_skill_configuration(cfg, agent_name);
}
static int relay_contains(const didactyl_config_t* cfg, const char* relay) {