diff --git a/README.md b/README.md index 9261005..abcfe3c 100644 --- a/README.md +++ b/README.md @@ -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.47 — Implement Phase 1 tool-driven soul template context assembly and context_* tools +> Last release update: v0.0.48 — Remove 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 diff --git a/context_template.md b/context_template.md index bb30512..9f6e009 100644 --- a/context_template.md +++ b/context_template.md @@ -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_json}} - - -- 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}} ``` diff --git a/docs/SKILLS.md b/docs/SKILLS.md index aa2bc42..9471ff6 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -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 | --- diff --git a/plans/prompt_templates.md b/plans/prompt_templates.md index 85ca459..9a39b2e 100644 --- a/plans/prompt_templates.md +++ b/plans/prompt_templates.md @@ -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 diff --git a/src/agent.c b/src/agent.c index 91941b2..c634643 100644 --- a/src/agent.c +++ b/src/agent.c @@ -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, diff --git a/src/main.h b/src/main.h index 58dbc59..4e810cf 100644 --- a/src/main.h +++ b/src/main.h @@ -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" diff --git a/src/prompt_template.c b/src/prompt_template.c index 5141837..5bfba25 100644 --- a/src/prompt_template.c +++ b/src/prompt_template.c @@ -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); diff --git a/src/prompt_template.h b/src/prompt_template.h index 779e4af..8ebd932 100644 --- a/src/prompt_template.h +++ b/src/prompt_template.h @@ -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,