# 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
