memory

package
v1.10.1 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 18 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 AlwaysExternalTools = map[string]bool{
	"browser":        true,
	"http_batch":     true,
	"transcribe":     true,
	"session_search": true,
}

AlwaysExternalTools are tools whose RESULT content originates outside the agent's trust boundary regardless of their arguments: network fetches, opaque transcribed audio, and recall of prior-session transcripts (which may themselves carry previously-injected content).

View Source
var PathReadingTools = map[string]bool{
	"read_file":    true,
	"search_files": true,
	"multi_grep":   true,
	"batch_read":   true,
	"json_query":   true,
	"head_tail":    true,
	"count_lines":  true,
	"checksum":     true,
	"word_count":   true,
	"sort":         true,
	"tr":           true,
	"diff":         true,
	"file_info":    true,
	"glob":         true,
	"tree":         true,
	"base64":       true,
}

PathReadingTools are tools that read filesystem content (or structure) into the transcript. They taint the episode only when one of their path arguments resolves OUTSIDE the workspace (see pathReadEscapes) — reads confined to the workspace, or to odek's own ~/.odek state, stay trusted so ordinary coding sessions remain recallable.

This must list every tool that surfaces file contents/structure to the model. A tool missing here would let an injected agent read a secret into a TRUSTED, recallable episode; when adding a new file-reading tool, add it here too.

View Source
var UntrustedToolNames = func() map[string]bool {
	m := make(map[string]bool, len(AlwaysExternalTools)+len(PathReadingTools))
	for k := range AlwaysExternalTools {
		m[k] = true
	}
	for k := range PathReadingTools {
		m[k] = true
	}
	return m
}()

UntrustedToolNames is the union of the two categories above. It is retained as the canonical "these tools can produce untrusted content" set for external references and documentation. The actual per-call decision is made by ToolCallTaints, which is argument-aware for the path-reading tools.

Functions

func BoolPtr

func BoolPtr(b bool) *bool

BoolPtr returns a pointer to a bool value.

func FactLooksUnsafe added in v1.2.0

func FactLooksUnsafe(fact string) bool

FactLooksUnsafe reports whether a fact embeds a download-and-execute / pipe-to-shell instruction. It is applied ONLY to AUTO-extracted facts (which are lower-trust and injected into every system prompt), not to facts the user adds explicitly via the memory tool. It does not catch every malicious fact — turning conversation into durable memory has an irreducible residual risk — but it closes the concrete download-and-run class.

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)
  • Mixed confusable scripts (Cyrillic/Greek homoglyphs mixed with Latin)
  • Prompt injection markers ("ignore previous instructions", etc.)
  • Credential exfiltration patterns (API keys, private keys, bearer tokens)

func ToolCallTaints added in v1.2.0

func ToolCallTaints(name, argsJSON string) bool

ToolCallTaints reports whether a single recorded tool call crossed the agent's trust boundary. It is the single source of truth shared by episode (memory) and skill (skills) provenance so the two stay in lockstep.

  • MCP adapter calls (name contains "__") always taint — third-party servers return arbitrary text.
  • AlwaysExternalTools always taint, regardless of arguments.
  • PathReadingTools taint only when one of their path arguments resolves OUTSIDE the workspace trust zone (workspace dir, the sandbox /workspace mount, or ~/.odek). Symlinks are resolved so e.g. /etc → /private/etc on macOS cannot disguise an escape. A malformed argument string taints conservatively; absent/empty paths default to the workspace (trusted).
  • Everything else (shell, patch, write_file, …) is trusted.

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 EmbeddingConfig added in v1.5.0

type EmbeddingConfig = embedding.Config

