diff --git a/README.md b/README.md index 8d8320b..e8c30aa 100644 --- a/README.md +++ b/README.md @@ -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.21 +## Current Status — v0.2.22 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.2.21 — Fix markdown context assembly and logging: add Anvil DM trigger skill config, preserve H1 title on fallback, convert full-mode turn snapshots to readable markdown, and sync Anvil service restart/deploy behavior +> Last release update: v0.2.22 — Improve markdown context documentation and skill authoring guidance: document context debug snapshot formats, clarify role-marker defaults, enrich skill_create/skill_edit tool schema guidance for markdown skills, and update tool docs examples - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index b88bf08..3cb8992 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -216,6 +216,91 @@ Use runtime context inspection endpoints (`GET /api/context/current`, `GET /api/ --- +## Context Debug Logging + +Didactyl can persist what it sends to the LLM in markdown files for inspection. + +Enable with runtime flag: + +```bash +./didactyl --context-debug full +``` + +Or with environment variable: + +```bash +DIDACTYL_CONTEXT_DEBUG=full ./didactyl +``` + +Modes: + +| Mode | Behavior | +|------|----------| +| `off` | Disable context logging | +| `init` | Log only initial pre-loop context | +| `full` | Log initial context and each tool-loop turn | + +Output files are written to: + +- `context.log.md` — rolling latest-first log +- `context.logs/_init.md` — initial context for a run +- `context.logs/_t001.md`, `_t002.md`, ... — turn snapshots + +### Snapshot Formats + +Initial snapshots (`*_init.md`) are the assembled markdown context document, including role markers and heading bump effects: + +```markdown +# Didactyl Agent + +system: +## Anvil Agent +... + +user: +Who was WSB historically? +``` + +Turn snapshots (`*_tNNN.md`) are human-readable markdown renderings of the OpenAI-compatible `messages` array used for that loop turn: + +```markdown +**Message 1 (system)** + +- **role**: system +- **content**: # Didactyl Agent + +## Anvil Agent +... + +--- + +**Message 2 (assistant)** + +- **role**: assistant +- **tool_calls**: + - **name**: nostr_admin_profile + - **arguments**: {} + +--- + +**Message 3 (tool)** + +- **role**: tool +- **tool_call_id**: toolu_... +- **content**: + - **success**: true + - **content**: + - **display_name**: WSB +``` + +Notes: + +- Internal transport fields such as `_ts` are omitted from snapshots. +- Tool result payloads are rendered as markdown structures when they contain JSON. +- Snapshot wrappers use bold message labels rather than markdown headings to avoid conflicting with message content headings. + +--- + ## Related Documentation - Skills spec: [SKILLS.md](SKILLS.md) diff --git a/docs/SKILLS.md b/docs/SKILLS.md index 136f144..eb67c42 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -81,6 +81,7 @@ The `content` field of a skill event IS the template — markdown instructions t ``` - **`content`** — the template in markdown. May include `{{...}}` template variables and `system:` / `user:` role markers. This is what goes into the context window. +- **Role Markers** — `system:`, `user:`, and `assistant:` are parsed only when they appear at the start of a line. Content without a marker defaults to `system:`. - **Heading Levels** — The runtime owns the `#` (h1) heading for the document title. All headings in skill content are automatically bumped down one level (`#` becomes `##`, `##` becomes `###`) during context assembly. Skill authors should use `##` for their top-level sections. - **`["description", "..."]`** — human-readable description for discovery and UI display. - Each `["tag", "value"]` is a separate tag on the Nostr event. diff --git a/docs/TOOLS.md b/docs/TOOLS.md index c910700..966902d 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -114,8 +114,8 @@ These tools manage skill and trigger lifecycle; skill semantics and trigger exec | Tool | Description | |---|---| -| `skill_create` | Create or update a skill definition as kind `31123`/`31124` and optionally auto-adopt it | -| `skill_edit` | Edit an existing self skill by d tag and republish it as kind `31123`/`31124` | +| `skill_create` | Create or update a skill definition as kind `31123`/`31124` and optionally auto-adopt it (content should be markdown with optional `system:`/`user:`/`assistant:` role markers) | +| `skill_edit` | Edit an existing self skill by d tag and republish it as kind `31123`/`31124` (content should remain markdown-first) | | `skill_list` | List available skills discovered online (agent + admin), with adoption status and optional filters | | `skill_adopt` | Adopt a skill by adding its address to kind `10123` adoption list | | `skill_remove` | Remove a skill address from kind `10123` adoption list | @@ -469,8 +469,10 @@ These examples show the JSON structure for tool calls. "name": "skill_create", "arguments": { "d": "weather-skill", - "content": "I can tell you the weather.", + "content": "system:\n## Weather Assistant\n\nYou provide concise weather summaries.\n\nuser:\n{{message}}", "description": "Fetches weather data", + "trigger": "dm", + "filter": "{\"from\":\"admin\"}", "auto_adopt": true } } diff --git a/src/agent.c b/src/agent.c index f02e175..da7b5e2 100644 --- a/src/agent.c +++ b/src/agent.c @@ -945,26 +945,89 @@ static char* render_messages_json_as_markdown(const char* messages_json) { cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role"); const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "message"; - char* msg_json = cJSON_PrintUnformatted(msg); - char* msg_md = msg_json ? json_to_markdown(msg_json, 8) : NULL; - const char* body = msg_md ? msg_md : (msg_json ? msg_json : ""); + if (i > 0) { + if (append_textf_local(&out, &cap, &used, "\n---\n\n") != 0) { + free(out); + cJSON_Delete(root); + return NULL; + } + } if (append_textf_local(&out, &cap, &used, - "### Message %d (%s)\n\n%s\n\n", + "**Message %d (%s)**\n\n", i + 1, - role_s, - body) != 0) { - free(msg_md); - free(msg_json); + role_s) != 0) { free(out); cJSON_Delete(root); return NULL; } - free(msg_md); - free(msg_json); + cJSON* field = NULL; + cJSON_ArrayForEach(field, msg) { + if (!field || !field->string) { + continue; + } + + const char* key = field->string; + if (strcmp(key, "_ts") == 0) { + continue; // Internal timestamp metadata; not useful in human-readable snapshots + } + + char* raw_value = NULL; + char* rendered_md = NULL; + + if (strcmp(key, "content") == 0 && strcmp(role_s, "tool") == 0 && cJSON_IsString(field) && field->valuestring) { + cJSON* tool_payload = cJSON_Parse(field->valuestring); + if (tool_payload && cJSON_IsObject(tool_payload)) { + cJSON* inner = cJSON_GetObjectItemCaseSensitive(tool_payload, "content"); + if (inner && cJSON_IsString(inner) && inner->valuestring) { + cJSON* inner_json = cJSON_Parse(inner->valuestring); + if (inner_json) { + cJSON_ReplaceItemInObjectCaseSensitive(tool_payload, "content", inner_json); + } + } + raw_value = cJSON_PrintUnformatted(tool_payload); + cJSON_Delete(tool_payload); + } + } + + if (!raw_value) { + if (cJSON_IsString(field) && field->valuestring) { + raw_value = strdup(field->valuestring); + } else { + raw_value = cJSON_PrintUnformatted(field); + } + } + + if (raw_value) { + rendered_md = json_to_markdown(raw_value, 8); + } + + const char* display = rendered_md ? rendered_md : (raw_value ? raw_value : ""); + if (append_textf_local(&out, + &cap, + &used, + "- **%s**: %s\n", + key, + display) != 0) { + free(rendered_md); + free(raw_value); + free(out); + cJSON_Delete(root); + return NULL; + } + + free(rendered_md); + free(raw_value); + } + + if (append_textf_local(&out, &cap, &used, "\n") != 0) { + free(out); + cJSON_Delete(root); + return NULL; + } } cJSON_Delete(root); diff --git a/src/main.h b/src/main.h index f662162..96fb2a1 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 2 -#define DIDACTYL_VERSION_PATCH 21 -#define DIDACTYL_VERSION "v0.2.21" +#define DIDACTYL_VERSION_PATCH 22 +#define DIDACTYL_VERSION "v0.2.22" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index b63cda7..c2bcadb 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -754,7 +754,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(t22, "type", "function"); cJSON_AddStringToObject(t22_fn, "name", "skill_create"); - cJSON_AddStringToObject(t22_fn, "description", "Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it"); + cJSON_AddStringToObject(t22_fn, "description", "Create or update a skill definition as kind 31123/31124 and optionally auto-adopt it. Write content as markdown context instructions: use role markers (system:/user:/assistant:) at line start when needed; unmarked content defaults to system. The runtime owns document H1 and will bump skill headings down one level, so prefer ## for top-level sections in skill content."); cJSON_AddStringToObject(t22_params, "type", "object"); cJSON_AddItemToObject(t22_params, "properties", t22_props); cJSON_AddItemToObject(t22_params, "required", t22_required); @@ -764,6 +764,9 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddItemToObject(t22_props, "d", p_skill_create_d); cJSON* p_skill_create_content = cJSON_CreateObject(); cJSON_AddStringToObject(p_skill_create_content, "type", "string"); + cJSON_AddStringToObject(p_skill_create_content, + "description", + "Markdown template for the skill body. May include role markers at line start (system:/user:/assistant:), markdown headings/lists/code fences, and template variables like {{message}} or {{skill_d_tag}}. Unmarked content defaults to system. Runtime will prepend document title and bump headings one level."); cJSON_AddItemToObject(t22_props, "content", p_skill_create_content); cJSON* p_skill_create_scope = cJSON_CreateObject(); cJSON_AddStringToObject(p_skill_create_scope, "type", "string"); @@ -993,7 +996,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddStringToObject(t25b, "type", "function"); cJSON_AddStringToObject(t25b_fn, "name", "skill_edit"); - cJSON_AddStringToObject(t25b_fn, "description", "Edit an existing self skill by d tag and republish it as kind 31123/31124"); + cJSON_AddStringToObject(t25b_fn, "description", "Edit an existing self skill by d tag and republish it as kind 31123/31124. Keep skill content markdown-first with optional role markers (system:/user:/assistant:) at line start; unmarked content defaults to system. Runtime owns H1 and bumps skill headings down one level."); cJSON_AddStringToObject(t25b_params, "type", "object"); cJSON_AddItemToObject(t25b_params, "properties", t25b_props); cJSON_AddItemToObject(t25b_params, "required", t25b_required); @@ -1004,6 +1007,9 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON* p_skill_edit_content = cJSON_CreateObject(); cJSON_AddStringToObject(p_skill_edit_content, "type", "string"); + cJSON_AddStringToObject(p_skill_edit_content, + "description", + "Replacement markdown template for this skill. Supports role markers (system:/user:/assistant:) at line start, markdown structure, and template variables ({{...}}). If omitted, existing content is retained."); cJSON_AddItemToObject(t25b_props, "content", p_skill_edit_content); cJSON* p_skill_edit_scope = cJSON_CreateObject();