Memory Guide
Abbyfile provides per-agent persistent memory as a file-based key-value store. Each agent’s memory lives at ~/.abbyfile/<name>/memory/ and persists across conversations.
Enabling Memory
agent.WithMemory(true),
This does two things:
- Creates a
FileStoreat~/.abbyfile/<name>/memory/ - Registers four builtin tools:
memory_read,memory_write,memory_list,memory_delete
CLI Commands
Memory is accessible directly from the command line:
# Write a value (overwrites if key exists)
./my-agent memory write notes "Project uses Go 1.24"
# Read a value
./my-agent memory read notes
# Append to existing key (or create it)
./my-agent memory append notes " -- confirmed on 2024-01-15"
# List all keys
./my-agent memory list
# Delete a key
./my-agent memory delete notes
MCP Exposure
When serve-mcp runs, memory is exposed in three ways:
1. Tools
Claude Code can call these tools during conversations:
memory_read–{"key": "notes"}– read a valuememory_write–{"key": "notes", "value": "content"}– write a valuememory_list–{}– list all keysmemory_delete–{"key": "notes"}– delete a key
These tools have appropriate MCP annotations:
// memory_read
ReadOnlyHint: true
IdempotentHint: true
OpenWorldHint: false
// memory_write
DestructiveHint: false // overwrites but not destructive in MCP sense
IdempotentHint: true
// memory_delete
OpenWorldHint: false
2. Resources
Memory keys are exposed as MCP resources:
memory://<name>/– JSON index of all keysmemory://<name>/{key}– individual key content
3. Prompts
A memory-context prompt template is registered:
- No arguments: returns a summary of all keys
- With
keyargument: returns the content of that specific key
Storage Details
Each key is stored as a file at ~/.abbyfile/<name>/memory/<key>.md. Keys:
- Must not be empty
- Must not contain path separators (
/or\) - Are case-sensitive
- Map directly to filenames (with
.mdextension)
Values are stored as plain text. There is no structured data format enforced – store whatever text makes sense for your agent.
Limits Configuration
By default, memory is unlimited. Use WithMemoryLimits() to set capacity bounds:
agent.WithMemoryLimits(memory.Limits{
MaxKeys: 100, // maximum number of keys (0 = unlimited)
MaxValueBytes: 10240, // maximum size per value in bytes (0 = unlimited)
MaxTotalBytes: 1048576, // maximum total storage in bytes (0 = unlimited)
})
Limits are enforced on write and append operations:
MaxKeys: checked when creating a new key (overwriting an existing key is always allowed)MaxValueBytes: checked against the full value size (for append, the existing + new content)MaxTotalBytes: checked against the sum of all stored values, accounting for the key being overwritten
When a limit is exceeded, the operation returns an error and the write does not happen.
Limits appear in the --describe manifest:
{
"name": "my-agent",
"memory": true,
"memoryLimits": {
"maxKeys": 100,
"maxValueBytes": 10240,
"maxTotalBytes": 1048576
}
}
Use Cases
Session notes: Store decisions, context, and observations that persist across conversations.
./my-agent memory write decisions "Using PostgreSQL for the data layer"
./my-agent memory append decisions "\nChose gRPC over REST for internal services"
Project health tracking: An agent can store test results and trends over time in persistent memory.
Context accumulation: Agents can build up knowledge about a project by writing to memory during conversations, then reading it back in future sessions.
Testing Patterns
Use t.TempDir() for isolated memory stores in tests:
func TestMyAgent_Memory(t *testing.T) {
dir := filepath.Join(t.TempDir(), "memory")
store, err := memory.NewFileStoreAt(dir, memory.Limits{})
if err != nil {
t.Fatal(err)
}
mgr := memory.NewManager(store)
// Write and read
if err := mgr.Set("key", "value"); err != nil {
t.Fatal(err)
}
got, err := mgr.Get("key")
if err != nil {
t.Fatal(err)
}
if got != "value" {
t.Errorf("Get() = %q, want %q", got, "value")
}
}
NewFileStoreAt() creates a store at a specific directory instead of ~/.abbyfile/<name>/memory/. This avoids polluting the user’s home directory during tests.
For testing limits:
func TestMemory_Limits(t *testing.T) {
dir := filepath.Join(t.TempDir(), "memory")
store, err := memory.NewFileStoreAt(dir, memory.Limits{MaxKeys: 2})
if err != nil {
t.Fatal(err)
}
store.Write("a", "1")
store.Write("b", "2")
err = store.Write("c", "3")
// err: key count 3 would exceed limit of 2 keys
}
For concurrency testing, use the Manager which wraps the store with a sync.RWMutex:
func TestMemory_Concurrent(t *testing.T) {
store, _ := memory.NewFileStoreAt(t.TempDir(), memory.Limits{})
mgr := memory.NewManager(store)
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
mgr.Set("key", "value")
mgr.Get("key")
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Integration Testing
For end-to-end testing with a built binary, override HOME to isolate memory:
tmpHome := t.TempDir()
cmd := exec.Command(binaryPath, "memory", "write", "test-key", "test-value")
cmd.Env = append(os.Environ(), "HOME="+tmpHome)
See internal/integration/agent_test.go for the full lifecycle test pattern (write, read, list, append, delete, verify deleted).