Files
didactyl/plans/agent_self_context.md

8.6 KiB

Agent Self-Context: Know Thyself

Problem

When the agent is asked about itself — its profile, contacts, relays, or recent notes — it has no idea. The context template currently injects administrator identity/profile/contacts/relays/notes into the system prompt, but nothing equivalent for the agent's own Nostr identity beyond a bare pubkey+npub from agent_identity.

The agent publishes its own kind 0 (profile), kind 3 (contacts), kind 10002 (relays), and kind 1 (notes) at startup, but never subscribes to or caches those events for self-awareness.

Solution Overview

Mirror the admin context pattern for the agent itself:

  1. Subscribe to the agent's own kind 0/3/10002/1 events from relays
  2. Cache them in nostr_handler.c (same pattern as g_admin_kind0_json etc.)
  3. Expose them via new nostr_handler_get_agent_* API functions
  4. Create context tools that format the cached data into context blocks
  5. Add to context template so the agent always knows about itself
  6. Add callable tool aliases like my_kind0_profile for on-demand use
flowchart TD
    A[Startup: publish kind 0/3/10002/1] --> B[Subscribe to own events]
    B --> C[Cache in nostr_handler globals]
    C --> D[Context tools read cache]
    D --> E[Context template injects into system prompt]
    D --> F[LLM can call tools on-demand]
    
    subgraph Admin Context - existing
        G[g_admin_kind0_json]
        H[g_admin_kind3 contacts]
        I[g_admin_kind10002_json]
        J[g_admin_kind1_notes]
    end
    
    subgraph Agent Context - new
        K[g_agent_kind0_json]
        L[g_agent_kind3 contacts]
        M[g_agent_kind10002_json]
        N[g_agent_kind1_notes]
    end

Detailed Changes

1. nostr_handler.c — Agent Self-Context Cache

Add new static globals mirroring the admin pattern:

static char* g_agent_kind0_json = NULL;       // kind 0 profile JSON
static char* g_agent_kind10002_json = NULL;    // kind 10002 relay list JSON
static char** g_agent_kind3_contacts = NULL;   // kind 3 contact pubkeys
static int g_agent_kind3_contact_count = 0;
static admin_kind1_note_t* g_agent_kind1_notes = NULL;  // reuse struct
static int g_agent_kind1_note_count = 0;

2. nostr_handler.c — Agent Self-Context Subscription

Create nostr_handler_subscribe_agent_context() that subscribes to kinds 0, 3, 10002, 1 filtered by the agent's own pubkey. This is separate from the self-skills subscription (which handles 31123/31124/10123).

The callback on_agent_context_event() will parse and cache events using the same logic as on_admin_context_event().

Alternative considered: Expanding nostr_handler_subscribe_self_skills() to include these kinds. Rejected because the self-skills sub has different EOSE handling and a callback for skill loading. Keeping them separate is cleaner.

3. nostr_handler.h — New API Functions

int nostr_handler_subscribe_agent_context(void);
char* nostr_handler_get_agent_kind0_context(void);
char* nostr_handler_get_agent_kind3_context(void);
char* nostr_handler_get_agent_kind10002_context(void);
char* nostr_handler_get_agent_kind1_notes_context(void);

4. main.c — Call Agent Context Subscription at Startup

Add nostr_handler_subscribe_agent_context() call after admin context subscription, before self-skills subscription.

5. tool_agent.c — New Context Tools

Create four new tool functions following the exact pattern from tool_admin.c:

Tool Name Description Data Source
nostr_agent_profile Agent's kind 0 profile metadata nostr_handler_get_agent_kind0_context()
nostr_agent_contacts Agent's kind 3 contact list nostr_handler_get_agent_kind3_context()
nostr_agent_relays Agent's kind 10002 relay list nostr_handler_get_agent_kind10002_context()
nostr_agent_notes Agent's recent kind 1 notes nostr_handler_get_agent_kind1_notes_context()

Each returns a content field with markdown-formatted context, e.g.:

## Agent Kind 0 Profile (source: nostr kind 0)

Agent kind 0 profile content (JSON): {"name":"Didactyl Agent","display_name":"Didactyl",...}

