-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
Problem: The Memory interface is designed to be extensible for custom memory strategies, but there's no documentation explaining:
- How to implement custom memory
- What the
Observation.RelevanceandObservation.Importancefields are for - Example patterns for different memory strategies
- When to use Stream vs custom implementations
Current State:
- Interface is clean and extensible ✅
Observationhas fields for advanced strategies (Relevance,Importance) ✅- But no guidance on using them ❌
Required Changes:
- Add comprehensive godoc to Memory interface (
memory.go):
// Memory is the core element of agent behavior. It maintains a comprehensive
// record of an agent's experience, recalls observations, and builds context
// windows for prompting.
//
// Built-in Implementations:
// - Void: No memory retention (stateless)
// - Stream: Sequential observations with configurable window size
//
// Custom Memory Strategies:
//
// The interface is designed for extension. Implement custom memory for:
// - Semantic search (RAG): Use Observation.Relevance embeddings
// - Importance weighting: Use Observation.Reply.Importance scores
// - Hierarchical: Combine multiple memory layers
// - Graph-based: Link related observations
//
// Example custom implementation:
//
// type CustomMemory struct {
// observations []*Observation
// }
//
// func (m *CustomMemory) Commit(obs *Observation) {
// // Calculate importance score
// obs.Reply.Importance = m.scoreImportance(obs)
// m.observations = append(m.observations, obs)
// }
//
// func (m *CustomMemory) Context(prompt Message) []Message {
// // Select most relevant observations
// relevant := m.selectRelevant(prompt, m.observations)
// return buildMessages(relevant)
// }
//
// Thread Safety:
//
// Memory implementations should be safe for concurrent use when shared across
// multiple agent instances. Use sync.Mutex or other synchronization as needed.
// Built-in implementations (Stream, Void) are already thread-safe.
type Memory interface {
// Purge clears all observations, resetting memory to initial state.
// Called by agent.PromptOnce() to start fresh session.
Purge()
// Commit stores a new observation from agent execution.
// Implementations can:
// - Calculate and store Relevance embeddings
// - Compute Importance scores
// - Apply eviction policies
// - Update indexes for search
Commit(*Observation)
// Context builds the message sequence for LLM prompting.
// Given the current prompt, select and order relevant observations.
// Implementations can:
// - Apply recency filters
// - Perform semantic search
// - Weight by importance
// - Enforce token budgets
Context(chatter.Message) []chatter.Message
}- Document Observation fields (
memory.go):
// Observation represents a single interaction cycle in the agent's experience.
// Contains the query sent, the reply received, and metadata for memory management.
type Observation struct {
// Created timestamp (GUID based on time)
Created guid.K
// Accessed timestamp - updated when observation is retrieved.
// Can be used for LRU eviction policies.
Accessed guid.K
// Query sent to LLM
Query Input
// Reply received from LLM
Reply Reply
}
// Input represents the query and its semantic representation
type Input struct {
// Content is the actual message sent to LLM
Content chatter.Message
// Relevance is the embedding vector for semantic search.
// Custom memory implementations can populate this for RAG/vector search.
// Format: quantized float8 for efficient storage.
// Example: embedder.Embed(Content) -> []float8.Float8
Relevance []float8.Float8
}
// Reply represents the LLM response and its metadata
type Reply struct {
// Content is the actual message received from LLM
Content chatter.Message
// Relevance is the embedding vector for the reply.
// Custom memory implementations can use this for similarity-based retrieval.
Relevance []float8.Float8
// Importance is a score (0.0-1.0) indicating the significance of this observation.
// Custom memory implementations can use this for:
// - Priority-based eviction (keep high importance)
// - Weighted recall (prefer important memories)
// - Reflection triggers (synthesize when importance crosses threshold)
//
// Scoring strategies:
// - Recency: Recent observations score higher
// - Novelty: Unique information scores higher
// - Explicit: Decoder assigns based on content
// - LLM-based: Ask LLM to score importance
Importance float64
}- Create memory implementation guide (
doc/MEMORY.md):
# Memory Implementation Guide
## Overview
The `Memory` interface enables custom memory strategies for agents. This guide explains how to implement and use custom memory.
## Built-in Implementations
### Void
- No memory retention
- Each prompt is independent
- Use for: Stateless agents, one-shot tasks
### Stream
- Sequential storage with FIFO eviction
- Configurable capacity: `NewStream(cap int, stratum)`
- Use for: Conversations, context-aware tasks
## Implementing Custom Memory
### Basic Structure
```go
type CustomMemory struct {
mu sync.Mutex // Thread safety
observations []*thinker.Observation
// ... custom fields
}
func (m *CustomMemory) Purge() {
m.mu.Lock()
defer m.mu.Unlock()
m.observations = nil
}
func (m *CustomMemory) Commit(obs *thinker.Observation) {
m.mu.Lock()
defer m.mu.Unlock()
// Custom logic here
}
func (m *CustomMemory) Context(prompt chatter.Message) []chatter.Message {
m.mu.Lock()
defer m.mu.Unlock()
// Custom selection logic
}Memory Strategy Patterns
Pattern 1: Importance-Weighted Memory
Retain observations based on importance scores:
type ImportanceMemory struct {
mu sync.Mutex
maxSize int
heap []*thinker.Observation // Priority queue
}
func (m *ImportanceMemory) Commit(obs *thinker.Observation) {
// Score importance
obs.Reply.Importance = m.scoreImportance(obs)
// Add to priority queue
m.heap = append(m.heap, obs)
sort.Slice(m.heap, func(i, j int) bool {
return m.heap[i].Reply.Importance > m.heap[j].Reply.Importance
})
// Evict lowest importance if full
if len(m.heap) > m.maxSize {
m.heap = m.heap[:m.maxSize]
}
}
func (m *ImportanceMemory) scoreImportance(obs *thinker.Observation) float64 {
// Example: recency + content length
recency := time.Now().Sub(obs.Created.Time()).Hours()
length := len(obs.Reply.Content.String())
return 1.0 / (1.0 + recency) * math.Log(float64(length))
}Pattern 2: Hybrid (Recent + Important)
Combine recent observations with historically important ones:
type HybridMemory struct {
recent *memory.Stream
important []*thinker.Observation
importanceThreshold float64
}
func (m *HybridMemory) Commit(obs *thinker.Observation) {
// Always add to recent
m.recent.Commit(obs)
// Add to important if significant
if obs.Reply.Importance > m.importanceThreshold {
m.important = append(m.important, obs)
}
}
func (m *HybridMemory) Context(prompt chatter.Message) []chatter.Message {
// Combine recent + important, deduplicate
ctx := m.recent.Context(prompt)
for _, obs := range m.important {
// Add if not already in recent context
ctx = append(ctx, obs.Query.Content, obs.Reply.Content)
}
return ctx
}Pattern 3: Semantic Search (RAG)
Use embeddings for similarity-based retrieval:
type SemanticMemory struct {
embedder Embedder // Your embedding service
vectorDB VectorDB // Your vector storage
}
func (m *SemanticMemory) Commit(obs *thinker.Observation) {
// Generate embeddings
obs.Query.Relevance = m.embedder.Embed(obs.Query.Content)
obs.Reply.Relevance = m.embedder.Embed(obs.Reply.Content)
// Store in vector DB
m.vectorDB.Insert(obs)
}
func (m *SemanticMemory) Context(prompt chatter.Message) []chatter.Message {
// Embed query
queryVector := m.embedder.Embed(prompt)
// Find similar observations
similar := m.vectorDB.Search(queryVector, topK=5)
// Build context
return buildContext(similar)
}Using Custom Memory
// Create custom memory
customMem := NewCustomMemory()
// Use with agent
agent := agent.NewAutomata(
llm,
customMem, // Your implementation
encoder,
decoder,
reasoner,
)Best Practices
- Thread Safety: Use mutex if memory is shared across agents
- Token Budgets: Consider LLM context limits when building context
- Eviction Policies: Define clear rules for what to forget
- Performance: Balance memory size vs retrieval speed
- Testing: Test with various context sizes and patterns
See Also
memory/stream.go- Reference implementationexamples/memory/- Example custom implementations
- Add example custom memory (
examples/11_custom_memory/memory.go):
package main
// Example: Priority-based memory that retains important observations
type PriorityMemory struct {
mu sync.Mutex
maxSize int
stratum chatter.Stratum
observations []*thinker.Observation
}
func NewPriorityMemory(maxSize int, stratum chatter.Stratum) *PriorityMemory {
return &PriorityMemory{
maxSize: maxSize,
stratum: stratum,
observations: make([]*thinker.Observation, 0),
}
}
func (m *PriorityMemory) Purge() {
m.mu.Lock()
defer m.mu.Unlock()
m.observations = nil
}
func (m *PriorityMemory) Commit(obs *thinker.Observation) {
m.mu.Lock()
defer m.mu.Unlock()
// Score importance (example: based on content length)
obs.Reply.Importance = m.scoreImportance(obs)
m.observations = append(m.observations, obs)
// Sort by importance
sort.Slice(m.observations, func(i, j int) bool {
return m.observations[i].Reply.Importance > m.observations[j].Reply.Importance
})
// Keep top N
if len(m.observations) > m.maxSize {
m.observations = m.observations[:m.maxSize]
}
}
func (m *PriorityMemory) Context(prompt chatter.Message) []chatter.Message {
m.mu.Lock()
defer m.mu.Unlock()
ctx := make([]chatter.Message, 0)
if len(m.stratum) > 0 {
ctx = append(ctx, m.stratum)
}
// Return observations in chronological order (not priority order)
sorted := make([]*thinker.Observation, len(m.observations))
copy(sorted, m.observations)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Created < sorted[j].Created
})
for _, obs := range sorted {
ctx = append(ctx, obs.Query.Content, obs.Reply.Content)
}
ctx = append(ctx, prompt)
return ctx
}
func (m *PriorityMemory) scoreImportance(obs *thinker.Observation) float64 {
// Example scoring: longer responses are more important
length := float64(len(obs.Reply.Content.String()))
return math.Min(1.0, length/1000.0)
}
// Usage example in main()
func main() {
llm, _ := autoconfig.FromNetRC("thinker")
// Use custom memory
customMem := NewPriorityMemory(20, "You are a helpful assistant")
agent := agent.NewAutomata(
llm,
customMem,
encoder,
decoder,
reasoner,
)
// Agent will use priority-based memory retention
result, _ := agent.Prompt(context.Background(), input)
}- Add to README.md:
### Custom Memory Implementations
The `Memory` interface is designed for extension. Implement custom strategies for:
- **Semantic search** (RAG): Use `Observation.Relevance` embeddings
- **Importance weighting**: Use `Observation.Reply.Importance` scores
- **Hierarchical memory**: Combine multiple memory layers
- **Graph-based**: Link related observations
See `doc/MEMORY.md` for implementation guide and `examples/11_custom_memory` for example.Estimated Effort: 3 hours
Skills Required: Technical writing, documentation, example creation
Benefits:
- ✅ Users understand extensibility points
- ✅ Clear guidance on using
RelevanceandImportancefields - ✅ Working examples to copy from
- ✅ No code changes needed - pure documentation