v0.2.2 - Re-apply v0.2.0 migration after history rewrite rollback

This commit is contained in:
Your Name
2026-03-22 12:02:12 -04:00
parent 0bd21dc41a
commit 90680e232d
17 changed files with 104 additions and 1326 deletions
+39 -86
View File
@@ -15,18 +15,14 @@
#include "nostr_handler.h"
#include "trigger_manager.h"
#include "tools/tools.h"
#include "prompt_template.h"
#include "cjson/cJSON.h"
#include "debug.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static didactyl_config_t* g_cfg = NULL;
static char* g_system_context = NULL;
static tools_context_t g_tools_ctx;
static struct trigger_manager* g_trigger_manager = NULL;
static prompt_template_t g_prompt_template;
static int g_has_prompt_template = 0;
#define AGENT_CONTEXT_PART_NAMES_MAX 512
static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX];
static int g_context_part_names_count = 0;
@@ -2094,20 +2090,14 @@ static cJSON* build_recent_admin_dm_history_messages(const char* current_message
return messages;
}
int agent_init(didactyl_config_t* config, const char* system_context) {
if (!config || !system_context) {
int agent_init(didactyl_config_t* config) {
if (!config) {
return -1;
}
g_cfg = config;
g_system_context = strdup(system_context);
if (!g_system_context) {
return -1;
}
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
return -1;
}
@@ -2124,8 +2114,6 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
g_tools_ctx.template_skill_lookup = template_skill_lookup_callback;
g_tools_ctx.template_skill_lookup_user_data = NULL;
prompt_template_free(&g_prompt_template);
g_has_prompt_template = (prompt_template_parse(g_system_context, &g_prompt_template) == 0);
clear_context_part_names();
return 0;
@@ -2322,82 +2310,51 @@ void agent_on_trigger(const char* skill_d_tag,
int agent_build_admin_messages_json(const char* current_user_message,
didactyl_sender_tier_t sender_tier,
char** out_messages_json) {
if (!out_messages_json || !g_cfg || !g_system_context) {
if (!out_messages_json || !g_cfg) {
return -1;
}
*out_messages_json = NULL;
clear_context_part_names();
cJSON* messages = NULL;
cJSON* dm_event = cJSON_CreateObject();
if (!dm_event) {
return -1;
}
cJSON_AddStringToObject(dm_event, "type", "dm");
cJSON_AddStringToObject(dm_event, "sender_pubkey", g_cfg->admin.pubkey);
cJSON_AddStringToObject(dm_event, "message", current_user_message ? current_user_message : "");
cJSON_AddStringToObject(dm_event,
"tier",
sender_tier == DIDACTYL_SENDER_ADMIN ? "admin" :
(sender_tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));
if (g_has_prompt_template) {
cJSON* dm_history = build_recent_admin_dm_history_messages(current_user_message);
if (!dm_history) {
dm_history = cJSON_CreateArray();
}
char* composed_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "api");
cJSON_Delete(dm_event);
if (!composed_context) {
composed_context = strdup("You are an AI agent. Respond to the message.");
}
llm_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
const char* provider_name = NULL;
if (llm_get_config(&cfg) == 0 && cfg.provider[0] != '\0') {
provider_name = cfg.provider;
}
cJSON* messages = cJSON_CreateArray();
if (!messages ||
append_simple_message(messages, "system", composed_context ? composed_context : "You are an AI agent. Respond to the message.") != 0 ||
append_recent_admin_dm_history(messages, current_user_message) != 0 ||
append_simple_message(messages, "user", current_user_message ? current_user_message : "") != 0) {
free(composed_context);
cJSON_Delete(messages);
return -1;
}
free(composed_context);
g_tools_ctx.template_sender_tier = (int)sender_tier;
g_tools_ctx.template_current_user_message = current_user_message;
g_tools_ctx.template_trigger_event_json = NULL;
messages = prompt_template_build_messages(&g_prompt_template,
provider_name,
&g_tools_ctx,
dm_history,
AGENT_HISTORY_TURNS,
template_emit_hook,
NULL);
cJSON_Delete(dm_history);
if (!messages) {
return -1;
}
} else {
messages = cJSON_CreateArray();
if (!messages) {
return -1;
}
char agent_identity[192];
snprintf(agent_identity,
sizeof(agent_identity),
"Agent identity\nYour pubkey (hex): %s",
g_cfg->keys.public_key_hex[0] != '\0' ? g_cfg->keys.public_key_hex : "unknown");
char* sender_verification = build_sender_verification_text(sender_tier);
if (!sender_verification ||
append_simple_message(messages, "system", g_system_context) != 0 ||
append_simple_message(messages, "system", agent_identity) != 0 ||
append_simple_message(messages, "system", sender_verification) != 0 ||
append_adopted_skills_context(messages) != 0) {
free(sender_verification);
cJSON_Delete(messages);
return -1;
}
free(sender_verification);
if (append_recent_admin_dm_history(messages, current_user_message) != 0) {
cJSON_Delete(messages);
return -1;
}
int n = cJSON_GetArraySize(messages);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(messages, 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 : "";
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
set_context_part_name(i, detect_context_section(role_s, content_s, i));
}
int n = cJSON_GetArraySize(messages);
for (int i = 0; i < n; i++) {
cJSON* msg = cJSON_GetArrayItem(messages, 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 : "";
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
set_context_part_name(i, detect_context_section(role_s, content_s, i));
}
char* messages_json = cJSON_PrintUnformatted(messages);
@@ -2420,7 +2377,7 @@ void agent_on_message(const char* sender_pubkey_hex,
void* user_data) {
(void)user_data;
if (!g_cfg || !g_system_context || !sender_pubkey_hex || !message) {
if (!g_cfg || !sender_pubkey_hex || !message) {
return;
}
@@ -2670,8 +2627,6 @@ void agent_on_message(const char* sender_pubkey_hex,
void agent_cleanup(void) {
tools_cleanup(&g_tools_ctx);
free(g_system_context);
g_system_context = NULL;
g_cfg = NULL;
g_trigger_manager = NULL;
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
@@ -2684,6 +2639,4 @@ void agent_cleanup(void) {
pthread_mutex_unlock(&g_adopted_skills_mutex);
clear_context_part_names();
prompt_template_free(&g_prompt_template);
g_has_prompt_template = 0;
}
+1 -1
View File
@@ -8,7 +8,7 @@
struct trigger_manager;
int agent_init(didactyl_config_t* config, const char* system_context);
int agent_init(didactyl_config_t* config);
void agent_set_trigger_manager(struct trigger_manager* trigger_manager);
void agent_on_trigger(const char* skill_d_tag,
const char* skill_content,
+16 -80
View File
@@ -1,9 +1,9 @@
#ifndef DIDACTYL_DEFAULT_EVENTS_H
#define DIDACTYL_DEFAULT_EVENTS_H
#define DIDACTYL_DEFAULT_SKILL_D_TAG "didactyl-default"
#define DIDACTYL_DEFAULT_SKILL_D_TAG "default_admin_dm"
#define DIDACTYL_DEFAULT_SKILL_KIND 31124
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"didactyl-default\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"]]"
#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"d\",\"default_admin_dm\"],[\"app\",\"didactyl\"],[\"scope\",\"private\"],[\"description\",\"Default admin DM handler\"],[\"trigger\",\"dm\"],[\"filter\",\"{\\\"from\\\":\\\"admin\\\"}\"]]"
#define DIDACTYL_STARTUP_KIND_PROFILE 0
#define DIDACTYL_STARTUP_KIND_CONTACTS 3
@@ -31,83 +31,19 @@ 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 =
"# %s Agent\n\n"
"You are %s, 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 `local_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"
"## Task Management\n"
"- Maintain and use your internal task list as short-term working memory.\n"
"- Break long or complex actions into clear tasks before executing them.\n"
"- Update task status as you complete steps so your plan stays accurate.\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.\n\n"
"---template---\n\n"
"- section: admin_identity\n"
" role: system\n"
" tool: admin_identity\n"
" skip_if_empty: true\n\n"
"- section: admin_profile\n"
" role: system\n"
" tool: nostr_admin_profile\n"
" skip_if_empty: true\n\n"
"- section: admin_contacts\n"
" role: system\n"
" tool: nostr_admin_contacts\n"
" skip_if_empty: true\n\n"
"- section: admin_relays\n"
" role: system\n"
" tool: nostr_admin_relays\n"
" skip_if_empty: true\n\n"
"- section: admin_notes\n"
" role: system\n"
" tool: nostr_admin_notes\n"
" skip_if_empty: true\n\n"
"- section: agent_identity\n"
" role: system\n"
" tool: agent_identity\n"
" skip_if_empty: true\n\n"
"- section: agent_profile\n"
" role: system\n"
" tool: nostr_agent_profile\n"
" skip_if_empty: true\n\n"
"- section: agent_contacts\n"
" role: system\n"
" tool: nostr_agent_contacts\n"
" skip_if_empty: true\n\n"
"- section: agent_relays\n"
" role: system\n"
" tool: nostr_agent_relays\n"
" skip_if_empty: true\n\n"
"- section: agent_notes\n"
" role: system\n"
" tool: nostr_agent_notes\n"
" skip_if_empty: true\n\n"
"- section: tasks\n"
" role: system\n"
" tool: task_list\n"
" skip_if_empty: true\n\n"
"- section: dm_history\n"
" role: expand\n"
" limit: 12\n\n"
"- section: conversation\n"
" role: user\n"
" tool: message_current\n"
" skip_if_empty: true\n";
"# 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";
#endif
+1 -15
View File
@@ -1245,9 +1245,6 @@ int main(int argc, char** argv) {
}
} else {
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
if (nostr_handler_load_system_context_from_adopted_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to load system context from adopted skills (continuing with fallback)");
}
}
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
@@ -1258,19 +1255,8 @@ int main(int argc, char** argv) {
startup_step_ok(7, "Reconcile/persist startup state", NULL);
const char* system_context = nostr_handler_get_system_context();
char system_context_fallback[512] = {0};
if (!system_context || system_context[0] == '\0') {
const char* startup_name = nostr_handler_get_startup_display_name();
snprintf(system_context_fallback,
sizeof(system_context_fallback),
"You are %s, a sovereign AI agent living on Nostr.",
(startup_name && startup_name[0] != '\0') ? startup_name : "the agent");
system_context = system_context_fallback;
}
startup_step_begin(8, "Initialize agent");
if (agent_init(&cfg, system_context) != 0) {
if (agent_init(&cfg) != 0) {
startup_step_fail(8, "Initialize agent", "agent_init failed");
fprintf(stderr, "Failed to initialize agent\n");
nostr_block_list_cleanup();
+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 1
#define DIDACTYL_VERSION "v0.2.1"
#define DIDACTYL_VERSION_PATCH 2
#define DIDACTYL_VERSION "v0.2.2"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
-144
View File
@@ -25,7 +25,6 @@ static void* g_dm_user_data = NULL;
static int g_poll_counter = 0;
static time_t g_start_time = 0;
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
static char* g_system_context = NULL;
static unsigned char* g_startup_published = NULL;
static int g_startup_publish_tracking_enabled = 0;
static int g_startup_kind1_already_exists = 0;
@@ -3121,9 +3120,6 @@ int nostr_handler_reconcile_startup_events(void) {
return -1;
}
free(g_system_context);
g_system_context = NULL;
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
@@ -3137,25 +3133,12 @@ int nostr_handler_reconcile_startup_events(void) {
g_startup_publish_tracking_enabled = 1;
}
if (g_cfg->default_skill.content && g_cfg->default_skill.content[0] != '\0') {
g_system_context = strdup(g_cfg->default_skill.content);
}
load_startup_display_name();
g_startup_kind1_already_exists = startup_self_kind1_exists();
if (g_startup_kind1_already_exists) {
DEBUG_INFO("[didactyl] startup phase: existing self kind-1 note found; skipping startup kind-1 publish");
}
if (!g_system_context) {
char default_ctx[512];
snprintf(default_ctx,
sizeof(default_ctx),
"You are %s, a sovereign AI agent living on Nostr.",
g_startup_display_name[0] != '\0' ? g_startup_display_name : "the agent");
g_system_context = strdup(default_ctx);
}
for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
}
@@ -3165,131 +3148,6 @@ int nostr_handler_reconcile_startup_events(void) {
return 0;
}
int nostr_handler_load_system_context_from_adopted_skills(void) {
if (!g_cfg || !g_pool || g_cfg->keys.public_key_hex[0] == '\0') {
return -1;
}
cJSON* adoption_filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!adoption_filter || !kinds || !authors) {
cJSON_Delete(adoption_filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
cJSON_AddItemToObject(adoption_filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(adoption_filter, "authors", authors);
cJSON_AddNumberToObject(adoption_filter, "limit", 1);
char* adoption_events_json = nostr_handler_query_json(adoption_filter, 3000);
cJSON_Delete(adoption_filter);
if (!adoption_events_json) {
return -1;
}
cJSON* adoption_events = cJSON_Parse(adoption_events_json);
free(adoption_events_json);
if (!adoption_events || !cJSON_IsArray(adoption_events) || cJSON_GetArraySize(adoption_events) <= 0) {
cJSON_Delete(adoption_events);
return -1;
}
cJSON* adoption = cJSON_GetArrayItem(adoption_events, 0);
cJSON* tags = adoption ? cJSON_GetObjectItemCaseSensitive(adoption, "tags") : NULL;
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(adoption_events);
return -1;
}
const char* first_addr = NULL;
int tag_n = cJSON_GetArraySize(tags);
for (int i = 0; i < tag_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, "a") == 0 && v->valuestring[0] != '\0') {
first_addr = v->valuestring;
break;
}
}
if (!first_addr) {
cJSON_Delete(adoption_events);
return -1;
}
int skill_kind = 0;
char skill_pubkey_hex[65] = {0};
char skill_d_tag[65] = {0};
if (parse_skill_address_local(first_addr, &skill_kind, skill_pubkey_hex, skill_d_tag) != 0) {
cJSON_Delete(adoption_events);
return -1;
}
cJSON* skill_filter = cJSON_CreateObject();
cJSON* skill_kinds = cJSON_CreateArray();
cJSON* skill_authors = cJSON_CreateArray();
cJSON* d_tags = cJSON_CreateArray();
if (!skill_filter || !skill_kinds || !skill_authors || !d_tags) {
cJSON_Delete(adoption_events);
cJSON_Delete(skill_filter);
cJSON_Delete(skill_kinds);
cJSON_Delete(skill_authors);
cJSON_Delete(d_tags);
return -1;
}
cJSON_AddItemToArray(skill_kinds, cJSON_CreateNumber(skill_kind));
cJSON_AddItemToObject(skill_filter, "kinds", skill_kinds);
cJSON_AddItemToArray(skill_authors, cJSON_CreateString(skill_pubkey_hex));
cJSON_AddItemToObject(skill_filter, "authors", skill_authors);
cJSON_AddItemToArray(d_tags, cJSON_CreateString(skill_d_tag));
cJSON_AddItemToObject(skill_filter, "#d", d_tags);
cJSON_AddNumberToObject(skill_filter, "limit", 1);
char* skill_events_json = nostr_handler_query_json(skill_filter, 3000);
cJSON_Delete(skill_filter);
cJSON_Delete(adoption_events);
if (!skill_events_json) {
return -1;
}
cJSON* skill_events = cJSON_Parse(skill_events_json);
free(skill_events_json);
if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
cJSON_Delete(skill_events);
return -1;
}
cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0);
cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
if (!content || !cJSON_IsString(content) || !content->valuestring) {
cJSON_Delete(skill_events);
return -1;
}
free(g_system_context);
g_system_context = strdup(content->valuestring);
cJSON_Delete(skill_events);
return g_system_context ? 0 : -1;
}
const char* nostr_handler_get_system_context(void) {
return g_system_context;
}
char* nostr_handler_get_self_events_by_kind_json(int kind) {
cJSON* out = cJSON_CreateArray();
if (!out) {
@@ -4212,8 +4070,6 @@ void nostr_handler_cleanup(void) {
g_cfg = NULL;
g_dm_callback = NULL;
g_dm_user_data = NULL;
free(g_system_context);
g_system_context = NULL;
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
g_seen_dm_count = 0;
g_seen_dm_next = 0;
-2
View File
@@ -59,10 +59,8 @@ nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription);
int nostr_handler_poll(int timeout_ms);
int nostr_handler_reconcile_startup_events(void);
int nostr_handler_load_system_context_from_adopted_skills(void);
void nostr_handler_refresh_relay_statuses(void);
int nostr_handler_sync_relays_from_config(void);
const char* nostr_handler_get_system_context(void);
char* nostr_handler_get_self_skill_events_json(void);
int nostr_handler_is_event_in_self_cache(const char* event_id_hex);
const char* nostr_handler_get_startup_display_name(void);
+5 -511
View File
@@ -1,18 +1,16 @@
#define _POSIX_C_SOURCE 200809L
#include "prompt_template.h"
#include "cjson/cJSON.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char* dup_range(const char* s, size_t n) {
char* out = (char*)malloc(n + 1U);
if (!out) return NULL;
if (n > 0) {
memcpy(out, s, n);
}
if (n > 0) memcpy(out, s, n);
out[n] = '\0';
return out;
}
@@ -32,52 +30,6 @@ static void rtrim_inplace(char* s) {
}
}
static int starts_with(const char* s, const char* prefix) {
if (!s || !prefix) return 0;
size_t n = strlen(prefix);
return strncmp(s, prefix, n) == 0;
}
static int add_line(char*** lines, int* count, int* cap, char* line) {
if (!lines || !count || !cap) return -1;
if (*count >= *cap) {
int next = (*cap == 0) ? 64 : (*cap * 2);
char** grown = (char**)realloc(*lines, (size_t)next * sizeof(char*));
if (!grown) return -1;
*lines = grown;
*cap = next;
}
(*lines)[*count] = line;
(*count)++;
return 0;
}
static int split_lines_inplace(char* s, char*** out_lines, int* out_count) {
if (!s || !out_lines || !out_count) return -1;
char** lines = NULL;
int count = 0;
int cap = 0;
char* p = s;
while (*p) {
char* start = p;
while (*p && *p != '\n') p++;
if (*p == '\n') {
*p = '\0';
p++;
}
if (add_line(&lines, &count, &cap, start) != 0) {
free(lines);
return -1;
}
}
*out_lines = lines;
*out_count = count;
return 0;
}
static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
if (!buf || !cap || !used || !s) return -1;
size_t n = strlen(s);
@@ -105,10 +57,11 @@ static const char* map_variable_tool_name(const char* var_name) {
if (strcmp(var_name, "admin_relays") == 0) return "nostr_admin_relays";
if (strcmp(var_name, "adopted_skills") == 0) return "adopted_skills";
if (strcmp(var_name, "triggering_event") == 0) return "trigger_event";
return NULL;
return var_name;
}
static char* resolve_inline_variables_local(const char* tpl, tools_context_t* tools_ctx) {
char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* tools_ctx) {
if (!tpl) return strdup("");
size_t cap = strlen(tpl) + 64U;
@@ -216,462 +169,3 @@ static char* resolve_inline_variables_local(const char* tpl, tools_context_t* to
return out;
}
static void init_section_defaults(prompt_template_section_t* sec) {
if (!sec) return;
memset(sec->name, 0, sizeof(sec->name));
memset(sec->role, 0, sizeof(sec->role));
snprintf(sec->role, sizeof(sec->role), "system");
sec->content_template = NULL;
sec->tool_name = NULL;
sec->tool_args = NULL;
sec->result_field = NULL;
sec->limit = 0;
sec->skip_if_empty = 0;
sec->provider_name = NULL;
sec->provider_content_template = NULL;
}
static int parse_int_or_zero(const char* s) {
if (!s) return 0;
while (*s && isspace((unsigned char)*s)) s++;
return atoi(s);
}
int prompt_template_parse(const char* skill_content, prompt_template_t* out_template) {
if (!skill_content || !out_template) {
return -1;
}
memset(out_template, 0, sizeof(*out_template));
const char* marker = strstr(skill_content, PROMPT_TEMPLATE_MARKER);
if (!marker) {
return -1;
}
size_t personality_len = (size_t)(marker - skill_content);
out_template->personality = dup_range(skill_content, personality_len);
if (!out_template->personality) {
return -1;
}
rtrim_inplace(out_template->personality);
const char* after = marker + strlen(PROMPT_TEMPLATE_MARKER);
while (*after == '\r' || *after == '\n') after++;
char* tpl = strdup(after);
if (!tpl) {
prompt_template_free(out_template);
return -1;
}
char** lines = NULL;
int line_count = 0;
if (split_lines_inplace(tpl, &lines, &line_count) != 0) {
free(tpl);
prompt_template_free(out_template);
return -1;
}
int current = -1;
int i = 0;
while (i < line_count) {
char* raw = lines[i];
rtrim_inplace(raw);
char* line = ltrim_inplace(raw);
if (*line == '\0') {
i++;
continue;
}
if (starts_with(line, "- section:")) {
if (out_template->section_count >= PROMPT_TEMPLATE_MAX_SECTIONS) {
break;
}
current = out_template->section_count;
init_section_defaults(&out_template->sections[current]);
out_template->section_count++;
char* name = line + strlen("- section:");
name = ltrim_inplace(name);
rtrim_inplace(name);
snprintf(out_template->sections[current].name,
sizeof(out_template->sections[current].name),
"%s",
name);
i++;
continue;
}
if (current < 0) {
i++;
continue;
}
if (starts_with(line, "role:")) {
char* role = line + strlen("role:");
role = ltrim_inplace(role);
rtrim_inplace(role);
snprintf(out_template->sections[current].role,
sizeof(out_template->sections[current].role),
"%s",
(*role) ? role : "system");
i++;
continue;
}
if (starts_with(line, "limit:")) {
char* lim = line + strlen("limit:");
out_template->sections[current].limit = parse_int_or_zero(lim);
i++;
continue;
}
if (starts_with(line, "skip_if_empty:")) {
char* flag = line + strlen("skip_if_empty:");
flag = ltrim_inplace(flag);
rtrim_inplace(flag);
out_template->sections[current].skip_if_empty =
(strcmp(flag, "true") == 0 || strcmp(flag, "1") == 0) ? 1 : 0;
i++;
continue;
}
if (starts_with(line, "tool:")) {
char* val = line + strlen("tool:");
val = ltrim_inplace(val);
rtrim_inplace(val);
free(out_template->sections[current].tool_name);
out_template->sections[current].tool_name = strdup(val);
i++;
continue;
}
if (starts_with(line, "args:")) {
char* val = line + strlen("args:");
val = ltrim_inplace(val);
rtrim_inplace(val);
free(out_template->sections[current].tool_args);
out_template->sections[current].tool_args = strdup((*val) ? val : "{}");
i++;
continue;
}
if (starts_with(line, "result_field:")) {
char* val = line + strlen("result_field:");
val = ltrim_inplace(val);
rtrim_inplace(val);
free(out_template->sections[current].result_field);
out_template->sections[current].result_field = strdup(val);
i++;
continue;
}
if (starts_with(line, "content:")) {
char* val = line + strlen("content:");
val = ltrim_inplace(val);
free(out_template->sections[current].content_template);
out_template->sections[current].content_template = NULL;
if (strcmp(val, "|") == 0) {
char* acc = strdup("");
size_t cap = acc ? 1U : 0U;
size_t used = 0U;
if (!acc) {
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
while (i < line_count) {
char* next_raw = lines[i];
char* next_ltrim = ltrim_inplace(next_raw);
if (starts_with(next_ltrim, "- section:") ||
starts_with(next_ltrim, "role:") ||
starts_with(next_ltrim, "limit:") ||
starts_with(next_ltrim, "skip_if_empty:") ||
starts_with(next_ltrim, "tool:") ||
starts_with(next_ltrim, "args:") ||
starts_with(next_ltrim, "result_field:") ||
starts_with(next_ltrim, "content:") ||
starts_with(next_ltrim, "provider:")) {
break;
}
char* piece = next_raw;
if (strncmp(piece, " ", 4) == 0) piece += 4;
else if (strncmp(piece, " ", 2) == 0) piece += 2;
rtrim_inplace(piece);
if (append_text(&acc, &cap, &used, piece) != 0 ||
append_text(&acc, &cap, &used, "\n") != 0) {
free(acc);
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
}
rtrim_inplace(acc);
out_template->sections[current].content_template = acc;
continue;
}
rtrim_inplace(val);
out_template->sections[current].content_template = strdup(val);
i++;
continue;
}
if (starts_with(line, "provider:")) {
i++;
if (i >= line_count) continue;
char* provider_line = ltrim_inplace(lines[i]);
rtrim_inplace(provider_line);
char* colon = strchr(provider_line, ':');
if (!colon) {
continue;
}
*colon = '\0';
char* provider_name = ltrim_inplace(provider_line);
rtrim_inplace(provider_name);
char* provider_val = ltrim_inplace(colon + 1);
rtrim_inplace(provider_val);
free(out_template->sections[current].provider_name);
out_template->sections[current].provider_name = strdup(provider_name);
free(out_template->sections[current].provider_content_template);
out_template->sections[current].provider_content_template = NULL;
if (strcmp(provider_val, "|") == 0) {
char* acc = strdup("");
size_t cap = acc ? 1U : 0U;
size_t used = 0U;
if (!acc) {
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
while (i < line_count) {
char* next = ltrim_inplace(lines[i]);
if (starts_with(next, "- section:") ||
starts_with(next, "role:") ||
starts_with(next, "limit:") ||
starts_with(next, "skip_if_empty:") ||
starts_with(next, "tool:") ||
starts_with(next, "args:") ||
starts_with(next, "result_field:") ||
starts_with(next, "content:") ||
starts_with(next, "provider:")) {
break;
}
char* piece = lines[i];
if (strncmp(piece, " ", 4) == 0) piece += 4;
else if (strncmp(piece, " ", 2) == 0) piece += 2;
rtrim_inplace(piece);
if (append_text(&acc, &cap, &used, piece) != 0 ||
append_text(&acc, &cap, &used, "\n") != 0) {
free(acc);
free(lines);
free(tpl);
prompt_template_free(out_template);
return -1;
}
i++;
}
rtrim_inplace(acc);
out_template->sections[current].provider_content_template = acc;
continue;
}
out_template->sections[current].provider_content_template = strdup(provider_val);
i++;
continue;
}
i++;
}
free(lines);
free(tpl);
return 0;
}
static int append_message_object(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 char* extract_tool_result_content(const prompt_template_section_t* sec, const char* tool_result_json) {
if (!tool_result_json) return strdup("");
cJSON* root = cJSON_Parse(tool_result_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return strdup(tool_result_json);
}
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
if (success && cJSON_IsBool(success) && !cJSON_IsTrue(success)) {
cJSON_Delete(root);
return strdup("");
}
const char* field_name = (sec && sec->result_field && sec->result_field[0] != '\0')
? sec->result_field
: "content";
cJSON* field = cJSON_GetObjectItemCaseSensitive(root, field_name);
if (!field) {
field = cJSON_GetObjectItemCaseSensitive(root, "content");
}
char* out = NULL;
if (field && cJSON_IsString(field) && field->valuestring) {
out = strdup(field->valuestring);
} else if (field) {
out = cJSON_PrintUnformatted(field);
} else {
out = cJSON_PrintUnformatted(root);
}
cJSON_Delete(root);
return out ? out : strdup("");
}
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
const char* provider_name,
tools_context_t* tools_ctx,
cJSON* dm_history_messages,
int dm_history_default_limit,
prompt_template_emit_hook_fn emit_hook,
void* emit_hook_user_data) {
if (!tmpl) return NULL;
cJSON* out = cJSON_CreateArray();
if (!out) return NULL;
int out_idx = 0;
if (tmpl->personality && tmpl->personality[0] != '\0') {
if (append_message_object(out, "system", tmpl->personality) != 0) {
cJSON_Delete(out);
return NULL;
}
if (emit_hook) emit_hook("system_prompt", out_idx, emit_hook_user_data);
out_idx++;
}
for (int i = 0; i < tmpl->section_count; i++) {
const prompt_template_section_t* sec = &tmpl->sections[i];
const char* role = sec->role[0] ? sec->role : "system";
if (strcmp(role, "expand") == 0) {
if (!dm_history_messages || !cJSON_IsArray(dm_history_messages)) {
continue;
}
int total = cJSON_GetArraySize(dm_history_messages);
int lim = sec->limit > 0 ? sec->limit : dm_history_default_limit;
int start = (lim > 0 && total > lim) ? (total - lim) : 0;
for (int j = start; j < total; j++) {
cJSON* item = cJSON_GetArrayItem(dm_history_messages, j);
if (!item || !cJSON_IsObject(item)) continue;
cJSON* dup = cJSON_Duplicate(item, 1);
if (!dup) {
cJSON_Delete(out);
return NULL;
}
cJSON_AddItemToArray(out, dup);
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
out_idx++;
}
continue;
}
char* resolved = NULL;
if (sec->tool_name && sec->tool_name[0] != '\0' && tools_ctx) {
const char* args_json = (sec->tool_args && sec->tool_args[0] != '\0') ? sec->tool_args : "{}";
char* tool_result = tools_execute(tools_ctx, sec->tool_name, args_json);
resolved = extract_tool_result_content(sec, tool_result);
free(tool_result);
} else {
const char* tpl = sec->content_template;
if (provider_name && sec->provider_name && sec->provider_content_template &&
strcmp(provider_name, sec->provider_name) == 0) {
tpl = sec->provider_content_template;
}
resolved = resolve_inline_variables_local(tpl ? tpl : "", tools_ctx);
}
if (!resolved) {
cJSON_Delete(out);
return NULL;
}
if (sec->skip_if_empty) {
char* chk = ltrim_inplace(resolved);
if (chk && *chk == '\0') {
free(resolved);
continue;
}
}
if (append_message_object(out, role, resolved) != 0) {
free(resolved);
cJSON_Delete(out);
return NULL;
}
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
out_idx++;
free(resolved);
}
return out;
}
void prompt_template_free(prompt_template_t* tmpl) {
if (!tmpl) return;
free(tmpl->personality);
tmpl->personality = NULL;
for (int i = 0; i < tmpl->section_count; i++) {
free(tmpl->sections[i].content_template);
tmpl->sections[i].content_template = NULL;
free(tmpl->sections[i].tool_name);
tmpl->sections[i].tool_name = NULL;
free(tmpl->sections[i].tool_args);
tmpl->sections[i].tool_args = NULL;
free(tmpl->sections[i].result_field);
tmpl->sections[i].result_field = NULL;
free(tmpl->sections[i].provider_name);
tmpl->sections[i].provider_name = NULL;
free(tmpl->sections[i].provider_content_template);
tmpl->sections[i].provider_content_template = NULL;
tmpl->sections[i].name[0] = '\0';
tmpl->sections[i].role[0] = '\0';
tmpl->sections[i].limit = 0;
tmpl->sections[i].skip_if_empty = 0;
}
tmpl->section_count = 0;
}
+1 -40
View File
@@ -1,47 +1,8 @@
#ifndef DIDACTYL_PROMPT_TEMPLATE_H
#define DIDACTYL_PROMPT_TEMPLATE_H
#include "cjson/cJSON.h"
#include "tools/tools.h"
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
#define PROMPT_TEMPLATE_MAX_ROLE_LEN 16
#define PROMPT_TEMPLATE_MARKER "---template---"
typedef struct {
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN];
char* content_template;
char* tool_name;
char* tool_args;
char* result_field;
int limit;
int skip_if_empty;
char* provider_name;
char* provider_content_template;
} prompt_template_section_t;
typedef struct {
char* personality;
prompt_template_section_t sections[PROMPT_TEMPLATE_MAX_SECTIONS];
int section_count;
} prompt_template_t;
typedef void (*prompt_template_emit_hook_fn)(const char* section_name,
int message_index,
void* user_data);
int prompt_template_parse(const char* skill_content, prompt_template_t* out_template);
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
const char* provider_name,
tools_context_t* tools_ctx,
cJSON* dm_history_messages,
int dm_history_default_limit,
prompt_template_emit_hook_fn emit_hook,
void* emit_hook_user_data);
void prompt_template_free(prompt_template_t* tmpl);
char* prompt_template_resolve_inline_variables(const char* tpl, tools_context_t* tools_ctx);
#endif
+2 -2
View File
@@ -508,7 +508,7 @@ static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char*
cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND;
snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG);
int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
int needed = snprintf(NULL, 0, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
if (needed <= 0) {
return -1;
}
@@ -518,7 +518,7 @@ static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char*
return -1;
}
snprintf(cfg->default_skill.content, (size_t)needed + 1U, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
snprintf(cfg->default_skill.content, (size_t)needed + 1U, "%s", DIDACTYL_DEFAULT_SKILL_TEMPLATE);
cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON);
if (!cfg->default_skill.tags_json) {