memory

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: May 29, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package memory provides persistent, agent-managed memory across sessions.

Architecture

Three tiers:

  1. Facts — Two typed files (user.md, env.md) with character caps, injected into the system prompt as a frozen snapshot. Managed by the agent via the memory tool (add/replace/remove/consolidate/read).

  2. Buffer — In-memory ring buffer on the Session struct. One-line summaries appended after each turn. Injected only when non-empty.

  3. Episodes — LLM-extracted durable facts written after sessions with ≥3 turns. Searchable via memory(search=...).

Merge-on-Write

When adding a fact, go-vector's RandomProjections provides a fast similarity check vs existing entries. cos > 0.7 = auto-merge, cos < 0.3 = auto-add, 0.3-0.7 = SimpleCall judgment. This saves ~80% of LLM calls on writes.

Index

Constants

View Source
const (
	// MergeThreshold is the cosine similarity threshold above which entries
	// are considered duplicates and auto-merged.
	MergeThreshold float32 = 0.7

	// AddThreshold is the cosine similarity below which entries are
	// considered distinct and auto-added without LLM judgment.
	AddThreshold float32 = 0.3
)

MergeThresholds for merge-on-write classification.

Variables

View Source
var UntrustedToolNames = map[string]bool{
	"browser":      true,
	"http_batch":   true,
	"transcribe":   true,
	"read_file":    true,
	"search_files": true,
	"multi_grep":   true,
}

UntrustedToolNames is the canonical set of tools whose results come from outside the agent's trust boundary. Any tool whose output reaches the model wrapped in <untrusted_content> belongs here.

This is the single source of truth — skills/selfimprove.go imports it to derive skill provenance from the same definition.

Functions

func BoolPtr

func BoolPtr(b bool) *bool

BoolPtr returns a pointer to a bool value.

func FormatBufferLine

func FormatBufferLine(role, message string) string

FormatBufferLine creates a timestamped buffer line. Format: "HH:MM role message"

func ScanContent

func ScanContent(content string) error

ScanContent checks memory content for security threats. Returns an error if the content contains patterns that could compromise the agent.

Checks:

  • Invisible Unicode characters (zero-width spaces, direction overrides, BOM)
  • Prompt injection markers ("ignore previous instructions", etc.)
  • Credential exfiltration patterns (API keys, private keys, bearer tokens)

Types

type Buffer

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

Buffer is a simple ring buffer for session-level turn summaries. Thread-safe only if accessed from a single goroutine (the loop engine owns the buffer and serializes all access).

func NewBuffer

func NewBuffer(cap int) *Buffer

NewBuffer creates a ring buffer with the given capacity. cap=0 means disabled (all appends are silently discarded).

func (*Buffer) Append

func (b *Buffer) Append(line string)

Append adds a line to the buffer. If the buffer is at capacity, the oldest line is evicted first.

func (*Buffer) Cap

func (b *Buffer) Cap() int

Cap returns the maximum number of lines.

func (*Buffer) Clear

func (b *Buffer) Clear()

Clear removes all lines from the buffer.

func (*Buffer) Len

func (b *Buffer) Len() int

Len returns the current number of lines.

func (*Buffer) Lines

func (b *Buffer) Lines() []string

Lines returns a copy of the current buffer contents (oldest first).

type EpisodeMeta

type EpisodeMeta struct {
	SessionID  string            `json:"session_id"`
	Turns      int               `json:"turns"`
	CreatedAt  time.Time         `json:"created_at"`
	Summary    string            `json:"summary"` // truncated for index listing
	Provenance EpisodeProvenance `json:"provenance,omitempty"`
}

EpisodeMeta holds metadata for a single episode.

type EpisodeProvenance added in v1.0.0

type EpisodeProvenance struct {
	Untrusted    bool     `json:"untrusted,omitempty"`
	Sources      []string `json:"sources,omitempty"`
	UserApproved bool     `json:"user_approved,omitempty"`
}

EpisodeProvenance carries the trust signals of the session that produced an episode. The default zero value means trusted.

An untrusted episode is one whose originating session ingested content from outside the agent's trust boundary (fetched pages, files outside the working directory, MCP tool output, audio transcription). Such episodes are stored on disk for audit but are NEVER auto- replayed into future sessions — they must be explicitly promoted (UserApproved=true) by the user first. This stops a one-shot prompt injection from becoming a persistent backdoor.

func DeriveProvenance added in v1.0.0

func DeriveProvenance(messages []llm.Message) EpisodeProvenance

DeriveProvenance walks a session's structured messages and returns the provenance an episode derived from those messages should carry. A message qualifies as untrusted if it contains a tool call whose name is in untrustedToolNames OR follows the MCP adapter naming convention (server__tool).