6. tools_internal.h — Declare New Functions

char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json);
char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json);

7. tools_dispatch.c — Register + Aliases

Add dispatch entries:

if (strcmp(tool_name, "nostr_agent_profile") == 0)  return execute_nostr_agent_profile(ctx, args_json);
if (strcmp(tool_name, "nostr_agent_contacts") == 0) return execute_nostr_agent_contacts(ctx, args_json);
if (strcmp(tool_name, "nostr_agent_relays") == 0)   return execute_nostr_agent_relays(ctx, args_json);
if (strcmp(tool_name, "nostr_agent_notes") == 0)    return execute_nostr_agent_notes(ctx, args_json);

// Friendly aliases
if (strcmp(tool_name, "my_kind0_profile") == 0) return execute_nostr_agent_profile(ctx, args_json);
if (strcmp(tool_name, "my_contacts") == 0)      return execute_nostr_agent_contacts(ctx, args_json);
if (strcmp(tool_name, "my_relays") == 0)         return execute_nostr_agent_relays(ctx, args_json);
if (strcmp(tool_name, "my_notes") == 0)          return execute_nostr_agent_notes(ctx, args_json);

8. tools_schema.c — OpenAI Function Schemas

Add 8 new tool schemas (4 canonical + 4 aliases), all with empty parameters (no-arg tools), following the pattern of admin_identity/nostr_admin_profile etc.

9. Context Template Update

Update the kind 31120 soul content in config.jsonc and context_template.md to add agent sections after admin sections:

- section: admin_notes
  role: system
  tool: nostr_admin_notes
  skip_if_empty: true

# NEW: Agent self-context sections
- section: agent_identity
  role: system
  tool: agent_identity
  skip_if_empty: true

- section: agent_profile
  role: system
  tool: nostr_agent_profile
  skip_if_empty: true

- section: agent_contacts
  role: system
  tool: nostr_agent_contacts
  skip_if_empty: true

- section: agent_relays
  role: system
  tool: nostr_agent_relays
  skip_if_empty: true

- section: agent_notes
  role: system
  tool: nostr_agent_notes
  skip_if_empty: true

- section: tasks
  role: system
  tool: task_list
  skip_if_empty: true

10. nostr_handler.c Cleanup

Add cleanup for agent context globals in nostr_handler_cleanup(), mirroring free_admin_context_locked().

File Change Summary

File Change Type Description
src/nostr_handler.h Modify Add 5 new function declarations
src/nostr_handler.c Modify Add agent context cache globals, subscription, event handler, getter functions, cleanup
src/main.c Modify Call nostr_handler_subscribe_agent_context() at startup
src/tools/tool_agent.c Modify Add 4 new context tool execute functions
src/tools/tools_internal.h Modify Declare 4 new execute functions
src/tools/tools_dispatch.c Modify Add 8 dispatch entries (4 tools + 4 aliases)
src/tools/tools_schema.c Modify Add 8 OpenAI function schemas
config.jsonc Modify Update kind 31120 template section
config.jsonc.example Modify Update kind 31120 template section
context_template.md Modify Add agent_* sections after admin_* sections

Context Token Impact

Each agent context section adds roughly the same token count as its admin counterpart:

  • agent_identity: ~40 tokens (already exists, just adding to template)
  • agent_profile: ~80-150 tokens (depends on profile richness)
  • agent_contacts: ~50-200 tokens (depends on contact count)
  • agent_relays: ~50-100 tokens
  • agent_notes: ~100-300 tokens (depends on note count/length)

Total additional context: ~320-790 tokens. With skip_if_empty: true, empty sections cost 0 tokens.

Design Decisions

  1. Separate subscription vs expanding self-skills sub: Separate is cleaner — different EOSE semantics, different callback needs.
  2. Cache from relay vs read from config: Cache from relay is more accurate (reflects what's actually published, not just what config says). The startup_events in config are the intent; the relay data is the reality.
  3. Alias naming: my_kind0_profile matches the existing my_npub/my_pubkey pattern. Also adding my_contacts, my_relays, my_notes for consistency.
  4. No new config section needed: The agent context subscription is unconditional — an agent should always know about itself. No agent_context.enabled toggle needed.