memory

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package memory is jess's durable agent memory: the facts an agent keeps across turns and sessions, and the machinery to recall the right ones for the current prompt.

The shape:

  • Entry is one memory item — a short text snippet tagged with a Kind, an agent ID, optional Key, and provenance Source. Storage is content-addressable, so callers don't have to invent IDs.
  • Kind classifies an Entry. KindUser / KindFeedback / KindProject / KindReference are the canonical categories, each with a KindPolicy in the KindRegistry. user/feedback are AlwaysInclude (injected every turn, bypassing recall); project/reference are recall-only. Hosts override per agent via KindRegistry.Set.
  • Store is the persistence interface: Append + Recall + Forget, concurrency-safe. Three implementations ship: NewInMemoryStore (testing, offline), NewJSONLStore (durable, tombstones, Compact), and NewChromemStore (vector, on chromem-go). Vector-aware backends also satisfy the VectorStore capability interface (SearchVector + Embedder).
  • Recaller is the read-side query strategy: given the current conversation, return the entries to inject. NewSimpleRecaller (token overlap) and NewVectorRecaller (cosine, needs a VectorStore) compose via NewHybridRecaller (reciprocal rank fusion, K=60).
  • Embedder turns text into vectors for the vector store and recaller. The embed/gomlx subpackage runs BERT-family sentence-transformers ONNX models in-process via GoMLX's pure-Go backend — no CGO, no subprocess, no ONNX Runtime sidecar.
  • RememberTool and RecallTool are jess/tool.Tool implementations the model calls to write and read memory itself. Set Entry.Source on tool-written entries so "why do you remember X?" and "forget session Y" stay answerable.

Wiring: hand a Store and Recaller to an agent via jess.WithMemory(store, recaller). jess injects the recalled entries as a leading user message before each LLM call inside its anti-corruption layer; this package itself stays vendor-free. Memory failures never block the LLM call — the inject path degrades to no-memory, never no-agent.

Pre-1.0 — API may change before v1. See CHANGELOG.md.

Index

Constants

This section is empty.

Variables

View Source
var DefaultKindPolicies = map[Kind]KindPolicy{
	KindUser:      {AlwaysInclude: true, MaxEntries: 8},
	KindFeedback:  {AlwaysInclude: true, MaxEntries: 8},
	KindProject:   {AlwaysInclude: false, MaxEntries: 6, AgeWeight: 0.5},
	KindReference: {AlwaysInclude: false, MaxEntries: 4, AgeWeight: 0},
}

DefaultKindPolicies is the baked-in policy map matching the canonical Kinds. Hosts customize via KindRegistry.Set, or register entirely new Kinds with their own policy.

