252 lines
9.4 KiB
Markdown
252 lines
9.4 KiB
Markdown
# Agent Memory System — Implementation Plan
|
|
|
|
## Overview
|
|
|
|
Add short-term and long-term memory to the Didactyl agent, stored as Nostr kind 30078 (addressable app data) events. Both memory types use freeform markdown content, **NIP-44 encrypted** (the agent encrypts to itself for privacy).
|
|
|
|
- **Short-term memory** — injected into every LLM prompt automatically; the agent's scratchpad for facts, context, and notes that should persist across conversations
|
|
- **Long-term memory** — retrieved on demand via a tool call; a larger store for reference material the agent doesn't need in every prompt
|
|
|
|
Both are global to the agent (not per-user), stored as single replaceable events keyed by `d` tag. Content is NIP-44 encrypted to the agent's own public key, ensuring only the agent can read its own memories.
|
|
|
|
## Nostr Event Structure
|
|
|
|
### Short-Term Memory Event
|
|
```json
|
|
{
|
|
"kind": 30078,
|
|
"content": "<NIP-44 encrypted markdown>",
|
|
"tags": [
|
|
["d", "short_term_memory"],
|
|
["app", "didactyl"]
|
|
]
|
|
}
|
|
```
|
|
Decrypted content example:
|
|
```markdown
|
|
## Current Context
|
|
|
|
- Working on project Alpha...
|
|
- Admin prefers concise responses...
|
|
```
|
|
|
|
### Long-Term Memory Event
|
|
```json
|
|
{
|
|
"kind": 30078,
|
|
"content": "<NIP-44 encrypted markdown>",
|
|
"tags": [
|
|
["d", "long_term_memory"],
|
|
["app", "didactyl"]
|
|
]
|
|
}
|
|
```
|
|
Decrypted content example:
|
|
```markdown
|
|
## Project Notes
|
|
|
|
### Alpha
|
|
- Started 2026-01-15...
|
|
|
|
## Preferences
|
|
- Admin timezone: UTC-3...
|
|
```
|
|
|
|
### Encryption Details
|
|
|
|
Memory content is NIP-44 encrypted using the agent's own keypair (self-encryption):
|
|
- **Encrypt:** `nostr_nip44_encrypt(agent_private_key, agent_public_key, plaintext, ciphertext, size)`
|
|
- **Decrypt:** `nostr_nip44_decrypt(agent_private_key, agent_public_key, ciphertext, plaintext, size)`
|
|
|
|
This follows the existing pattern in `tool_nostr_dm.c` (`execute_nostr_encrypt`/`execute_nostr_decrypt`). The `d` tags remain unencrypted (they must be for addressable event replacement to work), but the actual memory content is private.
|
|
|
|
## Size Limits
|
|
|
|
| Memory Type | Max Content Size | Rationale |
|
|
|---|---|---|
|
|
| Short-term | ~8,000 chars | In every prompt; ~2K tokens budget |
|
|
| Long-term | ~32,000 chars | On-demand only; well within 64KB relay limit |
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Agent Startup] --> B[Fetch kind 30078 from nostr]
|
|
B --> B2[NIP-44 decrypt content with agent keys]
|
|
B2 --> C[Cache short_term_memory plaintext in-memory]
|
|
B2 --> D[Cache long_term_memory plaintext in-memory]
|
|
|
|
E[Every Prompt Build] --> F[Soul template section: short_term_memory]
|
|
F --> G[tool: memory_short_term_read]
|
|
G --> H[Return cached plaintext STM content]
|
|
H --> I[Injected as system message in prompt]
|
|
|
|
J[Agent wants to recall LTM] --> K[Calls my_memory tool]
|
|
K --> L{Cached?}
|
|
L -->|Yes| M[Return cached plaintext LTM]
|
|
L -->|No| N[Query nostr for kind 30078 d=long_term_memory]
|
|
N --> N2[NIP-44 decrypt with agent keys]
|
|
N2 --> O[Cache plaintext result]
|
|
O --> M
|
|
|
|
P[Agent wants to save memory] --> Q[Calls memory_save tool]
|
|
Q --> R{type param}
|
|
R -->|short_term| S[NIP-44 encrypt + publish kind 30078]
|
|
S --> S2[Update STM cache with plaintext]
|
|
R -->|long_term| T[NIP-44 encrypt + publish kind 30078]
|
|
T --> T2[Update LTM cache with plaintext]
|
|
```
|
|
|
|
## New Tools
|
|
|
|
### 1. `memory_save` — Write memory
|
|
|
|
**Description:** Save content to agent short-term or long-term memory. Publishes as kind 30078 to nostr and updates the local cache.
|
|
|
|
**Parameters:**
|
|
| Param | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `type` | string | yes | `"short_term"` or `"long_term"` |
|
|
| `content` | string | yes | Markdown content to store |
|
|
|
|
**Behavior:**
|
|
1. Validate `type` is `"short_term"` or `"long_term"`
|
|
2. Enforce size limit based on type (on the plaintext, before encryption)
|
|
3. Determine d-tag: `"short_term_memory"` or `"long_term_memory"`
|
|
4. NIP-44 encrypt the content (agent encrypts to own public key)
|
|
5. Publish kind 30078 event with encrypted content, `["d", <d_tag>]` and `["app", "didactyl"]` tags via `nostr_handler_publish_kind_event()`
|
|
6. Update the in-memory cache (stores plaintext for fast access)
|
|
7. Return success with event_id
|
|
|
|
### 2. `my_memory` — Read long-term memory
|
|
|
|
**Description:** Retrieve the agent's long-term memory. Returns cached content or fetches from nostr if not yet loaded.
|
|
|
|
**Parameters:**
|
|
| Param | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| (none) | — | — | No parameters needed |
|
|
|
|
**Behavior:**
|
|
1. Check if LTM is cached in-memory (plaintext)
|
|
2. If cached, return the content
|
|
3. If not cached, query nostr for kind 30078 with `d=long_term_memory` authored by self
|
|
4. NIP-44 decrypt the content (agent decrypts with own keypair)
|
|
5. Cache the plaintext result
|
|
6. Return the content (or empty string if no memory exists yet)
|
|
|
|
### 3. `memory_short_term_read` — Internal tool for prompt injection
|
|
|
|
**Description:** Internal tool (not exposed to LLM as a callable tool) used by the soul template to inject short-term memory into every prompt.
|
|
|
|
**Behavior:**
|
|
1. Return cached short-term memory content
|
|
2. If cache is empty, return empty string (skip_if_empty will omit the section)
|
|
|
|
## Files to Modify/Create
|
|
|
|
### New File: `src/tools/tool_memory.c`
|
|
|
|
Contains implementations for:
|
|
- `execute_memory_save()` — handles both STM and LTM writes (NIP-44 encrypts before publishing)
|
|
- `execute_my_memory()` — handles LTM reads (NIP-44 decrypts on cache miss)
|
|
- `execute_memory_short_term_read()` — internal tool for prompt template injection (returns cached plaintext)
|
|
- Static cache variables for STM and LTM **plaintext** content with mutex protection
|
|
- `memory_init()` — called at startup to fetch existing memories from nostr and NIP-44 decrypt them into cache
|
|
- `memory_cleanup()` — free cached memory on shutdown
|
|
- Helper functions for NIP-44 self-encrypt/decrypt using agent's own keypair (pattern from `tool_nostr_dm.c`)
|
|
|
|
### Modified: `src/tools/tools_internal.h`
|
|
|
|
Add function declarations:
|
|
- `char* execute_memory_save(tools_context_t* ctx, const char* args_json);`
|
|
- `char* execute_my_memory(tools_context_t* ctx, const char* args_json);`
|
|
- `char* execute_memory_short_term_read(tools_context_t* ctx, const char* args_json);`
|
|
- `int memory_init(tools_context_t* ctx);` — fetches from nostr, NIP-44 decrypts, caches plaintext
|
|
- `void memory_cleanup(void);`
|
|
|
|
### Modified: `src/tools/tools_dispatch.c`
|
|
|
|
Add dispatch entries:
|
|
- `"memory_save"` → `execute_memory_save()`
|
|
- `"my_memory"` → `execute_my_memory()`
|
|
- `"memory_short_term_read"` → `execute_memory_short_term_read()`
|
|
|
|
### Modified: `src/tools/tools_schema.c`
|
|
|
|
Add OpenAI function schemas for:
|
|
- `memory_save` — exposed to LLM (type + content params)
|
|
- `my_memory` — exposed to LLM (no params)
|
|
- `memory_short_term_read` is NOT added to schema (internal only, called by template)
|
|
|
|
### Modified: `src/agent.c`
|
|
|
|
- Call `memory_init()` during `agent_init()` to load existing memories from nostr on startup
|
|
- Call `memory_cleanup()` during `agent_cleanup()`
|
|
- Add `"short_term_memory"` to context section detection in `detect_context_section()`
|
|
|
|
### Modified: Soul template in `config.jsonc.example`
|
|
|
|
Add a new template section for short-term memory injection:
|
|
|
|
```yaml
|
|
- section: short_term_memory
|
|
role: system
|
|
tool: memory_short_term_read
|
|
skip_if_empty: true
|
|
```
|
|
|
|
This goes after the existing context sections (admin identity, profile, etc.) and before the DM history expand section.
|
|
|
|
### Modified: `Makefile`
|
|
|
|
Add `src/tools/tool_memory.c` to the build.
|
|
|
|
## Startup Flow
|
|
|
|
1. `agent_init()` calls `memory_init()`
|
|
2. `memory_init()` queries nostr for kind 30078 events authored by self with d-tags `short_term_memory` and `long_term_memory`
|
|
3. For each event found, NIP-44 decrypt the content using the agent's own keypair
|
|
4. Decrypted plaintext results are cached in static variables protected by mutex
|
|
5. If no events found, caches remain empty (agent starts with blank memory)
|
|
6. If decryption fails (e.g., key mismatch from a previous agent identity), log a warning and start with blank memory
|
|
|
|
## Prompt Injection Flow (Short-Term Memory)
|
|
|
|
1. `prompt_template_build_messages()` encounters the `short_term_memory` section
|
|
2. Section has `tool: memory_short_term_read` — calls `execute_memory_short_term_read()`
|
|
3. Tool returns cached STM content as the `content` field
|
|
4. If empty and `skip_if_empty: true`, section is omitted from prompt
|
|
5. If non-empty, injected as a system message like:
|
|
```
|
|
## Short-Term Memory
|
|
|
|
<agent's markdown notes here>
|
|
```
|
|
|
|
## Cache Design
|
|
|
|
```c
|
|
// In tool_memory.c
|
|
static char* g_short_term_memory = NULL; // cached STM content
|
|
static char* g_long_term_memory = NULL; // cached LTM content
|
|
static int g_stm_loaded = 0; // whether STM has been fetched
|
|
static int g_ltm_loaded = 0; // whether LTM has been fetched
|
|
static pthread_mutex_t g_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
#define MEMORY_STM_MAX_CHARS 8000
|
|
#define MEMORY_LTM_MAX_CHARS 32000
|
|
```
|
|
|
|
## Implementation Order
|
|
|
|
1. Create `src/tools/tool_memory.c` with cache, init, cleanup, and all three tool functions
|
|
2. Add declarations to `tools_internal.h`
|
|
3. Add dispatch entries to `tools_dispatch.c`
|
|
4. Add schemas for `memory_save` and `my_memory` to `tools_schema.c`
|
|
5. Wire up `memory_init()`/`memory_cleanup()` in `agent.c`
|
|
6. Add `short_term_memory` template section to soul in `config.jsonc.example`
|
|
7. Update `Makefile` build
|
|
8. Update soul template in system prompt to mention memory capabilities
|
|
9. Test: save STM → verify prompt injection; save LTM → verify recall via my_memory
|