type EpisodeStore

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

EpisodeStore manages on-disk episode summaries (Tier 3 memory). Written after sessions with sufficient turns, searchable via SimpleCall. Index operations are protected by a mutex to prevent TOCTOU races between concurrent sessions sharing the same memory directory.

The in-memory idxCache avoids reading + unmarshalling index.json from disk on every FormatEpisodeContext call (which fires every loop turn). The cache is invalidated after every write.

func NewEpisodeStore

func NewEpisodeStore(dir string, rankFn RankStrategy) *EpisodeStore

NewEpisodeStore creates an EpisodeStore rooted at dir. If rankFn is nil, a default ranker is used (SimpleCall-based — requires LLM client).

func (*EpisodeStore) Read

func (e *EpisodeStore) Read(sessionID string) (string, error)

Read returns the full summary content for a session. sessionID is validated for path traversal before use.

func (*EpisodeStore) ReadIndex

func (e *EpisodeStore) ReadIndex() ([]EpisodeMeta, error)

ReadIndex reads the episode index from disk. Returns empty slice if the index file doesn't exist yet. Entries are ordered newest-first.

Uses an in-memory cache to avoid disk I/O on every Search() call. The cache is invalidated after every writeIndex().

func (*EpisodeStore) Search

func (e *EpisodeStore) Search(query string, limit int) ([]EpisodeMeta, error)

Search returns the most relevant episodes for a query, ranked by the configured RankStrategy. Limited to limit results.

func (*EpisodeStore) Write

func (e *EpisodeStore) Write(sessionID, summary string, turns int) error

Write stores an episode summary for a session. Creates the episodes directory and updates the index. Equivalent to WriteWithProvenance with a zero-value (trusted) provenance.

func (*EpisodeStore) WriteIfEnough

func (e *EpisodeStore) WriteIfEnough(sessionID, summary string, turns int) error

WriteIfEnough calls Write only if turns >= threshold. Returns nil without writing if the threshold isn't met.

func (*EpisodeStore) WriteIfEnoughWithProvenance added in v1.0.0

func (e *EpisodeStore) WriteIfEnoughWithProvenance(sessionID, summary string, turns int, prov EpisodeProvenance) error

WriteIfEnoughWithProvenance is the provenance-carrying counterpart of WriteIfEnough.

func (*EpisodeStore) WriteWithProvenance added in v1.0.0

func (e *EpisodeStore) WriteWithProvenance(sessionID, summary string, turns int, prov EpisodeProvenance) error

WriteWithProvenance stores an episode and attaches the given provenance to the index entry. An episode written with Untrusted=true is kept on disk but never auto-replayed (Search filters it out unless UserApproved=true). sessionID is validated for path traversal before use.

type FactStore

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

FactStore manages typed fact files (user.md and env.md) with character caps, duplicate prevention, and entry-level CRUD via substring matching. All write operations are protected by a mutex to prevent TOCTOU races between concurrent sessions sharing the same memory directory. The mutex guards only the in-memory read+parse+modify phase; the final disk write happens outside the lock to avoid blocking other sessions during file I/O.

func NewFactStore

func NewFactStore(dir string, capUser, capEnv int) *FactStore

NewFactStore creates a FactStore rooted at dir. Fact files are stored as dir/user.md and dir/env.md. Caps limit total file size.

func (*FactStore) Add

func (f *FactStore) Add(target, content string) error

Add appends a new entry to a fact file. Returns error if:

  • target is invalid
  • content is empty
  • content already exists (dedup)
  • adding would exceed the character cap

func (*FactStore) Entries

func (f *FactStore) Entries(target string) ([]string, error)

Entries returns the individual entries as a string slice.

func (*FactStore) Read

func (f *FactStore) Read(target string) (string, error)

Read returns the full content of a fact file. Returns empty string if the file doesn't exist yet.

func (*FactStore) Remove

func (f *FactStore) Remove(target, oldText string) error

Remove finds an entry by substring match and removes it. Returns error if the substring doesn't match exactly one entry.

func (*FactStore) Replace

func (f *FactStore) Replace(target, oldText, content string) error

Replace finds an entry by substring match and replaces it with new content. Returns error if the substring doesn't match exactly one entry.

type LLMClient

type LLMClient interface {
	SimpleCall(ctx context.Context, system, user string) (string, error)
}

LLMClient abstracts the LLM calls needed by the memory system (SimpleCall for consolidation, episode extraction, and search).

type MemoryConfig