EmbeddingConfig is memory's embedding backend selector. See embedding.Config.

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"`
	AutoApproved bool     `json:"auto_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, MCP tool output, audio transcription, prior-session recall, or reads of files outside the workspace). 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 (see `odek memory promote`). This stops a one-shot prompt injection from becoming a persistent backdoor.

AutoApproved is the opt-in escape valve: when the operator sets memory.auto_approve_episodes=true, untrusted episodes are stamped AutoApproved at creation so they are recalled without a manual promote. It is kept distinct from UserApproved so the audit trail still shows the approval was automatic (policy) rather than a human decision; Untrusted and Sources remain recorded either way.

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 taints the episode if it contains a tool call that crossed the trust boundary per ToolCallTaints.

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 with lifecycle management disabled (no dedup, no eviction). If rankFn is nil, a default ranker is used (SimpleCall-based — requires LLM client).

func NewEpisodeStoreWithLifecycle added in v1.2.0

func NewEpisodeStoreWithLifecycle(dir string, rankFn RankStrategy, dedupThreshold float32, maxEpisodes, ttlDays int) *EpisodeStore

NewEpisodeStoreWithLifecycle creates an EpisodeStore with dedup + eviction policy. dedupThreshold is the cosine above which a new episode replaces an existing near-duplicate (0 disables); maxEpisodes caps the stored count (0 disables); ttlDays evicts episodes older than that many days (0 disables).

func (*EpisodeStore) PendingReview added in v1.2.0

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

PendingReview returns the episodes that are untrusted and not yet user-approved — the ones currently excluded from recall that a user may want to promote. Ordered newest-first (as ReadIndex returns them).

func (*EpisodeStore) Promote added in v1.2.0

func (e *EpisodeStore) Promote(sessionID string) error

Promote marks a tainted episode as user-approved so it can be replayed into future sessions. This is the human-gated escape hatch for episodes whose originating session legitimately touched external content. It is intentionally NOT exposed to the agent (only via `odek memory promote`) so that a prompt-injected agent cannot self-approve poisoned memory.

Returns an error if the session is unknown or already approved.

func (*EpisodeStore) Prune added in v1.2.0

func (e *EpisodeStore) Prune() error

Prune evicts episodes by TTL and count cap (see NewEpisodeStoreWithLifecycle) and removes their summary files. Safe to call at session end or from a CLI. No-op when both the cap and TTL are disabled.

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) SetNotifier added in v1.2.0

func (e *EpisodeStore) SetNotifier(n MemoryNotifier)

SetNotifier replaces the episode store's lifecycle notifier. If n is nil a NoopMemoryNotifier is used so the fire path is always safe.

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.

Concurrency: the per-instance mutex serializes the read+parse+modify+write of a single FactStore. Because each odek session builds its own MemoryManager / FactStore over the *same* memory directory, cross-instance serialization on a shared directory is provided one level up by the MemoryManager per-directory lock (factsDirLock) wrapping the full read-modify-write — without it, concurrent session-end writes would lose each other's updates. Disk writes go to a unique temp file + atomic rename, so concurrent writers never clobber a shared temp file.

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"`
	ExtractFacts          *bool   `json:"extract_facts,omitempty"`
	ConsolidateOnEnd      *bool   `json:"consolidate_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"`

	// Episode lifecycle (see internal/memory/episodes.go). EpisodeDedupThreshold
	// is the cosine above which a new episode replaces an existing near-duplicate
	// (0 disables dedup). MaxEpisodes caps the stored episode count, evicting the
	// oldest beyond it (0 disables the cap). EpisodeTTLDays evicts episodes older
	// than that many days (0 disables TTL).
	EpisodeDedupThreshold float32 `json:"episode_dedup_threshold,omitempty"`
	MaxEpisodes           int     `json:"max_episodes,omitempty"`
	EpisodeTTLDays        int     `json:"episode_ttl_days,omitempty"`

	// AutoApproveEpisodes, when true, stamps untrusted episodes as approved at
	// session-end so they are recalled without a manual `odek memory promote`.
	// SECURITY: this is the opt-in escape valve that trades the human review
	// gate for convenience — a session that ingested external/untrusted content
	// can then influence future sessions automatically. Off (false) by default.
	AutoApproveEpisodes *bool `json:"auto_approve_episodes,omitempty"`

	// Embedding selects the semantic embedding backend used by episode
	// recall, episode dedup, the non-LLM episode ranker, and fact
	// merge-on-write. nil = local RandomProjections (lexical, zero-cost).
	// See EmbeddingConfig for the "http" provider that enables real
	// semantic similarity via any OpenAI-compatible embeddings API.
	Embedding *EmbeddingConfig `json:"embedding,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 MemoryEvent added in v1.2.0

type MemoryEvent struct {
	// Type is the lifecycle moment. One of:
	//   "fact_added"            — a new durable fact was appended (Target, Content)
	//   "fact_merged"           — merge-on-write folded a fact into an existing
	//                             near-duplicate (Target, Content, Similarity)
	//   "fact_replaced"         — an existing fact was replaced (Target, Content)
	//   "fact_removed"          — a fact was removed (Target, Content=old text)
	//   "fact_consolidated"     — LLM consolidation merged entries (Target,
	//                             Count=before, NewCount=after)
	//   "episode_stored"        — a session episode was extracted + persisted
	//                             (SessionID, Count=turns, Content=summary,
	//                             Untrusted)
	//   "episode_deduped"       — a new episode replaced a near-duplicate
	//                             (SessionID, Similarity)
	//   "episode_evicted"       — episodes were pruned by TTL/count cap
	//                             (Sessions, Count=number evicted)
	//   "episode_promoted"      — a tainted episode was user-approved for recall
	//                             (SessionID)
	//   "episode_pending_review"— an untrusted, unapproved episode was stored and
	//                             is excluded from recall until promoted
	//                             (SessionID)
	Type string

	Target     string    // fact scope: "user" or "env"
	SessionID  string    // episode session ID
	Content    string    // fact text or episode summary excerpt
	Count      int       // before-count (consolidate), turns (episode), or evicted count
	NewCount   int       // after-count (consolidate)
	Sessions   []string  // evicted session IDs (episode_evicted)
	Similarity float32   // cosine similarity for merge/dedup events
	Untrusted  bool      // episode derived from a session that touched untrusted content
	Timestamp  time.Time // when the event occurred (UTC)
}

MemoryEvent represents a memory lifecycle event emitted by the MemoryManager (and its EpisodeStore). Callers (Terminal, WebUI, Telegram, or embedding programs) consume these events to surface memory activity — facts being added/merged/consolidated and episodes being stored, deduped, evicted, or promoted. Previously every one of these moments was silent.

Not every field is set for every Type; see the per-Type notes below. The zero value of a field means "not applicable to this event".

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. Callers pass the RAW turn text; summarizeForBuffer derives a clean, bounded excerpt here so every entry point shares one policy.

Invariant: summarization lives here, not in Buffer.Append, so that RestoreBuffer (which calls Buffer.Append directly with already-formatted, already-summarized lines) never re-processes persisted summaries.

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 returns relevant past-session context to inject into the system message on each loop turn. It uses the cached go-vector index — zero LLM calls on this path — so it is safe to call every turn. Untrusted, unpromoted episodes are excluded. Returns empty string if no relevant episodes are 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) PendingReviewEpisodes added in v1.2.0

func (m *MemoryManager) PendingReviewEpisodes() ([]EpisodeMeta, error)

PendingReviewEpisodes lists episodes that are untrusted and not yet user-approved (currently excluded from recall).

func (*MemoryManager) PromoteEpisode added in v1.2.0

func (m *MemoryManager) PromoteEpisode(sessionID string) error

PromoteEpisode marks a tainted episode as user-approved so it can be recalled into future sessions. Human-gated escape hatch — see EpisodeStore.Promote.

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).

Invariant: these lines are already-formatted, already-summarized buffer entries. They go straight to Buffer.Append and MUST NOT be routed through AppendBuffer/summarizeForBuffer, which would corrupt the persisted summaries.

func (*MemoryManager) SearchEpisodes

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

SearchEpisodes returns the most relevant episodes for a query. SearchEpisodes is the explicit memory-search path (called by the memory tool, not by the per-turn recall loop). It retrieves candidates from the vector index and, when llm_search is enabled, LLM-reranks only those candidates — never all N episodes. This keeps relevance quality while bounding the LLM cost to O(candidates), not O(total episodes).

func (*MemoryManager) SetNotifier added in v1.2.0

func (m *MemoryManager) SetNotifier(n MemoryNotifier)

SetNotifier replaces the memory lifecycle notifier and propagates it to the underlying EpisodeStore so fact AND episode events share one sink. If n is nil a NoopMemoryNotifier is used so callers never have to nil-check.

type MemoryNotifier added in v1.2.0

type MemoryNotifier interface {
	Notify(event MemoryEvent)
}

MemoryNotifier is the observer interface for memory lifecycle events. Implementations should be non-blocking in the hot path (fact writes fire mid-loop); use channel-based or async dispatch for I/O.

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 estimates whether a new fact entry overlaps with existing entries, using the configured embedding backend (RandomProjections by default — see textEmbedder). This avoids ~80% of LLM calls during merge-on-write.

Lifecycle:

  1. NewMergeDetector(dims) — creates the embedder
  2. Fit(corpus) — prepares the embedder for the existing entries
  3. Classify(entry) → action + similarIdx + similarity
  4. After facts change → Fit(newCorpus) to refresh

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 over the default RandomProjections embedder.

func (*MergeDetector) AppendEntry

func (m *MergeDetector) AppendEntry(entry string)

AppendEntry adds a single entry to the corpus. The embedder is refreshed so new tokens from the entry are available for future Classify calls; for stateless backends only the new entry costs an embedding call (cache).

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 prepares the embedder and pre-computes embeddings for all corpus entries. Call whenever facts change (after add/replace/remove). On embedding failure the precomputed vectors are cleared, so Classify degrades to "nobody" (add without merge) rather than misclassifying.

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 MultiMemoryNotifier added in v1.2.0

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

MultiMemoryNotifier fans out each Notify call to all registered notifiers. Safe for concurrent use when the notifier slice is set before any calls to Notify (the standard pattern: constructed, set on MemoryManager, then called from the agent loop / session-end goroutines).

func NewMultiMemoryNotifier added in v1.2.0

func NewMultiMemoryNotifier(notifiers ...MemoryNotifier) *MultiMemoryNotifier

NewMultiMemoryNotifier creates a MultiMemoryNotifier that fans out to the given notifiers.

func (*MultiMemoryNotifier) Notify added in v1.2.0

func (m *MultiMemoryNotifier) Notify(event MemoryEvent)

Notify fans out the event to all registered notifiers.

type NoopMemoryNotifier added in v1.2.0

type NoopMemoryNotifier struct{}

NoopMemoryNotifier is a MemoryNotifier that discards all events. Used as the default when no notifier is configured, so callers never have to nil-check before firing.

func (NoopMemoryNotifier) Notify added in v1.2.0

func (NoopMemoryNotifier) Notify(event MemoryEvent)

Notify discards the event.

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.

Jump to

Keyboard shortcuts

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