Automatic Semantic Long-Term Recall
Chronos agents can store long-term memories, but by default a developer had to
retrieve them explicitly, and retrieval dumped every memory into the prompt.
Semantic recall closes that loop: on each turn the agent embeds the user's
message, retrieves the top-k most relevant long-term memories for that
(agent, user) scope, and injects only those — so an agent "remembers" across
sessions without any explicit recall call, and without flooding the context.
It builds on Memory: attach a vector index to a
memory.Manager and recall turns on automatically.
Quick start
import (
"github.com/spawn08/chronos/sdk/agent"
"github.com/spawn08/chronos/sdk/memory"
)
// A Manager with a semantic index: an EmbeddingsProvider + a VectorStore.
mgr := memory.NewManager("assistant", userID, memStore, provider).
WithVectorIndex(embedder, vectorStore, "text-embedding-3-small", 1536)
a, _ := agent.New("assistant", "Assistant").
WithModel(model).
WithUserID(userID).
WithMemoryManager(mgr). // recall is ON by default once the index is attached
Build()
That's it. Facts the agent stores (via LLM extraction or the remember tool)
are embedded on write; on the next turn the relevant ones are recalled and
injected as a Relevant user memories: system message.
How it works
- Embed-on-write.
ExtractMemoriesand theremembertool mirror each stored fact into the vector index, keyed by the same tenant-scoped ID as the relational record (so re-storing a key overwrites its vector).forgetandOptimizeMemorieskeep the index in sync. - Recall-on-turn.
Manager.Recall(ctx, query, topK)embeds the query, searches the per-agent collection, and returns candidates ranked by score. - Injection. The agent formats the top-k into one system message. Recall returns structured, ranked candidates — never a pre-formatted blob — so a future context-compaction budget can trim them before they reach the prompt.
Tenant isolation
Memories are scoped by (agentID, userID). Recall passes the tenant scope
token to the vector store as a metadata filter (storage.WithFilter), so the
store computes top-k within the caller's subset of a shared per-agent
collection — one user never recalls another's memories, even when they share a
collection and store identical text. Adapters that store metadata structurally
(Qdrant, pgvector, Pinecone, Chroma) apply the filter server-side; the others
filter client-side. Recall also re-checks the scope on every result as a
defense-in-depth guarantee, so a misbehaving adapter can never leak across
tenants.
Configuration
Recall is controlled with WithMemoryRecall:
a, _ := agent.New("assistant", "Assistant").
WithMemoryManager(mgr).
WithMemoryRecall(agent.RecallConfig{
TopK: 8, // memories to recall per turn (default 5)
ScoreThreshold: 0.75, // drop weak matches (default 0: keep all)
Disabled: false, // set true to fall back to the full-memory dump
}).
Build()
When recall is disabled, or the manager has no vector index, the agent falls back to the legacy behavior (inject all stored memories), so existing agents are unaffected.
Try it
go run ./examples/semantic_recall/
The example is fully offline (a hashing embedder + an in-memory vector store): it writes memories in one "session", recalls them by relevance in another, and shows that a second user recalls none of the first user's memories.