Tuning rationale:

  • user/feedback always-include, cap 8 each. Core memories the model should always operate against; 16 entries combined is a few KB of prompt budget — affordable.
  • project recall-only with age decay; cap 6 to avoid swamping.
  • reference recall-only without age decay (pointers don't "expire" — a link to a dashboard is still useful months later); cap 4.
View Source
var DefaultStopwords = []string{
	"the", "and", "are", "was", "were", "been", "being", "for", "with",
	"from", "that", "this", "these", "those", "its", "did", "does", "done",
	"what", "which", "who", "whom", "whose", "when", "where", "why", "how",
	"can", "could", "will", "would", "should", "shall", "may", "might",
	"must", "have", "has", "had", "you", "your", "yours", "our", "ours",
	"they", "them", "their", "his", "her", "hers", "she", "him",
	"about", "into", "than", "then", "not", "yes", "get", "got", "just",
	"any", "all", "some", "such", "out", "off", "over", "more", "most",
	"there", "here", "also", "only", "very", "much", "many",
}

DefaultStopwords is a standard English stopword list (3+ chars, since shorter tokens are already dropped by MinTokenLength). Common query glue that otherwise causes spurious keyword matches.

View Source
var ErrUnsupported = errors.New("memory: operation unsupported by this Store")

ErrUnsupported is returned by Stores that don't implement an optional capability (e.g. semantic Text matching when the backing Store only supports tag lookup). Callers check with errors.Is.

View Source
var FallbackKindPolicy = KindPolicy{AlwaysInclude: false, MaxEntries: 4, AgeWeight: 0}

FallbackKindPolicy is what KindRegistry returns for a Kind that has no specific policy registered (including unknown / freeform Kinds). Behaves like KindReference — recall-only, no age decay, modest cap. Safe default that won't bloat prompts.

Functions

func WithSource

func WithSource(ctx context.Context, src Source) context.Context

WithSource returns a context carrying src — the RememberTool reads it during Execute to stamp the saved Entry's provenance. Hosts call this once per agent run with the session-level info (SessionID, MessageID). Reason and Tool are typically set per- call by the tool itself, not threaded through ctx.

Calling WithSource is OPTIONAL. The tool falls back to a Source{Tool: "remember"} when ctx carries no Source — the entry still saves; just with less audit info.

Types

type ChromemOptions

type ChromemOptions struct {
	// Path is the persistence directory. Empty disables disk
	// persistence — entries vanish when the process exits.
	Path string
	// Compress enables gzip on persisted files. No effect when
	// Path is empty.
	Compress bool
	// CollectionName lets callers shard a single chromem DB
	// across multiple Stores. Default "jess-memory".
	CollectionName string
}

ChromemOptions configures NewChromemStore. Persistence path optional — empty means "in-memory only, lose state on restart." Compress requests gzip on persisted gob files; reduces disk by ~5x at the cost of ~5% extra Append latency.

type ChromemStore

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

ChromemStore is a Store backed by philippgille/chromem-go, the embedded pure-Go vector DB. Persists to gob files (chromem's native format) when a non-empty path is given to NewChromemStore; in-memory only otherwise.

Implements both Store and VectorStore — Recall does keyword/tag filtering plus content-substring match (chromem's whereDocument); SearchVector does cosine-similarity nearest neighbor.

Concurrency: safe across goroutines. chromem handles its own internal locking; the ChromemStore adds a mutex only around the embedder cache used for ID derivation.

func NewChromemStore

func NewChromemStore(embedder Embedder, opts ChromemOptions) (*ChromemStore, error)

NewChromemStore returns a Store backed by chromem-go. The Embedder is required — every Append calls it to produce the stored vector, and the Embedder's Name is recorded in entry metadata so future code can detect cross-embedder drift.

func (*ChromemStore) Append

func (s *ChromemStore) Append(ctx context.Context, e Entry) (Entry, error)

Append persists e. ID is content-addressed via entryID (same dedupe semantics as InMemoryStore). The embedding is computed lazily by chromem on AddDocument when Embedding is nil — we could also pre-compute via s.embedder, but offloading lets chromem batch under the hood if we later use AddDocuments.

chromem doesn't dedupe by ID — re-adding with the same ID silently shadows the prior entry. We track IDs ourselves so re-Appending the same content doesn't re-embed (re-embedding would burn the embedder's API/CPU budget for no gain).

func (*ChromemStore) Embedder

func (s *ChromemStore) Embedder() Embedder

Embedder returns the embedder the Store was constructed with. Implements VectorStore.

func (*ChromemStore) Forget

func (s *ChromemStore) Forget(ctx context.Context, id string) error

Forget removes the entry by ID. Idempotent.

func (*ChromemStore) Recall

func (s *ChromemStore) Recall(ctx context.Context, q Query, max int) ([]Entry, error)

Recall does metadata + content filtering and returns entries in newest-first order. Vector similarity is NOT used here — use SearchVector for that. This keeps Recall semantics consistent with the InMemoryStore so a HybridRecaller can mix VectorRecaller (vector path) and SimpleRecaller (keyword path) cleanly.

func (*ChromemStore) SearchVector

func (s *ChromemStore) SearchVector(ctx context.Context, vec []float32, max int, filter Query) ([]Entry, error)

SearchVector does cosine-similarity nearest-neighbor search. The filter narrows the candidate set the same way Query does in Recall — AgentID, Kind, Tags. Implements VectorStore.

type Embedder

type Embedder interface {
	// Embed produces a vector for the given text. Returns an error
	// for setup-time failures (model not loaded, network unreachable
	// when an API-based embedder needs it). The vector slice is owned
	// by the caller after return.
	Embed(ctx context.Context, text string) ([]float32, error)

	// Dim returns the embedding dimensionality this Embedder
	// produces. Useful for Store backends that allocate fixed-shape
	// matrices and want to fail fast on dimension mismatch.
	Dim() int

	// Name returns a short stable identifier for this embedder
	// (e.g. "ollama:nomic-embed-text", "gomlx:all-MiniLM-L6-v2",
	// "openai:text-embedding-3-small"). Stores tag entries with
	// this name so a recaller can verify it's querying against
	// vectors produced by the same embedder.
	Name() string
}

Embedder turns text into a vector for nearest-neighbor retrieval. Implementations are expected to be safe for concurrent use across distinct Embed calls — the harness will batch embeddings for memory writes and re-call for query-time recall.

Vectors returned by a single Embedder MUST have a stable dimensionality. Stores that index embeddings will reject mismatched dims. Implementations should document their dim in their type comment (e.g. "Returns 384-dim vectors").

type Entry

type Entry struct {
	// ID is a Store-assigned identifier. Empty on Append; populated
	// by the Store before return so callers can Forget specific
	// entries later. Stable across reads — implementations should
	// not renumber.
	ID string

	// Kind tags the entry's semantic category. See the Kind type
	// (kind.go) for the canonical taxonomy + per-Kind retrieval
	// policy. Stored as a plain string so raw literals work
	// without conversion. Empty Kind picks up FallbackKindPolicy.
	Kind string

	// AgentID scopes the entry. A multi-agent host (jess's design
	// expects this) stores per-agent memory so the "Coding" agent
	// doesn't surface "Research" agent preferences. Empty means
	// global (visible to all agents).
	AgentID string

	// Text is the memory content. Short prose. Implementations are
	// free to truncate at a sensible bound (suggested: 8KB) — the
	// goal is "things the model can read without burning context".
	Text string

	// Tags are optional searchable labels. Recallers use them for
	// fast filtering before scoring; a Tag-only Recall is a
	// reasonable cheap strategy.
	Tags []string

	// Key is an optional semantic identity. When set, Append
	// REPLACES any prior Entry with the same (AgentID, Key) pair:
	// the old entry is removed from the Store and the new entry
	// takes its place. Use for facts that update over time —
	// "user prefers tabs" → user changes mind → re-Append with
	// the same Key and the new Text supersedes the old. Empty
	// Key disables supersession (each Append is independent,
	// subject only to content-hash dedupe).
	Key string

	// Source records provenance: which session / message / tool
	// caused this entry to be saved. Useful for audit, "show me
	// why you remember this," and bulk-Forget by session. Zero-
	// valued Source is fine for manual programmatic Appends.
	Source Source

	// CreatedAt is set by Store.Append from time.Now if the caller
	// left it zero. Preserved on Recall so policies can prefer
	// recency.
	CreatedAt time.Time

	// Score is the recall relevance (cosine similarity) on the vector path; 0 on non-vector recall. Not persisted.
	Score float32
}

Entry is one memory record. The shape borrows from the auto-memory pattern Claude Code uses (typed snippets with descriptions) but without the markdown-file requirement — Entry is the in-process type; serialization is the Store's problem.

Fields are intentionally small. Memory entries are short prose snippets ("user prefers tabs over spaces", "the merge freeze starts 2026-03-05") plus enough metadata to find them again. Larger structured artifacts belong in a domain-specific store, not in agent memory.

type EntryGetter

type EntryGetter interface {
	Get(id string) (Entry, bool)
}

EntryGetter fetches a stored entry by id. Stores that can resolve an id implement it; the provenance ledger uses it to verify a memory ref's hash (drift / deletion detection). Stores that cannot resolve simply do not implement it, and refs to them are recorded but flagged unverifiable.

type HybridRecaller

type HybridRecaller struct {
	Recallers []Recaller
	K         int
}

HybridRecaller combines multiple Recallers via reciprocal rank fusion: each contributing Recaller ranks its candidates; the fused score for each entry is the sum of 1/(K+rank) across contributors, and the top max entries by fused score win.

The canonical use is one VectorRecaller + one SimpleRecaller — vector retrieval covers semantic matches ("what was that pricing model?"), token overlap covers keyword-exact matches ("the FOO_BAR_BAZ flag"). Either alone misses cases the other catches.

K=60 is the RRF constant from the original paper; small values favor top-ranked items, large values flatten the ranking. 60 has been the field default for over a decade.

func NewHybridRecaller

func NewHybridRecaller(recallers ...Recaller) *HybridRecaller

NewHybridRecaller returns a Recaller that fuses the given underlying Recallers with K=60. Pass them in priority order only for clarity — order does not affect fusion.

func (*HybridRecaller) Recall

func (r *HybridRecaller) Recall(ctx context.Context, store Store, agentID, conversationHint string, max int) ([]Entry, error)

Recall calls every contributing Recaller in sequence with an overfetched max (3x), then fuses results via RRF and returns the top max. Per-Recaller failures are logged into the error return path only when ALL contributors fail; partial failures degrade gracefully so a missing embedder doesn't kill the whole recall.

type InMemoryStore

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

InMemoryStore is a Store backed entirely by a map in process memory. Loses state on restart — intended for tests and short-lived agents, not for the durable-across-runs case that JSONLStore covers.

Safe for concurrent use across Append / Recall / Forget.

func NewInMemoryStore

func NewInMemoryStore() *InMemoryStore

NewInMemoryStore returns an empty InMemoryStore. Clock is time.Now; tests that need deterministic timestamps assign to .Now after construction.

func (*InMemoryStore) Append

func (s *InMemoryStore) Append(ctx context.Context, e Entry) (Entry, error)

Append persists e. Two interactions worth flagging:

  • Content-address dedupe: ID is hashed from (AgentID, Kind, Key, Text). Same content + same Key produces the same ID, so a re-Append is a no-op (returns the existing entry with merged tags; CreatedAt preserved).
  • Key-based supersession: if e.Key is set AND a prior entry exists at the same (AgentID, Key), the prior entry is REMOVED and the new one takes its place. Use Key for facts that update over time (preferences, current values).

These two interact: re-Appending identical content under the same Key returns the existing entry (content-dedupe wins). Appending DIFFERENT content under the same Key supersedes the prior entry.

func (*InMemoryStore) Forget

func (s *InMemoryStore) Forget(ctx context.Context, id string) error

Forget removes the entry with the given ID. Idempotent — no error for unknown IDs. Also clears any keyToID entry pointing at this ID so a subsequent Append with the same Key starts fresh instead of trying to supersede a no-longer-present entry.

func (*InMemoryStore) Get

func (s *InMemoryStore) Get(id string) (Entry, bool)

Get returns the entry for id and true, or the zero Entry and false if the id is unknown or has been tombstoned. Implements EntryGetter.

func (*InMemoryStore) Recall

func (s *InMemoryStore) Recall(ctx context.Context, q Query, max int) ([]Entry, error)

Recall returns the entries matching q in newest-first order. Scoring is the simple shape SimpleRecaller expects: any match on AgentID (including "" matching everything), Kind, all Tags, then text substring. Returns at most max entries.

func (*InMemoryStore) SetClock

func (s *InMemoryStore) SetClock(fn func() time.Time)

SetClock swaps the internal clock. Test-only helper; production code leaves it at the default time.Now.

type JSONLStore

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

JSONLStore is a Store that persists entries as line-delimited JSON. One file per Store; appended lines are written atomically (single Write call) so concurrent readers don't see partial records.

File format: one Entry per line, encoded as JSON. Forget writes a tombstone record (ID with no other fields) — the next Recall skips any ID with a later tombstone. The file is never rewritten in place; a maintenance routine (Compact) can be run offline to drop tombstoned entries and shrink the file.

Concurrency: a single process-wide mutex serializes file access. External processes writing the same file concurrently is undefined behavior — JSONL is not crash-safe under cross-process append races. For single-agent talon use that's fine.

func NewJSONLStore

func NewJSONLStore(path string) (*JSONLStore, error)

NewJSONLStore returns a Store backed by the file at path. The file is created (with parent directories) on first Append if it doesn't exist. Reading from a missing file returns no entries, not an error — a fresh install has no memories yet, that's normal.

func (*JSONLStore) Append

func (s *JSONLStore) Append(ctx context.Context, e Entry) (Entry, error)

Append persists e. Dedupes by ID (content-address from entryID), same as InMemoryStore — a second Append with the same content adds nothing. Tags get merged via a rewrite cycle (read all, merge, rewrite); for the common case (no dedupe) the path is a single O_APPEND write.

func (*JSONLStore) Compact

func (s *JSONLStore) Compact(ctx context.Context) error

Compact rewrites the file dropping tombstoned IDs and old versions of replaced entries. Run offline (when no Append is in flight). Safe to call: writes to a temp file and renames atomically.

func (*JSONLStore) Forget

func (s *JSONLStore) Forget(ctx context.Context, id string) error

Forget writes a tombstone record. Idempotent — re-tombstoning is harmless.

func (*JSONLStore) Get

func (s *JSONLStore) Get(id string) (Entry, bool)

Get returns the entry for id and true, or the zero Entry and false if the id is unknown or has been tombstoned. Implements EntryGetter. Performs a full file scan (same cost as Recall); suitable for occasional drift checks, not hot paths.

func (*JSONLStore) Recall

func (s *JSONLStore) Recall(ctx context.Context, q Query, max int) ([]Entry, error)

Recall reads the file, replays it (newest-wins per ID, tombstones remove), filters with matches(), sorts newest-first, truncates.

func (*JSONLStore) SetClock

func (s *JSONLStore) SetClock(fn func() time.Time)

SetClock swaps the internal clock. Test-only helper.

type Kind

type Kind string

Kind is the typed identifier for the canonical memory categories. Entry.Kind stays untyped (string) so callers can use raw literals without conversion — these constants are the names a typed caller would use, plus the keys the KindRegistry indexes policies under.

The four constants mirror Claude Code's auto-memory taxonomy. The distinction is semantic, not enforced: a host that wants "incident" or "decision" as a Kind can use any string. The registry has a sensible default for unknown kinds.

const (
	// KindUser: facts about who the user is — role, expertise,
	// long-running goals. Stable. Always loaded eagerly into the
	// prompt; recall doesn't apply (these are CORE memories).
	KindUser Kind = "user"

	// KindFeedback: explicit guidance the user gave about how to
	// approach work — corrections, preferences, "stop doing X."
	// Stable; load eagerly. Treated as policy the model should
	// follow without re-asking.
	KindFeedback Kind = "feedback"

	// KindProject: current work context — goals, decisions,
	// deadlines, incidents. Time-bounded and decays. Recall-only
	// (only loaded when the conversation references the relevant
	// project or topic).
	KindProject Kind = "project"

	// KindReference: pointers to external information — "bugs
	// live in Linear project X", "dashboard at grafana.io/foo".
	// Recall-only; rarely surfaced without an explicit trigger.
	KindReference Kind = "reference"
)

type KindPolicy

type KindPolicy struct {
	// AlwaysInclude bypasses recall scoring entirely — every
	// entry of this Kind for the agent (up to MaxEntries) is
	// injected on every turn. The right default for "user" and
	// "feedback" Kinds; the wrong default for everything else.
	AlwaysInclude bool

	// MaxEntries caps how many entries of this Kind reach the
	// prompt per turn. 0 means "use the ContextManager's global
	// cap." Bound this conservatively for AlwaysInclude Kinds —
	// each entry is a fixed budget cost.
	MaxEntries int

	// AgeWeight (only used when AlwaysInclude=false) scales the
	// recall-score penalty for older entries: 0 means "ignore
	// age," positive values prefer newer. Useful for KindProject
	// (recent decisions matter more than month-old ones).
	AgeWeight float64
}

KindPolicy is the per-Kind retrieval/injection policy the ContextManager honors when building the prompt view. Hosts override defaults via KindRegistry.Set.

type KindRegistry

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

KindRegistry holds per-Kind policies. Safe for concurrent reads after construction; writes happen at startup. Hosts construct one per Agent (so different agents can have different policies) or share one across an installation.

func NewKindRegistry

func NewKindRegistry() *KindRegistry

NewKindRegistry returns a registry seeded with DefaultKindPolicies. Hosts can override + extend via Set.

func (*KindRegistry) AlwaysIncludeKinds

func (r *KindRegistry) AlwaysIncludeKinds() []Kind

AlwaysIncludeKinds returns the registered Kinds with AlwaysInclude=true. The ContextManager uses this to pull always-included entries directly from the Store, bypassing recall.

func (*KindRegistry) PolicyFor

func (r *KindRegistry) PolicyFor(kind Kind) KindPolicy

PolicyFor returns the policy for kind, falling back to FallbackKindPolicy when none is registered. Callers don't need to special-case unknown Kinds.

func (*KindRegistry) Set

func (r *KindRegistry) Set(kind Kind, policy KindPolicy)

Set installs (or replaces) the policy for kind. Pass a custom Kind string to register policies for host-specific kinds.

type Query

type Query struct {
	AgentID string
	Kind    string
	Tags    []string

	// Text is a free-text snippet to match against Entry.Text. The
	// matching strategy is Store-defined: substring for simple
	// stores, vector similarity for embedding-backed ones.
	Text string
}

Query is the input to Store.Recall. Empty fields are wildcards; AgentID="" matches all agents including the global scope (entries with no AgentID), AgentID="x" matches "x" plus global.

type RecallOptions

type RecallOptions struct {
	AgentID string
	MaxCap  int
}

RecallOptions configures NewRecallTool. AgentID is required — memories are agent-scoped and a recall without it returns nothing useful. MaxCap is the hard upper bound on results per call (default 8, mirrors what jess's auto-recall uses); the model can request fewer via the `max` arg but never more.

type RecallTool

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

RecallTool is the tool the model uses to query the memory store on demand. The host's auto-recall pass already pulls relevant entries into the system prompt each turn, but "what did I tell you about X" style questions need a way for the model to explicitly look something up — without one, the model either fabricates or grep'es the workspace, neither of which is the right behavior.

RecallTool wraps a Recaller (typically the same one the host's auto-recall uses) so semantic search semantics stay consistent between automatic and explicit recall.

func NewRecallTool

func NewRecallTool(store Store, recaller Recaller, opts RecallOptions) *RecallTool

NewRecallTool builds the tool. Returns nil on impossible config (nil store or nil recaller) — callers should construct explicitly. Recaller is required because the tool's whole point is semantic search, not raw scan; pass NewHybridRecaller or NewVectorRecaller, not nil.

func (*RecallTool) Description

func (t *RecallTool) Description() string

Description is what the model sees. The phrasing is deliberate: the model needs to understand this is the ONLY way to query memory on demand (auto-recall handles the per-turn case), and that workspace grep is not a substitute.

func (*RecallTool) Execute

func (t *RecallTool) Execute(ctx context.Context, raw json.RawMessage) (json.RawMessage, error)

Execute satisfies tool.Tool. Decodes args, runs the recaller (or store.Recall when a kind filter is supplied — kind-scoped recall bypasses the semantic ranker so the model gets deterministic results when it asks for "all user-kind memories"). Returns a JSON list the model can quote from.

func (*RecallTool) Name

func (t *RecallTool) Name() string

Name satisfies tool.Tool.

func (*RecallTool) Schema

func (t *RecallTool) Schema() map[string]any

Schema satisfies tool.Tool.

type Recaller

type Recaller interface {
	// Recall returns at most max entries to inject. ConversationHint
	// is the last few messages of the running conversation —
	// implementations decide how much context they want; the
	// caller (ContextManager adapter) supplies as much as the
	// host's CompactionStrategy already keeps live.
	Recall(ctx context.Context, store Store, agentID string, conversationHint string, max int) ([]Entry, error)
}

Recaller is the read-side strategy: given the current conversation state, pick the entries to inject into the next LLM call.

Stores expose raw lookup. Recallers turn lookup into the right N entries for the moment. Separating them lets a host swap, say, a TF-IDF recaller for a semantic-search one without changing the Store.

type RememberOptions

type RememberOptions struct {
	// AgentID scopes every saved Entry to this agent. Empty means
	// global (visible to all agents on Recall). Set per-agent for
	// the canonical multi-agent setup.
	AgentID string
}

RememberOptions configures NewRememberTool. AgentID is required because every saved Entry needs to be scoped to one agent (per jess's multi-agent model). Hosts that don't multi-agent set this to "" for global scope.

type RememberTool

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

RememberTool is the tool the model uses to save a fact to memory. Hosts register it like any other tool:

store, _ := memory.NewChromemStore(emb, ...)
tool := memory.NewRememberTool(store, memory.RememberOptions{
    AgentID: "main",
})
agent := agentcore.NewAgent(
    ...
    agentcore.WithTools(append(otherTools, tool)...),
)

Provenance: source defaults to {Tool: "remember"}. Hosts that know which session/message triggered the save thread that through ctx via WithSource — the tool reads it on each Execute and stamps the resulting Entry. Without WithSource the entry still saves; just with less audit info.

func NewRememberTool

func NewRememberTool(store Store, opts RememberOptions) *RememberTool

NewRememberTool builds the tool. Returns nil on impossible config (nil Store) — callers should construct explicitly.

func (*RememberTool) Description

func (t *RememberTool) Description() string

Description is what the model reads when deciding whether to call the tool. Keep it concrete — "save a fact" is too vague; the model needs to know what's worth saving and how to shape the call.

func (*RememberTool) Execute

func (t *RememberTool) Execute(ctx context.Context, raw json.RawMessage) (json.RawMessage, error)

Execute satisfies tool.Tool. Decodes args, stamps source from ctx (if any), calls Store.Append. Returns JSON with the saved entry's ID + creation time so the model can reference it in its reply.

func (*RememberTool) Name

func (t *RememberTool) Name() string

Name satisfies tool.Tool.

func (*RememberTool) Schema

func (t *RememberTool) Schema() map[string]any

Schema satisfies tool.Tool. JSON-schema describing the args the model is expected to produce.

type SimpleRecaller

type SimpleRecaller struct {
	// IncludeKinds, when non-empty, limits recall to entries whose
	// Kind appears in the list. Useful for excluding low-signal
	// "reference" entries when you only want preferences and
	// project facts.
	IncludeKinds []string

	// MinTokenLength filters short tokens out of the text-overlap
	// score. Default 3 — "is", "of", "and" don't help retrieval.
	MinTokenLength int

	// RequireMatch drops entries with no lexical signal (score 0:
	// no token or tag overlap with the hint). Off by default, so a
	// hint that tokenizes to nothing (e.g. "Oh hi!") otherwise
	// returns the most recent entries by recency — irrelevant
	// recall. Hosts wanting strict relevance enable it via
	// WithRequireMatch so the keyword path is gated like the
	// vector path's WithMinScore floor.
	RequireMatch bool

	// Stopwords are dropped from the hint before scoring. Without
	// them, common words ("the", "what", "did") produce spurious
	// substring matches that pull in unrelated memories. Empty by
	// default; set via WithStopwords (DefaultStopwords is a ready
	// English list).
	Stopwords map[string]struct{}
}

SimpleRecaller is the default Recaller. It builds a Query from the conversation hint (current message text is the substring query) and asks the Store for matches, then re-scores by:

  1. text overlap (how many query tokens appear in entry text)
  2. tag presence (matched tags from conversation surface)
  3. recency (newer wins ties)

The strategy is intentionally cheap — no embeddings, no remote calls. Hosts that want semantic search plug their own Recaller without touching the Store.

func NewSimpleRecaller

func NewSimpleRecaller(opts ...SimpleRecallerOption) *SimpleRecaller

NewSimpleRecaller returns a Recaller with conservative defaults (no Kind filter, MinTokenLength=3).

func (*SimpleRecaller) Recall

func (r *SimpleRecaller) Recall(ctx context.Context, store Store, agentID string, conversationHint string, max int) ([]Entry, error)

Recall returns the top max entries for the agent. ConversationHint is parsed into tokens; tokens at or above MinTokenLength contribute to the substring query and to per-entry scoring.

type SimpleRecallerOption

type SimpleRecallerOption func(*SimpleRecaller)

SimpleRecallerOption configures a SimpleRecaller.

func WithRequireMatch

func WithRequireMatch() SimpleRecallerOption

WithRequireMatch makes the recaller drop entries with no token/tag overlap with the hint, instead of padding results by recency.

func WithStopwords

func WithStopwords(words ...string) SimpleRecallerOption

WithStopwords drops the given words (lowercased) from the hint before scoring. Pass DefaultStopwords for a standard English set, optionally extended with domain terms.

type Source

type Source struct {
	// SessionID is the conversation/agent-run identifier that
	// originated the save. Caller-defined shape; jess doesn't
	// enforce a format. Talon uses sessionKey (e.g.
	// "agent:main:main").
	SessionID string
	// MessageID is the specific message within SessionID that
	// triggered the save (typically the assistant turn whose
	// tool_call invoked the RememberTool).
	MessageID string
	// Tool names the tool that performed the save. Set to
	// "remember" for the standard RememberTool path; hosts that
	// wire their own save flow set their own identifier here.
	Tool string
	// Reason is free-form: "user said /remember", "model decided
	// this was important", etc. Useful for audit; not interpreted
	// by the recall pipeline.
	Reason string
}

Source captures where an Entry came from. Optional but strongly recommended for entries written by the RememberTool — without it, a user who asks "why do you remember X?" can't be answered and "forget everything from session Y" can't be implemented.

func SourceFromContext

func SourceFromContext(ctx context.Context) Source

SourceFromContext extracts the Source stamped via WithSource, or returns the zero Source if none. Exported so other tools that want the same provenance can pick it up consistently.

type Store

type Store interface {
	// Append persists e and returns its assigned ID. Implementations
	// may dedupe by Text + AgentID; in that case ID identifies the
	// existing entry. The returned entry carries the persisted form
	// (ID set, CreatedAt populated if it was zero).
	Append(ctx context.Context, e Entry) (Entry, error)

	// Recall returns at most max entries matching q. Ordering is
	// implementation-defined but should be "most relevant first"
	// — Recallers may re-rank on top of this.
	Recall(ctx context.Context, q Query, max int) ([]Entry, error)

	// Forget removes the entry identified by id. Returns no error
	// for unknown IDs — Forget is idempotent. Callers that need to
	// distinguish "removed" from "never existed" should Recall first.
	Forget(ctx context.Context, id string) error
}

Store is the persistence interface for memory entries. Implementations must be safe for concurrent use across Append / Recall / Forget; the agent host calls Append from the OnMessage hook and Recall from the ContextManager prepare path, which can interleave.

type VectorRecaller

type VectorRecaller struct {
	// Embedder is optional. When nil, VectorRecaller calls
	// store.(VectorStore).Embedder() to use whatever the Store
	// was built with — usually the right thing, and saves the
	// host from threading the same Embedder through twice.
	//
	// Set explicitly only when you want to query with an embedder
	// distinct from the one the Store indexed against. That's
	// almost always a mistake (the vectors would be in different
	// spaces) — the field exists for advanced testing only.
	Embedder Embedder
	// contains filtered or unexported fields
}

VectorRecaller is a Recaller that embeds the conversation hint and asks a VectorStore for nearest neighbors. Falls back with an error when the Store doesn't implement VectorStore — recallers don't silently degrade to keyword scoring, that's HybridRecaller's job.

Usually wrapped inside HybridRecaller; standalone use is fine when the host KNOWS the Store is vector-backed and wants pure semantic retrieval.

func NewVectorRecaller

func NewVectorRecaller(opts ...VectorRecallerOption) *VectorRecaller

NewVectorRecaller returns a VectorRecaller that uses the Store's own Embedder for query vectors. With no options it applies no relevance floor (every nearest neighbor is returned).

func (*VectorRecaller) Recall

func (r *VectorRecaller) Recall(ctx context.Context, store Store, agentID, conversationHint string, max int) ([]Entry, error)

Recall embeds conversationHint and asks the VectorStore for the top max nearest entries.

type VectorRecallerOption

type VectorRecallerOption func(*VectorRecaller)

VectorRecallerOption configures a VectorRecaller.

func WithMinScore

func WithMinScore(f float32) VectorRecallerOption

WithMinScore sets an absolute cosine relevance floor. Recalled entries with a Score below f are dropped. The default (0) keeps every nearest neighbor — set this to suppress off-topic recall.

type VectorStore

type VectorStore interface {
	Store

	// SearchVector returns the entries closest to vec, in
	// nearest-first order. Distance metric is implementation-
	// defined (chromem-go uses cosine). filter narrows the
	// candidate set the same way Query does in Store.Recall —
	// AgentID, Kind, Tags. Empty filter matches everything.
	SearchVector(ctx context.Context, vec []float32, max int, filter Query) ([]Entry, error)

	// Embedder returns the Embedder this Store was built with.
	// Lets Recallers produce query vectors with the same model
	// the stored vectors were produced with, without the caller
	// threading both through.
	Embedder() Embedder
}

VectorStore is an optional capability interface for Stores that support nearest-neighbor vector search. Recallers (like VectorRecaller) type-assert their Store argument to this interface; non-vector Stores fail the assertion and fall through to their text-only path.

Implementations document the embedder they index against; callers MUST pass a query vector produced by the same embedder model the Store was constructed with, or results are nonsense. The simplest way: every Store that satisfies VectorStore exposes an Embedder via Embedder() so callers don't have to track two configurations.

Directories

Path Synopsis
embed
gomlx
Package gomlx provides an in-process sentence embedder for jess memory, backed by the pure-Go gomlx/compute/gobackend ML runtime.
Package gomlx provides an in-process sentence embedder for jess memory, backed by the pure-Go gomlx/compute/gobackend ML runtime.

Jump to

Keyboard shortcuts

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