type MemoryConfig struct {
	Enabled               *bool   `json:"enabled,omitempty"`
	FactsLimitUser        int     `json:"facts_limit_user,omitempty"`
	FactsLimitEnv         int     `json:"facts_limit_env,omitempty"`
	BufferLines           int     `json:"buffer_lines,omitempty"`
	BufferEnabled         *bool   `json:"buffer_enabled,omitempty"`
	MergeOnWrite          *bool   `json:"merge_on_write,omitempty"`
	ExtractOnEnd          *bool   `json:"extract_on_end,omitempty"`
	LLMSearch             *bool   `json:"llm_search,omitempty"`
	LLMExtract            *bool   `json:"llm_extract,omitempty"`
	LLMConsolidate        *bool   `json:"llm_consolidate,omitempty"`
	MergeThreshold        float32 `json:"merge_threshold,omitempty"`
	AddThreshold          float32 `json:"add_threshold,omitempty"`
	MinTurnsForExtraction int     `json:"min_turns_for_extraction,omitempty"`
}

MemoryConfig holds configuration for the memory system. Mirrors the JSON config section. Bool fields use *bool so that JSON omitempty can distinguish "not set" (nil) from "explicitly false" (pointer to false).

func DefaultMemoryConfig

func DefaultMemoryConfig() MemoryConfig

DefaultMemoryConfig returns sensible defaults.

type MemoryManager

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

MemoryManager orchestrates all three tiers of memory: Facts (durable, in system prompt), Buffer (session-level context), and Episodes (on-disk extracted summaries with search).

func NewMemoryManager

func NewMemoryManager(memoryDir string, llc LLMClient, cfg MemoryConfig) *MemoryManager

NewMemoryManager creates a fully wired MemoryManager. If llc is nil, LLM-dependent features (consolidation, episode search) degrade gracefully (no LLM call, fallback behavior).

func (*MemoryManager) AddFact

func (m *MemoryManager) AddFact(target, content string) error

AddFact appends a new fact entry. Performs:

  1. Security scan (reject if dangerous)
  2. Dedup (FactStore handles this)
  3. Merge-on-write using go-vector RP if MergeOnWrite is enabled
  4. Fits the merge detector after mutation (incrementally — only re-embeds the changed entry instead of all entries, avoiding a double disk-read).

func (*MemoryManager) AppendBuffer

func (m *MemoryManager) AppendBuffer(role, message string)

AppendBuffer adds a turn summary to the in-memory ring buffer.

func (*MemoryManager) BuildSystemPrompt

func (m *MemoryManager) BuildSystemPrompt() string

BuildSystemPrompt returns the memory section to inject into the system prompt. Returns empty string if memory is disabled or nothing to show.

Security: all content is scanned for injection patterns. If detected, the section is prefixed with an explicit warning that the content is data, not instructions. Even clean content is wrapped with anti-injection framing.

func (*MemoryManager) ClearBuffer

func (m *MemoryManager) ClearBuffer()

ClearBuffer resets the buffer for a new session.

func (*MemoryManager) Consolidate

func (m *MemoryManager) Consolidate(target string) error

Consolidate uses the LLM to merge related entries in a target file for better density. Falls back to no-op if LLM is unavailable or LLMConsolidate is disabled in config.

func (*MemoryManager) FormatEpisodeContext

func (m *MemoryManager) FormatEpisodeContext(query string) string

FormatEpisodeContext searches episodes with a recency-based ranker (no LLM — safe for per-turn use without recursion risk) and returns formatted context to inject as a system message. Returns empty string if no episodes found or memory is disabled.

func (*MemoryManager) GetBuffer

func (m *MemoryManager) GetBuffer() []string

GetBuffer returns the current buffer lines (for system prompt injection).

func (*MemoryManager) OnSessionEnd

func (m *MemoryManager) OnSessionEnd(sessionID string, turns int, messages []string)

OnSessionEnd is called when a session ends. If turns >= threshold, extracts a narrative session summary using the LLM and stores it as an episode. sessionID is validated for path traversal before any file I/O.

Equivalent to OnSessionEndWithProvenance with a zero-value (trusted) provenance. Prefer the With-Provenance variant from callers that have access to the structured llm.Message slice — that lets us mark episodes derived from sessions that touched untrusted content, so they are never auto-replayed.

func (*MemoryManager) OnSessionEndWithProvenance added in v1.0.0

func (m *MemoryManager) OnSessionEndWithProvenance(sessionID string, turns int, messages []string, prov EpisodeProvenance)

OnSessionEndWithProvenance is the provenance-carrying counterpart of OnSessionEnd. Callers derive the provenance with DeriveProvenance and pass it through so the resulting episode inherits the trust signal.

func (*MemoryManager) ReadFacts

func (m *MemoryManager) ReadFacts() (userContent, envContent string, err error)

ReadFacts returns the full content of user and env fact files.

func (*MemoryManager) RemoveFact

func (m *MemoryManager) RemoveFact(target, oldText string) error

