402 lines
13 KiB
Markdown
402 lines
13 KiB
Markdown
# Markdown Context Window — Implementation Plan
|
|
|
|
The context window that the agent sees should be a nicely formatted markdown
|
|
document. Skills are the building blocks, and the runtime assembles them into
|
|
a coherent document with proper heading hierarchy, role markers for the LLM
|
|
API, and clean formatting throughout.
|
|
|
|
See also: [example_context_v2.md](example_context_v2.md) for visual examples.
|
|
|
|
---
|
|
|
|
## Design Decisions
|
|
|
|
### 1. One generic JSON-to-markdown converter
|
|
|
|
A single function converts any JSON value into readable markdown. No
|
|
per-tool formatters. Tools continue to return JSON; the template engine
|
|
calls the converter when injecting tool output into the markdown document.
|
|
|
|
```c
|
|
char* json_to_markdown(const char* json_string);
|
|
```
|
|
|
|
Conversion rules:
|
|
- **String**: output as-is (inline)
|
|
- **Number**: output as-is (inline)
|
|
- **Boolean**: `true` / `false`
|
|
- **Null**: *(empty)*
|
|
- **Object**: bullet list with bold keys
|
|
- `- **key**: value`
|
|
- Nested objects indent one level
|
|
- **Array of primitives**: comma-separated inline or bullet list
|
|
- **Array of objects**: bullet list, each item shows its fields
|
|
|
|
#### Examples
|
|
|
|
**Simple object**:
|
|
```json
|
|
{"name":"Simon","npub":"npub1kfc89...","nip05":"simon@nostr"}
|
|
```
|
|
```markdown
|
|
- **name**: Simon
|
|
- **npub**: npub1kfc89...
|
|
- **nip05**: simon@nostr
|
|
```
|
|
|
|
**Nested object**:
|
|
```json
|
|
{"agent":{"name":"Simon","npub":"npub1kfc89..."},"version":"v0.2.12"}
|
|
```
|
|
```markdown
|
|
- **agent**:
|
|
- **name**: Simon
|
|
- **npub**: npub1kfc89...
|
|
- **version**: v0.2.12
|
|
```
|
|
|
|
**Array of objects**:
|
|
```json
|
|
[{"content":"Deployed v0.2.12","created_at":1774263529},{"content":"Working on skills","created_at":1774263600}]
|
|
```
|
|
```markdown
|
|
- **content**: Deployed v0.2.12
|
|
- **created_at**: 1774263529
|
|
- **content**: Working on skills
|
|
- **created_at**: 1774263600
|
|
```
|
|
|
|
**Plain string** (already markdown):
|
|
```json
|
|
"You are Simon, a sovereign AI agent."
|
|
```
|
|
```markdown
|
|
You are Simon, a sovereign AI agent.
|
|
```
|
|
|
|
#### Edge case: tool content that is already markdown
|
|
|
|
Some tools may return markdown text in their `content` field (e.g., a skill
|
|
that already formats its own output). The converter detects this: if the
|
|
input is a plain JSON string (not an object/array), it passes through
|
|
unchanged. This means skill authors can return pre-formatted markdown from
|
|
tools if they want precise control.
|
|
|
|
#### Edge case: very large JSON
|
|
|
|
For large arrays (e.g., 50 Nostr events from a query), the converter should
|
|
truncate with a note: `*(... and 42 more items)*`. A reasonable default
|
|
limit is 8 items for arrays.
|
|
|
|
### 2. Runtime owns h1, heading bump for skills
|
|
|
|
- The runtime emits `# Agent Name` as the document title
|
|
- All headings in skill content get bumped one level: `#` → `##`, `##` → `###`
|
|
- Template variables are resolved BEFORE the heading bump
|
|
- Layer 2 skills (embedded via `{{skill_d_tag}}`) get the same bump as their
|
|
containing layer 1 skill (because they are resolved inline first)
|
|
|
|
### 3. Role markers parsed
|
|
|
|
Skill content can contain `system:` and `user:` markers at the start of a
|
|
line. The runtime splits on these markers to produce the API messages array.
|
|
|
|
- `system:` — content goes into the system message
|
|
- `user:` — content goes into the user message
|
|
- `assistant:` — content goes into an assistant message (rare, for few-shot)
|
|
- No marker — defaults to `system:`
|
|
- Multiple skills can contribute `system:` sections (concatenated in order)
|
|
- Multiple `user:` sections get concatenated
|
|
|
|
### 4. Skills separated by horizontal rules
|
|
|
|
Layer 1 skills are separated by `---` in the assembled document. This gives
|
|
visual structure and helps the LLM distinguish between different instruction
|
|
blocks.
|
|
|
|
### 5. Context log shows the markdown document
|
|
|
|
The `append_context_log()` function logs the assembled markdown document
|
|
so you can inspect exactly what the agent sees, formatted as readable
|
|
markdown rather than raw JSON message arrays.
|
|
|
|
---
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Create json_to_markdown converter
|
|
|
|
**File**: `src/json_to_markdown.c` / `src/json_to_markdown.h`
|
|
|
|
New module with a single public function:
|
|
|
|
```c
|
|
// Convert a JSON string to markdown text.
|
|
// Returns a malloc'd string. Caller frees.
|
|
// If input is a plain string (not object/array), passes through unchanged.
|
|
// Objects become bullet lists with bold keys.
|
|
// Arrays become bullet lists of items.
|
|
// Nested structures indent appropriately.
|
|
// Large arrays truncate at max_items with a note.
|
|
char* json_to_markdown(const char* json_string, int max_array_items);
|
|
```
|
|
|
|
Implementation:
|
|
- Parse JSON with cJSON
|
|
- Recursive walk of the JSON tree
|
|
- Build output string with the append_text pattern used elsewhere
|
|
- Handle: string, number, bool, null, object, array
|
|
- Indent nested structures with 2-space indentation
|
|
- Truncate arrays beyond max_array_items
|
|
|
|
### Step 2: Create context_roles splitter
|
|
|
|
**File**: `src/context_roles.c` / `src/context_roles.h`
|
|
|
|
New module that splits a markdown document on role markers:
|
|
|
|
```c
|
|
typedef struct {
|
|
char* system_content; // Everything under system: markers
|
|
char* user_content; // Everything under user: markers
|
|
char* assistant_content; // Everything under assistant: markers (rare)
|
|
} context_roles_t;
|
|
|
|
// Split markdown text on role markers (system:, user:, assistant:)
|
|
// at the start of a line. Unmarked content defaults to system.
|
|
// Returns 0 on success, -1 on error.
|
|
int context_roles_split(const char* markdown, context_roles_t* out);
|
|
|
|
void context_roles_free(context_roles_t* roles);
|
|
```
|
|
|
|
### Step 3: Create heading_bump utility
|
|
|
|
**File**: `src/context_format.c` / `src/context_format.h`
|
|
|
|
Utility functions for markdown formatting in context assembly:
|
|
|
|
```c
|
|
// Bump all markdown headings in text by one level (# -> ##, ## -> ###, etc.)
|
|
// Returns a malloc'd string. Caller frees.
|
|
char* context_bump_headings(const char* text);
|
|
|
|
// Build the runtime document title from agent identity.
|
|
// Returns "# Agent Name\n\n" as a malloc'd string.
|
|
char* context_build_title(tools_context_t* ctx);
|
|
```
|
|
|
|
### Step 4: Modify prompt_template_resolve_inline_variables
|
|
|
|
**File**: `src/prompt_template.c`
|
|
|
|
After extracting the `content` field from a tool result (line ~179), pass it
|
|
through `json_to_markdown()` before inserting into the template:
|
|
|
|
Current code (simplified):
|
|
```c
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(root, "content");
|
|
if (content && cJSON_IsString(content)) {
|
|
replacement_owned = strdup(content->valuestring);
|
|
}
|
|
```
|
|
|
|
New code:
|
|
```c
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(root, "content");
|
|
if (content && cJSON_IsString(content)) {
|
|
// Try to convert JSON content to markdown
|
|
char* md = json_to_markdown(content->valuestring, 8);
|
|
replacement_owned = md ? md : strdup(content->valuestring);
|
|
}
|
|
```
|
|
|
|
If the content is already a plain string (not JSON), `json_to_markdown`
|
|
passes it through unchanged. If it is a JSON object/array, it gets
|
|
converted to a markdown bullet list.
|
|
|
|
### Step 5: Modify build_context_from_triggers
|
|
|
|
**File**: `src/agent.c`
|
|
|
|
Update the context assembly function to:
|
|
|
|
1. Resolve template variables (already done)
|
|
2. Bump headings in each skill's content
|
|
3. Build the document title
|
|
4. Concatenate with `---` separators
|
|
5. Return the complete markdown document (not yet split by roles)
|
|
|
|
Current flow:
|
|
```
|
|
for each matching skill:
|
|
expand = resolve_skill_references(skill.content)
|
|
append expand to output with --- separator
|
|
```
|
|
|
|
New flow:
|
|
```
|
|
title = context_build_title(tools_ctx)
|
|
output = title
|
|
|
|
for each matching skill:
|
|
expanded = resolve_skill_references(skill.content)
|
|
resolved = prompt_template_resolve_inline_variables(expanded)
|
|
bumped = context_bump_headings(resolved)
|
|
append bumped to output with --- separator
|
|
```
|
|
|
|
### Step 6: Modify agent_on_message and agent_on_trigger
|
|
|
|
**File**: `src/agent.c`
|
|
|
|
Update the callers of `build_context_from_triggers` to use role splitting:
|
|
|
|
Current flow:
|
|
```
|
|
dm_context = build_context_from_triggers(DM, ...)
|
|
// dm_context goes entirely into system message
|
|
// user message is the raw DM text
|
|
llm_chat(dm_context, message)
|
|
```
|
|
|
|
New flow:
|
|
```
|
|
markdown_doc = build_context_from_triggers(DM, ...)
|
|
context_roles_split(markdown_doc, &roles)
|
|
// roles.system_content -> system message
|
|
// roles.user_content -> user message (or fall back to raw DM text)
|
|
llm_chat(roles.system_content, roles.user_content ?: message)
|
|
```
|
|
|
|
### Step 7: Update append_context_log
|
|
|
|
**File**: `src/agent.c`
|
|
|
|
Log the assembled markdown document instead of (or in addition to) the raw
|
|
JSON messages array. The log entry should show the readable markdown so you
|
|
can open `context.log.md` and see exactly what the agent sees.
|
|
|
|
Current format:
|
|
```
|
|
[{"role":"system","content":"...escaped..."},{"role":"user","content":"..."}]
|
|
```
|
|
|
|
New format:
|
|
```markdown
|
|
system:
|
|
# Simon — Didactyl Agent
|
|
|
|
You are Simon...
|
|
|
|
## Rules
|
|
- ...
|
|
|
|
---
|
|
|
|
## Chat
|
|
Respond helpfully...
|
|
|
|
user:
|
|
Hey Simon, who mentioned me today?
|
|
```
|
|
|
|
### Step 8: Update DM history formatting
|
|
|
|
**File**: `src/nostr_handler.c` (or wherever DM history is built)
|
|
|
|
The conversation history that gets injected into the context should be
|
|
formatted as a markdown list instead of inline JSON:
|
|
|
|
Current:
|
|
```
|
|
[{"role":"assistant","content":"Started up..."},{"role":"user","content":"Hello"}]
|
|
```
|
|
|
|
New:
|
|
```markdown
|
|
## Conversation History
|
|
|
|
- **You**: Started up and is online at 2026-03-23 09:22:48.
|
|
- **Admin**: Hello
|
|
- **You**: Hello! How can I help?
|
|
```
|
|
|
|
This could be handled by the generic `json_to_markdown` converter if the
|
|
history is passed as JSON, or by a dedicated history formatter if we want
|
|
the cleaner `**You**` / `**Admin**` labels instead of `**role**`.
|
|
|
|
Note: This is the one place where a small specialized formatter adds real
|
|
value — the generic converter would produce `**role**: assistant` /
|
|
`**content**: Started up...` which is less readable than `**You**: Started up...`.
|
|
A simple function that maps role names to display labels handles this.
|
|
|
|
### Step 9: Update documentation
|
|
|
|
**Files**: `docs/SKILLS.md`, `docs/CONTEXT.md`
|
|
|
|
Update the documentation to reflect:
|
|
- Skill content is markdown with role markers
|
|
- The runtime assembles a coherent markdown document
|
|
- Heading bump behavior
|
|
- The generic JSON-to-markdown conversion for template variables
|
|
- Updated examples showing the new format
|
|
|
|
---
|
|
|
|
## Files Changed
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `src/json_to_markdown.c` | **NEW** — Generic JSON to markdown converter |
|
|
| `src/json_to_markdown.h` | **NEW** — Header for converter |
|
|
| `src/context_roles.c` | **NEW** — Role marker splitter |
|
|
| `src/context_roles.h` | **NEW** — Header for role splitter |
|
|
| `src/context_format.c` | **NEW** — Heading bump and title builder |
|
|
| `src/context_format.h` | **NEW** — Header for context formatting |
|
|
| `src/prompt_template.c` | **MODIFY** — Use json_to_markdown for template variable output |
|
|
| `src/agent.c` | **MODIFY** — Use heading bump, role splitting, updated context log |
|
|
| `src/nostr_handler.c` | **MODIFY** — Format DM history as markdown list |
|
|
| `Makefile` | **MODIFY** — Add new source files to build |
|
|
| `docs/SKILLS.md` | **MODIFY** — Update skill content format documentation |
|
|
| `docs/CONTEXT.md` | **MODIFY** — Update context assembly documentation |
|
|
|
|
---
|
|
|
|
## What Does NOT Change
|
|
|
|
- **Tool implementations** — All tools continue to return JSON. No changes
|
|
to any tool_*.c files.
|
|
- **LLM API interface** — Still sends OpenAI-compatible messages array.
|
|
The markdown is the content within the messages, not the transport format.
|
|
- **Runtime tool results** — Tool call/result messages during multi-turn
|
|
loops stay as JSON. Only template variable injection gets markdown conversion.
|
|
- **Skill event format on Nostr** — Skills are still stored as kind 31123/31124
|
|
events. The content field is still markdown with role markers. No protocol change.
|
|
- **Adoption list** — No changes to kind 10123 or skill ordering.
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### Low risk
|
|
- `json_to_markdown` is a pure function with no side effects
|
|
- `context_roles_split` is a simple string splitter
|
|
- `context_bump_headings` is a line-by-line string operation
|
|
- All new code is additive — existing behavior preserved for skills without
|
|
role markers (defaults to system)
|
|
|
|
### Medium risk
|
|
- Changing `prompt_template_resolve_inline_variables` to use json_to_markdown
|
|
could change the content of existing template variables. Need to verify that
|
|
existing skills still work correctly with markdown output instead of raw JSON.
|
|
Mitigation: json_to_markdown passes through plain strings unchanged, so
|
|
skills that already return non-JSON content are unaffected.
|
|
|
|
### Testing approach
|
|
- Unit test json_to_markdown with various JSON inputs
|
|
- Unit test context_roles_split with various role marker patterns
|
|
- Unit test context_bump_headings with various heading levels
|
|
- Integration test: run existing skills and compare context.log.md output
|
|
before and after to verify no regressions
|