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:
@@ -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.60 — Add FIPS relay to default relays after laantungir
|
||||
> Last release update: v0.2.61 — Skill 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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user