Files
didactyl/plans/unified_prompt_context.md

6.1 KiB

Plan: Unified Prompt Context for HTTP API and Nostr Paths

Problem

The agent produces completely different LLM context depending on whether a message arrives via Nostr DM or the HTTP API CLI chat app.

Nostr Path (working correctly)

  • agent_on_message()agent_build_admin_messages_json() → tool loop
  • Builds 18 sections, ~8224 bytes of context including:
    • System prompt / personality (from soul template)
    • Agent identity (pubkey)
    • Sender verification (admin tier)
    • Admin context (kind 0 profile, relay list, recent posts)
    • Startup events memory
    • Adopted skills
    • DM history (decrypted from Nostr relays)
    • Current user message

HTTP API Path (broken)

  • CLI sends {messages: [{role: "user", content: "Hello"}]} to /api/prompt/run
  • run_prompt_with_tools() passes these raw messages directly to the LLM
  • Result: 1 section, ~35 bytes — just the bare user message, zero agent context

Solution: New POST /api/prompt/agent Endpoint

Add a new endpoint that mirrors the Nostr path's context assembly, so the CLI gets the same full agent context.

Architecture

flowchart TD
    A[Nostr DM arrives] --> B[agent_on_message]
    B --> C[agent_build_admin_messages_json]
    C --> D[Append user message]
    D --> E[Tool loop with llm_chat_with_tools_messages]
    E --> F[Send DM reply]

    G[CLI sends POST /api/prompt/agent] --> H[handle_prompt_agent]
    H --> C
    C --> I[Append user message]
    I --> J[Tool loop - same as run_prompt_with_tools but with context]
    J --> K[Return JSON response]

    style C fill:#4a9,stroke:#333,color:#fff

Both paths share agent_build_admin_messages_json() as the single source of truth for context assembly.

Request Format

{
  "message": "What is the capital of France?",
  "model": "claude-haiku-4.5",
  "max_turns": 4
}
Field Type Required Description
message string yes The user message to send to the agent
model string no Override the configured LLM model for this request
max_turns int no Max tool-use turns, default 4, max 16

Response Format

Same as existing /api/prompt/run:

{
  "success": true,
  "final_response": "The capital of France is Paris.",
  "turns": [...],
  "model_used": "claude-haiku-4.5",
  "total_input_tokens_estimate": 1973,
  "total_output_tokens_estimate": 12
}

Implementation Steps

1. Add handle_prompt_agent() in src/http_api.c

New function that:

  1. Parses the JSON body to extract message, optional model, optional max_turns
  2. Applies model override if present via maybe_model_override_begin()
  3. Calls agent_build_admin_messages_json(message, DIDACTYL_SENDER_ADMIN, &base_messages_json) — same call the Nostr path uses
  4. Parses the result into a cJSON array
  5. Appends {role: "user", content: message} to the array — same as agent_on_message() does at line 1916
  6. Builds tool schema via tools_build_openai_schema_json()
  7. Runs the same tool loop as run_prompt_with_tools() but using the context-enriched messages
  8. Logs context via agent_append_context_log("http_api_agent", "llm_chat_with_tools_messages", messages_json)
  9. Returns the same response format as /api/prompt/run

Key reference points in existing code:

  • Context building: agent_build_admin_messages_json() at src/agent.c:1737
  • User message append: append_simple_message() pattern at src/agent.c:1916
  • Tool loop: reuse the loop logic from run_prompt_with_tools() at src/http_api.c:278-345
  • Context logging: agent_append_context_log() at src/agent.c:1932

2. Register the Route in http_handler()

Add before the existing /api/prompt/run route at src/http_api.c:648:

if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/agent"), NULL)) {
    handle_prompt_agent(c, hm);
    return;
}

3. Update chat-didactyl-cli.js

Change the CLI to call the new endpoint:

  • Change callDidactyl() to POST to /api/prompt/agent instead of /api/prompt/run
  • Send {message: "user text", max_turns: N} instead of {messages: [...], max_turns: N}
  • The CLI no longer needs to maintain a transcript array for context — the server handles DM history from Nostr relays
  • Keep the transcript for local display purposes only

4. Context Logging Parity

Use a distinct but parallel phase label:

  • Nostr path: llm_chat_with_tools_messages (existing)
  • HTTP API agent path: llm_chat_with_tools_messages_agent_api (new)
  • HTTP API raw path: llm_chat_with_tools_messages_http_api (existing, unchanged)

This lets you distinguish the source in context.log.md while confirming the context structure is identical.

5. Update docs/API.md

Add documentation for the new POST /api/prompt/agent endpoint following the existing documentation style.

Files to Modify

File Change
src/http_api.c Add handle_prompt_agent() function and route registration
chat-didactyl-cli.js Switch to /api/prompt/agent, simplify payload
docs/API.md Document new endpoint

What Stays the Same

  • /api/prompt/run — unchanged, still accepts raw message arrays for custom/advanced use
  • /api/prompt/run-simple — unchanged
  • /api/context/current and /api/context/parts — unchanged
  • agent_build_admin_messages_json() — unchanged, already does exactly what we need
  • Nostr message handling — unchanged

Remaining Consideration: Conversation History

The Nostr path gets DM history by querying encrypted kind-4 events from relays. The new /api/prompt/agent endpoint will include this same history since it calls agent_build_admin_messages_json(). This means:

  • Messages sent via the CLI will NOT appear in the Nostr DM history (they are not published as Nostr events)
  • Messages sent via Nostr WILL appear in the context when using the CLI
  • This is acceptable — the CLI is a development/admin tool that piggybacks on the agent's full context

If in the future you want CLI messages to also appear in history, that would require either publishing them as Nostr DMs or maintaining a separate local history store — but that is out of scope for this change.