Skip to content

Add Reflect() method to Memory interface #42

@fogfish

Description

@fogfish

Problem: Reflection-based memory strategies (à la "Generative Agents" paper) need a hook to trigger periodic consolidation, but there's no standard way to signal reflection points.

Context:

  • Reflection is a pattern where agents periodically synthesize high-level insights from observations
  • Examples: "User prefers functional programming", "User is learning Go"
  • Requires LLM calls to analyze memory and extract patterns
  • Should be triggered at natural cycle boundaries (end of session, N observations, time-based)

Current Limitation:

  • No standard interface method for reflection
  • Custom implementations would need proprietary API
  • Agent can't signal "good time to reflect"

Proposed Solution: Add optional Reflect() method that agents can call to trigger memory consolidation.

Required Changes:

  1. Add Reflect() to Memory interface (memory.go):
// Memory is core element of agents behaviour...
type Memory interface {
    // Purge clears all observations
    Purge()

    // Commit stores new observation
    Commit(*Observation)

    // Context builds message sequence for prompting
    Context(chatter.Message) []chatter.Message
    
    // Reflect triggers memory consolidation and synthesis.
    // Called by agents at natural cycle boundaries to:
    //  - Synthesize high-level insights from observations
    //  - Compress old observations into summaries
    //  - Recalculate importance scores
    //  - Update indexes or embeddings
    //
    // Implementations that don't need reflection can no-op.
    // Return error if reflection fails (e.g., LLM call fails).
    //
    // Example triggers:
    //  - After N observations committed
    //  - At end of agent session
    //  - Periodically (time-based)
    //  - When memory is nearly full
    Reflect(context.Context) error
}
  1. Implement no-op for existing memories:

memory/void.go:

func (s *Void) Reflect(ctx context.Context) error {
    return nil  // No-op for stateless memory
}

memory/stream.go:

func (s *Stream) Reflect(ctx context.Context) error {
    return nil  // Stream doesn't perform reflection by default
}
  1. Add optional reflection trigger in Automata (agent/automata.go):
// PromptOnce forgets state and prompts within a new session.
// Triggers memory reflection before purging.
func (automata *Automata[A, B]) PromptOnce(ctx context.Context, input A, opt ...chatter.Opt) (B, error) {
    automata.mu.Lock()
    defer automata.mu.Unlock()
    
    // Trigger reflection before resetting
    if err := automata.memory.Reflect(ctx); err != nil {
        // Log but don't fail - reflection is best-effort
        // Could add logger here when available
    }
    
    automata.Purge()
    return automata.Prompt(ctx, input, opt...)
}
  1. Add explicit Reflect() public method (agent/automata.go):
// Reflect triggers memory consolidation and synthesis.
// Call this periodically for reflection-based memory implementations.
// Has no effect for simple memory types (Void, Stream).
//
// Example:
//   // After batch of requests
//   for _, input := range batch {
//       agent.Prompt(ctx, input)
//   }
//   agent.Reflect(ctx)  // Consolidate learnings
func (automata *Automata[A, B]) Reflect(ctx context.Context) error {
    return automata.memory.Reflect(ctx)
}
  1. Document in README.md:
### Memory Reflection

Advanced memory implementations can implement reflection - periodic synthesis of insights:

```go
type ReflectiveMemory struct {
    llm chatter.Chatter
    observations []*Observation
    insights []string
}

func (m *ReflectiveMemory) Reflect(ctx context.Context) error {
    // Ask LLM to synthesize insights
    prompt := buildReflectionPrompt(m.observations)
    reply, err := m.llm.Prompt(ctx, prompt)
    if err != nil {
        return err
    }
    
    // Store high-level insights
    m.insights = append(m.insights, extractInsights(reply)...)
    
    // Could also: compress observations, recalculate scores, etc.
    return nil
}

Trigger reflection:

// After session
agent.Prompt(ctx, input)
agent.Reflect(ctx)  // Consolidate learnings

// Or periodically
for i, input := range inputs {
    agent.Prompt(ctx, input)
    if i % 100 == 0 {
        agent.Reflect(ctx)
    }
}

See examples/12_reflective_memory for complete implementation.

  1. Add tests (memory/stream_test.go):
func TestStreamReflectNoOp(t *testing.T) {
    mem := NewStream(10, "")
    
    err := mem.Reflect(context.Background())
    
    it.Then(t).Should(
        it.Nil(err),  // Should not error
    )
}

Estimated Effort: 2 hours
Skills Required: Interface design, understanding reflection patterns

Breaking Changes: ⚠️ YES - Adds method to Memory interface.

Migration: All existing Memory implementations must add Reflect(context.Context) error method. Simple no-op for most:

func (m *MyMemory) Reflect(ctx context.Context) error {
    return nil
}

Benefits:

  • ✅ Standard interface for reflection-based strategies
  • ✅ Agents can trigger reflection at appropriate times
  • ✅ No-op for simple memories (no overhead)
  • ✅ Enables sophisticated memory patterns

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions