12 KiB
Didactyl Prompt Template System — Design Plan
Summary
Replace the hardcoded context assembly in src/agent.c with a template-driven system. The template lives inside the soul event (kind 31120) — because the template defines how the agent perceives the world, and that is fundamentally part of who the agent is.
Different agents (architect, eGirl, analyst) are different processes with different souls, different templates, different Nostr identities. Multi-agent is Approach A: multiple ./didactyl --config processes communicating via Nostr.
Core Principle
The soul IS the template. An agent's soul defines both its personality (prose instructions) and its perception (what context sections it sees, in what order, with what limits). You cannot meaningfully separate "who you are" from "how you see the world."
Current State
Today, context assembly is hardcoded in agent_build_admin_messages_json():
1. System prompt (soul content)
2. Admin identity (pubkey)
3. Admin kind 0 profile
4. Admin kind 10002 relay list
5. Startup events memory
6. Adopted skills memory
7. DM history (last 12 turns)
8. Admin recent notes (kind 1)
The order, formatting, limits, and section headers are all baked into C code. Changing anything requires editing src/agent.c and recompiling.
Proposed Design
Soul Event Structure
The kind 31120 soul event content gains a template section, delimited by a marker:
# Didactyl Agent
You are Didactyl, a sovereign AI agent living on Nostr.
## Communication Rules
- You communicate through encrypted Nostr direct messages.
- Keep responses concise and clear.
## Behavior
- Be helpful and technically accurate.
...
## Safety
- Never reveal your private key.
...
---template---
- section: admin_identity
role: system
content: |
This is your administrator! Admin pubkey: {{admin_pubkey}}
- section: admin_profile
role: system
content: |
Administrator profile: {{admin_kind0_json}}
- section: admin_relay_list
role: system
content: |
Administrator relay list: {{admin_kind10002_json}}
- section: startup_events
role: system
content: |
Startup events memory: {{startup_events_json}}
- section: adopted_skills
role: system
content: |
{{adopted_skills_content}}
- section: dm_history
role: expand
limit: 12
- section: admin_notes
role: system
limit: 10
content: |
{{admin_notes_content}}
Everything above ---template--- is the system prompt (personality/rules). Everything below defines the context assembly template.
If no ---template--- marker is found, the agent falls back to the current hardcoded assembly — backward compatible.
Template Syntax
Simple YAML-like format parsed in C. Each section has:
| Field | Required | Description |
|---|---|---|
section |
yes | Section name for logging and API identification |
role |
yes | Chat message role: system, user, assistant, or expand |
tool |
recommended | Tool name executed by template builder (for example nostr_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 |
Tool-Driven Context Resolution
Context sections should use tool: directives as the single runtime data path, which removes the legacy per-variable resolver duplication.
Special Section Types
role: expand — The dm_history section expands into multiple messages (user/assistant pairs). The limit field controls how many turns to include. This is the only section type that produces multiple chat messages from one template entry.
Provider-Specific Overrides
Within a section, you can specify provider-specific formatting:
- section: admin_identity
role: system
content: |
## Administrator Identity
This is your administrator! Admin pubkey: {{admin_pubkey}}
provider:
anthropic: |
<admin_identity>
This is your administrator! Admin pubkey: {{admin_pubkey}}
</admin_identity>
When the configured llm.provider matches a provider key, that override is used instead of the default content. This lets one soul/template work well across providers without needing separate soul events.
If no provider override matches, the default content is used.
Context Assembly Flow
flowchart TD
BOOT[Agent boots] --> LOAD_SOUL[Load soul event - kind 31120]
LOAD_SOUL --> PARSE[Parse soul content]
PARSE --> SPLIT{Contains ---template--- marker?}
SPLIT -->|Yes| EXTRACT[Extract personality above marker]
SPLIT -->|No| FALLBACK[Use hardcoded assembly - backward compat]
EXTRACT --> PARSE_TPL[Parse template sections below marker]
PARSE_TPL --> STORE[Store template_section_t array in memory]
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 --> 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 tool/literal output]
USE_OVERRIDE --> EMIT[Emit as chat message]
USE_DEFAULT --> EMIT
EMIT --> FOREACH
FOREACH --> DONE[Complete messages array]
DONE --> LLM[Send to LLM]
Context.log Formatting
With templates, the log formatter uses section names directly from the template instead of detecting them from content prefixes. The detect_context_section() function is replaced by the template section name.
Log format becomes:
[2026-03-02 14:54:30] phase=llm_chat_with_tools_messages sender=8ff747...
Sections: 8
============================================================
Section: system_prompt | role=system
============================================================
# Didactyl Agent
...
============================================================
Section: admin_identity | role=system
============================================================
This is your administrator! Admin pubkey: 8ff747...
============================================================
Section: dm_history | role=user
============================================================
Good afternoon.
Note: "Message 01" is replaced with "Section: admin_identity" — the section name from the template, which is much more meaningful.
Data Structures
typedef struct {
char name[64]; // section name
char role[16]; // system, user, assistant, expand
char* content_template; // content with {{var}} placeholders
int limit; // for expand sections, 0 = unlimited
char* provider_overrides; // JSON object of provider->content pairs, or NULL
} template_section_t;
typedef struct {
char* personality; // everything above ---template---
template_section_t* sections; // parsed template sections
int section_count;
} prompt_template_t;
Implementation Plan
New Files
| File | Purpose |
|---|---|
src/prompt_template.c |
Template parser, variable resolver, context builder |
src/prompt_template.h |
Public API: parse, build context, free |
Modified Files
| File | Change |
|---|---|
src/agent.c |
Replace agent_build_admin_messages_json() internals with template-driven builder. Keep function signature unchanged for API compatibility. |
src/agent.c |
Remove hardcoded append_admin_identity_context(), append_startup_events_context(), etc. — these become template variable resolvers |
src/agent.c |
Update format_context_payload_for_log() to use section names from template |
src/http_api.c |
Remove classify_part_name() / detect_context_section() — section names come from template |
Makefile |
Add src/prompt_template.c to SRCS |
Dockerfile.alpine-musl |
Add src/prompt_template.c to gcc command |
Implementation Order
- Create
src/prompt_template.hwith data structures and API - Implement template parser in
src/prompt_template.c— parse soul content, split at---template---, parse sections - Implement variable resolver — map
{{var}}names to data source functions - Implement context builder — iterate sections, resolve variables, emit messages
- Wire into
agent_build_admin_messages_json()— if template exists, use it; otherwise fall back to hardcoded - Update
format_context_payload_for_log()to use section names - Update
classify_part_name()inhttp_api.cto use section names from template - Update default soul in
config.json.exampleto include a---template---section - Test with existing soul (no template marker) — verify backward compatibility
- Test with template soul — verify new assembly
- Test provider overrides
Backward Compatibility
If the soul event content does NOT contain ---template---, the agent uses the current hardcoded assembly. This means:
- Existing agents continue to work without changes
- The template system is opt-in
- Migration is gradual — add a template section to your soul when ready
Example Souls
Architect Agent
# Technical Architect
You are a technical architect agent. You analyze systems, design solutions, and produce detailed technical plans.
## Behavior
- Think systematically about architecture
- Consider tradeoffs explicitly
- Produce diagrams when helpful
- Never implement code — only design
---template---
- section: admin_identity
role: system
content: |
Administrator pubkey: {{admin_pubkey}}
- section: admin_profile
role: system
content: |
Administrator profile: {{admin_kind0_json}}
- section: startup_events
role: system
content: |
System configuration and startup state: {{startup_events_json}}
- section: adopted_skills
role: system
content: |
{{adopted_skills_content}}
- section: dm_history
role: expand
limit: 20
- section: admin_notes
role: system
limit: 5
content: |
Recent administrator notes for context: {{admin_notes_content}}
eGirl Agent
# Social Butterfly
You are a friendly, social Nostr personality. You love interacting with people, commenting on their posts, and being part of the community.
## Personality
- Warm, enthusiastic, uses emoji freely
- Interested in what people are posting about
- Remembers details about conversations
- Keeps responses casual and fun
---template---
- section: admin_identity
role: system
content: |
Your creator: {{admin_pubkey}}
- section: admin_profile
role: system
content: |
Creator profile: {{admin_kind0_json}}
- section: admin_notes
role: system
limit: 20
content: |
Recent posts from people you follow — use these for social context and conversation starters: {{admin_notes_content}}
- section: adopted_skills
role: system
content: |
{{adopted_skills_content}}
- section: dm_history
role: expand
limit: 8
Note the differences:
- The eGirl sees 20 recent notes (social context) but only 8 DM turns
- The architect sees 5 notes but 20 DM turns (needs conversation continuity)
- The eGirl puts notes BEFORE skills (social context is primary)
- The architect puts skills BEFORE notes (technical knowledge is primary)
- No startup events for the eGirl (doesn't need system config details)
Nostr Shareability
Since the template is part of the soul event (kind 31120), sharing works naturally:
- Publish your soul → others get your complete agent personality + perception template
- Discover interesting agents on Nostr → adopt their soul as a starting point
- Community can develop and share optimized templates for different use cases
- Templates evolve through the same Nostr discovery mechanisms as skills
Security Notes
- Template variable resolution is sandboxed — only predefined variables are resolved
- No arbitrary code execution from templates
- The
limitfield is capped at compile-time maximums to prevent resource exhaustion - Provider overrides are optional and safe — they only change formatting, not data sources