v0.0.48 - Remove duplicated tools context, drop legacy variable resolver paths, and update template docs for tool-driven assembly

This commit is contained in:
Your Name
2026-03-08 10:57:19 -03:00
parent 61f2315d1e
commit c6bda8a6d3
8 changed files with 41 additions and 427 deletions
+5 -5
View File
@@ -53,11 +53,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa
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.47
## Current Status — v0.0.48
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.47Implement Phase 1 tool-driven soul template context assembly and context_* tools
> Last release update: v0.0.48Remove duplicated tools context, drop legacy variable resolver paths, and update template docs for tool-driven assembly
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
@@ -314,9 +314,9 @@ Didactyl builds tier-aware context:
- **ADMIN** request context — assembled from the soul's `---template---` section (if present), otherwise hardcoded order:
1. Soul personality (everything above `---template---` in kind `31120`)
2. Named template sections in order — e.g. `admin_profile`, `admin_notes`, `admin_relays`, `adopted_skills`, `dm_history`, `message`, `triggering_event`, `tools`
3. Each section resolves `{{variable}}` placeholders from live data at call time
4. Provider-specific content overrides per section (e.g. XML tags for Anthropic)
2. Named template sections in order using `tool:` directives (for example `context_admin_profile`, `context_admin_notes`, `context_tasks`, `context_user_message`, `dm_history`)
3. Each section executes its configured context tool, optionally extracting `result_field` (default: `content`)
4. Provider-specific content overrides per section remain supported for literal `content:` sections
5. Section names are used in `context.log` headers and `/api/context/parts` response
- **WoT** request context: Soul + WoT chat-only instruction + current user message (no tools)
- **STRANGER**: no LLM call when configured to reply statically
+21 -36
View File
@@ -3,55 +3,40 @@
```yaml
- section: admin_identity
role: system
content: |
## Administrator Identity (source: config.admin.pubkey)
This is your administrator! Admin pubkey (hex): {{admin_pubkey}}
tool: context_admin_identity
skip_if_empty: true
- section: admin_profile
role: system
content: |
## Administrator Kind 0 Profile (source: nostr kind 0)
tool: context_admin_profile
skip_if_empty: true
Administrator kind 0 profile content (JSON): {{admin_kind0_json}}
provider:
anthropic: |
<admin_kind0_profile source="nostr_kind_0">
{{admin_kind0_json}}
</admin_kind0_profile>
- section: admin_relay_list
- section: admin_contacts
role: system
content: |
## Administrator Relay List (source: nostr kind 10002)
tool: context_admin_contacts
skip_if_empty: true
Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}
- section: startup_events
- section: admin_relays
role: system
content: |
## Startup Events Memory (source: config.startup_events)
tool: context_admin_relays
skip_if_empty: true
Startup events memory (kinds/content/tags): {{startup_events_json}}
- section: adopted_skills
- section: admin_notes
role: system
content: |
{{adopted_skills_content}}
tool: context_admin_notes
skip_if_empty: true
- section: agent_tasks
- section: tasks
role: system
content: |
{{tasks_content}}
tool: context_tasks
skip_if_empty: true
- section: conversation
role: user
tool: context_user_message
skip_if_empty: true
- section: dm_history
role: expand
limit: 12
- section: admin_notes
role: system
content: |
## Administrator Recent Notes (source: nostr kind 1)
{{admin_notes_content}}
```
+1 -2
View File
@@ -121,7 +121,7 @@ The skill replaces the soul's system prompt but keeps the standard context assem
### Template Variables
A skill template can reference these context parts:
Skill templates may still use placeholders (for example `{{message}}` or `{{triggering_event}}`) for deterministic triggered-skill interpolation, but the soul runtime context is now assembled primarily through template `tool:` directives rather than a separate `{{tools}}`/variable-resolver layer.
| Variable | Source |
|----------|--------|
@@ -133,7 +133,6 @@ A skill template can reference these context parts:
| `{{dm_history}}` | Recent DM conversation |
| `{{message}}` | Current user message |
| `{{triggering_event}}` | For triggered skills, the event JSON |
| `{{tools}}` | Available tool schemas |
---
+11 -16
View File
@@ -106,22 +106,16 @@ Simple YAML-like format parsed in C. Each section has:
|---|---|---|
| `section` | yes | Section name for logging and API identification |
| `role` | yes | Chat message role: `system`, `user`, `assistant`, or `expand` |
| `content` | no | Content template with `{{variable}}` placeholders |
| `limit` | no | Integer limit for variable-length sections like DM history or notes |
| `tool` | recommended | Tool name executed by template builder (for example `context_admin_profile`) |
| `args` | no | JSON args string passed to the tool (default `{}`) |
| `result_field` | no | Field extracted from tool JSON result (default `content`) |
| `content` | optional fallback | Literal content when no `tool` is specified |
| `limit` | no | Integer limit for variable-length sections like DM history |
| `provider` | no | Provider-specific override — see below |
### Variable Placeholders
### Tool-Driven Context Resolution
| Variable | Source | Description |
|---|---|---|
| `{{admin_pubkey}}` | `config.admin.pubkey` | Admin hex pubkey |
| `{{admin_kind0_json}}` | `nostr_handler_get_admin_kind0_context()` | Admin profile JSON |
| `{{admin_kind10002_json}}` | `nostr_handler_get_admin_kind10002_context()` | Admin relay list JSON |
| `{{startup_events_json}}` | Serialized startup events | Startup events array |
| `{{adopted_skills_content}}` | Adoption list cache | Formatted skill instructions |
| `{{admin_notes_content}}` | `nostr_handler_get_admin_kind1_notes_context()` | Recent kind 1 notes |
| `{{agent_pubkey}}` | `config.keys.public_key_hex` | Agent own pubkey |
| `{{agent_npub}}` | Derived from pubkey | Agent npub |
Context sections should use `tool:` directives as the single runtime data path, which removes the legacy per-variable resolver duplication.
### Special Section Types
@@ -165,10 +159,11 @@ flowchart TD
DM[Incoming DM] --> BUILD[Build context from template]
BUILD --> EMIT_SOUL[Emit personality as first system message]
EMIT_SOUL --> FOREACH[For each template section]
FOREACH --> RESOLVE[Resolve {{variables}} from live data]
RESOLVE --> CHECK_PROVIDER{Provider override?}
FOREACH --> CALL_TOOL[Execute section tool or use literal content]
CALL_TOOL --> PICK_FIELD[Extract result_field or content]
PICK_FIELD --> CHECK_PROVIDER{Provider override?}
CHECK_PROVIDER -->|Yes| USE_OVERRIDE[Use provider-specific content]
CHECK_PROVIDER -->|No| USE_DEFAULT[Use default content]
CHECK_PROVIDER -->|No| USE_DEFAULT[Use tool/literal output]
USE_OVERRIDE --> EMIT[Emit as chat message]
USE_DEFAULT --> EMIT
EMIT --> FOREACH
-285
View File
@@ -62,11 +62,6 @@ 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;
typedef struct {
didactyl_sender_tier_t sender_tier;
const char* current_user_message;
} agent_template_resolver_ctx_t;
static uint64_t fnv1a64(const char* s) {
uint64_t h = 1469598103934665603ULL;
if (!s) return h;
@@ -1986,280 +1981,6 @@ static cJSON* build_recent_admin_dm_history_messages(const char* current_message
return messages;
}
static char* build_startup_events_json_string(void) {
if (!g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
return strdup("[]");
}
cJSON* arr = cJSON_CreateArray();
if (!arr) {
return NULL;
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (!se || se->kind == 31120) {
continue;
}
cJSON* item = cJSON_CreateObject();
if (!item) {
cJSON_Delete(arr);
return NULL;
}
cJSON_AddNumberToObject(item, "kind", se->kind);
cJSON_AddStringToObject(item, "content", se->content ? se->content : "");
cJSON* tags = NULL;
if (se->tags_json) {
tags = cJSON_Parse(se->tags_json);
}
if (!tags || !cJSON_IsArray(tags)) {
cJSON_Delete(tags);
tags = cJSON_CreateArray();
}
cJSON_AddItemToObject(item, "tags", tags);
cJSON_AddItemToArray(arr, item);
}
char* out = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
return out;
}
static char* build_adopted_skills_payload_string(void) {
cJSON* tmp = cJSON_CreateArray();
if (!tmp) {
return NULL;
}
if (append_adopted_skills_context(tmp) != 0) {
cJSON_Delete(tmp);
return NULL;
}
cJSON* first = cJSON_GetArrayItem(tmp, 0);
cJSON* content = first ? cJSON_GetObjectItemCaseSensitive(first, "content") : NULL;
char* out = (content && cJSON_IsString(content) && content->valuestring)
? strdup(content->valuestring)
: strdup("");
cJSON_Delete(tmp);
return out;
}
static char* build_tool_schemas_json_string(void) {
char* schema_json = tools_build_openai_schema_json(&g_tools_ctx);
return schema_json ? schema_json : strdup("[]");
}
static char* build_tasks_content_string(void) {
const char* fallback =
"### Current Tasks\n\n"
"Your active task list - short-term working memory for tracking plan steps.\n\n"
"- [ ] No active tasks yet.\n";
if (!g_cfg) {
return strdup(fallback);
}
const char* cwd = g_cfg->tools.shell.working_directory[0] != '\0'
? g_cfg->tools.shell.working_directory
: ".";
char tasks_path[1024];
int pn = 0;
if (strcmp(cwd, ".") == 0) {
pn = snprintf(tasks_path, sizeof(tasks_path), "tasks.json");
} else {
pn = snprintf(tasks_path, sizeof(tasks_path), "%s/%s", cwd, "tasks.json");
}
if (pn <= 0 || (size_t)pn >= sizeof(tasks_path)) {
return strdup(fallback);
}
FILE* fp = fopen(tasks_path, "rb");
if (!fp) {
return strdup(fallback);
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return strdup(fallback);
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return strdup(fallback);
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return strdup(fallback);
}
char* raw = (char*)malloc((size_t)len + 1U);
if (!raw) {
fclose(fp);
return strdup(fallback);
}
size_t n = fread(raw, 1, (size_t)len, fp);
fclose(fp);
if (n != (size_t)len) {
free(raw);
return strdup(fallback);
}
raw[len] = '\0';
cJSON* root = cJSON_Parse(raw);
free(raw);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return strdup(fallback);
}
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
if (!tasks || !cJSON_IsArray(tasks)) {
cJSON_Delete(root);
return strdup(fallback);
}
size_t cap = 1024U;
size_t used = 0U;
char* out = (char*)malloc(cap);
if (!out) {
cJSON_Delete(root);
return strdup(fallback);
}
out[0] = '\0';
if (appendf(&out,
&cap,
&used,
"### Current Tasks\n\n"
"Your active task list - short-term working memory for tracking plan steps.\n\n") != 0) {
free(out);
cJSON_Delete(root);
return strdup(fallback);
}
int written = 0;
int tcount = cJSON_GetArraySize(tasks);
for (int i = 0; i < tcount; i++) {
cJSON* task = cJSON_GetArrayItem(tasks, i);
if (!task || !cJSON_IsObject(task)) continue;
cJSON* id = cJSON_GetObjectItemCaseSensitive(task, "id");
cJSON* text = cJSON_GetObjectItemCaseSensitive(task, "text");
cJSON* status = cJSON_GetObjectItemCaseSensitive(task, "status");
if (!text || !cJSON_IsString(text) || !text->valuestring || text->valuestring[0] == '\0') {
continue;
}
const char* status_s = (status && cJSON_IsString(status) && status->valuestring)
? status->valuestring
: "pending";
const char* mark = " ";
if (strcmp(status_s, "done") == 0) {
mark = "x";
} else if (strcmp(status_s, "active") == 0) {
mark = "-";
}
int id_value = (id && cJSON_IsNumber(id)) ? (int)id->valuedouble : (i + 1);
if (appendf(&out, &cap, &used, "- [%s] %d. %s\n", mark, id_value, text->valuestring) != 0) {
free(out);
cJSON_Delete(root);
return strdup(fallback);
}
written++;
}
cJSON_Delete(root);
if (written <= 0) {
free(out);
return strdup(fallback);
}
return out;
}
static char* safe_strdup(const char* s) {
return strdup(s ? s : "");
}
static char* agent_template_resolve_var(const char* var_name, void* user_data) {
agent_template_resolver_ctx_t* ctx = (agent_template_resolver_ctx_t*)user_data;
if (!var_name || !g_cfg) {
return strdup("");
}
if (strcmp(var_name, "admin_pubkey") == 0) {
return safe_strdup(g_cfg->admin.pubkey);
}
if (strcmp(var_name, "agent_pubkey") == 0) {
return safe_strdup(g_cfg->keys.public_key_hex);
}
if (strcmp(var_name, "admin_kind0_json") == 0) {
char* v = nostr_handler_get_admin_kind0_context();
return v ? v : strdup("");
}
if (strcmp(var_name, "admin_kind3_json") == 0) {
char* v = nostr_handler_get_admin_kind3_context();
return v ? v : strdup("[]");
}
if (strcmp(var_name, "admin_kind10002_json") == 0) {
char* v = nostr_handler_get_admin_kind10002_context();
return v ? v : strdup("");
}
if (strcmp(var_name, "admin_profile_plain") == 0) {
char* v = build_admin_profile_plain_text();
return v ? v : strdup("");
}
if (strcmp(var_name, "admin_relay_list_plain") == 0) {
char* v = build_admin_relay_list_plain_text();
return v ? v : strdup("");
}
if (strcmp(var_name, "sender_verification") == 0) {
didactyl_sender_tier_t tier = ctx ? ctx->sender_tier : DIDACTYL_SENDER_ADMIN;
char* v = build_sender_verification_text(tier);
return v ? v : strdup("");
}
if (strcmp(var_name, "startup_events_json") == 0) {
char* v = build_startup_events_json_string();
return v ? v : strdup("[]");
}
if (strcmp(var_name, "adopted_skills_content") == 0) {
char* v = build_adopted_skills_payload_string();
return v ? v : strdup("");
}
if (strcmp(var_name, "tool_schemas_json") == 0) {
char* v = build_tool_schemas_json_string();
return v ? v : strdup("[]");
}
if (strcmp(var_name, "tasks_content") == 0 || strcmp(var_name, "tasks_json") == 0) {
char* v = build_tasks_content_string();
return v ? v : strdup("");
}
if (strcmp(var_name, "admin_notes_content") == 0 || strcmp(var_name, "admin_kind1_json") == 0) {
char* v = build_admin_recent_posts_text();
return v ? v : strdup("");
}
if (strcmp(var_name, "conversation_json") == 0) {
return safe_strdup(ctx ? ctx->current_user_message : "");
}
if (strcmp(var_name, "admin_recent_posts") == 0) {
char* v = build_admin_recent_posts_text();
return v ? v : strdup("");
}
return strdup("");
}
int agent_init(didactyl_config_t* config, const char* system_context) {
if (!config || !system_context) {
return -1;
@@ -2385,10 +2106,6 @@ int agent_build_admin_messages_json(const char* current_user_message,
dm_history = cJSON_CreateArray();
}
agent_template_resolver_ctx_t resolver_ctx;
resolver_ctx.sender_tier = sender_tier;
resolver_ctx.current_user_message = current_user_message;
llm_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
const char* provider_name = NULL;
@@ -2401,8 +2118,6 @@ int agent_build_admin_messages_json(const char* current_user_message,
messages = prompt_template_build_messages(&g_prompt_template,
provider_name,
agent_template_resolve_var,
&resolver_ctx,
&g_tools_ctx,
dm_history,
AGENT_HISTORY_TURNS,
+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 0
#define DIDACTYL_VERSION_PATCH 47
#define DIDACTYL_VERSION "v0.0.47"
#define DIDACTYL_VERSION_PATCH 48
#define DIDACTYL_VERSION "v0.0.48"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+1 -78
View File
@@ -97,81 +97,6 @@ static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
return 0;
}
static char* resolve_placeholders(const char* tpl,
prompt_var_resolver_fn resolver_fn,
void* resolver_user_data) {
if (!tpl) return strdup("");
size_t cap = strlen(tpl) + 64U;
char* out = (char*)malloc(cap ? cap : 128U);
if (!out) return NULL;
out[0] = '\0';
size_t used = 0;
const char* p = tpl;
while (*p) {
const char* open = strstr(p, "{{");
if (!open) {
if (append_text(&out, &cap, &used, p) != 0) {
free(out);
return NULL;
}
break;
}
if (open > p) {
char* literal = dup_range(p, (size_t)(open - p));
if (!literal) {
free(out);
return NULL;
}
int rc = append_text(&out, &cap, &used, literal);
free(literal);
if (rc != 0) {
free(out);
return NULL;
}
}
const char* close = strstr(open + 2, "}}");
if (!close) {
if (append_text(&out, &cap, &used, open) != 0) {
free(out);
return NULL;
}
break;
}
char* name = dup_range(open + 2, (size_t)(close - (open + 2)));
if (!name) {
free(out);
return NULL;
}
char* name_trim = ltrim_inplace(name);
rtrim_inplace(name_trim);
char* val = resolver_fn ? resolver_fn(name_trim, resolver_user_data) : NULL;
if (!val) {
val = strdup("");
}
int rc = 0;
if (val) {
rc = append_text(&out, &cap, &used, val);
}
free(val);
free(name);
if (rc != 0) {
free(out);
return NULL;
}
p = close + 2;
}
return out;
}
static void init_section_defaults(prompt_template_section_t* sec) {
if (!sec) return;
memset(sec->name, 0, sizeof(sec->name));
@@ -515,8 +440,6 @@ static char* extract_tool_result_content(const prompt_template_section_t* sec, c
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
const char* provider_name,
prompt_var_resolver_fn resolver_fn,
void* resolver_user_data,
tools_context_t* tools_ctx,
cJSON* dm_history_messages,
int dm_history_default_limit,
@@ -577,7 +500,7 @@ cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
strcmp(provider_name, sec->provider_name) == 0) {
tpl = sec->provider_content_template;
}
resolved = resolve_placeholders(tpl ? tpl : "", resolver_fn, resolver_user_data);
resolved = strdup(tpl ? tpl : "");
}
if (!resolved) {
cJSON_Delete(out);
-3
View File
@@ -28,7 +28,6 @@ typedef struct {
int section_count;
} prompt_template_t;
typedef char* (*prompt_var_resolver_fn)(const char* var_name, void* user_data);
typedef void (*prompt_template_emit_hook_fn)(const char* section_name,
int message_index,
void* user_data);
@@ -37,8 +36,6 @@ int prompt_template_parse(const char* soul_content, prompt_template_t* out_templ
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
const char* provider_name,
prompt_var_resolver_fn resolver_fn,
void* resolver_user_data,
tools_context_t* tools_ctx,
cJSON* dm_history_messages,
int dm_history_default_limit,