Files
didactyl/plans/prompt_templates.md

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

  1. Create src/prompt_template.h with data structures and API
  2. Implement template parser in src/prompt_template.c — parse soul content, split at ---template---, parse sections
  3. Implement variable resolver — map {{var}} names to data source functions
  4. Implement context builder — iterate sections, resolve variables, emit messages
  5. Wire into agent_build_admin_messages_json() — if template exists, use it; otherwise fall back to hardcoded
  6. Update format_context_payload_for_log() to use section names
  7. Update classify_part_name() in http_api.c to use section names from template
  8. Update default soul in config.json.example to include a ---template--- section
  9. Test with existing soul (no template marker) — verify backward compatibility
  10. Test with template soul — verify new assembly
  11. 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 limit field is capped at compile-time maximums to prevent resource exhaustion
  • Provider overrides are optional and safe — they only change formatting, not data sources