RemoveFact removes a fact entry by substring.

func (*MemoryManager) ReplaceFact

func (m *MemoryManager) ReplaceFact(target, oldText, content string) error

ReplaceFact replaces an existing fact entry.

func (*MemoryManager) RestoreBuffer

func (m *MemoryManager) RestoreBuffer(lines []string)

RestoreBuffer loads buffer lines from a saved slice (e.g., from session).

func (*MemoryManager) SearchEpisodes

func (m *MemoryManager) SearchEpisodes(query string, limit int) ([]EpisodeMeta, error)

SearchEpisodes returns the most relevant episodes for a query.

type MemoryTool

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

MemoryTool wraps a MemoryManager as a odek-compatible Tool.

func NewMemoryTool

func NewMemoryTool(mm *MemoryManager) *MemoryTool

NewMemoryTool creates a tool that exposes memory CRUD + search to the agent.

func (*MemoryTool) Call

func (t *MemoryTool) Call(args string) (string, error)

func (*MemoryTool) Description

func (t *MemoryTool) Description() string

func (*MemoryTool) Name

func (t *MemoryTool) Name() string

func (*MemoryTool) Schema

func (t *MemoryTool) Schema() any

type MergeDetector

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

MergeDetector uses RandomProjections to quickly estimate whether a new fact entry overlaps with existing entries. This avoids ~80% of LLM calls during merge-on-write.

Lifecycle:

  1. NewMergeDetector(dims) — creates RP embedder
  2. Fit(corpus) — builds vocabulary from existing entries
  3. Classify(entry) → action + similarIdx + similarity
  4. After facts change → Fit(newCorpus) to rebuild vocabulary

Thresholds control the classification:

  • mergeThreshold: cosine above this → auto-merge (default 0.7)
  • addThreshold: cosine below this → auto-add (default 0.3)
  • Between thresholds: "judge" — requires LLM to decide

func NewMergeDetector

func NewMergeDetector(dims int) *MergeDetector

NewMergeDetector creates a MergeDetector with the given output dimensionality for the RP embedder. Pass 0 for default (256). Uses default thresholds (0.7 merge, 0.3 add).

func NewMergeDetectorWithThresholds

func NewMergeDetectorWithThresholds(dims int, mergeThreshold, addThreshold float32) *MergeDetector

NewMergeDetectorWithThresholds creates a MergeDetector with custom thresholds.

func (*MergeDetector) AppendEntry

func (m *MergeDetector) AppendEntry(entry string)

AppendEntry adds a single entry to the corpus. Only the new entry is embedded, avoiding a full re-embed of all existing entries. The RP vocabulary is still refreshed so new tokens from the entry are available for future Classify calls.

func (*MergeDetector) Classify

func (m *MergeDetector) Classify(entry string) (action string, similarIdx int, similarity float32)

Classify returns the merge decision for a new entry vs the fitted corpus.

Returns:

  • action: "merge" | "add" | "judge" | "nobody"
  • similarIdx: index of the most similar corpus entry (for merge/judge)
  • similarity: cosine similarity [0, 1]

"nobody" means the corpus is empty — there's nothing to compare against.

func (*MergeDetector) Corpus

func (m *MergeDetector) Corpus() []string

Corpus returns the current corpus (for inspection).

func (*MergeDetector) Fit

func (m *MergeDetector) Fit(corpus []string)

Fit builds the RP vocabulary and pre-computes embeddings for all corpus entries. Call whenever facts change (after add/replace/remove).

func (*MergeDetector) ReplaceEntry

func (m *MergeDetector) ReplaceEntry(idx int, entry string)

ReplaceEntry replaces an entry at the given index. Only the changed entry is re-embedded, avoiding a full re-embed of all existing entries.

type RankStrategy

type RankStrategy func(query string, episodes []EpisodeMeta) ([]EpisodeMeta, error)

RankStrategy is an injectable function for ranking episodes by relevance to a query. The default implementation uses SimpleCall; tests can inject a deterministic mock.

func NewLLMRanker

func NewLLMRanker(llm LLMClient) RankStrategy

NewLLMRanker creates a RankStrategy that uses an LLM client to rank episodes by semantic relevance to the query. Falls back to recency ordering if the LLM call fails or returns unparseable output.

func NewRPRanker

func NewRPRanker(dims int) RankStrategy

NewRPRanker creates a RankStrategy that uses RandomProjections (go-vector) for semantic similarity search over episodes. No LLM calls — pure vector math. Falls back to recency ordering if RP fitting fails.

The ranker fits the RP embedder on episode summaries on each call. With the typical number of episodes (< 100, 120-char summaries each), fitting takes < 1ms and is negligible compared to LLM latency.

Jump to

Keyboard shortcuts

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