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:
- Subscribe to the agent's own kind 0/3/10002/1 events from relays
- Cache them in
nostr_handler.c(same pattern asg_admin_kind0_jsonetc.) - Expose them via new
nostr_handler_get_agent_*API functions - Create context tools that format the cached data into context blocks
- Add to context template so the agent always knows about itself
- Add callable tool aliases like
my_kind0_profilefor 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
- Separate subscription vs expanding self-skills sub: Separate is cleaner — different EOSE semantics, different callback needs.
- 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.
- Alias naming:
my_kind0_profilematches the existingmy_npub/my_pubkeypattern. Also addingmy_contacts,my_relays,my_notesfor consistency. - No new config section needed: The agent context subscription is unconditional — an agent should always know about itself. No
agent_context.enabledtoggle needed.