Help us shape the future of Go! Take the 2026 Go Developer Survey

memory

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

Hybrid conversation memory

The memory package is a host-side Go service for applications built with Flow. It combines a bounded Redis working history with an injected long-term semantic store and a model completer. It is deliberately not a .nflow DSL bundle: the host owns the LLM and storage connections and supplies them when constructing the service.

Request flow

For each incoming message, Service.Reply validates the user/conversation scope, reads the latest configured number of turns, searches long-term memory with the current message, assembles the system prompt + retrieved memories + recent turns + current user message, and calls the supplied completer. After a successful completion it appends the user/assistant exchange to working memory. An explicit Remember flag or an importance score at/above the configured threshold immediately upserts the exchange into long-term memory.

The default importance scorer is a small configurable keyword matcher; it is a transparent baseline, not a claim of semantic importance detection. Hosts can inject a classifier or model-backed ImportanceScorer. A custom ShouldRecall policy can skip long-term search for messages that do not need it. Retrieved memories are added in a separate system message and explicitly labeled as untrusted reference data.

Example

working, err := memory.NewRedisWorkingStore(redisClient, memory.RedisConfig{
    Prefix:   "myapp:conversation-memory",
    MaxTurns: 32,
    TTL:      14 * 24 * time.Hour,
})
if err != nil {
    return err
}

service, err := memory.NewService(memory.Config{
    SystemPrompt:        "Answer clearly and use relevant user context.",
    RecentTurns:         8,
    RecallTopK:          4,
    ImportanceThreshold: 0.8,
    Keywords:            []string{"remember", "my preference is", "my timezone is"},
}, working, semanticStore, chatCompleter)
if err != nil {
    return err
}

reply, err := service.Reply(ctx, memory.Request{
    Scope:       memory.Scope{UserID: userID, ConversationID: conversationID},
    UserMessage: text,
})

Backends and LEANN

WorkingStore is implemented by RedisWorkingStore. Keys are derived from both user and conversation IDs, Redis list updates and retention trimming run in a transaction, and history is bounded (256 turns by default). The caller owns the Redis client lifecycle.

LongTermStore defines Search(scope, query, limit) and idempotent Upsert(record) operations. The full scope is passed on every call so adapters can keep users and conversations isolated. LEANN can be integrated behind this interface, but there is no LEANN adapter in this package: the upstream project currently documents Python APIs and a CLI, not a stable Go or HTTP service contract. Implement the adapter against the LEANN runtime/version actually deployed rather than relying on a guessed wire format.

Background promotion

A host scheduler may call PromoteOlder for each active scope with a cutoff and batch limit. The service upserts each aged turn using its stable turn ID, then removes only those IDs from Redis. If a write succeeds but deletion fails, a retry is safe provided the long-term adapter honors idempotent upsert-by-ID. This package does not own a scheduler or discover active scopes; the application does.

Failure behavior

Retrieval or completion failures stop the request before it is persisted. A Redis or long-term write failure is returned after the model answer has been generated; in that case the Response may contain Assistant, and callers should report/retry persistence separately rather than blindly repeating a potentially costly model call. The package does not silently swallow memory failures.

Documentation

Overview

Package memory provides a host-side hybrid conversation-memory service for Flow applications. It keeps recent turns in a bounded working store, retrieves scoped long-term memories, builds an LLM prompt, and promotes important or aged turns through backend interfaces.

Typical use:

working, _ := memory.NewRedisWorkingStore(redisClient, memory.RedisConfig{})
service, _ := memory.NewService(memory.Config{}, working, leannAdapter, chatAdapter)
reply, err := service.Reply(ctx, memory.Request{Scope: scope, UserMessage: "..."})

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CompleteFunc

type CompleteFunc func(context.Context, []Message) (string, error)

CompleteFunc adapts a function to Completer.

func (CompleteFunc) Complete

func (f CompleteFunc) Complete(ctx context.Context, messages []Message) (string, error)

Complete implements Completer.

type Completer

type Completer interface {
	Complete(ctx context.Context, messages []Message) (string, error)
}

Completer calls the configured LLM with the assembled prompt.

type Config

type Config struct {
	SystemPrompt        string
	RecentTurns         int
	RecallTopK          int
	RecallMinScore      float64
	MaxRecallCharacters int
	ImportanceThreshold float64
	Keywords            []string
	Scorer              ImportanceScorer
	ShouldRecall        RecallPolicy
}

Config controls prompt assembly, retrieval, and automatic memory promotion.

type ImportanceScoreFunc

type ImportanceScoreFunc func(context.Context, string) (float64, error)

ImportanceScoreFunc adapts a function to ImportanceScorer.

func (ImportanceScoreFunc) Score

func (f ImportanceScoreFunc) Score(ctx context.Context, message string) (float64, error)

Score implements ImportanceScorer.

type ImportanceScorer

type ImportanceScorer interface {
	Score(ctx context.Context, userMessage string) (float64, error)
}

ImportanceScorer estimates whether a user message merits immediate promotion. The returned score must be in [0, 1].

type KeywordScorer

type KeywordScorer struct {
	Keywords   []string
	MatchScore float64
}

KeywordScorer gives a configurable score when a message contains any configured retention phrase. It is a transparent baseline; production callers can inject a classifier or model-backed ImportanceScorer instead.

func (KeywordScorer) Score

func (s KeywordScorer) Score(_ context.Context, message string) (float64, error)

