Documentation
¶
Overview ¶
Package memory provides persistent, agent-managed memory across sessions.
Architecture ¶
Three tiers:
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).
Buffer — In-memory ring buffer on the Session struct. One-line summaries appended after each turn. Injected only when non-empty.
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
- func BoolPtr(b bool) *bool
- func FormatBufferLine(role, message string) string
- func ScanContent(content string) error
- type Buffer
- type EpisodeMeta
- type EpisodeStore
- func (e *EpisodeStore) Read(sessionID string) (string, error)
- func (e *EpisodeStore) ReadIndex() ([]EpisodeMeta, error)
- func (e *EpisodeStore) Search(query string, limit int) ([]EpisodeMeta, error)
- func (e *EpisodeStore) Write(sessionID, summary string, turns int) error
- func (e *EpisodeStore) WriteIfEnough(sessionID, summary string, turns int) error
- type FactStore
- type LLMClient
- type MemoryConfig
- type MemoryManager
- func (m *MemoryManager) AddFact(target, content string) error
- func (m *MemoryManager) AppendBuffer(role, message string)
- func (m *MemoryManager) BuildSystemPrompt() string
- func (m *MemoryManager) ClearBuffer()
- func (m *MemoryManager) Consolidate(target string) error
- func (m *MemoryManager) FormatEpisodeContext(query string) string
- func (m *MemoryManager) GetBuffer() []string
- func (m *MemoryManager) OnSessionEnd(sessionID string, turns int, messages []string)
- func (m *MemoryManager) ReadFacts() (userContent, envContent string, err error)
- func (m *MemoryManager) RemoveFact(target, oldText string) error
- func (m *MemoryManager) ReplaceFact(target, oldText, content string) error
- func (m *MemoryManager) RestoreBuffer(lines []string)
- func (m *MemoryManager) SearchEpisodes(query string, limit int) ([]EpisodeMeta, error)
- type MemoryTool
- type MergeDetector
- type RankStrategy
Constants ¶
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 ¶
This section is empty.
Functions ¶
func FormatBufferLine ¶
FormatBufferLine creates a timestamped buffer line. Format: "HH:MM role message"
func ScanContent ¶
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 ¶
NewBuffer creates a ring buffer with the given capacity. cap=0 means disabled (all appends are silently discarded).
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
}
EpisodeMeta holds metadata for a single episode.
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. sessionID is validated for path traversal before use.
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.
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 ¶
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 ¶
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) Read ¶
Read returns the full content of a fact file. Returns empty string if the file doesn't exist yet.
type LLMClient ¶
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:
- Security scan (reject if dangerous)
- Dedup (FactStore handles this)
- Merge-on-write using go-vector RP if MergeOnWrite is enabled
- 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.
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) 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:
- NewMergeDetector(dims) — creates RP embedder
- Fit(corpus) — builds vocabulary from existing entries
- Classify(entry) → action + similarIdx + similarity
- 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.