Score returns MatchScore for a case-insensitive substring match, or zero.

type LongTermStore

type LongTermStore interface {
	Search(ctx context.Context, scope Scope, query string, limit int) ([]SearchHit, error)
	Upsert(ctx context.Context, record Record) error
}

LongTermStore is the adapter boundary for LEANN or another semantic-memory backend. Search and Upsert must honor the complete Scope; Upsert must be idempotent by record ID.

type MemoryRecord deprecated added in v0.1.1

type MemoryRecord = Record

Deprecated: use Record.

type Message

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

Message is a role/content pair in the prompt sent to a chat model.

type RecallPolicy

type RecallPolicy func(ctx context.Context, scope Scope, query string) bool

RecallPolicy can skip semantic search for messages that do not need older context.

type Record

type Record struct {
	ID         string    `json:"id"`
	Scope      Scope     `json:"scope"`
	Text       string    `json:"text"`
	Score      float64   `json:"score"`
	SourceTurn string    `json:"source_turn"`
	CreatedAt  time.Time `json:"created_at"`
	Reason     string    `json:"reason"`
}

Record is an idempotently upserted long-term memory. ID is derived from the source turn, so a retried promotion replaces rather than duplicates it.

type RedisConfig

type RedisConfig struct {
	Prefix   string
	MaxTurns int
	TTL      time.Duration
}

RedisConfig controls Redis key names, the maximum turns retained per scope, and optional expiration. A positive MaxTurns is always enforced.

type RedisWorkingStore

type RedisWorkingStore struct {
	// contains filtered or unexported fields
}

RedisWorkingStore stores completed turns in one bounded Redis list per scope.

func NewRedisWorkingStore

func NewRedisWorkingStore(client redis.UniversalClient, config RedisConfig) (*RedisWorkingStore, error)

NewRedisWorkingStore creates a Redis-backed working store. It does not take ownership of the client; the caller remains responsible for closing it.

func (*RedisWorkingStore) Append

func (s *RedisWorkingStore) Append(ctx context.Context, scope Scope, turn Turn) error

Append adds a turn atomically and trims the oldest turns over the configured cap.

func (*RedisWorkingStore) Delete

func (s *RedisWorkingStore) Delete(ctx context.Context, scope Scope, turnIDs []string) error

Delete removes only turns whose IDs are supplied. It preserves concurrent appends and turns that were not successfully promoted.

func (*RedisWorkingStore) Older

func (s *RedisWorkingStore) Older(ctx context.Context, scope Scope, before time.Time, limit int) ([]Turn, error)

Older returns at most limit turns with timestamps strictly before before.

func (*RedisWorkingStore) Recent

func (s *RedisWorkingStore) Recent(ctx context.Context, scope Scope, limit int) ([]Turn, error)

Recent returns the newest limit turns in chronological order.

type Request

type Request struct {
	Scope        Scope
	UserMessage  string
	SystemPrompt string
	Remember     bool // explicit user/application request to retain this turn
}

Request is one incoming conversation message.

type Response

type Response struct {
	Assistant  string
	TurnID     string
	Recalled   []SearchHit
	Remembered bool
	Importance float64
}

Response is the generated answer and memory actions completed for this turn.

type Scope

type Scope struct {
	UserID         string `json:"user_id"`
	ConversationID string `json:"conversation_id"`
}

Scope is the isolation boundary for conversation and durable memory. Backends must use both fields when reading, searching, writing, and deleting.

type SearchHit

type SearchHit struct {
	ID    string  `json:"id"`
	Text  string  `json:"text"`
	Score float64 `json:"score,omitempty"`
}

SearchHit is a relevant long-term memory returned for the current query.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service runs the hybrid memory flow around an LLM completion.

func NewService

func NewService(config Config, working WorkingStore, semantic LongTermStore, model Completer) (*Service, error)

NewService validates dependencies and applies conservative defaults.

func (*Service) PromoteOlder

func (s *Service) PromoteOlder(ctx context.Context, scope Scope, before time.Time, limit int) (int, error)

PromoteOlder copies at most limit turns older than before into long-term storage, then removes exactly those turns from working storage. It is safe to retry when an upsert succeeds but deletion fails because records are keyed by source turn ID. A scheduler can call this per active scope in bounded batches.

func (*Service) Reply

func (s *Service) Reply(ctx context.Context, request Request) (Response, error)

Reply reads recent and relevant long-term memory, calls the model, then persists the completed turn. Persistence errors are returned even though Assistant may already contain a generated answer; callers should not repeat the model call blindly.

type Turn

type Turn struct {
	ID               string    `json:"id"`
	User             string    `json:"user"`
	Assistant        string    `json:"assistant"`
	CreatedAt        time.Time `json:"created_at"`
	ImportanceScore  float64   `json:"importance_score,omitempty"`
	ExplicitRemember bool      `json:"explicit_remember,omitempty"`
}

Turn is one completed user/assistant exchange stored in working memory.

type WorkingStore

type WorkingStore interface {
	Recent(ctx context.Context, scope Scope, limit int) ([]Turn, error)
	Append(ctx context.Context, scope Scope, turn Turn) error
	Older(ctx context.Context, scope Scope, before time.Time, limit int) ([]Turn, error)
	Delete(ctx context.Context, scope Scope, turnIDs []string) error
}

WorkingStore holds bounded, ordered short-term conversation turns. Recent must return turns oldest-to-newest. Delete removes only the supplied IDs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL